From 9b2ec3ab6a0c371cf1c692ed96e46724e0edb8c0 Mon Sep 17 00:00:00 2001 From: "xiangbin.li" Date: Fri, 9 Oct 2020 20:57:09 +0100 Subject: [PATCH 1/2] add Nextcloud 20 unified searching Signed-off-by: xiangbin.li --- lib/AppInfo/Application.php | 101 ++++--------------- lib/AppInfo/Application20.php | 106 ++++++++++++++++++++ lib/AppInfo/ApplicationLegacy.php | 97 ++++++++++++++++++ lib/Db/ImageMapper.php | 19 ++++ lib/Db/PersonMapper.php | 35 +++++++ lib/Search/PersonSearchProvider.php | 147 ++++++++++++++++++++++++++++ 6 files changed, 423 insertions(+), 82 deletions(-) create mode 100644 lib/AppInfo/Application20.php create mode 100644 lib/AppInfo/ApplicationLegacy.php create mode 100644 lib/Search/PersonSearchProvider.php diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 4dd37906..f231615e 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -1,97 +1,34 @@ - * @copyright Copyright (c) 2017-2018, 2020 Matias De lellis + * @copyright Copyright (c) 2020 Xiangbin Li >dassio@icloud.com> * - * @author Roeland Jago Douma - * @author Matias De lellis + * @author Xiangbin Li * * @license GNU AGPL version 3 or any later version * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . * */ -namespace OCA\FaceRecognition\AppInfo; - -use OCP\AppFramework\App; -use OCP\AppFramework\IAppContainer; -use OCP\EventDispatcher\IEventDispatcher; -use OCP\Files\IRootFolder; -use OCP\Files\Node; -use OCP\IUserManager; - -use OCA\Files\Event\LoadSidebar; - -use OCA\FaceRecognition\Listener\LoadSidebarListener; -use OCA\FaceRecognition\Watcher; - -class Application extends App { - - /** @var string */ - public const APP_NAME = 'facerecognition'; - - /** - * Application constructor. - * - * @param array $urlParams - */ - public function __construct(array $urlParams = []) { - parent::__construct(self::APP_NAME, $urlParams); - - $this->connectWatcher(); - $this->connectSearch(); - $this->addServiceListeners(); - } - private function connectWatcher() { - /** @var IRootFolder $root */ - $root = $this->getContainer()->query(IRootFolder::class); - - $root->listen('\OC\Files', 'postWrite', function (Node $node) { - /** @var Watcher $watcher */ - $watcher = \OC::$server->query(Watcher::class); - $watcher->postWrite($node); - }); - - // We want to react on postDelete and not preDelete as in preDelete we don't know if - // file actually got deleted (locked, other errors...) - $root->listen('\OC\Files', 'postDelete', function (Node $node) { - /** @var Watcher $watcher */ - $watcher = \OC::$server->query(Watcher::class); - $watcher->postDelete($node); - }); - - // Watch for user deletion, so we clean up user data, after user gets deleted - $userManager = $this->getContainer()->query(IUserManager::class); - $userManager->listen('\OC\User', 'postDelete', function (\OC\User\User $user) { - /** @var Watcher $watcher */ - $watcher = \OC::$server->query(Watcher::class); - $watcher->postUserDelete($user); - }); - } +namespace OCA\FaceRecognition\AppInfo; - private function connectSearch() { - $this->getContainer()->getServer()->getSearch()->registerProvider( - 'OCA\FaceRecognition\Search\Provider', - array('app'=>'facerecognition', 'apps' => array('files')) - ); +$version = \OCP\Util::getVersion()[0]; +if ($version >= 20) { + class Application extends Application20 { } - - private function addServiceListeners() { - /** @var IEventDispatcher $dispatcher */ - $dispatcher = \OC::$server->query(IEventDispatcher::class); - $dispatcher->addServiceListener(LoadSidebar::class, LoadSidebarListener::class); +} else { + class Application extends ApplicationLegacy { } - } + diff --git a/lib/AppInfo/Application20.php b/lib/AppInfo/Application20.php new file mode 100644 index 00000000..0de459dc --- /dev/null +++ b/lib/AppInfo/Application20.php @@ -0,0 +1,106 @@ + + * @copyright Copyright (c) 2017-2018, 2020 Matias De lellis + * + * @author Roeland Jago Douma + * @author Matias De lellis + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\FaceRecognition\AppInfo; + +use OCP\AppFramework\Bootstrap\IBootstrap; +use OCP\AppFramework\App; +use OCP\AppFramework\IAppContainer; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\IRootFolder; +use OCP\Files\Node; +use OCP\IUserManager; +use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\AppFramework\Bootstrap\IBootContext; + +use OCA\Files\Event\LoadSidebar; +use OCA\FaceRecognition\Listener\LoadSidebarListener; +use OCA\FaceRecognition\Watcher; +use OCA\FaceRecognition\Search\PersonSearchProvider; + +class Application20 extends App implements IBootstrap { + + public const APP_NAME= 'facerecognition'; + /** + * Application constructor. + * + * @param array $urlParams + */ + public function __construct(array $urlParams = []) { + parent::__construct(self::APP_NAME, $urlParams); + + $this->connectWatcher(); + $this->connectSearch(); + $this->addServiceListeners(); + } + + public function register(IRegistrationContext $context): void { + $context->registerSearchProvider(PersonSearchProvider::class); + } + + public function boot(IBootContext $context): void { + + } + + private function connectWatcher() { + /** @var IRootFolder $root */ + $root = $this->getContainer()->query(IRootFolder::class); + + $root->listen('\OC\Files', 'postWrite', function (Node $node) { + /** @var Watcher $watcher */ + $watcher = \OC::$server->query(Watcher::class); + $watcher->postWrite($node); + }); + + // We want to react on postDelete and not preDelete as in preDelete we don't know if + // file actually got deleted (locked, other errors...) + $root->listen('\OC\Files', 'postDelete', function (Node $node) { + /** @var Watcher $watcher */ + $watcher = \OC::$server->query(Watcher::class); + $watcher->postDelete($node); + }); + + // Watch for user deletion, so we clean up user data, after user gets deleted + $userManager = $this->getContainer()->query(IUserManager::class); + $userManager->listen('\OC\User', 'postDelete', function (\OC\User\User $user) { + /** @var Watcher $watcher */ + $watcher = \OC::$server->query(Watcher::class); + $watcher->postUserDelete($user); + }); + } + + private function connectSearch() { + $this->getContainer()->getServer()->getSearch()->registerProvider( + 'OCA\FaceRecognition\Search\Provider', + array('app'=>'facerecognition', 'apps' => array('files')) + ); + } + + private function addServiceListeners() { + /** @var IEventDispatcher $dispatcher */ + $dispatcher = \OC::$server->query(IEventDispatcher::class); + $dispatcher->addServiceListener(LoadSidebar::class, LoadSidebarListener::class); + } + +} diff --git a/lib/AppInfo/ApplicationLegacy.php b/lib/AppInfo/ApplicationLegacy.php new file mode 100644 index 00000000..a9b504ca --- /dev/null +++ b/lib/AppInfo/ApplicationLegacy.php @@ -0,0 +1,97 @@ + + * @copyright Copyright (c) 2017-2018, 2020 Matias De lellis + * + * @author Roeland Jago Douma + * @author Matias De lellis + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\FaceRecognition\AppInfo; + +use OCP\AppFramework\App; +use OCP\AppFramework\IAppContainer; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\IRootFolder; +use OCP\Files\Node; +use OCP\IUserManager; + +use OCA\Files\Event\LoadSidebar; + +use OCA\FaceRecognition\Listener\LoadSidebarListener; +use OCA\FaceRecognition\Watcher; + +class ApplicationLegacy extends App { + + /** @var string */ + public const APP_NAME = 'facerecognition'; + + /** + * Application constructor. + * + * @param array $urlParams + */ + public function __construct(array $urlParams = []) { + parent::__construct(self::APP_NAME, $urlParams); + + $this->connectWatcher(); + $this->connectSearch(); + $this->addServiceListeners(); + } + + private function connectWatcher() { + /** @var IRootFolder $root */ + $root = $this->getContainer()->query(IRootFolder::class); + + $root->listen('\OC\Files', 'postWrite', function (Node $node) { + /** @var Watcher $watcher */ + $watcher = \OC::$server->query(Watcher::class); + $watcher->postWrite($node); + }); + + // We want to react on postDelete and not preDelete as in preDelete we don't know if + // file actually got deleted (locked, other errors...) + $root->listen('\OC\Files', 'postDelete', function (Node $node) { + /** @var Watcher $watcher */ + $watcher = \OC::$server->query(Watcher::class); + $watcher->postDelete($node); + }); + + // Watch for user deletion, so we clean up user data, after user gets deleted + $userManager = $this->getContainer()->query(IUserManager::class); + $userManager->listen('\OC\User', 'postDelete', function (\OC\User\User $user) { + /** @var Watcher $watcher */ + $watcher = \OC::$server->query(Watcher::class); + $watcher->postUserDelete($user); + }); + } + + private function connectSearch() { + $this->getContainer()->getServer()->getSearch()->registerProvider( + 'OCA\FaceRecognition\Search\Provider', + array('app'=>'facerecognition', 'apps' => array('files')) + ); + } + + private function addServiceListeners() { + /** @var IEventDispatcher $dispatcher */ + $dispatcher = \OC::$server->query(IEventDispatcher::class); + $dispatcher->addServiceListener(LoadSidebar::class, LoadSidebarListener::class); + } + +} diff --git a/lib/Db/ImageMapper.php b/lib/Db/ImageMapper.php index 8854482e..1111373d 100644 --- a/lib/Db/ImageMapper.php +++ b/lib/Db/ImageMapper.php @@ -199,6 +199,25 @@ public function findImages(string $userId, int $model): array { return $images; } + /** + * return the first find image as prson's avatar + */ + public function getPersonAvatar(Person $person) { + $qb = $this->db->getQueryBuilder(); + $qb->select('i.id', 'i.file') + ->from($this->getTableName(), 'i') + ->innerJoin('i', 'facerecog_faces', 'f', $qb->expr()->eq('f.image', 'i.id')) + ->innerJoin('i', 'facerecog_persons', 'p', $qb->expr()->eq('f.person', 'p.id')) + ->where($qb->expr()->eq('p.id', $qb->createNamedParameter($person->getId()))); + + $qb->setParameter('query', $query); + + $qb->setMaxResults(1); + + $images = $this->findEntities($qb); + return $images[0]; + } + public function findFromPersonLike(string $userId, int $model, string $name, $offset = null, $limit = null): array { $qb = $this->db->getQueryBuilder(); $qb->select('i.id', 'i.file') diff --git a/lib/Db/PersonMapper.php b/lib/Db/PersonMapper.php index e2d7df0c..02057c50 100644 --- a/lib/Db/PersonMapper.php +++ b/lib/Db/PersonMapper.php @@ -438,4 +438,39 @@ private function isFaceInClusters(int $faceId, array $clusters): bool { } return false; } + + /** + * Search Person by name + * + */ + public function findPersonsLike(string $userId, int $model, string $name, $offset = null, $limit = null): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('p.name','p.id') + ->from($this->getTableName(), 'p') + ->innerJoin('p', 'facerecog_faces', 'f', $qb->expr()->eq('f.person', 'p.id')) + ->innerJoin('p', 'facerecog_images', 'i', $qb->expr()->eq('f.image', 'i.id')) + ->where($qb->expr()->eq('p.user', $qb->createNamedParameter($userId))) + ->andWhere($qb->expr()->eq('model', $qb->createNamedParameter($model))) + ->andWhere($qb->expr()->eq('is_processed', $qb->createNamedParameter(True))) + ->andWhere($qb->expr()->like($qb->func()->lower('p.name'), $qb->createParameter('query'))); + + $query = '%' . $this->db->escapeLikeParameter(strtolower($name)) . '%'; + $qb->setParameter('query', $query); + + $qb->setFirstResult($offset); + $qb->setMaxResults($limit); + + //different clusters could have the same name + $personNames = []; + $uniquePeople = []; + $people = $this->findEntities($qb); + foreach ($people as $person) { + if (!in_array($person->getName(), $personNames)) { + $personNames[] = $person->getName(); + $uniquePeople[]= $person; + } + } + return $uniquePeople; + } + } diff --git a/lib/Search/PersonSearchProvider.php b/lib/Search/PersonSearchProvider.php new file mode 100644 index 00000000..45c2981d --- /dev/null +++ b/lib/Search/PersonSearchProvider.php @@ -0,0 +1,147 @@ + + * + * @author Li Xiangbin + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\FaceRecognition\Search; + +use OCP\Search\IProvider; +use OCP\IL10N; +use OCP\IUser; +use OCP\Search\ISearchQuery; +use OCP\Search\SearchResult; +use OCP\Search\SearchResultEntry; +use OCP\Files\IRootFolder; +use OCP\IURLGenerator; +use OCP\Files\IMimeTypeDetector; + +use OCA\FaceRecognition\Db\Person; +use OCA\FaceRecognition\Db\PersonMapper; +use OCA\FaceRecognition\Db\ImageMapper; +use OCA\FaceRecognition\Service\SettingsService; +use OCA\FaceRecognition\Model\IModel; + +/** + * Provide search results from the 'facerecognition' app + */ +class PersonSearchProvider implements IProvider { + + /** @var PersonMapper personMapper */ + private $personMapper; + + /** @var ImageMapper imageMapper */ + private $imageMapper; + + /** @var SettingsService Settings service */ + private $settingsService; + + /** @var IL10N */ + private $l10n; + + /** @var IMimeTypeDetector */ + private $mimeTypeDetector; + + /** @var IURLGenerator */ + private $urlGenerator; + + /** @var IRootFolder */ + private $rootFolder; + + /** @var IModel*/ + private $modelId; + + public function __construct(PersonMapper $personMapper, + ImageMapper $imageMapper, + SettingsService $settingsService, + IL10N $l10n, + ImimeTypeDetector $mimeTypeDetector, + IURLGenerator $urlGenerator, + IRootFolder $rootFolder) { + $this->personMapper = $personMapper; + $this->imageMapper = $imageMapper; + $this->settingsService = $settingsService; + $this->l10n = $l10n; + $this->urlGenerator = $urlGenerator; + $this->rootFolder = $rootFolder; + $this->mimeTypeDetector = $mimeTypeDetector; + $this->modelId =$this->settingsService->getCurrentFaceModel(); + } + + /** + * @inheritDoc + */ + public function getId(): string { + return 'facerecognition'; + } + + /** + * @inheritDoc + */ + public function getName(): string { + return $this->l10n->t('Face Recognition'); + } + + /** + * @inheritDoc + */ + public function getOrder(string $route, array $routeParameters): int { + return 10; + } + + /** + * @inheritDoc + */ + public function search(IUser $user, ISearchQuery $query) : SearchResult { + $page = $query->getCursor() ?? 0; + $limit = $query->getLimit(); + return SearchResult::paginated( + $this->l10n->t('Face Recognition'), + array_map(function (Person $result) { + $personName = $result->getName(); + $link = '/index/settings/user/facerecognition?name=' . \OCP\Util::encodePath($personName); + + $image = $this->imageMapper->getPersonAvatar($result); + $file = $this->rootFolder->getById($image->getFile())[0]; + $file = new \OC\Search\Result\File($file->getFileInfo()); + // Generate thumbnail url + $thumbnailUrl = $file->has_preview + ? $this->urlGenerator->linkToRouteAbsolute('core.Preview.getPreviewByFileId', ['x' => 32, 'y' => 32, 'fileId' => $file->id]) + : ''; + + return new SearchResultEntry( + $thumbnailUrl, + $personName, + '', + $this->urlGenerator->getAbsoluteURL($link), + $result->type === 'folder' ? 'icon-folder' : $this->mimeTypeDetector->mimeTypeIcon($file->mime_type) + ); + }, + $this->personMapper->findPersonsLike($user->getUID(), + $this->modelId, + $query->getTerm(), + $page * $limit, + $limit) + ), + $page); + } + +} From b33c44780622e96cd96fe69bcc07acbe012ccdc8 Mon Sep 17 00:00:00 2001 From: "xiangbin.li" Date: Wed, 14 Oct 2020 22:08:29 +0100 Subject: [PATCH 2/2] update according to #336 Signed-off-by: xiangbin.li --- img/avatar.webp | Bin 0 -> 4690 bytes lib/AppInfo/Application20.php | 8 ----- lib/Db/ImageMapper.php | 19 ----------- lib/Db/PersonMapper.php | 20 +++--------- lib/Search/PersonSearchProvider.php | 48 ++++++++++++---------------- lib/Service/UrlService.php | 6 ++-- 6 files changed, 29 insertions(+), 72 deletions(-) create mode 100644 img/avatar.webp diff --git a/img/avatar.webp b/img/avatar.webp new file mode 100644 index 0000000000000000000000000000000000000000..ea488c82f6379cfa1c2ee0275a58569cb410f73c GIT binary patch literal 4690 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 - $this->connectSearch(); $this->addServiceListeners(); } @@ -90,13 +89,6 @@ private function connectWatcher() { }); } - private function connectSearch() { - $this->getContainer()->getServer()->getSearch()->registerProvider( - 'OCA\FaceRecognition\Search\Provider', - array('app'=>'facerecognition', 'apps' => array('files')) - ); - } - private function addServiceListeners() { /** @var IEventDispatcher $dispatcher */ $dispatcher = \OC::$server->query(IEventDispatcher::class); diff --git a/lib/Db/ImageMapper.php b/lib/Db/ImageMapper.php index 1111373d..8854482e 100644 --- a/lib/Db/ImageMapper.php +++ b/lib/Db/ImageMapper.php @@ -199,25 +199,6 @@ public function findImages(string $userId, int $model): array { return $images; } - /** - * return the first find image as prson's avatar - */ - public function getPersonAvatar(Person $person) { - $qb = $this->db->getQueryBuilder(); - $qb->select('i.id', 'i.file') - ->from($this->getTableName(), 'i') - ->innerJoin('i', 'facerecog_faces', 'f', $qb->expr()->eq('f.image', 'i.id')) - ->innerJoin('i', 'facerecog_persons', 'p', $qb->expr()->eq('f.person', 'p.id')) - ->where($qb->expr()->eq('p.id', $qb->createNamedParameter($person->getId()))); - - $qb->setParameter('query', $query); - - $qb->setMaxResults(1); - - $images = $this->findEntities($qb); - return $images[0]; - } - public function findFromPersonLike(string $userId, int $model, string $name, $offset = null, $limit = null): array { $qb = $this->db->getQueryBuilder(); $qb->select('i.id', 'i.file') diff --git a/lib/Db/PersonMapper.php b/lib/Db/PersonMapper.php index 02057c50..8e905ac5 100644 --- a/lib/Db/PersonMapper.php +++ b/lib/Db/PersonMapper.php @@ -443,14 +443,14 @@ private function isFaceInClusters(int $faceId, array $clusters): bool { * Search Person by name * */ - public function findPersonsLike(string $userId, int $model, string $name, $offset = null, $limit = null): array { + public function findPersonsLike(string $userId, int $modelId, string $name, $offset = null, $limit = null): array { $qb = $this->db->getQueryBuilder(); - $qb->select('p.name','p.id') + $qb->selectDistinct('p.name') ->from($this->getTableName(), 'p') ->innerJoin('p', 'facerecog_faces', 'f', $qb->expr()->eq('f.person', 'p.id')) ->innerJoin('p', 'facerecog_images', 'i', $qb->expr()->eq('f.image', 'i.id')) ->where($qb->expr()->eq('p.user', $qb->createNamedParameter($userId))) - ->andWhere($qb->expr()->eq('model', $qb->createNamedParameter($model))) + ->andWhere($qb->expr()->eq('model', $qb->createNamedParameter($modelId))) ->andWhere($qb->expr()->eq('is_processed', $qb->createNamedParameter(True))) ->andWhere($qb->expr()->like($qb->func()->lower('p.name'), $qb->createParameter('query'))); @@ -459,18 +459,8 @@ public function findPersonsLike(string $userId, int $model, string $name, $offse $qb->setFirstResult($offset); $qb->setMaxResults($limit); - - //different clusters could have the same name - $personNames = []; - $uniquePeople = []; - $people = $this->findEntities($qb); - foreach ($people as $person) { - if (!in_array($person->getName(), $personNames)) { - $personNames[] = $person->getName(); - $uniquePeople[]= $person; - } - } - return $uniquePeople; + + return $this->findEntities($qb); } } diff --git a/lib/Search/PersonSearchProvider.php b/lib/Search/PersonSearchProvider.php index 45c2981d..235e2b97 100644 --- a/lib/Search/PersonSearchProvider.php +++ b/lib/Search/PersonSearchProvider.php @@ -26,19 +26,19 @@ use OCP\Search\IProvider; use OCP\IL10N; +use OCP\IURLGenerator; use OCP\IUser; use OCP\Search\ISearchQuery; use OCP\Search\SearchResult; use OCP\Search\SearchResultEntry; use OCP\Files\IRootFolder; -use OCP\IURLGenerator; -use OCP\Files\IMimeTypeDetector; use OCA\FaceRecognition\Db\Person; use OCA\FaceRecognition\Db\PersonMapper; use OCA\FaceRecognition\Db\ImageMapper; use OCA\FaceRecognition\Service\SettingsService; use OCA\FaceRecognition\Model\IModel; +use OCA\FaceRecognition\Service\UrlService; /** * Provide search results from the 'facerecognition' app @@ -57,33 +57,33 @@ class PersonSearchProvider implements IProvider { /** @var IL10N */ private $l10n; - /** @var IMimeTypeDetector */ - private $mimeTypeDetector; - /** @var IURLGenerator */ private $urlGenerator; + /** @var UrlService */ + private $urlService; + /** @var IRootFolder */ private $rootFolder; - /** @var IModel*/ + /** @var int*/ private $modelId; public function __construct(PersonMapper $personMapper, ImageMapper $imageMapper, SettingsService $settingsService, IL10N $l10n, - ImimeTypeDetector $mimeTypeDetector, + UrlService $urlService, IURLGenerator $urlGenerator, IRootFolder $rootFolder) { $this->personMapper = $personMapper; - $this->imageMapper = $imageMapper; - $this->settingsService = $settingsService; - $this->l10n = $l10n; - $this->urlGenerator = $urlGenerator; - $this->rootFolder = $rootFolder; - $this->mimeTypeDetector = $mimeTypeDetector; - $this->modelId =$this->settingsService->getCurrentFaceModel(); + $this->imageMapper = $imageMapper; + $this->settingsService = $settingsService; + $this->l10n = $l10n; + $this->urlService = $urlService; + $this->urlGenerator = $urlGenerator; + $this->rootFolder = $rootFolder; + $this->modelId = $this->settingsService->getCurrentFaceModel(); } /** @@ -104,6 +104,9 @@ public function getName(): string { * @inheritDoc */ public function getOrder(string $route, array $routeParameters): int { + if ($route === 'settings.PersonalSettings.index' && $routeParameters["section"] === 'facerecognition') { + return 0; + } return 10; } @@ -117,22 +120,13 @@ public function search(IUser $user, ISearchQuery $query) : SearchResult { $this->l10n->t('Face Recognition'), array_map(function (Person $result) { $personName = $result->getName(); - $link = '/index/settings/user/facerecognition?name=' . \OCP\Util::encodePath($personName); - - $image = $this->imageMapper->getPersonAvatar($result); - $file = $this->rootFolder->getById($image->getFile())[0]; - $file = new \OC\Search\Result\File($file->getFileInfo()); - // Generate thumbnail url - $thumbnailUrl = $file->has_preview - ? $this->urlGenerator->linkToRouteAbsolute('core.Preview.getPreviewByFileId', ['x' => 32, 'y' => 32, 'fileId' => $file->id]) - : ''; - return new SearchResultEntry( - $thumbnailUrl, + $this->urlGenerator->imagePath('facerecognition','avatar.webp'), $personName, '', - $this->urlGenerator->getAbsoluteURL($link), - $result->type === 'folder' ? 'icon-folder' : $this->mimeTypeDetector->mimeTypeIcon($file->mime_type) + $this->urlService->getRedirectToPersonUrl($personName), + '', + true, ); }, $this->personMapper->findPersonsLike($user->getUID(), diff --git a/lib/Service/UrlService.php b/lib/Service/UrlService.php index 4e1f845a..b3730e6b 100644 --- a/lib/Service/UrlService.php +++ b/lib/Service/UrlService.php @@ -120,12 +120,12 @@ public function getRedirectToFileUrl(int $fileId) { /** * Redirects to the facerecognition page to show photos of an person. * - * @param int $personId person id to show + * @param string $personName person id to show */ - public function getRedirectToPersonUrl(string $personId) { + public function getRedirectToPersonUrl(string $personName) { $params = [ 'section' => 'facerecognition', - 'name' => $personId + 'name' => $personName ]; return $this->urlGenerator->linkToRoute('settings.PersonalSettings.index', $params); }