Initial places implementation
parent
7f21e8802d
commit
62579b1b89
|
@ -27,6 +27,7 @@ return [
|
||||||
w(['name' => 'Page#albums', 'url' => '/albums/{id}', 'verb' => 'GET'], 'id'),
|
w(['name' => 'Page#albums', 'url' => '/albums/{id}', 'verb' => 'GET'], 'id'),
|
||||||
w(['name' => 'Page#recognize', 'url' => '/recognize/{name}', 'verb' => 'GET'], 'name'),
|
w(['name' => 'Page#recognize', 'url' => '/recognize/{name}', 'verb' => 'GET'], 'name'),
|
||||||
w(['name' => 'Page#facerecognition', 'url' => '/facerecognition/{name}', 'verb' => 'GET'], 'name'),
|
w(['name' => 'Page#facerecognition', 'url' => '/facerecognition/{name}', 'verb' => 'GET'], 'name'),
|
||||||
|
w(['name' => 'Page#places', 'url' => '/places/{id}', 'verb' => 'GET'], 'id'),
|
||||||
w(['name' => 'Page#tags', 'url' => '/tags/{name}', 'verb' => 'GET'], 'name'),
|
w(['name' => 'Page#tags', 'url' => '/tags/{name}', 'verb' => 'GET'], 'name'),
|
||||||
|
|
||||||
// Public folder share
|
// Public folder share
|
||||||
|
@ -61,6 +62,9 @@ return [
|
||||||
['name' => 'People#facerecognitionPeople', 'url' => '/api/facerecognition/people', 'verb' => 'GET'],
|
['name' => 'People#facerecognitionPeople', 'url' => '/api/facerecognition/people', 'verb' => 'GET'],
|
||||||
['name' => 'People#facerecognitionPeoplePreview', 'url' => '/api/facerecognition/people/preview/{id}', 'verb' => 'GET'],
|
['name' => 'People#facerecognitionPeoplePreview', 'url' => '/api/facerecognition/people/preview/{id}', 'verb' => 'GET'],
|
||||||
|
|
||||||
|
['name' => 'Places#places', 'url' => '/api/places', 'verb' => 'GET'],
|
||||||
|
['name' => 'Places#preview', 'url' => '/api/places/preview/{id}', 'verb' => 'GET'],
|
||||||
|
|
||||||
['name' => 'Archive#archive', 'url' => '/api/archive/{id}', 'verb' => 'PATCH'],
|
['name' => 'Archive#archive', 'url' => '/api/archive/{id}', 'verb' => 'PATCH'],
|
||||||
|
|
||||||
['name' => 'Image#preview', 'url' => '/api/image/preview/{id}', 'verb' => 'GET'],
|
['name' => 'Image#preview', 'url' => '/api/image/preview/{id}', 'verb' => 'GET'],
|
||||||
|
|
|
@ -29,6 +29,9 @@ use OCA\Memories\Db\TimelineRoot;
|
||||||
use OCA\Memories\Exif;
|
use OCA\Memories\Exif;
|
||||||
use OCP\App\IAppManager;
|
use OCP\App\IAppManager;
|
||||||
use OCP\AppFramework\Controller;
|
use OCP\AppFramework\Controller;
|
||||||
|
use OCP\AppFramework\Http;
|
||||||
|
use OCP\AppFramework\Http\DataDisplayResponse;
|
||||||
|
use OCP\AppFramework\Http\JSONResponse;
|
||||||
use OCP\Files\File;
|
use OCP\Files\File;
|
||||||
use OCP\Files\Folder;
|
use OCP\Files\Folder;
|
||||||
use OCP\Files\IRootFolder;
|
use OCP\Files\IRootFolder;
|
||||||
|
@ -291,6 +294,45 @@ class ApiBase extends Controller
|
||||||
return $node;
|
return $node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a list of file ids, return the first preview image possible.
|
||||||
|
*/
|
||||||
|
protected function getPreviewFromImageList(array &$list)
|
||||||
|
{
|
||||||
|
// Get preview manager
|
||||||
|
$previewManager = \OC::$server->get(\OCP\IPreview::class);
|
||||||
|
|
||||||
|
// Try to get a preview
|
||||||
|
$userFolder = $this->rootFolder->getUserFolder($this->getUID());
|
||||||
|
foreach ($list as &$img) {
|
||||||
|
// Get the file
|
||||||
|
$files = $userFolder->getById($img);
|
||||||
|
if (0 === \count($files)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check read permission
|
||||||
|
if (!($files[0]->getPermissions() & \OCP\Constants::PERMISSION_READ)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get preview image
|
||||||
|
try {
|
||||||
|
$preview = $previewManager->getPreview($files[0], 512, 512, false);
|
||||||
|
$response = new DataDisplayResponse($preview->getContent(), Http::STATUS_OK, [
|
||||||
|
'Content-Type' => $preview->getMimeType(),
|
||||||
|
]);
|
||||||
|
$response->cacheFor(3600 * 24, false, false);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if albums are enabled for this user.
|
* Check if albums are enabled for this user.
|
||||||
*/
|
*/
|
||||||
|
@ -329,6 +371,14 @@ class ApiBase extends Controller
|
||||||
return \OCA\Memories\Util::facerecognitionIsEnabled($this->config, $this->getUID());
|
return \OCA\Memories\Util::facerecognitionIsEnabled($this->config, $this->getUID());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if geolocation is enabled for this user.
|
||||||
|
*/
|
||||||
|
protected function geoPlacesIsEnabled(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to get one file or null from a fiolder.
|
* Helper to get one file or null from a fiolder.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -237,6 +237,13 @@ class DaysController extends ApiBase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter only for one place
|
||||||
|
if ($this->geoPlacesIsEnabled()) {
|
||||||
|
if ($locationId = $this->request->getParam('place')) {
|
||||||
|
$transforms[] = [$this->timelineQuery, 'transformPlaceFilter', (int) $locationId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Limit number of responses for day query
|
// Limit number of responses for day query
|
||||||
$limit = $this->request->getParam('limit');
|
$limit = $this->request->getParam('limit');
|
||||||
if ($limit) {
|
if ($limit) {
|
||||||
|
|
|
@ -215,6 +215,16 @@ class PageController extends Controller
|
||||||
return $this->main();
|
return $this->main();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
*
|
||||||
|
* @NoCSRFRequired
|
||||||
|
*/
|
||||||
|
public function places()
|
||||||
|
{
|
||||||
|
return $this->main();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @NoAdminRequired
|
* @NoAdminRequired
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2022 Varun Patil <radialapps@gmail.com>
|
||||||
|
* @author Varun Patil <radialapps@gmail.com>
|
||||||
|
* @license AGPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\Memories\Controller;
|
||||||
|
|
||||||
|
use OCP\AppFramework\Http;
|
||||||
|
use OCP\AppFramework\Http\JSONResponse;
|
||||||
|
|
||||||
|
class PlacesController extends ApiBase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
*
|
||||||
|
* @NoCSRFRequired
|
||||||
|
*
|
||||||
|
* Get list of places with counts of images
|
||||||
|
*/
|
||||||
|
public function places(): JSONResponse
|
||||||
|
{
|
||||||
|
$user = $this->userSession->getUser();
|
||||||
|
if (null === $user) {
|
||||||
|
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check tags enabled for this user
|
||||||
|
if (!$this->geoPlacesIsEnabled()) {
|
||||||
|
return new JSONResponse(['message' => 'Places not enabled'], Http::STATUS_PRECONDITION_FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this isn't the timeline folder then things aren't going to work
|
||||||
|
$root = $this->getRequestRoot();
|
||||||
|
if ($root->isEmpty()) {
|
||||||
|
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run actual query
|
||||||
|
$list = $this->timelineQuery->getPlaces($root);
|
||||||
|
|
||||||
|
return new JSONResponse($list, Http::STATUS_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
*
|
||||||
|
* @NoCSRFRequired
|
||||||
|
*
|
||||||
|
* Get preview for a location
|
||||||
|
*/
|
||||||
|
public function preview(int $id): Http\Response
|
||||||
|
{
|
||||||
|
$user = $this->userSession->getUser();
|
||||||
|
if (null === $user) {
|
||||||
|
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check tags enabled for this user
|
||||||
|
if (!$this->geoPlacesIsEnabled()) {
|
||||||
|
return new JSONResponse(['message' => 'Places not enabled'], Http::STATUS_PRECONDITION_FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this isn't the timeline folder then things aren't going to work
|
||||||
|
$root = $this->getRequestRoot();
|
||||||
|
if ($root->isEmpty()) {
|
||||||
|
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run actual query
|
||||||
|
$list = $this->timelineQuery->getPlacePreviews($id, $root);
|
||||||
|
if (null === $list || 0 === \count($list)) {
|
||||||
|
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
shuffle($list);
|
||||||
|
|
||||||
|
// Get preview from image list
|
||||||
|
return $this->getPreviewFromImageList(array_map(static function ($item) {
|
||||||
|
return $item['fileid'];
|
||||||
|
}, $list));
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,7 +24,6 @@ declare(strict_types=1);
|
||||||
namespace OCA\Memories\Controller;
|
namespace OCA\Memories\Controller;
|
||||||
|
|
||||||
use OCP\AppFramework\Http;
|
use OCP\AppFramework\Http;
|
||||||
use OCP\AppFramework\Http\DataDisplayResponse;
|
|
||||||
use OCP\AppFramework\Http\JSONResponse;
|
use OCP\AppFramework\Http\JSONResponse;
|
||||||
|
|
||||||
class TagsController extends ApiBase
|
class TagsController extends ApiBase
|
||||||
|
@ -90,38 +89,11 @@ class TagsController extends ApiBase
|
||||||
if (null === $list || 0 === \count($list)) {
|
if (null === $list || 0 === \count($list)) {
|
||||||
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
shuffle($list);
|
||||||
|
|
||||||
// Get preview manager
|
// Get preview from image list
|
||||||
$previewManager = \OC::$server->get(\OCP\IPreview::class);
|
return $this->getPreviewFromImageList(array_map(static function ($item) {
|
||||||
|
return $item['fileid'];
|
||||||
// Try to get a preview
|
}, $list));
|
||||||
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
|
|
||||||
foreach ($list as &$img) {
|
|
||||||
// Get the file
|
|
||||||
$files = $userFolder->getById($img['fileid']);
|
|
||||||
if (0 === \count($files)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check read permission
|
|
||||||
if (!($files[0]->getPermissions() & \OCP\Constants::PERMISSION_READ)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get preview image
|
|
||||||
try {
|
|
||||||
$preview = $previewManager->getPreview($files[0], 512, 512, false);
|
|
||||||
$response = new DataDisplayResponse($preview->getContent(), Http::STATUS_OK, [
|
|
||||||
'Content-Type' => $preview->getMimeType(),
|
|
||||||
]);
|
|
||||||
$response->cacheFor(3600 * 24, false, false);
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ class TimelineQuery
|
||||||
use TimelineQueryFilters;
|
use TimelineQueryFilters;
|
||||||
use TimelineQueryFolders;
|
use TimelineQueryFolders;
|
||||||
use TimelineQueryLivePhoto;
|
use TimelineQueryLivePhoto;
|
||||||
|
use TimelineQueryPlaces;
|
||||||
use TimelineQueryPeopleFaceRecognition;
|
use TimelineQueryPeopleFaceRecognition;
|
||||||
use TimelineQueryPeopleRecognize;
|
use TimelineQueryPeopleRecognize;
|
||||||
use TimelineQueryTags;
|
use TimelineQueryTags;
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCA\Memories\Db;
|
||||||
|
|
||||||
|
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||||
|
use OCP\IDBConnection;
|
||||||
|
|
||||||
|
trait TimelineQueryPlaces
|
||||||
|
{
|
||||||
|
protected IDBConnection $connection;
|
||||||
|
|
||||||
|
public function transformPlaceFilter(IQueryBuilder &$query, string $userId, int $locationId)
|
||||||
|
{
|
||||||
|
$query->innerJoin('m', 'memories_geo', 'mg', $query->expr()->andX(
|
||||||
|
$query->expr()->eq('mg.fileid', 'm.fileid'),
|
||||||
|
$query->expr()->eq('mg.osm_id', $query->createNamedParameter($locationId)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPlaces(TimelineRoot &$root)
|
||||||
|
{
|
||||||
|
$query = $this->connection->getQueryBuilder();
|
||||||
|
|
||||||
|
// SELECT location name and count of photos
|
||||||
|
$count = $query->func()->count($query->createFunction('DISTINCT m.fileid'), 'count');
|
||||||
|
$query->select('p.osm_id', 'p.name', $count)->from('memories_planet', 'p');
|
||||||
|
|
||||||
|
// WHERE there are items with this osm_id
|
||||||
|
$query->innerJoin('p', 'memories_geo', 'mg', $query->expr()->eq('mg.osm_id', 'p.osm_id'));
|
||||||
|
|
||||||
|
// WHERE these items are memories indexed photos
|
||||||
|
$query->innerJoin('mg', 'memories', 'm', $query->expr()->eq('m.fileid', 'mg.fileid'));
|
||||||
|
|
||||||
|
// WHERE these photos are in the user's requested folder recursively
|
||||||
|
$query = $this->joinFilecache($query, $root, true, false);
|
||||||
|
|
||||||
|
// GROUP and ORDER by tag name
|
||||||
|
$query->groupBy('p.osm_id');
|
||||||
|
$query->orderBy($query->createFunction('LOWER(p.name)'), 'ASC');
|
||||||
|
$query->addOrderBy('p.osm_id'); // tie-breaker
|
||||||
|
|
||||||
|
// FETCH all tags
|
||||||
|
$sql = str_replace('*PREFIX*memories_planet', 'memories_planet', $query->getSQL());
|
||||||
|
$cursor = $this->executeQueryWithCTEs($query, $sql);
|
||||||
|
$places = $cursor->fetchAll();
|
||||||
|
|
||||||
|
// Post process
|
||||||
|
foreach ($places as &$row) {
|
||||||
|
$row['osm_id'] = (int) $row['osm_id'];
|
||||||
|
$row['count'] = (int) $row['count'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $places;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPlacePreviews(int $id, TimelineRoot &$root)
|
||||||
|
{
|
||||||
|
$query = $this->connection->getQueryBuilder();
|
||||||
|
|
||||||
|
// SELECT all photos with this tag
|
||||||
|
$query->select('f.fileid', 'f.etag')->from('memories_geo', 'mg')
|
||||||
|
->where($query->expr()->eq('mg.osm_id', $query->createNamedParameter($id)))
|
||||||
|
;
|
||||||
|
|
||||||
|
// WHERE these items are memories indexed photos
|
||||||
|
$query->innerJoin('mg', 'memories', 'm', $query->expr()->eq('m.fileid', 'mg.fileid'));
|
||||||
|
|
||||||
|
// WHERE these photos are in the user's requested folder recursively
|
||||||
|
$query = $this->joinFilecache($query, $root, true, false);
|
||||||
|
|
||||||
|
// MAX 8
|
||||||
|
$query->setMaxResults(8);
|
||||||
|
|
||||||
|
// FETCH tag previews
|
||||||
|
$cursor = $this->executeQueryWithCTEs($query);
|
||||||
|
$ans = $cursor->fetchAll();
|
||||||
|
|
||||||
|
// Post-process
|
||||||
|
foreach ($ans as &$row) {
|
||||||
|
$row['fileid'] = (int) $row['fileid'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ans;
|
||||||
|
}
|
||||||
|
}
|
|
@ -154,6 +154,13 @@ class TimelineWrite
|
||||||
$exifJson = json_encode(['error' => 'Exif data encoding error']);
|
$exifJson = json_encode(['error' => 'Exif data encoding error']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store location data
|
||||||
|
if (\array_key_exists('GPSLatitude', $exif) && \array_key_exists('GPSLongitude', $exif)) {
|
||||||
|
$lat = $exif['GPSLatitude'];
|
||||||
|
$lon = $exif['GPSLongitude'];
|
||||||
|
$this->updateGeoData($file, (float) $lat, (float) $lon);
|
||||||
|
}
|
||||||
|
|
||||||
if ($prevRow) {
|
if ($prevRow) {
|
||||||
// Update existing row
|
// Update existing row
|
||||||
// No need to set objectid again
|
// No need to set objectid again
|
||||||
|
@ -268,4 +275,43 @@ class TimelineWrite
|
||||||
|
|
||||||
return $query->executeStatement();
|
return $query->executeStatement();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add geolocation data for a file.
|
||||||
|
*/
|
||||||
|
public function updateGeoData(File &$file, float $lat, float $lon): void
|
||||||
|
{
|
||||||
|
// Make query to memories_planet table
|
||||||
|
$query = $this->connection->getQueryBuilder();
|
||||||
|
$query->select('osm_id')
|
||||||
|
->from('memories_planet')
|
||||||
|
->where($query->createFunction('ST_Contains(`geometry`, ST_GeomFromText(\'POINT('.$lon.' '.$lat.')\', 4326))'))
|
||||||
|
;
|
||||||
|
|
||||||
|
// Remove memories_planet has no *PREFIX*
|
||||||
|
$sql = $query->getSQL();
|
||||||
|
$sql = str_replace('*PREFIX*memories_planet', 'memories_planet', $sql);
|
||||||
|
|
||||||
|
// Run query
|
||||||
|
$result = $this->connection->executeQuery($sql);
|
||||||
|
$rows = $result->fetchAll();
|
||||||
|
|
||||||
|
// Delete previous records
|
||||||
|
$query = $this->connection->getQueryBuilder();
|
||||||
|
$query->delete('memories_geo')
|
||||||
|
->where($query->expr()->eq('fileid', $query->createNamedParameter($file->getId(), IQueryBuilder::PARAM_INT)))
|
||||||
|
;
|
||||||
|
|
||||||
|
// Insert records
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$query = $this->connection->getQueryBuilder();
|
||||||
|
$query->insert('memories_geo')
|
||||||
|
->values([
|
||||||
|
'fileid' => $query->createNamedParameter($file->getId(), IQueryBuilder::PARAM_INT),
|
||||||
|
'osm_id' => $query->createNamedParameter($row['osm_id'], IQueryBuilder::PARAM_INT),
|
||||||
|
])
|
||||||
|
;
|
||||||
|
$query->executeStatement();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,7 @@ import AlbumIcon from "vue-material-design-icons/ImageAlbum.vue";
|
||||||
import ArchiveIcon from "vue-material-design-icons/PackageDown.vue";
|
import ArchiveIcon from "vue-material-design-icons/PackageDown.vue";
|
||||||
import CalendarIcon from "vue-material-design-icons/Calendar.vue";
|
import CalendarIcon from "vue-material-design-icons/Calendar.vue";
|
||||||
import PeopleIcon from "vue-material-design-icons/AccountBoxMultiple.vue";
|
import PeopleIcon from "vue-material-design-icons/AccountBoxMultiple.vue";
|
||||||
|
import MarkerIcon from "vue-material-design-icons/MapMarker.vue";
|
||||||
import TagsIcon from "vue-material-design-icons/Tag.vue";
|
import TagsIcon from "vue-material-design-icons/Tag.vue";
|
||||||
import MapIcon from "vue-material-design-icons/Map.vue";
|
import MapIcon from "vue-material-design-icons/Map.vue";
|
||||||
|
|
||||||
|
@ -88,6 +89,7 @@ export default defineComponent({
|
||||||
ArchiveIcon,
|
ArchiveIcon,
|
||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
PeopleIcon,
|
PeopleIcon,
|
||||||
|
MarkerIcon,
|
||||||
TagsIcon,
|
TagsIcon,
|
||||||
MapIcon,
|
MapIcon,
|
||||||
},
|
},
|
||||||
|
@ -246,6 +248,11 @@ export default defineComponent({
|
||||||
icon: CalendarIcon,
|
icon: CalendarIcon,
|
||||||
title: t("memories", "On this day"),
|
title: t("memories", "On this day"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "places",
|
||||||
|
icon: MarkerIcon,
|
||||||
|
title: t("memories", "Places"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "tags",
|
name: "tags",
|
||||||
icon: TagsIcon,
|
icon: TagsIcon,
|
||||||
|
|
|
@ -290,6 +290,8 @@ export default defineComponent({
|
||||||
return this.t("memories", "On this day");
|
return this.t("memories", "On this day");
|
||||||
case "tags":
|
case "tags":
|
||||||
return this.t("memories", "Tags");
|
return this.t("memories", "Tags");
|
||||||
|
case "places":
|
||||||
|
return this.t("memories", "Places");
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -669,6 +671,12 @@ export default defineComponent({
|
||||||
query.set("tag", <string>this.$route.params.name);
|
query.set("tag", <string>this.$route.params.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Places
|
||||||
|
if (this.$route.name === "places" && this.$route.params.name) {
|
||||||
|
const name = <string>this.$route.params.name;
|
||||||
|
query.set("place", <string>name.split("-", 2)[0]);
|
||||||
|
}
|
||||||
|
|
||||||
// Albums
|
// Albums
|
||||||
if (this.$route.name === "albums" && this.$route.params.name) {
|
if (this.$route.name === "albums" && this.$route.params.name) {
|
||||||
const user = <string>this.$route.params.user;
|
const user = <string>this.$route.params.user;
|
||||||
|
@ -737,6 +745,8 @@ export default defineComponent({
|
||||||
data = await dav.getPeopleData(this.$route.name as any);
|
data = await dav.getPeopleData(this.$route.name as any);
|
||||||
} else if (this.$route.name === "albums" && !this.$route.params.name) {
|
} else if (this.$route.name === "albums" && !this.$route.params.name) {
|
||||||
data = await dav.getAlbumsData("3");
|
data = await dav.getAlbumsData("3");
|
||||||
|
} else if (this.$route.name === "places" && !this.$route.params.name) {
|
||||||
|
data = await dav.getPlacesData();
|
||||||
} else {
|
} else {
|
||||||
// Try the cache
|
// Try the cache
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -68,6 +68,10 @@ export default defineComponent({
|
||||||
return getPreviewUrl(mock, true, 512);
|
return getPreviewUrl(mock, true, 512);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.place) {
|
||||||
|
return API.PLACE_PREVIEW(this.place.fileid);
|
||||||
|
}
|
||||||
|
|
||||||
return API.TAG_PREVIEW(this.data.name);
|
return API.TAG_PREVIEW(this.data.name);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -92,6 +96,10 @@ export default defineComponent({
|
||||||
: "recognize";
|
: "recognize";
|
||||||
},
|
},
|
||||||
|
|
||||||
|
place() {
|
||||||
|
return this.data.flag & constants.c.FLAG_IS_PLACE ? this.data : null;
|
||||||
|
},
|
||||||
|
|
||||||
album() {
|
album() {
|
||||||
return this.data.flag & constants.c.FLAG_IS_ALBUM
|
return this.data.flag & constants.c.FLAG_IS_ALBUM
|
||||||
? <IAlbum>this.data
|
? <IAlbum>this.data
|
||||||
|
@ -114,6 +122,13 @@ export default defineComponent({
|
||||||
return { name: "albums", params: { user, name } };
|
return { name: "albums", params: { user, name } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.place) {
|
||||||
|
const id = this.place.fileid.toString();
|
||||||
|
const placeName = this.place.name || id;
|
||||||
|
const name = `${id}-${placeName}`;
|
||||||
|
return { name: "places", params: { name } };
|
||||||
|
}
|
||||||
|
|
||||||
return { name: "tags", params: { name: this.data.name } };
|
return { name: "tags", params: { name: this.data.name } };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -43,10 +43,14 @@ export default defineComponent({
|
||||||
methods: {
|
methods: {
|
||||||
createMatter() {
|
createMatter() {
|
||||||
this.name = <string>this.$route.params.name || "";
|
this.name = <string>this.$route.params.name || "";
|
||||||
|
|
||||||
|
if (this.$route.name === "places") {
|
||||||
|
this.name = this.name.split("-", 2)[1];
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
back() {
|
back() {
|
||||||
this.$router.push({ name: "tags" });
|
this.$router.push({ name: this.$route.name });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -58,6 +58,10 @@ export default defineComponent({
|
||||||
: TopMatterType.NONE;
|
: TopMatterType.NONE;
|
||||||
case "albums":
|
case "albums":
|
||||||
return TopMatterType.ALBUM;
|
return TopMatterType.ALBUM;
|
||||||
|
case "places":
|
||||||
|
return this.$route.params.name
|
||||||
|
? TopMatterType.TAG
|
||||||
|
: TopMatterType.NONE;
|
||||||
default:
|
default:
|
||||||
return TopMatterType.NONE;
|
return TopMatterType.NONE;
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,6 +94,15 @@ export default new Router({
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: "/places/:name*",
|
||||||
|
component: Timeline,
|
||||||
|
name: "places",
|
||||||
|
props: (route) => ({
|
||||||
|
rootTitle: t("memories", "Places"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "/tags/:name*",
|
path: "/tags/:name*",
|
||||||
component: Timeline,
|
component: Timeline,
|
||||||
|
|
|
@ -47,6 +47,14 @@ export class API {
|
||||||
return gen(`${BASE}/albums/download?name={user}/{name}`, { user, name });
|
return gen(`${BASE}/albums/download?name={user}/{name}`, { user, name });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PLACE_LIST() {
|
||||||
|
return gen(`${BASE}/places`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PLACE_PREVIEW(place: number | string) {
|
||||||
|
return gen(`${BASE}/places/preview/{place}`, { place });
|
||||||
|
}
|
||||||
|
|
||||||
static TAG_LIST() {
|
static TAG_LIST() {
|
||||||
return gen(`${BASE}/tags`);
|
return gen(`${BASE}/tags`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,3 +8,4 @@ export * from "./dav/folders";
|
||||||
export * from "./dav/onthisday";
|
export * from "./dav/onthisday";
|
||||||
export * from "./dav/tags";
|
export * from "./dav/tags";
|
||||||
export * from "./dav/other";
|
export * from "./dav/other";
|
||||||
|
export * from "./dav/places";
|
||||||
|
|
|
@ -220,6 +220,10 @@ export function convertFlags(photo: IPhoto) {
|
||||||
}
|
}
|
||||||
delete photo.isface;
|
delete photo.isface;
|
||||||
}
|
}
|
||||||
|
if (photo.isplace) {
|
||||||
|
photo.flag |= constants.c.FLAG_IS_PLACE;
|
||||||
|
delete photo.isplace;
|
||||||
|
}
|
||||||
if (photo.istag) {
|
if (photo.istag) {
|
||||||
photo.flag |= constants.c.FLAG_IS_TAG;
|
photo.flag |= constants.c.FLAG_IS_TAG;
|
||||||
delete photo.istag;
|
delete photo.istag;
|
||||||
|
@ -317,9 +321,10 @@ export const constants = {
|
||||||
FLAG_IS_TAG: 1 << 5,
|
FLAG_IS_TAG: 1 << 5,
|
||||||
FLAG_IS_FACE_RECOGNIZE: 1 << 6,
|
FLAG_IS_FACE_RECOGNIZE: 1 << 6,
|
||||||
FLAG_IS_FACE_RECOGNITION: 1 << 7,
|
FLAG_IS_FACE_RECOGNITION: 1 << 7,
|
||||||
FLAG_IS_ALBUM: 1 << 8,
|
FLAG_IS_PLACE: 1 << 8,
|
||||||
FLAG_SELECTED: 1 << 9,
|
FLAG_IS_ALBUM: 1 << 9,
|
||||||
FLAG_LEAVING: 1 << 10,
|
FLAG_SELECTED: 1 << 10,
|
||||||
|
FLAG_LEAVING: 1 << 11,
|
||||||
},
|
},
|
||||||
|
|
||||||
TagDayID: TagDayID,
|
TagDayID: TagDayID,
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { IDay, IPhoto, ITag } from "../../types";
|
||||||
|
import { constants } from "../Utils";
|
||||||
|
import axios from "@nextcloud/axios";
|
||||||
|
import { API } from "../API";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of tags and convert to Days response
|
||||||
|
*/
|
||||||
|
export async function getPlacesData(): Promise<IDay[]> {
|
||||||
|
// Query for photos
|
||||||
|
let data: {
|
||||||
|
osm_id: number;
|
||||||
|
count: number;
|
||||||
|
name: string;
|
||||||
|
previews: IPhoto[];
|
||||||
|
}[] = [];
|
||||||
|
try {
|
||||||
|
const res = await axios.get<typeof data>(API.PLACE_LIST());
|
||||||
|
data = res.data;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add flag to previews
|
||||||
|
data.forEach((t) => t.previews?.forEach((preview) => (preview.flag = 0)));
|
||||||
|
|
||||||
|
// Convert to days response
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
dayid: constants.TagDayID.TAGS,
|
||||||
|
count: data.length,
|
||||||
|
detail: data.map(
|
||||||
|
(tag) =>
|
||||||
|
({
|
||||||
|
...tag,
|
||||||
|
id: tag.osm_id,
|
||||||
|
fileid: tag.osm_id,
|
||||||
|
flag: constants.c.FLAG_IS_TAG,
|
||||||
|
istag: true,
|
||||||
|
isplace: true,
|
||||||
|
} as ITag)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
|
@ -104,6 +104,8 @@ export type IPhoto = {
|
||||||
isalbum?: boolean;
|
isalbum?: boolean;
|
||||||
/** Is this a face */
|
/** Is this a face */
|
||||||
isface?: "recognize" | "facerecognition";
|
isface?: "recognize" | "facerecognition";
|
||||||
|
/** Is this a place */
|
||||||
|
isplace?: boolean;
|
||||||
/** Optional datetaken epoch */
|
/** Optional datetaken epoch */
|
||||||
datetaken?: number;
|
datetaken?: number;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue