From 3f9c8a383e0a530491703065919ea424252993dd Mon Sep 17 00:00:00 2001 From: Braun Patrik Date: Sun, 9 Jul 2017 22:00:42 +0200 Subject: [PATCH] implementing Information panel for showing Exif info at lightbox --- README.md | 2 +- backend/middlewares/user/AuthenticationMWs.ts | 2 +- backend/model/mysql/enitites/PhotoEntity.ts | 57 ++++---- backend/model/threading/DiskMangerWorker.ts | 8 +- common/entities/PhotoDTO.ts | 1 + frontend/app/app.module.ts | 2 + .../info-panel.lightbox.gallery.component.css | 38 ++++++ ...info-panel.lightbox.gallery.component.html | 94 +++++++++++++ .../info-panel.lightbox.gallery.component.ts | 84 ++++++++++++ .../lightbox/lightbox.gallery.component.css | 125 +++++++++--------- .../lightbox/lightbox.gallery.component.html | 89 +++++++------ .../lightbox/lightbox.gallery.component.ts | 67 +++++++++- 12 files changed, 434 insertions(+), 135 deletions(-) create mode 100644 frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.css create mode 100644 frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.html create mode 100644 frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.ts diff --git a/README.md b/README.md index 8da413d3..9db04ec3 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ To configure it. Run `PiGallery2` first to create `config.json` file, then edit * Custom lightbox for full screen photo viewing * keyboard support for navigation * showing low-res thumbnail while full image loads - * Information panel for showing **Exif info** - `In progress` + * Information panel for showing **Exif info** * Client side caching (directories and search results) * Rendering **photos** with GPS coordinates **on google map** * .gpx file support - `future plan` diff --git a/backend/middlewares/user/AuthenticationMWs.ts b/backend/middlewares/user/AuthenticationMWs.ts index b3291fca..108e6558 100644 --- a/backend/middlewares/user/AuthenticationMWs.ts +++ b/backend/middlewares/user/AuthenticationMWs.ts @@ -34,7 +34,7 @@ export class AuthenticationMWs { public static async authenticate(req: Request, res: Response, next: NextFunction) { if (Config.Client.authenticationRequired === false) { - req.session.user = {name: "", role: UserRoles.Admin}; + req.session.user = {name: "Admin", role: UserRoles.Admin}; return next(); } try { diff --git a/backend/model/mysql/enitites/PhotoEntity.ts b/backend/model/mysql/enitites/PhotoEntity.ts index 4b6b467a..46e1421f 100644 --- a/backend/model/mysql/enitites/PhotoEntity.ts +++ b/backend/model/mysql/enitites/PhotoEntity.ts @@ -1,32 +1,32 @@ -import {Entity, EmbeddableEntity, Column, Embedded, PrimaryGeneratedColumn, ManyToOne} from "typeorm"; +import {Column, EmbeddableEntity, Embedded, Entity, ManyToOne, PrimaryGeneratedColumn} from "typeorm"; import {DirectoryDTO} from "../../../../common/entities/DirectoryDTO"; import { - PhotoDTO, - PhotoMetadata, - CameraMetadata, - ImageSize, - PositionMetaData + CameraMetadata, + ImageSize, + PhotoDTO, + PhotoMetadata, + PositionMetaData } from "../../../../common/entities/PhotoDTO"; import {DirectoryEntity} from "./DirectoryEntity"; @Entity() export class PhotoEntity implements PhotoDTO { - @PrimaryGeneratedColumn() - id: number; + @PrimaryGeneratedColumn() + id: number; - @Column("string") - name: string; + @Column("string") + name: string; - @ManyToOne(type => DirectoryEntity, directory => directory.photos) - directory: DirectoryDTO; + @ManyToOne(type => DirectoryEntity, directory => directory.photos) + directory: DirectoryDTO; - @Embedded(type => PhotoMetadataEntity) - metadata: PhotoMetadataEntity; + @Embedded(type => PhotoMetadataEntity) + metadata: PhotoMetadataEntity; - readyThumbnails: Array = []; + readyThumbnails: Array = []; - readyIcon: boolean = false; + readyIcon: boolean = false; } @@ -34,20 +34,23 @@ export class PhotoEntity implements PhotoDTO { @EmbeddableEntity() export class PhotoMetadataEntity implements PhotoMetadata { - @Column("string") - keywords: Array; + @Column("string") + keywords: Array; - @Column("string") - cameraData: CameraMetadata; + @Column("string") + cameraData: CameraMetadata; - @Column("string") - positionData: PositionMetaData; + @Column("string") + positionData: PositionMetaData; - @Column("string") - size: ImageSize; + @Column("string") + size: ImageSize; - @Column("number") - creationDate: number; + @Column("number") + creationDate: number; + + @Column("number") + fileSize: number; } /* @@ -113,4 +116,4 @@ export class PhotoMetadataEntity implements PhotoMetadata { @Column("int") height: number; - }*/ \ No newline at end of file + }*/ diff --git a/backend/model/threading/DiskMangerWorker.ts b/backend/model/threading/DiskMangerWorker.ts index d8a064a2..290a0394 100644 --- a/backend/model/threading/DiskMangerWorker.ts +++ b/backend/model/threading/DiskMangerWorker.ts @@ -32,7 +32,7 @@ export class DiskMangerWorker { private static loadPhotoMetadata(fullPath: string): Promise { return new Promise((resolve, reject) => { - fs.readFile(fullPath, function (err, data) { + fs.readFile(fullPath, (err, data) => { if (err) { return reject({file: fullPath, error: err}); } @@ -41,11 +41,15 @@ export class DiskMangerWorker { cameraData: {}, positionData: null, size: {}, - creationDate: 0 + creationDate: 0, + fileSize: 0 }; try { + fs.stat(fullPath, (err, data) => { + metadata.fileSize = data.size; + }); try { const exif = exif_parser.create(data).parse(); metadata.cameraData = { diff --git a/common/entities/PhotoDTO.ts b/common/entities/PhotoDTO.ts index aee529cd..c82658d7 100644 --- a/common/entities/PhotoDTO.ts +++ b/common/entities/PhotoDTO.ts @@ -15,6 +15,7 @@ export interface PhotoMetadata { positionData: PositionMetaData; size: ImageSize; creationDate: number; + fileSize: number; } export interface ImageSize { diff --git a/frontend/app/app.module.ts b/frontend/app/app.module.ts index 84ef4859..8435732e 100644 --- a/frontend/app/app.module.ts +++ b/frontend/app/app.module.ts @@ -44,6 +44,7 @@ import {NotificationService} from "./model/notification.service"; import {ClipboardModule} from "ngx-clipboard"; import {NavigationService} from "./model/navigation.service"; +import {InfoPanelLightboxComponent} from "./gallery/lightbox/infopanel/info-panel.lightbox.gallery.component"; @Injectable() export class GoogleMapsConfig { @@ -86,6 +87,7 @@ export class GoogleMapsConfig { GalleryNavigatorComponent, GalleryPhotoComponent, AdminComponent, + InfoPanelLightboxComponent, //Settings UserMangerSettingsComponent, DatabaseSettingsComponent, diff --git a/frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.css b/frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.css new file mode 100644 index 00000000..87c2e943 --- /dev/null +++ b/frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.css @@ -0,0 +1,38 @@ +.content { + background-color: #F7F7F7; + height: 100%; +} + +.row { + margin-left: 0; + margin-right: 0; + padding: 10px; +} + +.details-icon { + margin-top: 10px; + font-size: x-large; +} + +.details-main { + font-size: x-large; +} + +.details-sub { + color: #555; +} + +.details-sub div { + padding-left: 5px; + padding-right: 5px; +} + +.sebm-google-map-container { + width: 100%; + height: 100%; +} + +#map { + width: 400px; + height: 400px; +} diff --git a/frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.html b/frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.html new file mode 100644 index 00000000..9e403072 --- /dev/null +++ b/frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.html @@ -0,0 +1,94 @@ +
+
+
+

Info

+
+
+
+
+ +
+
+
+ {{photo.name}} +
+
+
{{photo.metadata.size.width}} x {{photo.metadata.size.height}}
+
{{calcMpx()}}MP
+
{{calcFileSize()}}
+
+
+
+ +
+
+ +
+
+
+ + {{getYear()}} + + {{getDate()}} +
+
+
{{getDay()}}, {{getTime()}}
+
+
+
+ +
+
+ +
+
+
+ {{photo.metadata.cameraData.model || "Camera"}} +
+
+
ISO{{photo.metadata.cameraData.ISO}}
+
f/{{photo.metadata.cameraData.fStop}}
+
+ {{toFraction(photo.metadata.cameraData.exposure)}}s +
+
+ {{photo.metadata.cameraData.focalLength}}mm +
+
{{photo.metadata.cameraData.lens}}
+
+
+
+ +
+
+ +
+
+
+ {{getPositionText() || "Position"}} +
+
+
+ {{photo.metadata.positionData.GPSData.latitude.toFixed(3)}}, + {{photo.metadata.positionData.GPSData.longitude.toFixed(3)}} +
+
+
+
+ +
+ + + + +
+
diff --git a/frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.ts b/frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.ts new file mode 100644 index 00000000..a36583e0 --- /dev/null +++ b/frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.ts @@ -0,0 +1,84 @@ +import {Component, Input} from "@angular/core"; +import {PhotoDTO} from "../../../../../common/entities/PhotoDTO"; + +@Component({ + selector: 'info-panel', + styleUrls: ['./info-panel.lightbox.gallery.component.css'], + templateUrl: './info-panel.lightbox.gallery.component.html', +}) +export class InfoPanelLightboxComponent { + @Input() photo: PhotoDTO; + + + constructor() { + } + + calcMpx() { + return (this.photo.metadata.size.width * this.photo.metadata.size.height / 1000000).toFixed(2); + } + + calcFileSize() { + let postFixes = ["B", "KB", "MB", "GB", "TB"]; + let index = 0; + let size = this.photo.metadata.fileSize; + while (size > 1000 && index < postFixes.length - 1) { + size /= 1000; + index++; + } + return size.toFixed(2) + postFixes[index]; + } + + getCurrentYear() { + return (new Date()).getFullYear(); + } + + getYear() { + const date = new Date(this.photo.metadata.creationDate); + return date.getFullYear(); + } + + getDate() { + const date = new Date(this.photo.metadata.creationDate); + let locale = "en-us"; + return date.toLocaleString(locale, {month: "long"}) + " " + date.getDay(); + } + + getTime() { + const date = new Date(this.photo.metadata.creationDate); + return date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds(); + } + + getDay() { + const date = new Date(this.photo.metadata.creationDate); + let locale = "en-us"; + return date.toLocaleString(locale, {weekday: "long"}); + } + + toFraction(f) { + if (f > 1) { + return f; + } + return "1/" + Math.ceil(((f < 1.0) ? f : (f % Math.floor(f))) * 10000) + } + + hasGPS() { + return this.photo.metadata.positionData && this.photo.metadata.positionData.GPSData && + this.photo.metadata.positionData.GPSData.latitude && this.photo.metadata.positionData.GPSData.longitude + } + + getPositionText(): string { + if (!this.photo.metadata.positionData) { + return ""; + } + let str = this.photo.metadata.positionData.city || + this.photo.metadata.positionData.state; + + if (str.length != 0) { + str += ", "; + } + str += this.photo.metadata.positionData.country; + + return str; + } +} + diff --git a/frontend/app/gallery/lightbox/lightbox.gallery.component.css b/frontend/app/gallery/lightbox/lightbox.gallery.component.css index 179d531a..08b19c36 100644 --- a/frontend/app/gallery/lightbox/lightbox.gallery.component.css +++ b/frontend/app/gallery/lightbox/lightbox.gallery.component.css @@ -1,89 +1,96 @@ .lightbox { - position: fixed; /* Stay in place */ - z-index: 1100; /* Sit on top */ - left: 0; - top: 0; - width: 100%; /* Full width */ - height: 100%; /* Full height */ - overflow: hidden; - display: flex; /* add */ - justify-content: center; /* add to align horizontal */ - align-items: center; /* add to align vertical */ - cursor: pointer; - transition: all 0.3s ease-in-out; + position: fixed; /* Stay in place */ + z-index: 1100; /* Sit on top */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: hidden; + display: flex; /* add */ + justify-content: center; /* add to align horizontal */ + align-items: center; /* add to align vertical */ + cursor: pointer; + transition: all 0.3s ease-in-out; } gallery-lightbox-photo { - transition: all 0.3s ease-in-out; - overflow: hidden; + transition: all 0.3s ease-in-out; + overflow: hidden; } -.blackCanvas{ - position: fixed; /* Stay in place */ - z-index: 1099; /* Sit on top */ - left: 0; - top: 0; - width: 100%; /* Full width */ - height: 100%; /* Full height */ - background-color: black; - transition: all 0.3s ease-in-out; +.blackCanvas { + position: fixed; /* Stay in place */ + z-index: 1099; /* Sit on top */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + background-color: black; + transition: all 0.3s ease-in-out; } - - .navigation-arrow { - width: 30%; - height: 100%; - position: static; - display: inline-block; - padding: 15px; - cursor: pointer; - font-size: x-large; + width: 30%; + height: 100%; + position: static; + display: inline-block; + padding: 15px; + cursor: pointer; + font-size: x-large; } .navigation-arrow span { - top: 43%; + top: 43%; } #controllers-container { - z-index: 1100; - width: 100%; - height: 100%; - left: 0; - top: 0; - position: fixed;; - color: white; + z-index: 1100; + width: 100%; + height: 100%; + left: 0; + top: 0; + position: fixed; + color: white; + transition: all 0.3s ease-in-out; } #rightArrow { - float: right; - text-align: right; + float: right; + text-align: right; } #controls { - top: 0; - height: initial; - text-align: right; - width: 100%; - padding: 5px; - font-size: large; + top: 0; + height: initial; + text-align: right; + width: 100%; + padding: 5px; + font-size: large; } #controls span { - margin-left: 6px; - margin-right: 6px; - color: white; - cursor: pointer; + margin-left: 6px; + margin-right: 6px; + color: white; + cursor: pointer; } .highlight { - opacity: 0.4; - transition: opacity .2s ease-out; - -moz-transition: opacity .2s ease-out; - -webkit-transition: opacity .2s ease-out; - -o-transition: opacity .2s ease-out; + opacity: 0.4; + transition: opacity .2s ease-out; + -moz-transition: opacity .2s ease-out; + -webkit-transition: opacity .2s ease-out; + -o-transition: opacity .2s ease-out; } .highlight:hover { - opacity: 1.0; -} \ No newline at end of file + opacity: 1.0; +} + +info-panel { + z-index: 1100; /* Sit on top */ + position: fixed; + height: 100vh; + right: 0; + transition: all 0.3s ease-in-out; +} diff --git a/frontend/app/gallery/lightbox/lightbox.gallery.component.html b/frontend/app/gallery/lightbox/lightbox.gallery.component.html index f5dcb50b..c013e606 100644 --- a/frontend/app/gallery/lightbox/lightbox.gallery.component.html +++ b/frontend/app/gallery/lightbox/lightbox.gallery.component.html @@ -1,47 +1,54 @@
-
+
+
+ + + + +
+
+ + + + + + + +
- + + +
+ - -
-
- - - - - - - - -
- - - -
+
diff --git a/frontend/app/gallery/lightbox/lightbox.gallery.component.ts b/frontend/app/gallery/lightbox/lightbox.gallery.component.ts index 3bcfda8d..f21ef059 100644 --- a/frontend/app/gallery/lightbox/lightbox.gallery.component.ts +++ b/frontend/app/gallery/lightbox/lightbox.gallery.component.ts @@ -4,6 +4,7 @@ import { ElementRef, EventEmitter, HostListener, + OnDestroy, Output, QueryList, ViewChild @@ -20,8 +21,9 @@ import {Subscription} from "rxjs"; styleUrls: ['./lightbox.gallery.component.css'], templateUrl: './lightbox.gallery.component.html', }) -export class GalleryLightboxComponent { +export class GalleryLightboxComponent implements OnDestroy { @Output('onLastElement') onLastElement = new EventEmitter(); + @ViewChild("root") elementRef: ElementRef; public navigation = {hasPrev: true, hasNext: true}; public photoDimension: Dimension = {top: 0, left: 0, width: 0, height: 0}; @@ -35,14 +37,23 @@ export class GalleryLightboxComponent { public visible = false; private changeSubscription: Subscription = null; - @ViewChild("root") elementRef: ElementRef; + + public infoPanelVisible = false; + public infoPanelWidth = 0; + public contentWidth = 0; constructor(public fullScreenService: FullScreenService, private changeDetector: ChangeDetectorRef, private overlayService: OverlayService) { } - //noinspection JSUnusedGlobalSymbols + ngOnDestroy(): void { + if (this.changeSubscription != null) { + this.changeSubscription.unsubscribe(); + } + } + +//noinspection JSUnusedGlobalSymbols @HostListener('window:resize', ['$event']) onResize() { if (this.activePhoto) { @@ -130,11 +141,13 @@ export class GalleryLightboxComponent { }; this.blackCanvasOpacity = 1.0; this.showPhoto(this.gridPhotoQL.toArray().indexOf(selectedPhoto)); + this.contentWidth = this.getScreenWidth(); }, 0); } public hide() { this.enableAnimation(); + this.hideInfoPanel(); this.fullScreenService.exitFullScreen(); this.lightboxDimension = this.activePhoto.getDimension(); @@ -196,6 +209,52 @@ export class GalleryLightboxComponent { } } + iPvisibilityTimer = null; + + public toggleInfoPanel() { + + + if (this.infoPanelWidth != 400) { + this.showInfoPanel(); + } else { + this.hideInfoPanel(); + } + + } + + recalcPositions() { + + this.photoDimension = this.calcLightBoxPhotoDimension(this.activePhoto.gridPhoto.photo); + this.contentWidth = this.getScreenWidth(); + this.lightboxDimension = { + top: 0, + left: 0, + width: this.getScreenWidth(), + height: this.getScreenHeight() + }; + }; + + showInfoPanel() { + this.infoPanelVisible = true; + this.infoPanelWidth = 0; + setTimeout(() => { + this.infoPanelWidth = 400; + this.recalcPositions(); + }, 0); + if (this.iPvisibilityTimer != null) { + clearTimeout(this.iPvisibilityTimer); + } + } + + hideInfoPanel() { + this.infoPanelWidth = 0; + this.iPvisibilityTimer = setTimeout(() => { + this.iPvisibilityTimer = null; + this.infoPanelVisible = false; + }, 1000); + this.recalcPositions(); + } + private enableAnimation() { this.transition = null; } @@ -214,7 +273,7 @@ export class GalleryLightboxComponent { } private getScreenWidth() { - return window.innerWidth; + return Math.max(window.innerWidth - this.infoPanelWidth, 0); } private getScreenHeight() {