Skip to content

Commit

Permalink
Search support for the Music app front-end
Browse files Browse the repository at this point in the history
Implemented "filtering" search for the Music app front-end. The search
happens purely within the javascript code, and the results from the PHP
back-end search plug-in are not utilized.

refs owncloud#662, owncloud#367
  • Loading branch information
paulijar committed Jan 11, 2020
1 parent ed07576 commit 88026d8
Show file tree
Hide file tree
Showing 9 changed files with 309 additions and 23 deletions.
25 changes: 25 additions & 0 deletions css/style-search.css
@@ -0,0 +1,25 @@
/**
* ownCloud - Music app
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Pauli Järvinen <pauli.jarvinen@gmail.com>
* @copyright Pauli Järvinen 2020
*/

/* Hide the default results div because we have a custom "filter like" search logic */
.app-music #searchresults {
visibility: hidden;
height: 0;
}

/* Only matching items should be shown when the searchmode is active */
.searchmode .track-list li:not(.matched),
.searchmode .track-list li.more-less,
.searchmode .album-area .track-list li:not(.matched),
.searchmode .album-area:not(.matched),
.searchmode .artist-area:not(.matched),
.searchmode .folder-area:not(.matched) {
display: none;
}
8 changes: 4 additions & 4 deletions css/style-tracklist.css
Expand Up @@ -5,7 +5,7 @@
* later. See the COPYING file.
*
* @author Pauli Järvinen <pauli.jarvinen@gmail.com>
* @copyright Pauli Järvinen 2018
* @copyright Pauli Järvinen 2018 - 2020
*/

/**
Expand All @@ -21,7 +21,7 @@
display: table-cell;
}

.track-list.collapsed .collapsible {
#app-view:not(.searchmode) .track-list.collapsed .collapsible {
display: none !important;
}

Expand Down Expand Up @@ -57,8 +57,8 @@
display: none;
}

.track-list.collapsed li.more-less:not(.collapsible),
.track-list:not(.collapsed) li.more-less.collapsible {
#app-view:not(.searchmode) .track-list.collapsed li.more-less:not(.collapsible),
#app-view:not(.searchmode) .track-list:not(.collapsed) li.more-less.collapsible {
display: inline-block;
}

Expand Down
181 changes: 181 additions & 0 deletions js/app/controllers/searchcontroller.js
@@ -0,0 +1,181 @@
/**
* ownCloud - Music app
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Pauli Järvinen <pauli.jarvinen@gmail.com>
* @copyright Pauli Järvinen 2020
*/

/**
* This controller implements the search/filtering logic for all views. The details
* of the search still vary per-view.
*
* The controller is attached to the div#searchresults, but this element is always hidden.
* The element's only purpose is, that having it present makes the ownCloud/Nextcloud core
* show the searchbox element within the header.
*
* When the search query is written to the searchbox, the core automatically sends the query
* to the backend. However, we disregard any results from the backend and conduct the search
* on our own, on the frontend.
*/
angular.module('Music').controller('SearchController', [
'$scope', '$rootScope', 'libraryService', '$timeout', '$document', 'gettextCatalog',
function ($scope, $rootScope, libraryService, $timeout, $document, gettextCatalog) {

var searchbox = $('#searchbox');
var queryString = searchbox.val().trim();

/** Conduct the search when there is a pause in typing in text */
var checkQueryChange = _.debounce(function() {
if (queryString != searchbox.val().trim()) {
$scope.$apply(onEnterSearchString);
}
}, 250);
searchbox.bind('propertychange change keyup input paste', checkQueryChange);

/** Handle clearing the searchbox. This has to be registered to the parent form
* of the #searchbox element.
*/
$('.searchbox').on('reset', function() {
queryString = '';
clearSearch();
});

/** Catch ctrl+f except when the Settings view is active */
$document.bind('keydown', function(e) {
if ($rootScope.currentView !== '#/settings' && e.ctrlKey && e.key === 'f') {
searchbox.focus();
return false;
}
return true;
});

/** Run search when enter pressed within the searchbox */
searchbox.bind('keydown', function (event) {
if (event.which === 13) {
onEnterSearchString();
}
});

function onEnterSearchString() {
queryString = searchbox.val().trim();

if (queryString.length > 0) {
runSearch();
} else {
clearSearch();
}
}

function runSearch() {
// reset previous matches
$('.matched').removeClass('matched');

var matchingTracks = [];
var view = $rootScope.currentView;

if (view == '#') {
matchingTracks = searchInAlbumsView();
} else if (view == '#/folders') {
matchingTracks = searchInFoldersView();
} else if (view == '#/alltracks') {
matchingTracks = searchInAllTracksView();
} else if (view.startsWith('#/playlist/')) {
matchingTracks = searchInPlaylistView();
} else {
OC.Notification.showTemporary(gettextCatalog.getString('Search not available in this view'));
}

// inform the track-list directive about changed search matches
$rootScope.$emit('searchMatchedTracks', matchingTracks);

$('#app-view').addClass('searchmode');

$rootScope.$emit('inViewObserverReInit');
}

function searchInAlbumsView() {
var matchingTracks = libraryService.searchTracks(queryString);
var matchingAlbums = libraryService.searchAlbums(queryString);
var matchingArtists = libraryService.searchArtists(queryString);

// add children of matching albums/artists
matchingAlbums = _.union(
matchingAlbums,
_.flatten(_.pluck(matchingArtists, 'albums'))
);
matchingTracks = _.union(
matchingTracks,
_.flatten(_.pluck(matchingAlbums, 'tracks'))
);

// mark track matches
_(matchingTracks).each(function(track) {
$('#track-' + track.id).addClass('matched');
$('#album-' + track.albumId).addClass('matched').parent().addClass('matched');
});

// mark album matches
_(matchingAlbums).each(function(album) {
$('#album-' + album.id).addClass('matched').parent().addClass('matched');
});

// mark artist matches
_(matchingArtists).each(function(artist) {
$('#artist-' + artist.id).addClass('matched');
});

return matchingTracks;
}

function searchInFoldersView() {
var matchingTracks = libraryService.searchTracks(queryString);
var matchingFolders = libraryService.searchFolders(queryString);

// add children of matching folders
matchingTracks = _.union(
matchingTracks,
_.chain(matchingFolders).pluck('tracks').flatten().pluck('track').value()
);

// mark track matches
_(matchingTracks).each(function(track) {
$('#track-' + track.id).addClass('matched');
$('#folder-' + track.folderId).addClass('matched');
});

// mark folder matches
_(matchingFolders).each(function(folder) {
$('#folder-' + folder.id).addClass('matched');
});

return matchingTracks;
}

function searchInAllTracksView() {
var matchingTracks = libraryService.searchTracks(queryString);
_(matchingTracks).each(function(track) {
$('#track-' + track.id).addClass('matched');
});
return matchingTracks;
}

function searchInPlaylistView() {
var matchingTracks = libraryService.searchTracks(queryString);
_(matchingTracks).each(function(track) {
$('li[data-track-id=' + track.id + ']').addClass('matched');
});

// return no tracks because this view uses no track-list directives
return [];
}

function clearSearch() {
$rootScope.$emit('searchOff');
$('#app-view').removeClass('searchmode');
$('.matched').removeClass('matched');
$rootScope.$emit('inViewObserverReInit');
}
}]);
52 changes: 38 additions & 14 deletions js/app/directives/inviewobserver.js
Expand Up @@ -5,7 +5,7 @@
* later. See the COPYING file.
*
* @author Pauli Järvinen <pauli.jarvinen@gmail.com>
* @copyright 2019 Pauli Järvinen
* @copyright 2019, 2020 Pauli Järvinen
*
*/

Expand Down Expand Up @@ -41,6 +41,11 @@ function($rootScope, $timeout, inViewService) {
$rootScope.$on('resize', throttledOnScroll);
$rootScope.$on('trackListCollapsed', throttledOnScroll);

$rootScope.$on('inViewObserverReInit', function() {
resetAll();
initInViewRange();
});

var debouncedNotifyLeave = _.debounce(function() {
_(_instances).each(function(inst) {
if (inst.leaveViewPending) {
Expand Down Expand Up @@ -76,7 +81,7 @@ function($rootScope, $timeout, inViewService) {

// loop from the begining until we find the first instance in viewport
for (i = 0; i < length; ++i) {
if (updateInViewStatus(_instances[i])) {
if (updateInViewStatus(_instances[i]) && !instanceIsInvisible(_instances[i])) {
_firstIndexInView = i;
_lastIndexInView = i;
break;
Expand All @@ -86,7 +91,7 @@ function($rootScope, $timeout, inViewService) {
// if some instance was found, then continue looping until we have found
// all the instances in viewport
for (++i; i < length; ++i) {
if (updateInViewStatus(_instances[i])) {
if (updateInViewStatus(_instances[i]) || instanceIsInvisible(_instances[i])) {
_lastIndexInView = i;
} else {
break;
Expand All @@ -105,7 +110,7 @@ function($rootScope, $timeout, inViewService) {

// Check if instances in the beginning of the range have slided off
for (i = _firstIndexInView; i <= _lastIndexInView; ++i) {
if (!updateInViewStatus(_instances[i])) {
if (!updateInViewStatus(_instances[i]) || instanceIsInvisible(_instances[i])) {
++_firstIndexInView;
} else {
break;
Expand All @@ -114,7 +119,7 @@ function($rootScope, $timeout, inViewService) {

// Check if instances in the end of the range have slided off
for (i = _lastIndexInView; i > _firstIndexInView; --i) {
if (!updateInViewStatus(_instances[i])) {
if (!updateInViewStatus(_instances[i]) || instanceIsInvisible(_instances[i])) {
--_lastIndexInView;
} else {
break;
Expand All @@ -132,7 +137,7 @@ function($rootScope, $timeout, inViewService) {
// did not move downwards
if (prevFirst === _firstIndexInView) {
for (i = _firstIndexInView - 1; i >= 0; --i) {
if (updateInViewStatus(_instances[i])) {
if (updateInViewStatus(_instances[i]) || instanceIsInvisible(_instances[i])) {
--_firstIndexInView;
} else {
break;
Expand All @@ -144,7 +149,7 @@ function($rootScope, $timeout, inViewService) {
// did not move upwards
if (prevLast === _lastIndexInView) {
for (i = _lastIndexInView + 1; i < length; ++i) {
if (updateInViewStatus(_instances[i])) {
if (updateInViewStatus(_instances[i]) || instanceIsInvisible(_instances[i])) {
++_lastIndexInView;
} else {
break;
Expand All @@ -158,9 +163,8 @@ function($rootScope, $timeout, inViewService) {
* Update in-view-port status of the given instance
*/
function updateInViewStatus(inst) {
var elem = inst.element;
var wasInViewPort = inst.inViewPort;
inst.inViewPort = elemInViewPort(elem);
inst.inViewPort = instanceInViewPort(inst);

if (!wasInViewPort && inst.inViewPort) {
if (!inst.leaveViewPending) {
Expand All @@ -184,15 +188,22 @@ function($rootScope, $timeout, inViewService) {
return inst.inViewPort;
}

function elemInViewPort(elem) {
return inViewService.isElementInViewPort(elem, 500, 500);
function instanceInViewPort(inst) {
return inViewService.isElementInViewPort(inst.element, 500, 500);
}

function instanceIsInvisible(inst) {
return (inst.element.offsetHeight <= 0);
}

function onEnterView(inst) {
inst.pendingEnterView = null;
_(inst.listeners).each(function(listener) {
listener.onEnterView();
});

if (!instanceIsInvisible(inst)) {
_(inst.listeners).each(function(listener) {
listener.onEnterView();
});
}
}

function onLeaveView(inst) {
Expand All @@ -202,6 +213,19 @@ function($rootScope, $timeout, inViewService) {
});
}

function resetAll() {
_(_instances).each(function(inst) {
if (inst.pendingEnterView) {
$timeout.cancel(inst.pendingEnterView);
inst.pendingEnterView = null;
}
if (inst.inViewPort) {
onLeaveView(inst);
inst.inViewPort = false;
}
});
}

return {
scope: {},
controller: function($scope, $element) {
Expand Down

0 comments on commit 88026d8

Please sign in to comment.