From: Jaroslav Kysela Date: Tue, 19 Dec 2017 20:39:59 +0000 (+0100) Subject: dvr: cleanups for the previous patch X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=e3c9b6dc6350c65fb9ad5d036f5fd26b5b851e4b;p=thirdparty%2Ftvheadend.git dvr: cleanups for the previous patch - add toggle button for the previously recorded state - add show/hide duplicates button - many C and JS code cleanups (renaming, logic change) --- diff --git a/src/api/api_dvr.c b/src/api/api_dvr.c index 80d9e7ec3..3333f72d6 100644 --- a/src/api/api_dvr.c +++ b/src/api/api_dvr.c @@ -80,21 +80,17 @@ api_dvr_entry_grid_upcoming ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) { dvr_entry_t *de; - - LIST_FOREACH(de, &dvrentries, de_global_link) - if (dvr_entry_is_upcoming(de)) - idnode_set_add(ins, (idnode_t*)de, &conf->filter, perm->aa_lang_ui); -} - -static void -api_dvr_entry_grid_duplicate - ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) -{ - dvr_entry_t *de; - - LIST_FOREACH(de, &dvrentries, de_global_link) - if (dvr_entry_is_duplicate(de)) - idnode_set_add(ins, (idnode_t*)de, &conf->filter, perm->aa_lang_ui); + int duplicates = htsmsg_get_s32_or_default(args, "duplicates", 1); + + if (duplicates) { + LIST_FOREACH(de, &dvrentries, de_global_link) + if (dvr_entry_is_upcoming(de)) + idnode_set_add(ins, (idnode_t*)de, &conf->filter, perm->aa_lang_ui); + } else { + LIST_FOREACH(de, &dvrentries, de_global_link) + if (dvr_entry_is_upcoming_nodup(de)) + idnode_set_add(ins, (idnode_t*)de, &conf->filter, perm->aa_lang_ui); + } } static void @@ -346,18 +342,42 @@ api_dvr_entry_remove } static void -api_dvr_previouslyrecorded(access_t *perm, idnode_t *self) +api_dvr_prevrec_toggle(access_t *perm, idnode_t *self) { - dvr_entry_t *de = (dvr_entry_t *)self; - if (de->de_sched_state == DVR_SCHEDULED) - dvr_entry_previously_recorded(de); + dvr_entry_set_prevrec((dvr_entry_t *)self, -1); +} + +static int +api_dvr_entry_prevrec_toggle + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) +{ + return api_idnode_handler(&dvr_entry_class, perm, args, resp, api_dvr_prevrec_toggle, "prevrec", 0); +} + +static void +api_dvr_prevrec_unset(access_t *perm, idnode_t *self) +{ + dvr_entry_set_prevrec((dvr_entry_t *)self, 0); +} + +static int +api_dvr_entry_prevrec_unset + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) +{ + return api_idnode_handler(&dvr_entry_class, perm, args, resp, api_dvr_prevrec_unset, "prevrec", 0); +} + +static void +api_dvr_prevrec_set(access_t *perm, idnode_t *self) +{ + dvr_entry_set_prevrec((dvr_entry_t *)self, 1); } static int -api_dvr_entry_previouslyrecorded +api_dvr_entry_prevrec_set ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) { - return api_idnode_handler(&dvr_entry_class, perm, args, resp, api_dvr_previouslyrecorded, "remove", 0); + return api_idnode_handler(&dvr_entry_class, perm, args, resp, api_dvr_prevrec_set, "prevrec", 0); } static void @@ -547,7 +567,6 @@ void api_dvr_init ( void ) { "dvr/entry/class", ACCESS_RECORDER, api_idnode_class, (void*)&dvr_entry_class }, { "dvr/entry/grid", ACCESS_RECORDER, api_idnode_grid, api_dvr_entry_grid }, { "dvr/entry/grid_upcoming", ACCESS_RECORDER, api_idnode_grid, api_dvr_entry_grid_upcoming }, - { "dvr/entry/grid_duplicate", ACCESS_RECORDER, api_idnode_grid, api_dvr_entry_grid_duplicate }, { "dvr/entry/grid_finished", ACCESS_RECORDER, api_idnode_grid, api_dvr_entry_grid_finished }, { "dvr/entry/grid_failed", ACCESS_RECORDER, api_idnode_grid, api_dvr_entry_grid_failed }, { "dvr/entry/grid_removed", ACCESS_RECORDER, api_idnode_grid, api_dvr_entry_grid_removed }, @@ -558,7 +577,9 @@ void api_dvr_init ( void ) { "dvr/entry/rerecord/allow", ACCESS_RECORDER, api_dvr_entry_rerecord_allow, NULL }, { "dvr/entry/stop", ACCESS_RECORDER, api_dvr_entry_stop, NULL }, /* Stop active recording gracefully */ { "dvr/entry/cancel", ACCESS_RECORDER, api_dvr_entry_cancel, NULL }, /* Cancel scheduled or active recording */ - { "dvr/entry/previouslyrecorded", ACCESS_RECORDER, api_dvr_entry_previouslyrecorded, NULL }, /* Add selected schedules as previously recorded */ + { "dvr/entry/prevrec/toggle", ACCESS_RECORDER, api_dvr_entry_prevrec_toggle, NULL }, + { "dvr/entry/prevrec/set", ACCESS_RECORDER, api_dvr_entry_prevrec_set, NULL }, + { "dvr/entry/prevrec/unset", ACCESS_RECORDER, api_dvr_entry_prevrec_unset, NULL }, { "dvr/entry/remove", ACCESS_RECORDER, api_dvr_entry_remove, NULL }, /* Remove recorded files from storage */ { "dvr/entry/filemoved", ACCESS_ADMIN, api_dvr_entry_file_moved, NULL }, { "dvr/entry/move/finished", ACCESS_RECORDER, api_dvr_entry_move_finished, NULL }, diff --git a/src/dvr/dvr.h b/src/dvr/dvr.h index af87292c3..8595436c3 100644 --- a/src/dvr/dvr.h +++ b/src/dvr/dvr.h @@ -622,7 +622,7 @@ void dvr_entry_cancel_delete(dvr_entry_t *de, int rerecord); void dvr_entry_cancel_remove(dvr_entry_t *de, int rerecord); -void dvr_entry_previously_recorded(dvr_entry_t *de); +void dvr_entry_set_prevrec(dvr_entry_t *de, int cmd); int dvr_entry_file_moved(const char *src, const char *dst); @@ -636,7 +636,7 @@ htsmsg_t *dvr_entry_class_retention_list ( void *o, const char *lang ); htsmsg_t *dvr_entry_class_removal_list ( void *o, const char *lang ); int dvr_entry_is_upcoming(dvr_entry_t *entry); -int dvr_entry_is_duplicate(dvr_entry_t *entry); +int dvr_entry_is_upcoming_nodup(dvr_entry_t *entry); int dvr_entry_is_finished(dvr_entry_t *entry, int flags); int dvr_entry_verify(dvr_entry_t *de, access_t *a, int readonly); diff --git a/src/dvr/dvr_db.c b/src/dvr/dvr_db.c index 7a150e766..7d1b5bda8 100644 --- a/src/dvr/dvr_db.c +++ b/src/dvr/dvr_db.c @@ -134,15 +134,15 @@ dvr_entry_trace_time2_(const char *file, int line, int dvr_entry_is_upcoming(dvr_entry_t *entry) { dvr_entry_sched_state_t state = entry->de_sched_state; - return state == DVR_RECORDING || state == DVR_SCHEDULED || state == DVR_NOSTATE; + return state == DVR_RECORDING || state == DVR_SCHEDULED || state == DVR_NOSTATE; } -int dvr_entry_is_duplicate(dvr_entry_t *entry) +int dvr_entry_is_upcoming_nodup(dvr_entry_t *entry) { - dvr_entry_sched_state_t state = entry->de_sched_state; - if (_dvr_duplicate_event(entry) == 0){ - return state == DVR_RECORDING || state == DVR_SCHEDULED || state == DVR_NOSTATE; - }else return 0; + dvr_entry_sched_state_t state = entry->de_sched_state; + if (_dvr_duplicate_event(entry)) + return 0; + return state == DVR_RECORDING || state == DVR_SCHEDULED || state == DVR_NOSTATE; } int dvr_entry_is_finished(dvr_entry_t *entry, int flags) @@ -659,10 +659,9 @@ dvr_entry_status(dvr_entry_t *de) case SM_CODE_INVALID_TARGET: return N_("File not created"); case SM_CODE_USER_ACCESS: - return N_("User access error"); case SM_CODE_USER_LIMIT: - return N_("User limit reached"); case SM_CODE_NO_SPACE: + case SM_CODE_PREVIOUSLY_RECORDED: return streaming_code2txt(de->de_last_error); default: break; @@ -4203,33 +4202,42 @@ dvr_entry_cancel(dvr_entry_t *de, int rerecord) return de; } -static void -dvr_entry_add_previously_recorded(dvr_entry_t *de) +/* + * Toggle/set/unset previously recorded state + */ +void +dvr_entry_set_prevrec(dvr_entry_t *de, int cmd) { - de->de_dont_reschedule = 1; // Set as not reschedule - de->de_file_removed = 1; // Set as file removed - de->de_start = gclk(); // Need in case you want to record it again after. - de->de_stop = gclk() + 1000; - dvr_entry_completed(de, SM_CODE_OK); // mark as completed - idnode_changed(&de->de_id); + if (de->de_sched_state == DVR_RECORDING) + return; - dvr_entry_retention_timer(de); + if (cmd < 0) /* toggle */ + cmd = de->de_dont_reschedule ? 0 : 1; - htsp_dvr_entry_update(de); - idnode_notify_changed(&de->de_id); + if (cmd == !!de->de_dont_reschedule) + return; - tvhinfo(LS_DVR, "\"%s\" on \"%s\": " - "set as previously recorded", - lang_str_get(de->de_title, NULL), DVR_CH_NAME(de)); -} + if (cmd) { + de->de_dont_reschedule = 1; + de->de_dont_rerecord = 1; + de->de_file_removed = 1; + dvr_entry_completed(de, SM_CODE_PREVIOUSLY_RECORDED); + } else { + de->de_dont_reschedule = 0; + de->de_dont_rerecord = 0; + de->de_file_removed = 0; + dvr_entry_set_timer(de); + } -/** - * Add a schedule as previously recorded - */ -void -dvr_entry_previously_recorded(dvr_entry_t *de) -{ - dvr_entry_add_previously_recorded(de); + dvr_entry_retention_timer(de); + + htsp_dvr_entry_update(de); + idnode_notify_changed(&de->de_id); + + tvhinfo(LS_DVR, "\"%s\" on \"%s\": " + "%sset as previously recorded", + lang_str_get(de->de_title, NULL), DVR_CH_NAME(de), + cmd ? "" : "un"); } /** diff --git a/src/tvheadend.h b/src/tvheadend.h index 49edfb522..3f1f69097 100644 --- a/src/tvheadend.h +++ b/src/tvheadend.h @@ -541,6 +541,7 @@ typedef enum { #define SM_CODE_USER_LIMIT 106 #define SM_CODE_WEAK_STREAM 107 #define SM_CODE_USER_REQUEST 108 +#define SM_CODE_PREVIOUSLY_RECORDED 109 #define SM_CODE_NO_FREE_ADAPTER 200 #define SM_CODE_MUX_NOT_ENABLED 201 diff --git a/src/webui/static/app/dvr.js b/src/webui/static/app/dvr.js index 109a85f28..bd1936e72 100644 --- a/src/webui/static/app/dvr.js +++ b/src/webui/static/app/dvr.js @@ -223,13 +223,13 @@ tvheadend.filesizeRenderer = function(st) { tvheadend.displayDuplicate = function(value, meta, record) { - if (value == null) - return ''; - var is_dup = record.data['duplicate']; - if (is_dup) - return "" + value + ""; - else - return value; + if (value == null) + return ''; + var is_dup = record.data['duplicate']; + if (is_dup) + return "" + value + ""; + else + return value; } /** Render an entry differently if it is a duplicate */ @@ -242,21 +242,40 @@ tvheadend.displayWithDuplicateRenderer = function(value, meta, record) { } tvheadend.displayWithYearAndDuplicateRenderer = function(value, meta, record) { - return function() { - return function(value, meta, record) { - value = tvheadend.getDisplayTitle(value, record); - return tvheadend.displayDuplicate(value, meta, record); + return function() { + return function(value, meta, record) { + value = tvheadend.getDisplayTitle(value, record); + return tvheadend.displayDuplicate(value, meta, record); + } } - } } tvheadend.displayWithYearRenderer = function(value, meta, record) { - return function() { - return function(value, meta, record) { - value = tvheadend.getDisplayTitle(value, record); - return value; + return function() { + return function(value, meta, record) { + value = tvheadend.getDisplayTitle(value, record); + return value; + } + } +} + +tvheadend.dvrButtonFcn = function(store, select, _url, q) { + var r = select.getSelections(); + if (r && r.length > 0) { + var uuids = []; + for (var i = 0; i < r.length; i++) + uuids.push(r[i].id); + tvheadend.AjaxConfirm({ + url: _url, + params: { + uuid: Ext.encode(uuids) + }, + success: function(d) { + store.reload(); + }, + question: q, + }); } - } } /** @@ -269,6 +288,8 @@ tvheadend.dvr_upcoming = function(panel, index) { var elist = 'enabled,' + (tvheadend.accessUpdate.admin ? list + ',owner,creator' : list) + ',retention,removal'; + var duplicates = 0; + var buttonFcn = tvheadend.dvrButtonFcn; var stopButton = { name: 'stop', @@ -281,22 +302,8 @@ tvheadend.dvr_upcoming = function(panel, index) { }); }, callback: function(conf, e, store, select) { - var r = select.getSelections(); - if (r && r.length > 0) { - var uuids = []; - for (var i = 0; i < r.length; i++) - uuids.push(r[i].id); - tvheadend.AjaxConfirm({ - url: 'api/dvr/entry/stop', - params: { - uuid: Ext.encode(uuids) - }, - success: function(d) { - store.reload(); - }, - question: _('Do you really want to gracefully stop/unschedule the selection?') - }); - } + buttonFcn(store, select, 'api/dvr/entry/stop', + _('Do you really want to gracefully stop/unschedule the selection?')); } }; @@ -311,77 +318,43 @@ tvheadend.dvr_upcoming = function(panel, index) { }); }, callback: function(conf, e, store, select) { - var r = select.getSelections(); - if (r && r.length > 0) { - var uuids = []; - for (var i = 0; i < r.length; i++) - uuids.push(r[i].id); - tvheadend.AjaxConfirm({ - url: 'api/dvr/entry/cancel', - params: { - uuid: Ext.encode(uuids) - }, - success: function(d) { - store.reload(); - }, - question: _('Do you really want to abort/unschedule the selection?') - }); - } + buttonFcn(store, select, 'api/dvr/entry/cancel', + _('Do you really want to abort/unschedule the selection?')); } }; - var previouslyrecordedButton = { - name: 'previouslyrecorded', + var prevrecButton = { + name: 'prevrec', builder: function() { return new Ext.Toolbar.Button({ - tooltip: _('Add as recorded the selected program'), - iconCls: 'previouslyrecorded', - text: _('Add as recorded'), + tooltip: _('Toggle the previously recorded state.'), + iconCls: 'prevrec', + text: _('Previously recorded'), disabled: true }); }, callback: function(conf, e, store, select) { - var r = select.getSelections(); - if (r && r.length > 0) { - var uuids = []; - for (var i = 0; i < r.length; i++) - uuids.push(r[i].id); - tvheadend.AjaxConfirm({ - url: 'api/dvr/entry/previouslyrecorded', - params: { - uuid: Ext.encode(uuids) - }, - success: function(d) { - store.reload(); - }, - question: _('Do you really want to add the selected recordings as previosly recorded?') - }); - } + buttonFcn(store, select, 'api/dvr/entry/prevrec/toggle', + _('Do you really want to toggle the previously recorded state for the selected recordings?')); } }; - var showSkippedButton = { - name: 'showskipped', - builder: function() { - return new Ext.Toolbar.Button({ - tooltip: _('Show / hide skipped recordings'), - iconCls: 'showskipped', - text: _('View skipped recordings'), - disabled: false, - pressed: false, - enableToggle: true - }); - }, - callback: function(conf, e, store) { - if (store.proxy.url === 'api/dvr/entry/grid_duplicate'){ - store.proxy.setUrl('api/dvr/entry/grid_upcoming',true); - store.load(); - }else{ - store.proxy.setUrl('api/dvr/entry/grid_duplicate',true); - store.load(); - } - } - }; + 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; + this.setText(duplicates ? _('Hide duplicates') : _('Show duplicates')); + store.baseParams.duplicates = duplicates; + store.reload(); + } + }; function selected(s, abuttons) { var recording = 0; @@ -391,7 +364,7 @@ tvheadend.dvr_upcoming = function(panel, index) { }); abuttons.stop.setDisabled(recording < 1); abuttons.abort.setDisabled(recording < 1); - abuttons.previouslyrecorded.setDisabled(recording >= 1); + abuttons.prevrec.setDisabled(recording >= 1); } function beforeedit(e, grid) { @@ -401,11 +374,14 @@ tvheadend.dvr_upcoming = function(panel, index) { tvheadend.idnode_grid(panel, { url: 'api/dvr/entry', - gridURL: 'api/dvr/entry/grid_duplicate', + gridURL: 'api/dvr/entry/grid_upcoming', titleS: _('Upcoming Recording'), titleP: _('Upcoming / Current Recordings'), iconCls: 'upcomingRec', tabIndex: index, + extraParams: function(params) { + params.duplicates = duplicates + }, add: { url: 'api/dvr/entry', params: { @@ -434,7 +410,7 @@ tvheadend.dvr_upcoming = function(panel, index) { filesize: { renderer: tvheadend.filesizeRenderer() }, - genre : { + genre: { renderer: function(vals, meta, record) { return function(vals, meta, record) { var r = []; @@ -458,7 +434,7 @@ tvheadend.dvr_upcoming = function(panel, index) { actions, tvheadend.contentTypeAction, ], - tbar: [stopButton, abortButton, previouslyrecordedButton, showSkippedButton], + tbar: [stopButton, abortButton, prevrecButton, dupButton], selected: selected, beforeedit: beforeedit }); @@ -472,6 +448,7 @@ tvheadend.dvr_upcoming = function(panel, index) { tvheadend.dvr_finished = function(panel, index) { var actions = tvheadend.dvrRowActions(); + var buttonFcn = tvheadend.dvrButtonFcn; var downloadButton = { name: 'download', @@ -503,21 +480,7 @@ tvheadend.dvr_finished = function(panel, index) { }); }, callback: function(conf, e, store, select) { - var r = select.getSelections(); - if (r && r.length > 0) { - var uuids = []; - for (var i = 0; i < r.length; i++) - uuids.push(r[i].id); - tvheadend.Ajax({ - url: 'api/dvr/entry/rerecord/toggle', - params: { - uuid: Ext.encode(uuids) - }, - success: function(d) { - store.reload(); - } - }); - } + buttonFcn(store, select, 'api/dvr/entry/rerecord/toggle'); } }; @@ -532,21 +495,7 @@ tvheadend.dvr_finished = function(panel, index) { }); }, callback: function(conf, e, store, select) { - var r = select.getSelections(); - if (r && r.length > 0) { - var uuids = []; - for (var i = 0; i < r.length; i++) - uuids.push(r[i].id); - tvheadend.Ajax({ - url: 'api/dvr/entry/move/failed', - params: { - uuid: Ext.encode(uuids) - }, - success: function(d) { - store.reload(); - } - }); - } + buttonFcn(store, select, 'api/dvr/entry/move/failed'); } }; @@ -561,22 +510,8 @@ tvheadend.dvr_finished = function(panel, index) { }); }, callback: function(conf, e, store, select) { - var r = select.getSelections(); - if (r && r.length > 0) { - var uuids = []; - for (var i = 0; i < r.length; i++) - uuids.push(r[i].id); - tvheadend.AjaxConfirm({ - url: 'api/dvr/entry/remove', - params: { - uuid: Ext.encode(uuids) - }, - success: function(d) { - store.reload(); - }, - question: _('Do you really want to remove the selected recordings from storage?') - }); - } + buttonFcn(store, select, 'api/dvr/entry/remove', + _('Do you really want to remove the selected recordings from storage?')); } }; @@ -642,6 +577,7 @@ tvheadend.dvr_finished = function(panel, index) { tvheadend.dvr_failed = function(panel, index) { var actions = tvheadend.dvrRowActions(); + var buttonFcn = tvheadend.dvrButtonFcn; var downloadButton = { name: 'download', @@ -673,21 +609,7 @@ tvheadend.dvr_failed = function(panel, index) { }); }, callback: function(conf, e, store, select) { - var r = select.getSelections(); - if (r && r.length > 0) { - var uuids = []; - for (var i = 0; i < r.length; i++) - uuids.push(r[i].id); - tvheadend.Ajax({ - url: 'api/dvr/entry/rerecord/toggle', - params: { - uuid: Ext.encode(uuids) - }, - success: function(d) { - store.reload(); - } - }); - } + buttonFcn(store, select, 'api/dvr/entry/rerecord/toggle'); } }; @@ -702,21 +624,7 @@ tvheadend.dvr_failed = function(panel, index) { }); }, callback: function(conf, e, store, select) { - var r = select.getSelections(); - if (r && r.length > 0) { - var uuids = []; - for (var i = 0; i < r.length; i++) - uuids.push(r[i].id); - tvheadend.Ajax({ - url: 'api/dvr/entry/move/finished', - params: { - uuid: Ext.encode(uuids) - }, - success: function(d) { - store.reload(); - } - }); - } + buttonFcn(store, select, 'api/dvr/entry/move/finished'); } }; @@ -784,6 +692,7 @@ tvheadend.dvr_failed = function(panel, index) { tvheadend.dvr_removed = function(panel, index) { var actions = tvheadend.dvrRowActions(); + var buttonFcn = tvheadend.dvrButtonFcn; var rerecordButton = { name: 'rerecord', @@ -796,21 +705,7 @@ tvheadend.dvr_removed = function(panel, index) { }); }, callback: function(conf, e, store, select) { - var r = select.getSelections(); - if (r && r.length > 0) { - var uuids = []; - for (var i = 0; i < r.length; i++) - uuids.push(r[i].id); - tvheadend.Ajax({ - url: 'api/dvr/entry/rerecord/toggle', - params: { - uuid: Ext.encode(uuids) - }, - success: function(d) { - store.reload(); - } - }); - } + buttonFcn(store, select, 'api/dvr/entry/rerecord/toggle'); } }; diff --git a/src/webui/static/app/ext.css b/src/webui/static/app/ext.css index 0bab023c2..8b44bc8d6 100644 --- a/src/webui/static/app/ext.css +++ b/src/webui/static/app/ext.css @@ -224,12 +224,12 @@ background-image: url(../icons/delete.png) !important; } -.previouslyrecorded { +.prevrec { background-image: url(../icons/accept.png) !important; } -.showskipped { - background-image: url(../icons/other_filters.png) !important; +.duprec { + background-image: url(../icons/control_repeat_blue.png) !important; } .cancel { diff --git a/src/webui/static/app/idnode.js b/src/webui/static/app/idnode.js index 8d9c8bb39..3761c6aa7 100644 --- a/src/webui/static/app/idnode.js +++ b/src/webui/static/app/idnode.js @@ -1727,6 +1727,7 @@ tvheadend.idnode_grid = function(panel, conf) /* Store */ var params = {}; if (conf.all) params['all'] = 1; + if (conf.extraParams) conf.extraParams(params); store = new Ext.data.JsonStore({ root: 'entries', url: conf.gridURL || (conf.url + '/grid'), diff --git a/src/webui/static/icons/control_repeat_blue.png b/src/webui/static/icons/control_repeat_blue.png new file mode 120000 index 000000000..03a996e4e --- /dev/null +++ b/src/webui/static/icons/control_repeat_blue.png @@ -0,0 +1 @@ +../../../../vendor/famfamsilk/control_repeat_blue.png \ No newline at end of file