diff --git a/airtime_mvc/application/common/PodcastManager.php b/airtime_mvc/application/common/PodcastManager.php index 24f9a44fd..026e5e605 100644 --- a/airtime_mvc/application/common/PodcastManager.php +++ b/airtime_mvc/application/common/PodcastManager.php @@ -64,7 +64,7 @@ class PodcastManager { // episodes in the list of episodes to ingest, don't skip this episode - we should try to ingest the // most recent episode when the user first sets the podcast to automatic ingest. // If the publication date of this episode is before the ingest timestamp, we don't need to ingest it - if ((empty($ts) && !empty($episodes)) || strtotime($episodeData["pub_date"]) < strtotime($ts)) { + if ((empty($ts) && ($i > 0)) || strtotime($episodeData["pub_date"]) < strtotime($ts)) { continue; } $episode = PodcastEpisodesQuery::create()->findOneByDbEpisodeGuid($episodeData["guid"]); diff --git a/airtime_mvc/application/controllers/LibraryController.php b/airtime_mvc/application/controllers/LibraryController.php index 06139e93f..452451547 100644 --- a/airtime_mvc/application/controllers/LibraryController.php +++ b/airtime_mvc/application/controllers/LibraryController.php @@ -222,6 +222,7 @@ class LibraryController extends Zend_Controller_Action $message = null; $noPermissionMsg = _("You don't have permission to delete selected items."); + Logging::info($mediaItems); foreach ($mediaItems as $media) { if ($media["type"] === "audioclip") { diff --git a/airtime_mvc/application/services/PodcastEpisodeService.php b/airtime_mvc/application/services/PodcastEpisodeService.php index 8733bf24c..05640d371 100644 --- a/airtime_mvc/application/services/PodcastEpisodeService.php +++ b/airtime_mvc/application/services/PodcastEpisodeService.php @@ -267,16 +267,23 @@ class Application_Service_PodcastEpisodeService extends Application_Service_Thir public function _getImportedPodcastEpisodeArray($podcast, $episodes) { $rss = Application_Service_PodcastService::getPodcastFeed($podcast->getDbUrl()); $episodeIds = array(); + $episodeFiles = array(); foreach ($episodes as $e) { array_push($episodeIds, $e->getDbEpisodeGuid()); + $episodeFiles[$e->getDbEpisodeGuid()] = $e->getDbFileId(); } $episodesArray = array(); foreach ($rss->get_items() as $item) { + $itemId = $item->get_id(); + $ingested = in_array($itemId, $episodeIds) ? (empty($episodeFiles[$itemId]) ? -1 : 1) : 0; + $file = $ingested > 0 && !empty($episodeFiles[$itemId]) ? + CcFiles::getSanitizedFileById($episodeFiles[$itemId]) : array(); /** @var SimplePie_Item $item */ array_push($episodesArray, array( - "guid" => $item->get_id(), - "ingested" => in_array($item->get_id(), $episodeIds), + "podcast_id" => $podcast->getDbId(), + "guid" => $itemId, + "ingested" => $ingested, "title" => $item->get_title(), // From the RSS spec best practices: // 'An item's author element provides the e-mail address of the person who wrote the item' @@ -284,7 +291,8 @@ class Application_Service_PodcastEpisodeService extends Application_Service_Thir "description" => $item->get_description(), "pub_date" => $item->get_gmdate(), "link" => $item->get_link(), - "enclosure" => $item->get_enclosure() + "enclosure" => $item->get_enclosure(), + "file" => $file )); } diff --git a/airtime_mvc/application/views/scripts/library/publish-dialog.phtml b/airtime_mvc/application/views/scripts/library/publish-dialog.phtml index fd1b463f8..32fa93b1b 100644 --- a/airtime_mvc/application/views/scripts/library/publish-dialog.phtml +++ b/airtime_mvc/application/views/scripts/library/publish-dialog.phtml @@ -45,7 +45,7 @@
diff --git a/airtime_mvc/public/css/dashboard.css b/airtime_mvc/public/css/dashboard.css index 0902d7d9e..84d2fd1ca 100644 --- a/airtime_mvc/public/css/dashboard.css +++ b/airtime_mvc/public/css/dashboard.css @@ -200,7 +200,7 @@ thead th.ui-state-default { color: #ccc; } -thead th.ui-state-default:not([class*='checkbox']):not([class*='type']):not([class*='image']) { +thead th.ui-state-default:not([class*='checkbox']):not([class*='type']):not([class*='image']):not([class*='imported']) { cursor: move; } diff --git a/airtime_mvc/public/css/playlist_builder.css b/airtime_mvc/public/css/playlist_builder.css index 41a5304a1..d6cad09de 100644 --- a/airtime_mvc/public/css/playlist_builder.css +++ b/airtime_mvc/public/css/playlist_builder.css @@ -186,7 +186,7 @@ } .btn-toolbar { - margin: 0; + /*margin: 0;*/ padding: 5px; } diff --git a/airtime_mvc/public/css/styles.css b/airtime_mvc/public/css/styles.css index b19966e3d..41bf99c0c 100644 --- a/airtime_mvc/public/css/styles.css +++ b/airtime_mvc/public/css/styles.css @@ -908,7 +908,7 @@ dl.inline-list dd { .DataTables_sort_wrapper .ui-icon { display: block; float: right; - margin: 5px 0; + margin: 6px 0 0 5px; } .dataTables_type { float:right; @@ -3953,12 +3953,20 @@ li .ui-state-hover { /* Podcasts */ +.DataTables_sort_wrapper { + display: -webkit-box; + display: -moz-box; + display: -ms-flexbox; + display: -webkit-flex; + display: flex; + -webkit-justify-content: flex-start; + justify-content: flex-start; +} + [id^="podcast_episodes"][id$="_wrapper"] { position: relative; - height: 100%; - float: left; flex: 1 100%; - margin: 4px 0 !important; + /*margin: 4px 0 0 !important;*/ min-height: 0; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; @@ -4029,9 +4037,29 @@ li .ui-state-hover { float: left; } +.podcast_episodes_imported { + text-align: center !important; +} + +.imported-flag { + margin-bottom: -2px; +} + +.loading-icon { + background: url("img/loading.gif") no-repeat center center; + min-width: 24px; + width: 100%; + height: 16px; + display: block; +} + /* UI Revamp Video */ #whatsnew { + display: -webkit-box; + display: -moz-box; + display: -ms-flexbox; + display: -webkit-flex; display: flex; flex-direction: column; justify-content: center; @@ -4087,6 +4115,10 @@ li .ui-state-hover { } .publish-sources { + display: -webkit-box; + display: -moz-box; + display: -ms-flexbox; + display: -webkit-flex; display: flex; margin-bottom: 10px; } diff --git a/airtime_mvc/public/js/airtime/library/events/library_showbuilder.js b/airtime_mvc/public/js/airtime/library/events/library_showbuilder.js index 29ba68947..130c79b52 100644 --- a/airtime_mvc/public/js/airtime/library/events/library_showbuilder.js +++ b/airtime_mvc/public/js/airtime/library/events/library_showbuilder.js @@ -263,6 +263,40 @@ var AIRTIME = (function(AIRTIME) { return true; } + mod.addToSchedule = function (selected) { + console.log(selected); + var aMediaIds = [], aSchedIds = [], aData = []; + + $.each(selected, function () { + aMediaIds.push({ + "id": this.id, + "type": this.ftype + }); + }); + + // process selected files/playlists. + $("#show_builder_table").find("tr.sb-selected").each(function (i, el) { + aData.push($(el).data("aData")); + }); + + // process selected schedule rows to add media after. + $.each(aData, function () { + aSchedIds.push({ + "id": this.id, + "instance": this.instance, + "timestamp": this.timestamp + }); + }); + + if (aSchedIds.length == 0) { + if (!addToCurrentOrNext(aSchedIds)) { + return; + } + } + + AIRTIME.showbuilder.fnAdd(aMediaIds, aSchedIds); + }; + mod.setupLibraryToolbar = function() { var $toolbar = $(".lib-content .fg-toolbar:first"); @@ -284,45 +318,14 @@ var AIRTIME = (function(AIRTIME) { return; } - var selected = AIRTIME.library.getSelectedData(), data, i, length, temp, aMediaIds = [], aSchedIds = [], aData = []; + var selected = AIRTIME.library.getSelectedData(), aMediaIds = []; if ($("#show_builder_table").is(":visible")) { - for (i = 0, length = selected.length; i < length; i++) { - data = selected[i]; - aMediaIds.push({ - "id": data.id, - "type": data.ftype - }); - } - - // process selected files/playlists. - $("#show_builder_table tr.sb-selected").each(function (i, el) { - aData.push($(el).data("aData")); - }); - - // process selected schedule rows to add media - // after. - for (i = 0, length = aData.length; i < length; i++) { - temp = aData[i]; - aSchedIds.push({ - "id": temp.id, - "instance": temp.instance, - "timestamp": temp.timestamp - }); - } - - if (aSchedIds.length == 0) { - if (!addToCurrentOrNext(aSchedIds)) { - return; - } - } - - AIRTIME.showbuilder.fnAdd(aMediaIds, aSchedIds); + mod.addToSchedule(selected); } else { - for (i = 0, length = selected.length; i < length; i++) { - data = selected[i]; - aMediaIds.push([data.id, data.ftype]); - } + $.each(selected, function () { + aMediaIds.push([this.id, this.ftype]); + }); // check if a playlist/block is open before adding items if ($('.active-tab .obj_type').val() == 'playlist' diff --git a/airtime_mvc/public/js/airtime/library/library.js b/airtime_mvc/public/js/airtime/library/library.js index 4ea7128a1..f21388676 100644 --- a/airtime_mvc/public/js/airtime/library/library.js +++ b/airtime_mvc/public/js/airtime/library/library.js @@ -476,7 +476,11 @@ var AIRTIME = (function(AIRTIME) { } chosenItems = {}; - oTable.fnStandingRedraw(); + if (oTable == $datatables[mod.DataTableTypeEnum.PODCAST_EPISODES]) { + mod.podcastEpisodeTableWidget.reload(); + } else { + oTable.fnStandingRedraw(); + } //Re-enable the delete button AIRTIME.button.enableButton("btn-group #sb-trash", false); @@ -1246,6 +1250,8 @@ var AIRTIME = (function(AIRTIME) { }; mod.setCurrentTable = function (table) { + // FIXME: This is hacky... + mod.podcastEpisodeDataTable.fnClearTable(); var dt = $datatables[table], wrapper = $(dt).closest(".dataTables_wrapper"); $("#library_content").find(".dataTables_wrapper").hide(); @@ -1262,12 +1268,11 @@ var AIRTIME = (function(AIRTIME) { mod.initPodcastDatatable = function() { var aoColumns = [ - /* Title */ { "sTitle" : $.i18n._("Title") , "mDataProp" : "title" , "sClass" : "library_title" , "sWidth" : "170px" }, - /* Creator */ { "sTitle" : $.i18n._("Creator") , "mDataProp" : "creator" , "sClass" : "library_creator" , "sWidth" : "160px" }, - /* Upload Time { "sTitle" : $.i18n._("Uploaded") , "mDataProp" : "utime" , "bVisible" : false , "sClass" : "library_upload_time" , "sWidth" : "155px" }, */ - /* Website */ { "sTitle" : $.i18n._("Description") , "mDataProp" : "description" , "bVisible" : false , "sWidth" : "150px" }, - /* Year */ { "sTitle" : $.i18n._("Owner") , "mDataProp" : "owner" , "bVisible" : false , "sWidth" : "60px" }, - /* URL */ { "sTitle" : $.i18n._("Feed URL") , "mDataProp" : "url" , "bVisible" : false , "sWidth" : "60px" }, + /* Title */ { "sTitle" : $.i18n._("Title") , "mDataProp" : "title" , "sClass" : "library_title" , "sWidth" : "170px" }, + /* Creator */ { "sTitle" : $.i18n._("Creator") , "mDataProp" : "creator" , "sClass" : "library_creator" , "sWidth" : "160px" }, + /* Website */ { "sTitle" : $.i18n._("Description") , "mDataProp" : "description" , "bVisible" : false , "sWidth" : "150px" }, + /* Year */ { "sTitle" : $.i18n._("Owner") , "mDataProp" : "owner" , "bVisible" : false , "sWidth" : "60px" }, + /* URL */ { "sTitle" : $.i18n._("Feed URL") , "mDataProp" : "url" , "bVisible" : false , "sWidth" : "60px" } ]; var ajaxSourceURL = baseUrl+"rest/podcast"; @@ -1277,6 +1282,21 @@ var AIRTIME = (function(AIRTIME) { podcastToolbarButtons[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.NEW].eventHandlers.click = AIRTIME.podcast.createUrlDialog; podcastToolbarButtons[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.EDIT].eventHandlers.click = AIRTIME.podcast.editSelectedPodcasts; podcastToolbarButtons[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.DELETE].eventHandlers.click = AIRTIME.podcast.deleteSelectedPodcasts; + // TODO: only enable this if user has exactly one podcast selected + podcastToolbarButtons["ViewPodcast"] = { + title : $.i18n._("View Podcast"), + iconClass : "icon-chevron-right", + extraBtnClass : "btn-small", + elementId : "", + eventHandlers : { + click: function () { + var podcast = mod.podcastTableWidget.getSelectedRows()[0]; + mod.podcastEpisodeTableWidget.reload(podcast.id); + mod.podcastTableWidget._clearSelection(); + mod.setCurrentTable(mod.DataTableTypeEnum.PODCAST_EPISODES); + } + } + }; // Add a button to view the station podcast podcastToolbarButtons["StationPodcast"] = { title : $.i18n._("Station Podcast"), @@ -1305,6 +1325,7 @@ var AIRTIME = (function(AIRTIME) { mod.podcastTableWidget.assignDblClickHandler(function () { var podcast = mod.podcastDataTable.fnGetData($(this).index()); mod.podcastEpisodeTableWidget.reload(podcast.id); + mod.podcastTableWidget._clearSelection(); mod.setCurrentTable(mod.DataTableTypeEnum.PODCAST_EPISODES); }); @@ -1319,6 +1340,41 @@ var AIRTIME = (function(AIRTIME) { */ mod._initPodcastEpisodeDatatable = function () { var buttons = { + backBtn: { + title : $.i18n._('Back to Podcasts'), + iconClass : 'icon-chevron-left', + extraBtnClass : 'btn-small', + elementId : '', + eventHandlers : { + click: function (e) { + mod.setCurrentTable(mod.DataTableTypeEnum.PODCAST); + } + } + } + }, + defaults = AIRTIME.widgets.Table.getStandardToolbarButtons(); + defaults[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.NEW].title = "Import"; + defaults[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.NEW].eventHandlers.click = function () { + var episodes = mod.podcastEpisodeTableWidget.getSelectedRows(); + AIRTIME.podcast.importSelectedEpisodes(episodes, mod.podcastEpisodeTableWidget); + }; + + defaults[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.DELETE].eventHandlers.click = function () { + var podcastId, data = [], episodes = mod.podcastEpisodeTableWidget.getSelectedRows(); + $.each(episodes, function () { + data.push({id: this.file.id, type: this.file.ftype}); + }); + mod.fnDeleteItems(data); + }; + + // Reassign these because integer keys take precedence in iteration order - we want to order based on insertion + defaults = { + newBtn : defaults[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.NEW], + editBtn: defaults[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.EDIT], + delBtn : defaults[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.DELETE] + }; + + $.extend(true, buttons, defaults, { addToScheduleBtn: { // TODO: compatibility with checkAddButton function title : $.i18n._('Add to Schedule'), @@ -1326,33 +1382,52 @@ var AIRTIME = (function(AIRTIME) { extraBtnClass : 'btn-small', elementId : '', eventHandlers : { - click: function (e) { - console.log(mod.podcastEpisodeDataTable.fnGetData($(this).index())); + click: function () { + var data = [], selected = mod.podcastEpisodeTableWidget.getSelectedRows(); + $.each(selected, function () { data.push(this.file); }); + mod.addToSchedule(data); } } } - }; + }); + mod.podcastEpisodeTableWidget = AIRTIME.podcast.initPodcastEpisodeDatatable( $("#podcast_episodes_table"), { - bServerSide : false, - sAjaxSource : null, - // Initialize the table with empty data so we can defer loading - // If we load sequentially there's a delay before the table appears - aaData : {}, - aoColumns : [ + aoColumns : [ /* GUID */ { "sTitle" : "" , "mDataProp" : "guid" , "sClass" : "podcast_episodes_guid" , "bVisible" : false }, + /* Ingested */ { "sTitle" : $.i18n._("Imported?") , "mDataProp" : "importIcon" , "sClass" : "podcast_episodes_imported" , "sWidth" : "120px" }, /* Title */ { "sTitle" : $.i18n._("Title") , "mDataProp" : "title" , "sClass" : "podcast_episodes_title" , "sWidth" : "170px" }, /* Author */ { "sTitle" : $.i18n._("Author") , "mDataProp" : "author" , "sClass" : "podcast_episodes_author" , "sWidth" : "170px" }, /* Description */ { "sTitle" : $.i18n._("Description") , "mDataProp" : "description" , "sClass" : "podcast_episodes_description" , "sWidth" : "300px" }, /* Link */ { "sTitle" : $.i18n._("Link") , "mDataProp" : "link" , "sClass" : "podcast_episodes_link" , "sWidth" : "170px" }, /* Publication Date */ { "sTitle" : $.i18n._("Publication Date") , "mDataProp" : "pub_date" , "sClass" : "podcast_episodes_pub_date" , "sWidth" : "170px" } - ] + ], + bServerSide : false, + sAjaxSource : null, + // Initialize the table with empty data so we can defer loading + // If we load sequentially there's a delay before the table appears + aaData : {}, + oColVis : { + aiExclude: [0, 1, 2] + }, + oColReorder: { + iFixedColumns: 3 // Checkbox + imported + } }, - buttons + buttons, + { hideIngestCheckboxes: false } ); mod.podcastEpisodeDataTable = $datatables[mod.DataTableTypeEnum.PODCAST_EPISODES] = mod.podcastEpisodeTableWidget.getDatatable(); + mod.podcastEpisodeTableWidget.assignDblClickHandler(function () { + var data = mod.podcastEpisodeDataTable.fnGetData($(this).index()); + if (data.file.length > 0) { + mod.dblClickAdd(data.file, data.file.ftype); + } else { + AIRTIME.podcast.importSelectedEpisodes([data], mod.podcastEpisodeTableWidget); + } + }); }; mod.libraryInit = libraryInit; diff --git a/airtime_mvc/public/js/airtime/library/podcast.js b/airtime_mvc/public/js/airtime/library/podcast.js index 5abb66dbf..3cb40e0b0 100644 --- a/airtime_mvc/public/js/airtime/library/podcast.js +++ b/airtime_mvc/public/js/airtime/library/podcast.js @@ -7,7 +7,7 @@ var AIRTIME = (function (AIRTIME) { mod = AIRTIME.podcast; - var endpoint = 'rest/podcast/', PodcastTable, $stationPodcastTab; + var endpoint = 'rest/podcast/', PodcastEpisodeTable, $stationPodcastTab; /** * PodcastController constructor. @@ -59,20 +59,6 @@ var AIRTIME = (function (AIRTIME) { }); }; - $scope.importEpisodes = function () { - var episodes = self.episodeTable.getSelectedRows(); - // TODO: Should we implement a batch endpoint for this instead? - jQuery.each(episodes, function () { - $http.post(endpoint + $scope.podcast.id + '/episodes', { - csrf_token: $scope.csrf, - episode: this - }).success(function () { - self.reloadEpisodeTable(); - self.episodeTable.getDatatable().fnDraw(); - }); - }); - }; - /** * Close the tab and discard any changes made to the podcast data. */ @@ -122,7 +108,7 @@ var AIRTIME = (function (AIRTIME) { elementId : '', eventHandlers : { click: function () { - $scope.importEpisodes(); + mod.importSelectedEpisodes(self.episodeTable.getSelectedRows(), self.episodeTable); } } } @@ -130,7 +116,11 @@ var AIRTIME = (function (AIRTIME) { self.episodeTable = AIRTIME.podcast.initPodcastEpisodeDatatable( $scope.tab.contents.find('.podcast_episodes'), params, - buttons + buttons, + { + hideIngestCheckboxes: true, + podcastId: $scope.podcast.id + } ); self.reloadEpisodeTable(); }; @@ -139,7 +129,7 @@ var AIRTIME = (function (AIRTIME) { * Reload the podcast episode table. */ PodcastController.prototype.reloadEpisodeTable = function() { - this.episodeTable.reload(this.$scope.podcast.id); + this.episodeTable.reload(); }; /** @@ -166,13 +156,13 @@ var AIRTIME = (function (AIRTIME) { */ function StationPodcastController($scope, $http, podcast, tab) { // Super call to parent controller - var self = PodcastController.call(this, $scope, $http, podcast, tab); + PodcastController.call(this, $scope, $http, podcast, tab); // Store the station podcast tab in module scope so it can be checked if the user clicks the // Station Podcast button again - this way we don't have to go back to the server to get the ID. $stationPodcastTab = tab; /** - * Override the tab close function to 'unset' the module-scope $stationPodcastTab + * Override the tab close function to 'unset' the module-scope $stationPodcastTab. * * @override */ @@ -181,25 +171,7 @@ var AIRTIME = (function (AIRTIME) { $stationPodcastTab = undefined; }; - self.deleteSelectedEpisodes = function () { - var episodes = self.episodeTable.getSelectedRows(); - jQuery.each(episodes, function () { - $http.delete(endpoint + $scope.podcast.id + '/episodes/' + this.id + '?csrf_token=' + $scope.csrf) - .success(function () { - self.reloadEpisodeTable(); - }); - }); - }; - - self.openSelectedTabEditors = function () { - var episodes = self.episodeTable.getSelectedRows(); - $.each(episodes, function () { - var uid = AIRTIME.library.MediaTypeStringEnum.FILE + "_" + this.file_id; - $.get(baseUrl + "library/edit-file-md/id/" + this.file_id, {format: "json"}, function (json) { - AIRTIME.playlist.fileMdEdit(json, uid); - }); - }); - }; + return this; } /** @@ -209,6 +181,34 @@ var AIRTIME = (function (AIRTIME) { */ StationPodcastController.prototype = Object.create(PodcastController.prototype); + /** + * Remove the selected episodes from the station podcast feed. + */ + StationPodcastController.prototype.unpublishSelectedEpisodes = function () { + var self = this, $scope = self.$scope, + episodes = self.episodeTable.getSelectedRows(); + jQuery.each(episodes, function () { + self.$http.delete(endpoint + $scope.podcast.id + '/episodes/' + this.id + '?csrf_token=' + $scope.csrf) + .success(function () { + self.reloadEpisodeTable(); + }); + }); + }; + + /** + * Open metadata editor tabs for each of the selected episodes. + */ + StationPodcastController.prototype.openSelectedTabEditors = function () { + var self = this, + episodes = self.episodeTable.getSelectedRows(); + jQuery.each(episodes, function () { + var uid = AIRTIME.library.MediaTypeStringEnum.FILE + "_" + this.file_id; + jQuery.get(baseUrl + "library/edit-file-md/id/" + this.file_id, {format: "json"}, function (json) { + AIRTIME.playlist.fileMdEdit(json, uid); + }); + }); + }; + /** * Initialize the Station podcast episode table. * @@ -223,9 +223,7 @@ var AIRTIME = (function (AIRTIME) { extraBtnClass : '', elementId : '', eventHandlers : { - click: function () { - self.openSelectedTabEditors(); - } + click: self.openSelectedTabEditors.bind(self) } }, deleteBtn: { @@ -234,9 +232,7 @@ var AIRTIME = (function (AIRTIME) { extraBtnClass : 'btn-danger', elementId : '', eventHandlers : { - click: function () { - self.deleteSelectedEpisodes(); - } + click: self.unpublishSelectedEpisodes.bind(self) } }, slideToggle: {} @@ -253,12 +249,20 @@ var AIRTIME = (function (AIRTIME) { this.episodeTable = AIRTIME.podcast.initPodcastEpisodeDatatable( $scope.tab.contents.find('.podcast_episodes'), params, - buttons + buttons, + { + hideIngestCheckboxes: true, + podcastId: $scope.podcast.id + } ); }; + /** + * Initialize the Station podcast. + */ StationPodcastController.prototype.initialize = function() { PodcastController.prototype.initialize.call(this); + // We want to override the default tab name behaviour and use "Station Podcast" for clarity this.$scope.tab.setName(jQuery.i18n._("Station Podcast")); }; @@ -354,36 +358,118 @@ var AIRTIME = (function (AIRTIME) { * * @private */ - function _initPodcastTable() { - PodcastTable = function(wrapperDOMNode, bItemSelection, toolbarButtons, dataTablesOptions) { - // Just call the superconstructor. For clarity/extensibility + function _initPodcastEpisodeTable() { + PodcastEpisodeTable = function(wrapperDOMNode, bItemSelection, toolbarButtons, dataTablesOptions, config) { + this.config = config; // Internal configuration object + this._setupImportListener(); + // Call the superconstructor return AIRTIME.widgets.Table.call(this, wrapperDOMNode, bItemSelection, toolbarButtons, dataTablesOptions); }; // Subclass AIRTIME.widgets.Table - PodcastTable.prototype = Object.create(AIRTIME.widgets.Table.prototype); - PodcastTable.prototype.constructor = PodcastTable; - PodcastTable.prototype._SELECTORS = Object.freeze({ + PodcastEpisodeTable.prototype = Object.create(AIRTIME.widgets.Table.prototype); + PodcastEpisodeTable.prototype.constructor = PodcastEpisodeTable; + PodcastEpisodeTable.prototype._SELECTORS = Object.freeze({ SELECTION_CHECKBOX: ".airtime_table_checkbox:has(input)", SELECTION_TABLE_ROW: "tr:has(td.airtime_table_checkbox > input)" }); - PodcastTable.prototype._datatablesCheckboxDataDelegate = function(rowData, callType, dataToSave) { - if (rowData.ingested) return null; // Don't create checkboxes for ingested items + + /** + * @override + * + * Override the checkbox delegate function in the Table object to change + * the row's checkbox and import status columns depending on the status + * of the episode (unimported: 0, imported: 1, pending import: -1). + * + * @param rowData + * @param callType + * @param dataToSave + * + * @returns {string} + * @private + */ + PodcastEpisodeTable.prototype._datatablesCheckboxDataDelegate = function(rowData, callType, dataToSave) { + var importIcon = "", + pendingIcon = ""; + if (this.config.hideIngestCheckboxes && rowData.ingested && rowData.ingested != 0) { + return rowData.ingested > 0 ? importIcon : pendingIcon; + } + rowData.importIcon = (rowData.ingested != 0) ? (rowData.ingested > 0 ? importIcon : pendingIcon) : null; return AIRTIME.widgets.Table.prototype._datatablesCheckboxDataDelegate.call(this, rowData, callType, dataToSave); }; - // Since we're sometimes using a static source, define a separate function to fetch and 'reload' the table data - // We use this when we save the Podcast because we need to flag rows the user is ingesting - PodcastTable.prototype.reload = function (id) { + + /** + * Reload the episode table. + * Since we're sometimes using a static source, define a separate function to fetch and reload the table data. + * We use this when we save the Podcast because we need to flag rows the user is ingesting. + * + * @param [id] optional podcast identifier + */ + PodcastEpisodeTable.prototype.reload = function (id) { + // When using static source data, we instantiate an empty table + // and pass this function the ID of the podcast we want to display. + if (id) this.config.podcastId = id; var dt = this._datatable; - $.get(endpoint + id + '/episodes', function (json) { + dt.block({ + message: "", + theme: true, + applyPlatformOpacityRules: false + }); + $.get(endpoint + this.config.podcastId + '/episodes', function (json) { dt.fnClearTable(); dt.fnAddData(JSON.parse(json)); + dt.fnDraw(); + dt.unblock(); }); }; + + /** + * Setup an interval that checks for any pending imports and reloads + * the table once imports are finished. + * + * TODO: remember selection + * + * @private + */ + PodcastEpisodeTable.prototype._setupImportListener = function () { + var self = this; + self.importListener = setInterval(function () { + var podcastId = self.config.podcastId, pendingRows = []; + if (!podcastId) return false; + var dt = self.getDatatable(), data = dt.fnGetData(); + // Iterate over the table data to check for any rows pending import + $.each(data, function () { + if (this.ingested == -1) { + pendingRows.push(this.guid); + } + }); + console.log(pendingRows); + if (pendingRows.length > 0) { + $.get(endpoint + podcastId + '/episodes', function (json) { + data = JSON.parse(json); + var delta = false; + $.each(data, function () { + var idx = pendingRows.indexOf(this.guid); + console.log(idx); + if (idx > -1 && this.ingested != -1) { + delta = true; + pendingRows.slice(idx, 0); + } + }); + if (delta) { // Has there been a change? + // We already have the data, so there's no reason to call + // reload() here; this also provides a smoother transition + dt.fnClearTable(); + dt.fnAddData(data); + } + }); + } + }, 15000); // Run every 15 seconds + }; } /** * Create and show the URL dialog for podcast creation. */ - mod.createUrlDialog = function() { + mod.createUrlDialog = function () { $.get('/render/podcast-url-dialog', function(json) { $(document.body).append(json.html); $("#podcast_url_dialog").dialog({ @@ -402,7 +488,7 @@ var AIRTIME = (function (AIRTIME) { * * FIXME: we should probably be passing the serialized form into this function instead */ - mod.addPodcast = function() { + mod.addPodcast = function () { $.post(endpoint, $("#podcast_url_dialog").find("form").serialize(), function(json) { _initAppFromResponse(json); $("#podcast_url_dialog").dialog("close"); @@ -412,7 +498,7 @@ var AIRTIME = (function (AIRTIME) { /** * Open a tab to view and edit the station podcast. */ - mod.openStationPodcast = function() { + mod.openStationPodcast = function () { if (typeof $stationPodcastTab === 'undefined') { $.get(endpoint + 'station', function(json) { _initAppFromResponse(json); @@ -425,7 +511,7 @@ var AIRTIME = (function (AIRTIME) { /** * Create a bulk request to edit all currently selected podcasts. */ - mod.editSelectedPodcasts = function() { + mod.editSelectedPodcasts = function () { _bulkAction(AIRTIME.library.podcastTableWidget.getSelectedRows(), HTTPMethods.GET, function(json) { json.forEach(function(data) { _initAppFromResponse(data); @@ -436,7 +522,7 @@ var AIRTIME = (function (AIRTIME) { /** * Create a bulk request to delete all currently selected podcasts. */ - mod.deleteSelectedPodcasts = function() { + mod.deleteSelectedPodcasts = function () { if (confirm($.i18n._("Are you sure you want to delete the selected podcasts from your library?"))) { _bulkAction(AIRTIME.library.podcastTableWidget.getSelectedRows(), HTTPMethods.DELETE, function () { AIRTIME.library.podcastDataTable.fnDraw(); @@ -444,51 +530,73 @@ var AIRTIME = (function (AIRTIME) { } }; + /** + * Import one or more podcast episodes. + * + * @param {Array} episodes array of episode data to be imported + * @param {PodcastEpisodeTable} dt PodcastEpisode table containing the data + */ + mod.importSelectedEpisodes = function (episodes, dt) { + $.each(episodes, function () { + var podcastId = this.podcast_id; + $.post(endpoint + podcastId + '/episodes', JSON.stringify({ + csrf_token: $("#csrf").val(), + episode: this + }), function () { + dt.reload(podcastId); + }); + }); + }; + /** * Initialize the internal datatable for the podcast editor view to hold episode data passed back from the server. * * Selection for the internal table represents episodes marked for ingest and is disabled for ingested episodes. * - * @param {Object} domNode the jQuery DOM node to create the table inside. + * @param {jQuery} domNode the jQuery DOM node to create the table inside. * @param {Object} params JSON object containing datatables parameters to override * @param {Object} buttons JSON object containing datatables button parameters + * @param {Object} config JSON object containing internal PodcastEpisodeTable parameters + * @param {boolean} config.hideIngestCheckboxes flag denoting whether or not to hide checkboxes for ingested items * - * @returns {*} the created Table object + * @returns {Table} the created Table object */ - mod.initPodcastEpisodeDatatable = function(domNode, params, buttons) { - if ('slideToggle' in buttons) + mod.initPodcastEpisodeDatatable = function (domNode, params, buttons, config) { + if ('slideToggle' in buttons) { buttons = $.extend(true, { slideToggle: { - title : '', - iconClass : 'spl-no-r-margin icon-chevron-up', - extraBtnClass : 'toggle-editor-form', - elementId : '', - eventHandlers : {} + title: '', + iconClass: 'spl-no-r-margin icon-chevron-up', + extraBtnClass: 'toggle-editor-form', + elementId: '', + eventHandlers: {} } }, buttons); - params = $.extend(params, + } + params = $.extend(true, params, { oColVis: { - sAlign : 'right', - aiExclude : [0, 1], - buttonText : $.i18n._("Columns"), + sAlign: 'right', + aiExclude: [0, 1], + buttonText: $.i18n._("Columns"), iOverlayFade: 0, - oColReorder : { + oColReorder: { iFixedColumns: 1 // Checkbox } } } ); - if (typeof PodcastTable === 'undefined') { - _initPodcastTable(); + if (typeof PodcastEpisodeTable === 'undefined') { + _initPodcastEpisodeTable(); } - var podcastEpisodesTableWidget = new PodcastTable( + var podcastEpisodesTableWidget = new PodcastEpisodeTable( domNode, // DOM node to create the table inside. true, // Enable item selection buttons, // Toolbar buttons - params // Datatables overrides. + params, // Datatables overrides. + config // Internal config ); podcastEpisodesTableWidget.getDatatable().addTitles("td"); diff --git a/airtime_mvc/public/js/airtime/widgets/table.js b/airtime_mvc/public/js/airtime/widgets/table.js index 3793bd204..234a42712 100644 --- a/airtime_mvc/public/js/airtime/widgets/table.js +++ b/airtime_mvc/public/js/airtime/widgets/table.js @@ -31,7 +31,6 @@ var AIRTIME = (function(AIRTIME) { self._selectedRowVisualIdxMax = -1; self._$wrapperDOMNode = null; self._toolbarButtons = null; - //Save some of the constructor parameters self._$wrapperDOMNode = $(wrapperDOMNode); self._toolbarButtons = toolbarButtons; @@ -44,7 +43,7 @@ var AIRTIME = (function(AIRTIME) { // If selection is enabled, add in the checkbox column. if (bItemSelection) { dataTablesOptions["aoColumns"].unshift( - /* Checkbox */ { "sTitle" : "", "mData" : self._datatablesCheckboxDataDelegate, "bSortable" : false , "bSearchable" : false , "sWidth" : "16px" , "sClass" : "airtime_table_checkbox" } + /* Checkbox */ { "sTitle" : "", "mData" : self._datatablesCheckboxDataDelegate.bind(this), "bSortable" : false , "bSearchable" : false , "sWidth" : "24px" , "sClass" : "airtime_table_checkbox" } ); } @@ -68,7 +67,7 @@ var AIRTIME = (function(AIRTIME) { }), "oColVis": { "sAlign": "right", - "aiExclude": self.colVisExcludeColumns, + "aiExclude": self._colVisExcludeColumns, "buttonText": $.i18n._("Columns"), "iOverlayFade": 0 }, @@ -428,7 +427,6 @@ var AIRTIME = (function(AIRTIME) { //Static initializers / Class variables - /** Predefined toolbar buttons that you can add to the table. Use getStandardToolbarButtons(). */ Table.prototype._SELECTORS = Object.freeze({ SELECTION_CHECKBOX: ".airtime_table_checkbox", SELECTION_TABLE_ROW: "tr"