Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add separate categories for "live photos" and "panoramas" and support for 360 PhotoSphere viewer #1357

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
10 changes: 10 additions & 0 deletions lib/Controller/DaysController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
20 changes: 20 additions & 0 deletions lib/Controller/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
15 changes: 15 additions & 0 deletions lib/Db/TimelineQueryFilters.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 */
Expand Down
45 changes: 45 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
"@nextcloud/sharing": "^0.2.3",
"@nextcloud/upload": "1.6.0",
"@nextcloud/vue": "^8.19.0",
"@photo-sphere-viewer/autorotate-plugin": "^5.11.1",
"@photo-sphere-viewer/core": "^5.11.1",
"filerobot-image-editor": "^4.8.1",
"fuse.js": "^7.0.0",
"hammerjs": "^2.0.8",
Expand Down
12 changes: 12 additions & 0 deletions src/components/Explore.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions src/components/Timeline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
146 changes: 146 additions & 0 deletions src/components/viewer/PhotoSphere.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<template>
<div id="viewer" class="viewer__photosphere top-left fill-block">
<NcButton
v-if="!hideCloseButton"
id="close-photosphere-viewer"
:ariaLabel="t('memories', 'Close')"
:title="t('memories', 'Close')"
type="tertiary"
@click="close"
>
<CloseThickIcon :size="20" />
</NcButton>
</div>
</template>

<script lang="ts">
import { defineComponent, type PropType } from 'vue';

import { API } from '@services/API';
import type { IPhoto } from '@typings';
import * as utils from '@services/utils';

import NcButton from '@nextcloud/vue/dist/Components/NcButton.js';
import CloseThickIcon from 'vue-material-design-icons/CloseThick.vue';

import { Viewer } from '@photo-sphere-viewer/core';
import { AutorotatePlugin } from '@photo-sphere-viewer/autorotate-plugin';
import { events } from '@photo-sphere-viewer/core';
import '@photo-sphere-viewer/core/index.css';

export default defineComponent({
props: {
photo: {
type: Object as PropType<IPhoto>,
required: true,
},
},

components: {
NcButton,
CloseThickIcon,
},

emits: {
close: () => true,
},

data: () => ({
viewer: null as Viewer | null,
hideCloseButton: false,
}),

async mounted() {
// Create the photosphere viewer
console.assert(document.getElementById('viewer'), 'PhotoSphere container not found');
this.viewer = new Viewer({
container: 'viewer',
panorama: API.IMAGE_DECODABLE(this.photo.fileid, this.photo.etag),
caption: this.exifTitle() + this.exifDate(),
description: this.exifDesc(),
navbar: ['autorotate', 'zoom', 'move', 'description', 'caption', 'fullscreen'],
plugins: [
[
AutorotatePlugin,
{
autorotatePitch: '5deg',
autostartOnIdle: false,
autostartDelay: null,
},
],
],
});

// Hide the close button when the PhotoSphere panel is open
this.viewer.addEventListener('show-panel', ({ panelId: string }) => {
this.hideCloseButton = true;
});

this.viewer.addEventListener('hide-panel', ({ panelId: string }) => {
this.hideCloseButton = false;
});

// Handle keyboard
window.addEventListener('keydown', this.handleKeydown, true);
},

beforeDestroy() {
this.close();
},

methods: {
exifTitle(): string {
const title = this.photo?.imageInfo?.exif?.Title;
if (title) return '<b>' + title + '</b> — ';
return '';
},

exifDesc(): string | undefined {
const desc = this.photo?.imageInfo?.exif?.Description;
return desc;
},

exifDate(): string {
const date = this.photo?.imageInfo?.datetaken;
if (!date) return '';
return utils.getLongDateStr(new Date(date * 1000), false, true);
},

close() {
window.removeEventListener('keydown', this.handleKeydown, true);
this.$emit('close');
},

handleKeydown(event: KeyboardEvent) {
event.stopImmediatePropagation();

if (event.key === 'Escape') {
event.preventDefault();
this.close();
}
},
},
});
</script>

<style lang="scss">
// Take full screen size
.viewer__photosphere {
z-index: 10100;
background-color: black;

box-sizing: content-box;
.psv-container,
.psv-container * {
box-sizing: content-box !important;
}

// Overlay top-right close button
#close-photosphere-viewer {
position: absolute;
right: 0;
top: 0;
z-index: 9999;
}
}
</style>
Loading