Skip to content

Commit

Permalink
Web UI options for reloading and removing an individual podcast channel
Browse files Browse the repository at this point in the history
To achieve this, a support had to be added to the listHeading directive
to attach an actions menu. The menu contents are injected when
instantiating the directive.
  • Loading branch information
paulijar committed Aug 5, 2021
1 parent 872961c commit f026eea
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 14 deletions.
22 changes: 18 additions & 4 deletions css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,15 @@ h2 span:hover .play {

.album-area > h2 {
display: table-cell;
position: relative;
max-width: 424px;
font-size: 21px;
line-height: 30px;
padding-bottom: 21px;
white-space: nowrap;
}

.album-area > h2.placeholder {
overflow: hidden;
}

Expand All @@ -122,7 +126,7 @@ h2 span:hover .play {
text-overflow: ellipsis;
}

.album-area > h2.with-details > span {
.album-area > h2.with-actions > span {
max-width: calc(100% - 24px);
}

Expand Down Expand Up @@ -185,7 +189,8 @@ div:hover > .play-pause,
background-image: url(../img/pause-big.svg)
}

#app-view .icon-details {
#app-view .icon-details,
#app-view .icon-more {
display: table-cell;
vertical-align: middle;
margin: 0;
Expand All @@ -195,14 +200,23 @@ div:hover > .play-pause,
opacity: 0.5;
}

#app-view :hover > .icon-details {
#app-view :hover > .icon-details,
#app-view :hover > .icon-more {
visibility: visible;
}

#app-view .icon-details:hover {
#app-view .icon-details:hover,
#app-view .icon-more:hover {
opacity: 1;
}

#app-view .heading-actions {
display: block;
font-size: 13px;
top: 40px;
right: -10px;
}

.muted {
opacity: .5;
display: inline;
Expand Down
2 changes: 2 additions & 0 deletions js/app/controllers/podcastsviewcontroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ angular.module('Music').controller('PodcastsViewController', [
};

$scope.showAddPodcast = podcastService.showAddPodcastDialog;
$scope.reloadChannel = podcastService.reloadPodcastChannel;
$scope.removeChannel = podcastService.removePodcastChannel;

/**
* Two functions for the alphabet-navigation directive integration
Expand Down
5 changes: 4 additions & 1 deletion js/app/directives/inviewobserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,11 +311,14 @@ function($rootScope, $timeout, inViewService) {
controller.viewPortMargin = Number(attributes.inViewObserverMargin) || 500;

// Remove this instance from the static array if this would still be there upon destruction.
// This seems to happen when the album view contents are updated during/after scanning.
// This happens when removing individual channels from the podcasts view but also seems to
// happen when the album library view contents are updated during/after scanning.
scope.$on('$destroy', function() {
var index = _instances.indexOf(controller);
if (index !== -1) {
_instances.splice(index, 1);
_firstIndexInView = 0;
_lastIndexInView = -1;
}
});

Expand Down
77 changes: 74 additions & 3 deletions js/app/directives/listheading.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ function ($rootScope, gettextCatalog) {
var playText = gettextCatalog.getString('Play');
var playIconSrc = OC.filePath('music', 'dist', playIconName);
var detailsText = gettextCatalog.getString('Details');
var actionsMenu = null;
var actionsMenuOwner = null;

/**
* Set up the contents for a given heading element
Expand All @@ -45,6 +47,7 @@ function ($rootScope, gettextCatalog) {
if (data.tooltip) {
outerSpan.setAttribute('title', data.tooltip);
}
outerSpan.className = 'heading';

var innerSpan = document.createElement('span');
innerSpan.innerHTML = data.heading;
Expand Down Expand Up @@ -72,23 +75,85 @@ function ($rootScope, gettextCatalog) {
detailsButton.className = 'icon-details';
detailsButton.setAttribute('title', detailsText);
fragment.appendChild(detailsButton);
data.element.className = 'with-details';
data.element.className = 'with-actions';
}
else if (data.actions) {
var moreButton = document.createElement('button');
moreButton.className = 'icon-more';
fragment.appendChild(moreButton);
data.element.className = 'with-actions';
}
else {
data.element.className = '';
}

return fragment;
}

function createActionsMenu(actions) {
var container = document.createElement('div');
container.className = 'popovermenu bubble heading-actions';
var list = document.createElement('ul');
container.appendChild(list);

for (var action of actions) {
var listitem = document.createElement('li');
var link = document.createElement('a');
link.className = 'icon-' + action.icon;
var span = document.createElement('span');
span.innerText = gettextCatalog.getString(action.text);
// Note: l10n-extract cannot find localised string defined like above.
// Ensure that the same string can be extracted from somewhere else.
$(listitem).data('callback', action.callback);
link.appendChild(span);
listitem.appendChild(link);
list.appendChild(listitem);
}

return container;
}

function toggleActionsMenu() {
if (actionsMenu) {
actionsMenuOwner.removeChild(actionsMenu);
actionsMenu = null;
}

if (actionsMenuOwner !== data.element) {
actionsMenu = createActionsMenu(data.actions);
data.element.appendChild(actionsMenu);
actionsMenuOwner = data.element;
}

if (!actionsMenu) {
actionsMenuOwner = null;
}
}

function closeActionsMenu() {
actionsMenuOwner.removeChild(actionsMenu);
actionsMenu = null;
actionsMenuOwner = null;
}

var ngElem = $(data.element);

/**
* Click handlers
*/
ngElem.on('click', 'span', function(_e) {
ngElem.on('click', '.heading', function(_e) {
data.onClick(data.model);
});
ngElem.on('click', 'button', function(_e) {
ngElem.on('click', '.icon-details', function(_e) {
data.onDetailsClick(data.model);
});
ngElem.on('click', '.icon-more', function(_e) {
toggleActionsMenu();
});
ngElem.on('click', 'li', function(_e) {
$(this).data('callback')(data.model);
closeActionsMenu();
});

/**
* Drag&Drop compatibility
Expand Down Expand Up @@ -124,6 +189,7 @@ function ($rootScope, gettextCatalog) {

function setupPlaceholder(data) {
data.element.innerHTML = data.heading;
data.element.className = 'placeholder';
}

/**
Expand Down Expand Up @@ -153,11 +219,16 @@ function ($rootScope, gettextCatalog) {
model: scope.$eval(attrs.model),
onClick: scope.$eval(attrs.onClick),
onDetailsClick: scope.$eval(attrs.onDetailsClick),
actions: scope.$eval(attrs.actions),
getDraggable: scope.$eval(attrs.getDraggable),
element: element[0],
scope: scope
};

if (data.onDetailsClick && data.actions) {
console.error('Invalid configuration for list heading, a heading cannot have both details button and actions menu');
}

// Populate the heading first with a placeholder.
// The placeholder is replaced with the actual content once the element
// enters the viewport (with some margins).
Expand Down
10 changes: 7 additions & 3 deletions js/app/services/libraryservice.js
Original file line number Diff line number Diff line change
Expand Up @@ -279,10 +279,10 @@ angular.module('Music').service('libraryService', [function() {
return idx;
},
setPodcasts: function(podcastsData) {
podcastChannels = podcastsData;
sortByTextField(podcastChannels, 'title');
sortByTextField(podcastsData, 'title');
// set the parent references for each episode
_.forEach(podcastChannels, initPodcastChannel);
_.forEach(podcastsData, initPodcastChannel);
podcastChannels = podcastsData;
},
addPodcastChannel: function(channel) {
initPodcastChannel(channel);
Expand All @@ -294,6 +294,10 @@ angular.module('Music').service('libraryService', [function() {
var idx = _.findIndex(podcastChannels, { id: channel.id });
podcastChannels[idx] = channel;
},
removePodcastChannel: function(channel) {
var idx = _.findIndex(podcastChannels, { id: channel.id });
podcastChannels.splice(idx, 1);
},
addPlaylist: function(playlist) {
playlists.push(wrapPlaylist(playlist));
},
Expand Down
32 changes: 32 additions & 0 deletions js/app/services/podcastservice.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,38 @@ function($rootScope, $timeout, $q, libraryService, gettextCatalog, Restangular)
}
};
processNextChannel();
},

// Remove a single previously subscribed podcast channel
removePodcastChannel: function(channel) {
const doDelete = function() {
Restangular.one('podcasts', channel.id).remove().then(
function (result) {
if (!result.success) {
OC.Notification.showTemporary(
gettextCatalog.getString('Could not remove the channel "{{ title }}"', { title: channel.title }));
} else {
libraryService.removePodcastChannel(channel);
$timeout(() => $rootScope.$emit('viewContentChanged'));
}
},
function (_error) {
OC.Notification.showTemporary(
gettextCatalog.getString('Could not remove the channel "{{ title }}"', { title: channel.title }));
}
);
};

OC.dialogs.confirm(
gettextCatalog.getString('Are you sure to remove the podcast channel "{{ title }}"?', { title: channel.title }),
gettextCatalog.getString('Remove channel'),
function(confirmed) {
if (confirmed) {
doDelete();
}
},
true
);
}
};
}]);
9 changes: 7 additions & 2 deletions templates/fake-template.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<!-- this template is just a fake template it's purpose is to get fetched
while extraction, because this string is located in PHP code -->
<!-- this is just a fake template whose purpose is to get fetched while extracting localized content;
the listed strings are defined in a manner which doesn't support extraction from where they are used -->

<!-- localized strings in PHP code -->
<span translate>Music</span>
<span translate>Unknown album</span>
<span translate>Unknown artist</span>
Expand All @@ -11,3 +13,6 @@
<span translate>Loading…</span>
<span translate>Close</span>
<span translate>(file is not within your music collection folder)</span>

<!-- localizations for the podcasts action menu -->
<span translate>Reload</span>
6 changes: 5 additions & 1 deletion templates/partials/podcastsview.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@
tooltip="channel.title"
on-click="playChannel"
model="channel"
actions="[
{ icon: 'reload', text: 'Reload', callback: reloadChannel },
{ icon: 'delete', text: 'Remove', callback: removeChannel }
]"
show-play-icon="true">
</list-heading>
<div ng-click="playAlbum(album)" class="albumart" cover="{{ channel.image }}" albumart="{{ channel.title }}"></div>
<div class="albumart" cover="{{ channel.image }}" albumart="{{ channel.title }}"></div>
<img class="play overlay svg" alt="{{ 'Play' | translate }}"
src="<?php \OCA\Music\Utility\HtmlUtil::printSvgPath('play-overlay') ?>" ng-click="playChannel(channel)" />
<track-list
Expand Down

0 comments on commit f026eea

Please sign in to comment.