From a1d38f8d254f4a4fd470b37e2764ccb708613ebb Mon Sep 17 00:00:00 2001 From: Jo Van Bulck Date: Thu, 28 Nov 2024 00:37:09 +0100 Subject: [PATCH] feat: add panorama and live photo views Fixes #676 --- appinfo/routes.php | 2 ++ lib/Controller/DaysController.php | 10 ++++++++++ lib/Controller/PageController.php | 20 ++++++++++++++++++++ lib/Db/TimelineQueryFilters.php | 15 +++++++++++++++ src/components/Explore.vue | 12 ++++++++++++ src/components/Timeline.vue | 10 ++++++++++ src/router.ts | 16 ++++++++++++++++ src/services/API.ts | 2 ++ src/services/strings.ts | 4 ++++ 9 files changed, 91 insertions(+) diff --git a/appinfo/routes.php b/appinfo/routes.php index b54e2e78a..f6ae31edc 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -17,6 +17,8 @@ function w($base, $param) ['name' => 'Page#main', 'url' => '/', 'verb' => 'GET'], ['name' => 'Page#favorites', 'url' => '/favorites', 'verb' => 'GET'], ['name' => 'Page#videos', 'url' => '/videos', 'verb' => 'GET'], + ['name' => 'Page#livephotos', 'url' => '/livephotos', 'verb' => 'GET'], + ['name' => 'Page#panoramas', 'url' => '/panoramas', 'verb' => 'GET'], ['name' => 'Page#archive', 'url' => '/archive', 'verb' => 'GET'], ['name' => 'Page#thisday', 'url' => '/thisday', 'verb' => 'GET'], ['name' => 'Page#map', 'url' => '/map', 'verb' => 'GET'], diff --git a/lib/Controller/DaysController.php b/lib/Controller/DaysController.php index dc5d8a142..4c0b2c36f 100644 --- a/lib/Controller/DaysController.php +++ b/lib/Controller/DaysController.php @@ -115,6 +115,16 @@ private function getTransformations(): array $transforms[] = [$this->tq, 'transformVideoFilter']; } + // Filter only live photos + if ($this->request->getParam('live')) { + $transforms[] = [$this->tq, 'transformLivePhotoFilter']; + } + + // Filter only panoramas + if ($this->request->getParam('pano')) { + $transforms[] = [$this->tq, 'transformPanoFilter']; + } + // Filter geological bounds if ($bounds = $this->request->getParam('mapbounds')) { $transforms[] = [$this->tq, 'transformMapBoundsFilter', $bounds]; diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index 3f16c90d6..6d42b3614 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -149,6 +149,26 @@ public function videos(): Response return $this->main(); } + /** + * @NoAdminRequired + * + * @NoCSRFRequired + */ + public function livephotos(): Response + { + return $this->main(); + } + + /** + * @NoAdminRequired + * + * @NoCSRFRequired + */ + public function panoramas(): Response + { + return $this->main(); + } + /** * @NoAdminRequired * diff --git a/lib/Db/TimelineQueryFilters.php b/lib/Db/TimelineQueryFilters.php index 4dac2e663..b21c61650 100644 --- a/lib/Db/TimelineQueryFilters.php +++ b/lib/Db/TimelineQueryFilters.php @@ -9,6 +9,11 @@ use OCP\DB\QueryBuilder\IQueryFunction; use OCP\ITags; +// Wikipedia defines a panoramic image as having an aspect ratio of at least 2:1, +// but some phones approach this with regular photos. Hence, we conservatively set +// the threshold to 3:1 for true panoramas. +const PANOROMA_ASPECT_RATIO = 3; + trait TimelineQueryFilters { public function transformFavoriteFilter(IQueryBuilder &$query, bool $aggregate): void @@ -37,6 +42,16 @@ public function transformVideoFilter(IQueryBuilder &$query, bool $aggregate): vo $query->andWhere($query->expr()->eq('m.isvideo', $query->expr()->literal(1))); } + public function transformLivePhotoFilter(IQueryBuilder &$query, bool $aggregate): void + { + $query->andWhere($query->expr()->neq('m.liveid', $query->expr()->literal(''))); + } + + public function transformPanoFilter(IQueryBuilder &$query, bool $aggregate): void + { + $query->andWhere('m.w >= '.PANOROMA_ASPECT_RATIO.' * m.h'); + } + public function transformLimit(IQueryBuilder &$query, bool $aggregate, int $limit): void { /** @psalm-suppress RedundantCondition */ diff --git a/src/components/Explore.vue b/src/components/Explore.vue index c8b5f63ee..b1004041f 100644 --- a/src/components/Explore.vue +++ b/src/components/Explore.vue @@ -54,6 +54,8 @@ import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'; import FolderIcon from 'vue-material-design-icons/Folder.vue'; import StarIcon from 'vue-material-design-icons/Star.vue'; import VideoIcon from 'vue-material-design-icons/PlayCircle.vue'; +import LivePhotoIcon from './icons/LivePhoto.vue'; +import PanoramaVariantIcon from 'vue-material-design-icons/PanoramaVariant.vue'; import ArchiveIcon from 'vue-material-design-icons/PackageDown.vue'; import CalendarIcon from 'vue-material-design-icons/Calendar.vue'; import MapIcon from 'vue-material-design-icons/Map.vue'; @@ -103,6 +105,16 @@ export default defineComponent({ icon: VideoIcon, link: '/videos', }, + { + name: t('memories', 'Live photos'), + icon: LivePhotoIcon, + link: '/livephotos', + }, + { + name: t('memories', 'Panoramas'), + icon: PanoramaVariantIcon, + link: '/panoramas', + }, { name: t('memories', 'Archive'), icon: ArchiveIcon, diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue index 0fcf9134e..32ccccc08 100644 --- a/src/components/Timeline.vue +++ b/src/components/Timeline.vue @@ -620,6 +620,16 @@ export default defineComponent({ set(DaysFilterType.VIDEOS); } + // Live photos + if (this.routeIsLivePhotos) { + set(DaysFilterType.LIVE); + } + + // Panoramas + if (this.routeIsPanoramas) { + set(DaysFilterType.PANO); + } + // Folder if (this.routeIsFolders || this.routeIsFolderShare) { const path = utils.getFolderRoutePath(this.config.folders_path); diff --git a/src/router.ts b/src/router.ts index 015a9c6ad..3d54f818d 100644 --- a/src/router.ts +++ b/src/router.ts @@ -18,6 +18,8 @@ export type RouteId = | 'Folders' | 'Favorites' | 'Videos' + | 'LivePhotos' + | 'Panoramas' | 'Albums' | 'Archive' | 'ThisDay' @@ -60,6 +62,20 @@ export const routes: { [key in RouteId]: RouteConfig } = { props: (route: Route) => ({ rootTitle: t('memories', 'Videos') }), }, + Panoramas: { + path: '/panoramas', + component: Timeline, + name: 'panoramas', + props: (route: Route) => ({ rootTitle: t('memories', 'Panoramas') }), + }, + + LivePhotos: { + path: '/livephotos', + component: Timeline, + name: 'livephotos', + props: (route: Route) => ({ rootTitle: t('memories', 'Live photos') }), + }, + Albums: { path: '/albums/:user?/:name?', component: ClusterView, diff --git a/src/services/API.ts b/src/services/API.ts index effb62a0f..a58a71284 100644 --- a/src/services/API.ts +++ b/src/services/API.ts @@ -20,6 +20,8 @@ function tok(url: string) { export const enum DaysFilterType { FAVORITES = 'fav', VIDEOS = 'vid', + LIVE = 'live', + PANO = 'pano', FOLDER = 'folder', ARCHIVE = 'archive', ALBUM = 'albums', diff --git a/src/services/strings.ts b/src/services/strings.ts index aa40a02d1..14e5bdd9e 100644 --- a/src/services/strings.ts +++ b/src/services/strings.ts @@ -41,6 +41,10 @@ export function viewName(routeName: string): string { return t('memories', 'People'); case _m.routes.Videos.name: return t('memories', 'Videos'); + case _m.routes.LivePhotos.name: + return t('memories', 'Live photos'); + case _m.routes.Panoramas.name: + return t('memories', 'Panoramas'); case _m.routes.Albums.name: return t('memories', 'Albums'); case _m.routes.Archive.name: