diff --git a/appinfo/routes.php b/appinfo/routes.php index 9c02d49b..815bb651 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -31,6 +31,12 @@ 'url' => '/face/{id}/thumb/{size}', 'verb' => 'GET' ], + // Delete a single face + [ + 'name' => 'face#deleteFace', + 'url' => '/face/{id}', + 'verb' => 'DELETE' + ], // Get persons from path [ 'name' => 'file#getPersonsFromPath', diff --git a/css/facerecognition.scss b/css/facerecognition.scss index 74da3fa5..52710f94 100644 --- a/css/facerecognition.scss +++ b/css/facerecognition.scss @@ -23,15 +23,26 @@ } .face-preview { - background-color: rgba(210, 210, 210, .75); + background-color: rgba(210, 210, 210, 0.75); border-radius: 3px; margin: 2px; height: 50px; width: 50px; + + .icon { + display: none; + height: 17px; + width: 17px; + cursor: pointer; + } } .face-preview:hover { - opacity: .5; + opacity: 0.5; + + .icon { + display: inline-block; + } } /* @@ -44,7 +55,7 @@ } .face-preview-dialog { - background-color: rgba(210, 210, 210, .75); + background-color: rgba(210, 210, 210, 0.75); border-radius: 25px; height: 50px; width: 50px; diff --git a/js/fr-dialogs.js b/js/fr-dialogs.js index d40196b7..f2c84500 100644 --- a/js/fr-dialogs.js +++ b/js/fr-dialogs.js @@ -87,6 +87,66 @@ const FrDialogs = { input.select(); }); }, + + deleteFace: function (thumbUrl, callback) { + return $.when(this._getMessageTemplate()).then(function ($tmpl) { + var dialogName = 'fr-dialog-content'; + var dialogId = '#' + dialogName; + var $dlg = $tmpl.octemplate({ + dialog_name: dialogName, + title: t('facerecognition', 'Remove tagged face'), + message: t('facerecognition', 'Really remove recognized face?'), + type: 'notice' + }); + var div = $('
').attr('style', 'display:flex; align-items: center'); + var thumb = $(''); + + div.append(thumb); + $dlg.append(div); + + $('body').append($dlg); + + // wrap callback in _.once(): + // only call callback once and not twice (button handler and close + // event) but call it for the close event, if ESC or the x is hit + if (callback !== undefined) { + callback = _.once(callback); + } + + var buttonlist = [{ + text: t('facerecognition', 'Cancel'), + click: function () { + if (callback !== undefined) { + callback(false); + } + $(dialogId).ocdialog('close'); + } + }, { + text: t('facerecognition', 'Confirm'), + click: function () { + if (callback !== undefined) { + callback(true); + } + $(dialogId).ocdialog('close'); + }, + defaultButton: true + } + ]; + + $(dialogId).ocdialog({ + closeOnEscape: true, + modal: true, + buttons: buttonlist, + close: function () { + // callback is already fired if Yes/No is clicked directly + if (callback !== undefined) { + callback(false); + } + } + }); + + }); + }, _getMessageTemplate: function () { var defer = $.Deferred(); if (!this.$messageTemplate) { @@ -95,9 +155,9 @@ const FrDialogs = { self.$messageTemplate = $(tmpl); defer.resolve(self.$messageTemplate); }) - .fail(function (jqXHR, textStatus, errorThrown) { - defer.reject(jqXHR.status, errorThrown); - }); + .fail(function (jqXHR, textStatus, errorThrown) { + defer.reject(jqXHR.status, errorThrown); + }); } else { defer.resolve(this.$messageTemplate); } diff --git a/js/personal.js b/js/personal.js index 971756a4..2094f2dd 100644 --- a/js/personal.js +++ b/js/personal.js @@ -1,293 +1,306 @@ (function (OC, window, $, undefined) { -'use strict'; + 'use strict'; -$(document).ready(function () { + $(document).ready(function () { -const state = { - OK: 0, - FALSE: 1, - SUCCESS: 2, - ERROR: 3 -} + const state = { + OK: 0, + FALSE: 1, + SUCCESS: 2, + ERROR: 3 + } -/* - * Faces in memory handlers. - */ -var Persons = function (baseUrl) { - this._baseUrl = baseUrl; - this._enabled = false; - this._clusters = []; - this._cluster = undefined; - this._clustersByName = undefined; - this._loaded = false; -}; + /* + * Faces in memory handlers. + */ + var Persons = function (baseUrl) { + this._baseUrl = baseUrl; + this._enabled = false; + this._clusters = []; + this._cluster = undefined; + this._clustersByName = undefined; + this._loaded = false; + }; -Persons.prototype = { - load: function () { - var deferred = $.Deferred(); - var self = this; - $.get(this._baseUrl+'/clusters').done(function (response) { - self._enabled = response.enabled; - self._clusters = response.clusters; - self._loaded = true; - deferred.resolve(); - }).fail(function () { - deferred.reject(); - }); - return deferred.promise(); - }, - loadCluster: function (id) { - this.unsetActive(); + Persons.prototype = { + load: function () { + var deferred = $.Deferred(); + var self = this; + $.get(this._baseUrl + '/clusters').done(function (response) { + self._enabled = response.enabled; + self._clusters = response.clusters; + self._loaded = true; + deferred.resolve(); + }).fail(function () { + deferred.reject(); + }); + return deferred.promise(); + }, + loadCluster: function (id) { + this.unsetActive(); - var deferred = $.Deferred(); - var self = this; - $.get(this._baseUrl+'/cluster/'+id).done(function (cluster) { - self._cluster = cluster; - deferred.resolve(); - }).fail(function () { - deferred.reject(); - }); - return deferred.promise(); - }, - loadClustersByName: function (personName) { - var deferred = $.Deferred(); - var self = this; - $.get(this._baseUrl+'/person/'+personName).done(function (clusters) { - self._clustersByName = clusters.clusters; - deferred.resolve(); - }).fail(function () { - deferred.reject(); - }); - return deferred.promise(); - }, - unsetActive: function () { - this._cluster = undefined; - this._clustersByName = undefined; - }, - getActive: function () { - return this._cluster; - }, - getActiveByName: function () { - return this._clustersByName; - }, - getById: function (clusterId) { - var ret = undefined; - for (var cluster of this._clusters) { - if (cluster.id === clusterId) { - ret = cluster; - break; + var deferred = $.Deferred(); + var self = this; + $.get(this._baseUrl + '/cluster/' + id).done(function (cluster) { + self._cluster = cluster; + deferred.resolve(); + }).fail(function () { + deferred.reject(); + }); + return deferred.promise(); + }, + loadClustersByName: function (personName) { + var deferred = $.Deferred(); + var self = this; + $.get(this._baseUrl + '/person/' + personName).done(function (clusters) { + self._clustersByName = clusters.clusters; + deferred.resolve(); + }).fail(function () { + deferred.reject(); + }); + return deferred.promise(); + }, + unsetActive: function () { + this._cluster = undefined; + this._clustersByName = undefined; + }, + getActive: function () { + return this._cluster; + }, + getActiveByName: function () { + return this._clustersByName; + }, + getById: function (clusterId) { + var ret = undefined; + for (var cluster of this._clusters) { + if (cluster.id === clusterId) { + ret = cluster; + break; + } + }; + return ret; + }, + isLoaded: function () { + return this._loaded; + }, + isEnabled: function () { + return this._enabled; + }, + sortBySize: function () { + if (this._clusters !== undefined) + this._clusters.sort(function (a, b) { + return b.count - a.count; + }); + if (this._clustersByName !== undefined) + this._clustersByName.sort(function (a, b) { + return b.count - a.count; + }); + }, + getAll: function () { + return this._clusters; + }, + renameCluster: function (clusterId, personName) { + var self = this; + var deferred = $.Deferred(); + var opt = { name: personName }; + $.ajax({ + url: this._baseUrl + '/cluster/' + clusterId, + method: 'PUT', + contentType: 'application/json', + data: JSON.stringify(opt) + }).done(function (data) { + self._clusters.forEach(function (cluster) { + if (cluster.id === clusterId) { + cluster.name = personName; + } + }); + deferred.resolve(); + }).fail(function () { + deferred.reject(); + }); + return deferred.promise(); } }; - return ret; - }, - isLoaded: function () { - return this._loaded; - }, - isEnabled: function () { - return this._enabled; - }, - sortBySize: function () { - if (this._clusters !== undefined) - this._clusters.sort(function(a, b) { - return b.count - a.count; - }); - if (this._clustersByName !== undefined) - this._clustersByName.sort(function(a, b) { - return b.count - a.count; - }); - }, - getAll: function () { - return this._clusters; - }, - renameCluster: function (clusterId, personName) { - var self = this; - var deferred = $.Deferred(); - var opt = { name: personName }; - $.ajax({url: this._baseUrl + '/cluster/' + clusterId, - method: 'PUT', - contentType: 'application/json', - data: JSON.stringify(opt) - }).done(function (data) { - self._clusters.forEach(function (cluster) { - if (cluster.id === clusterId) { - cluster.name = personName; - } - }); - deferred.resolve(); - }).fail(function () { - deferred.reject(); - }); - return deferred.promise(); - } -}; -/* - * View. - */ -var View = function (persons) { - this._persons = persons; -}; + /* + * View. + */ + var View = function (persons) { + this._persons = persons; + }; -View.prototype = { - reload: function (name) { - var self = this; - this._persons.load().done(function () { - self.renderContent(); - }).fail(function () { - OC.Notification.showTemporary(t('facerecognition', 'There was an error trying to show your friends')); - }); - }, - setEnabledUser: function (enabled) { - var self = this; - $.ajax({ - type: 'POST', - url: OC.generateUrl('apps/facerecognition/setuservalue'), - data: { - 'type': 'enabled', - 'value': enabled + View.prototype = { + reload: function (name) { + var self = this; + this._persons.load().done(function () { + self.renderContent(); + }).fail(function () { + OC.Notification.showTemporary(t('facerecognition', 'There was an error trying to show your friends')); + }); + }, + setEnabledUser: function (enabled) { + var self = this; + $.ajax({ + type: 'POST', + url: OC.generateUrl('apps/facerecognition/setuservalue'), + data: { + 'type': 'enabled', + 'value': enabled + }, + success: function () { + if (enabled) { + OC.Notification.showTemporary(t('facerecognition', 'The analysis is enabled, please be patient, you will soon see your friends here.')); + } else { + OC.Notification.showTemporary(t('facerecognition', 'The analysis is disabled. Soon all the information found for facial recognition will be removed.')); + } + self.reload(); + } + }); }, - success: function () { - if (enabled) { - OC.Notification.showTemporary(t('facerecognition', 'The analysis is enabled, please be patient, you will soon see your friends here.')); - } else { - OC.Notification.showTemporary(t('facerecognition', 'The analysis is disabled. Soon all the information found for facial recognition will be removed.')); + renderContent: function () { + this._persons.sortBySize(); + var context = { + loaded: this._persons.isLoaded(), + appName: t('facerecognition', 'Face Recognition'), + welcomeHint: t('facerecognition', 'Here you can see photos of your friends that are recognized'), + enableDescription: t('facerecognition', 'Analyze my images and group my loved ones with similar faces'), + loadingMsg: t('facerecognition', 'Looking for your recognized friends'), + showMoreButton: t('facerecognition', 'Show all groups with the same name'), + emptyMsg: t('facerecognition', 'The analysis is disabled'), + emptyHint: t('facerecognition', 'Enable it to find your loved ones'), + loadingIcon: OC.imagePath('core', 'loading.gif') + }; + + if (this._persons.isEnabled() === true) { + context.enabled = true; + context.clusters = this._persons.getAll(); + + context.emptyMsg = t('facerecognition', 'Your friends have not been recognized yet'); + context.emptyHint = t('facerecognition', 'Please, be patient'); } - self.reload(); - } - }); - }, - renderContent: function () { - this._persons.sortBySize(); - var context = { - loaded: this._persons.isLoaded(), - appName: t('facerecognition', 'Face Recognition'), - welcomeHint: t('facerecognition', 'Here you can see photos of your friends that are recognized'), - enableDescription: t('facerecognition', 'Analyze my images and group my loved ones with similar faces'), - loadingMsg: t('facerecognition', 'Looking for your recognized friends'), - showMoreButton: t('facerecognition', 'Show all groups with the same name'), - emptyMsg: t('facerecognition', 'The analysis is disabled'), - emptyHint: t('facerecognition', 'Enable it to find your loved ones'), - loadingIcon: OC.imagePath('core', 'loading.gif') - }; - if (this._persons.isEnabled() === true) { - context.enabled = true; - context.clusters = this._persons.getAll(); + if (this._persons.getActive() !== undefined) + context.cluster = this._persons.getActive(); - context.emptyMsg = t('facerecognition', 'Your friends have not been recognized yet'); - context.emptyHint = t('facerecognition', 'Please, be patient'); - } + if (this._persons.getActiveByName() !== undefined) + context.clustersByName = this._persons.getActiveByName(); - if (this._persons.getActive() !== undefined) - context.cluster = this._persons.getActive(); + var html = Handlebars.templates['personal'](context); + $('#div-content').html(html); - if (this._persons.getActiveByName() !== undefined) - context.clustersByName = this._persons.getActiveByName(); + const observer = lozad('.face-preview'); + observer.observe(); - var html = Handlebars.templates['personal'](context); - $('#div-content').html(html); + var self = this; - const observer = lozad('.face-preview'); - observer.observe(); + $('#enableFacerecognition').click(function () { + var enabled = $(this).is(':checked'); + if (enabled === false) { + OC.dialogs.confirm( + t('facerecognition', 'You will lose all the information analyzed, and if you re-enable it, you will start from scratch.'), + t('facerecognition', 'Do you want to deactivate the grouping by faces?'), + function (result) { + if (result === true) { + self.setEnabledUser(false); + } else { + $('#enableFacerecognition').prop('checked', true); + } + }, + true + ); + } else { + self.setEnabledUser(true); + } + }); - var self = this; + $('#facerecognition .person-name').click(function () { + var id = $(this).parent().data('id'); + self._persons.loadCluster(id).done(function () { + self.renderContent(); + }).fail(function () { + OC.Notification.showTemporary(t('facerecognition', 'There was an error when trying to find photos of your friend')); + }); + }); - $('#enableFacerecognition').click(function() { - var enabled = $(this).is(':checked'); - if (enabled === false) { - OC.dialogs.confirm( - t('facerecognition', 'You will lose all the information analyzed, and if you re-enable it, you will start from scratch.'), - t('facerecognition', 'Do you want to deactivate the grouping by faces?'), - function (result) { - if (result === true) { - self.setEnabledUser (false); - } else { - $('#enableFacerecognition').prop('checked', true); + $('#facerecognition .icon-rename').click(function () { + var id = $(this).parent().data('id'); + var person = self._persons.getById(id); + FrDialogs.rename( + person.name, + person.faces[0]['thumb-url'], + function (result, value) { + if (result === true && value) { + self._persons.renameCluster(id, value).done(function () { + self._persons.unsetActive(); + self.renderContent(); + }).fail(function () { + OC.Notification.showTemporary(t('facerecognition', 'There was an error renaming this person')); + }); + } } - }, - true - ); - } else { - self.setEnabledUser (true); - } - }); + ); + }); - $('#facerecognition .person-name').click(function () { - var id = $(this).parent().data('id'); - self._persons.loadCluster(id).done(function () { - self.renderContent(); - }).fail(function () { - OC.Notification.showTemporary(t('facerecognition', 'There was an error when trying to find photos of your friend')); - }); - }); + $('#facerecognition .face-preview .icon-delete-white').click(function () { + var face_id = $(this).data('face-id'); + var thumbUrl = $(this).parent().data('background-image'); + FrDialogs.deleteFace( + thumbUrl, + function (result) { + if (result === true) { - $('#facerecognition .icon-rename').click(function () { - var id = $(this).parent().data('id'); - var person = self._persons.getById(id); - FrDialogs.rename( - person.name, - person.faces[0]['thumb-url'], - function(result, value) { - if (result === true && value) { - self._persons.renameCluster (id, value).done(function () { - self._persons.unsetActive(); - self.renderContent(); - }).fail(function () { - OC.Notification.showTemporary(t('facerecognition', 'There was an error renaming this person')); + } }); - } - } - ); - }); + }); - $('#facerecognition #show-more-clusters').click(function () { - var personName = self._persons.getActive().name; - self._persons.loadClustersByName(personName).done(function () { - self.renderContent(); - }).fail(function () { - OC.Notification.showTemporary(t('facerecognition', 'There was an error when trying to find photos of your friend')); - }); - }); + $('#facerecognition #show-more-clusters').click(function () { + var personName = self._persons.getActive().name; + self._persons.loadClustersByName(personName).done(function () { + self.renderContent(); + }).fail(function () { + OC.Notification.showTemporary(t('facerecognition', 'There was an error when trying to find photos of your friend')); + }); + }); - $('#facerecognition .icon-view-previous').click(function () { - self._persons.unsetActive(); - self.renderContent(); - }); - } -}; + $('#facerecognition .icon-view-previous').click(function () { + self._persons.unsetActive(); + self.renderContent(); + }); + } + }; -/* - * Main app. - */ -var persons = new Persons(OC.generateUrl('/apps/facerecognition')); + /* + * Main app. + */ + var persons = new Persons(OC.generateUrl('/apps/facerecognition')); -var view = new View(persons); + var view = new View(persons); -view.renderContent(); + view.renderContent(); -persons.load().done(function () { - view.renderContent(); -}).fail(function () { - OC.Notification.showTemporary(t('facerecognition', 'There was an error trying to show your friends')); -}); + persons.load().done(function () { + view.renderContent(); + }).fail(function () { + OC.Notification.showTemporary(t('facerecognition', 'There was an error trying to show your friends')); + }); -var egg = new Egg("up,up,down,down,left,right,left,right,b,a", function() { - if (!OC.isUserAdmin()) { - OC.Notification.showTemporary(t('facerecognition', 'You must be administrator to configure this feature')); - return; - } - $.ajax({ - type: 'POST', - url: OC.generateUrl('apps/facerecognition/setappvalue'), - data: { - 'type': 'obfuscate_faces', - 'value': 'toggle' - }, - success: function (data) { - location.reload(); - } - }); -}).listen(); + var egg = new Egg("up,up,down,down,left,right,left,right,b,a", function () { + if (!OC.isUserAdmin()) { + OC.Notification.showTemporary(t('facerecognition', 'You must be administrator to configure this feature')); + return; + } + $.ajax({ + type: 'POST', + url: OC.generateUrl('apps/facerecognition/setappvalue'), + data: { + 'type': 'obfuscate_faces', + 'value': 'toggle' + }, + success: function (data) { + location.reload(); + } + }); + }).listen(); -}); // $(document).ready(function () { + }); // $(document).ready(function () { })(OC, window, jQuery); // (function (OC, window, $, undefined) { \ No newline at end of file diff --git a/js/templates/personal.handlebars b/js/templates/personal.handlebars index 3df27e9a..97065e76 100644 --- a/js/templates/personal.handlebars +++ b/js/templates/personal.handlebars @@ -7,17 +7,20 @@
{{#each clustersByName}} -

- {{this.name}} - -

-
- {{#each this.faces}} - -
-
- {{/each}} +

+ {{this.name}} + +

+
+ {{#each this.faces}} +
+ + + +
+ {{/each}} +
{{/each}}
{{else if cluster}} @@ -32,9 +35,12 @@
{{#each cluster.faces}} +
-
+
+ +
{{/each}}
@@ -45,28 +51,33 @@

{{appName}}

- +
{{#each clusters}} -

- {{this.name}} - -

-
- {{#each this.faces}} - -
-
- {{/each}} +

+ {{this.name}} + +

+
+ {{#each this.faces}} +
+ + + +
+ {{/each}} +
{{/each}}
{{else if loaded}}

{{appName}}

- +
@@ -86,7 +97,7 @@

{{loadingMsg}}

- +
{{/if}} diff --git a/lib/Controller/FaceController.php b/lib/Controller/FaceController.php index 3ee9ce3e..6ae26864 100644 --- a/lib/Controller/FaceController.php +++ b/lib/Controller/FaceController.php @@ -122,6 +122,15 @@ public function getThumb ($id, $size) { return $resp; } + /** + * @NoAdminRequired + */ + public function deleteFace($id) { + $this->faceMapper->deleteFace($id); + + return new JSONResponse(['result'=>true]); + } + private function hipsterize(&$image, &$face) { $imgResource = $image->resource(); diff --git a/lib/Controller/FileController.php b/lib/Controller/FileController.php index f8a6962e..94994e3a 100644 --- a/lib/Controller/FileController.php +++ b/lib/Controller/FileController.php @@ -109,6 +109,7 @@ public function getPersonsFromPath(string $fullpath) { $facePerson = array(); $facePerson['name'] = $person->getName(); $facePerson['person_id'] = $person->getId(); + $facePerson['face_id'] = $face->getId(); $facePerson['thumb_url'] = $this->getThumbUrl($face->getId()); $facePerson['face_left'] = $face->getLeft(); $facePerson['face_right'] = $face->getRight(); diff --git a/lib/Controller/PersonController.php b/lib/Controller/PersonController.php index f26664a4..4c4cbd2e 100644 --- a/lib/Controller/PersonController.php +++ b/lib/Controller/PersonController.php @@ -127,6 +127,7 @@ public function index() { continue; $face = []; + $face['face-id'] = $personFace->getId(); $face['thumb-url'] = $this->getThumbUrl($personFace->getId()); $face['file-url'] = $fileUrl; $faces[] = $face; @@ -161,6 +162,7 @@ public function find(int $id) { if (NULL === $fileUrl) continue; $face = []; + $face['face-id'] = $personFace->getId(); $face['thumb-url'] = $this->getThumbUrl($personFace->getId()); $face['file-url'] = $fileUrl; $faces[] = $face; @@ -199,6 +201,7 @@ public function findByName(string $personName) { continue; $face = []; + $face['face-id'] = $personFace->getId(); $face['thumb-url'] = $this->getThumbUrl($personFace->getId()); $face['file-url'] = $fileUrl; $faces[] = $face; diff --git a/lib/Db/FaceMapper.php b/lib/Db/FaceMapper.php index 11a0c407..2639e312 100644 --- a/lib/Db/FaceMapper.php +++ b/lib/Db/FaceMapper.php @@ -185,6 +185,20 @@ public function removeFromImage(int $imageId) { ->execute(); } + /** + * Remove single face from database + * + * @param int $faceId Face which should be removed + */ + public function deleteFace(int $faceId) { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->getTableName()) + ->where($qb->expr->eq('id', $qb->createNamedParameter($faceId))) + ->execute(); + + //TODO Question is there the need to cleanup cluster? + } + /** * Deletes all faces from that user. * diff --git a/src/views/PersonsTab.vue b/src/views/PersonsTab.vue index 5c1a2c61..edb75530 100644 --- a/src/views/PersonsTab.vue +++ b/src/views/PersonsTab.vue @@ -45,6 +45,7 @@
{{ person.name }}
+ @@ -195,6 +196,24 @@ export default { } ) }, + deleteFace: function(person) { + const self = this + FrDialogs.deleteFace( + person.thumb_url, + function(result) { + if (result === true) { + var deleteUrl = OC.generateUrl('/apps/facerecognition/face/' + person.face_id); + Axios.delete(deleteUrl).then(function(response) { + self.getFacesInfo(self.fileInfo); + }).catch(function(error) { + self.error = error; + console.error('Error removing face', error); + }) + + } + } + ); + }, processFacesData(data, isDirectory) { this.isDirectory = isDirectory this.isEnabledByUser = data.enabled