1
0
mirror of https://github.com/xuthus83/pigallery2.git synced 2025-01-14 14:43:17 +08:00

implementing Information panel for showing Exif info at lightbox

This commit is contained in:
Braun Patrik 2017-07-09 22:00:42 +02:00
parent 2c315d7bd5
commit 3f9c8a383e
12 changed files with 434 additions and 135 deletions

View File

@ -65,7 +65,7 @@ To configure it. Run `PiGallery2` first to create `config.json` file, then edit
* Custom lightbox for full screen photo viewing * Custom lightbox for full screen photo viewing
* keyboard support for navigation * keyboard support for navigation
* showing low-res thumbnail while full image loads * 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) * Client side caching (directories and search results)
* Rendering **photos** with GPS coordinates **on google map** * Rendering **photos** with GPS coordinates **on google map**
* .gpx file support - `future plan` * .gpx file support - `future plan`

View File

@ -34,7 +34,7 @@ export class AuthenticationMWs {
public static async authenticate(req: Request, res: Response, next: NextFunction) { public static async authenticate(req: Request, res: Response, next: NextFunction) {
if (Config.Client.authenticationRequired === false) { if (Config.Client.authenticationRequired === false) {
req.session.user = <UserDTO>{name: "", role: UserRoles.Admin}; req.session.user = <UserDTO>{name: "Admin", role: UserRoles.Admin};
return next(); return next();
} }
try { try {

View File

@ -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 {DirectoryDTO} from "../../../../common/entities/DirectoryDTO";
import { import {
PhotoDTO, CameraMetadata,
PhotoMetadata, ImageSize,
CameraMetadata, PhotoDTO,
ImageSize, PhotoMetadata,
PositionMetaData PositionMetaData
} from "../../../../common/entities/PhotoDTO"; } from "../../../../common/entities/PhotoDTO";
import {DirectoryEntity} from "./DirectoryEntity"; import {DirectoryEntity} from "./DirectoryEntity";
@Entity() @Entity()
export class PhotoEntity implements PhotoDTO { export class PhotoEntity implements PhotoDTO {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@Column("string") @Column("string")
name: string; name: string;
@ManyToOne(type => DirectoryEntity, directory => directory.photos) @ManyToOne(type => DirectoryEntity, directory => directory.photos)
directory: DirectoryDTO; directory: DirectoryDTO;
@Embedded(type => PhotoMetadataEntity) @Embedded(type => PhotoMetadataEntity)
metadata: PhotoMetadataEntity; metadata: PhotoMetadataEntity;
readyThumbnails: Array<number> = []; readyThumbnails: Array<number> = [];
readyIcon: boolean = false; readyIcon: boolean = false;
} }
@ -34,20 +34,23 @@ export class PhotoEntity implements PhotoDTO {
@EmbeddableEntity() @EmbeddableEntity()
export class PhotoMetadataEntity implements PhotoMetadata { export class PhotoMetadataEntity implements PhotoMetadata {
@Column("string") @Column("string")
keywords: Array<string>; keywords: Array<string>;
@Column("string") @Column("string")
cameraData: CameraMetadata; cameraData: CameraMetadata;
@Column("string") @Column("string")
positionData: PositionMetaData; positionData: PositionMetaData;
@Column("string") @Column("string")
size: ImageSize; size: ImageSize;
@Column("number") @Column("number")
creationDate: number; creationDate: number;
@Column("number")
fileSize: number;
} }
/* /*
@ -113,4 +116,4 @@ export class PhotoMetadataEntity implements PhotoMetadata {
@Column("int") @Column("int")
height: number; height: number;
}*/ }*/

View File

@ -32,7 +32,7 @@ export class DiskMangerWorker {
private static loadPhotoMetadata(fullPath: string): Promise<PhotoMetadata> { private static loadPhotoMetadata(fullPath: string): Promise<PhotoMetadata> {
return new Promise<PhotoMetadata>((resolve, reject) => { return new Promise<PhotoMetadata>((resolve, reject) => {
fs.readFile(fullPath, function (err, data) { fs.readFile(fullPath, (err, data) => {
if (err) { if (err) {
return reject({file: fullPath, error: err}); return reject({file: fullPath, error: err});
} }
@ -41,11 +41,15 @@ export class DiskMangerWorker {
cameraData: {}, cameraData: {},
positionData: null, positionData: null,
size: {}, size: {},
creationDate: 0 creationDate: 0,
fileSize: 0
}; };
try { try {
fs.stat(fullPath, (err, data) => {
metadata.fileSize = data.size;
});
try { try {
const exif = exif_parser.create(data).parse(); const exif = exif_parser.create(data).parse();
metadata.cameraData = <CameraMetadata> { metadata.cameraData = <CameraMetadata> {

View File

@ -15,6 +15,7 @@ export interface PhotoMetadata {
positionData: PositionMetaData; positionData: PositionMetaData;
size: ImageSize; size: ImageSize;
creationDate: number; creationDate: number;
fileSize: number;
} }
export interface ImageSize { export interface ImageSize {

View File

@ -44,6 +44,7 @@ import {NotificationService} from "./model/notification.service";
import {ClipboardModule} from "ngx-clipboard"; import {ClipboardModule} from "ngx-clipboard";
import {NavigationService} from "./model/navigation.service"; import {NavigationService} from "./model/navigation.service";
import {InfoPanelLightboxComponent} from "./gallery/lightbox/infopanel/info-panel.lightbox.gallery.component";
@Injectable() @Injectable()
export class GoogleMapsConfig { export class GoogleMapsConfig {
@ -86,6 +87,7 @@ export class GoogleMapsConfig {
GalleryNavigatorComponent, GalleryNavigatorComponent,
GalleryPhotoComponent, GalleryPhotoComponent,
AdminComponent, AdminComponent,
InfoPanelLightboxComponent,
//Settings //Settings
UserMangerSettingsComponent, UserMangerSettingsComponent,
DatabaseSettingsComponent, DatabaseSettingsComponent,

View File

@ -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;
}

View File

@ -0,0 +1,94 @@
<div class="content">
<div class="row">
<div class="col-sm-12">
<h1>Info</h1>
</div>
</div>
<div class="row">
<div class="col-sm-2">
<span class="details-icon glyphicon glyphicon-picture"></span>
</div>
<div class="col-sm-10">
<div class="details-main">
{{photo.name}}
</div>
<div class="details-sub">
<div class="col-sm-4">{{photo.metadata.size.width}} x {{photo.metadata.size.height}}</div>
<div class="col-sm-4">{{calcMpx()}}MP</div>
<div class="col-sm-4" *ngIf="photo.metadata.fileSize">{{calcFileSize()}}</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-2">
<span class="details-icon glyphicon glyphicon-calendar"></span>
</div>
<div class="col-sm-10">
<div class="details-main">
<ng-container *ngIf="getYear() !== getCurrentYear()">
{{getYear()}}
</ng-container>
{{getDate()}}
</div>
<div class="details-sub">
<div class="col-sm-12">{{getDay()}}, {{getTime()}}</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-2">
<span class="details-icon glyphicon glyphicon-camera"></span>
</div>
<div class="col-sm-10">
<div class="details-main">
{{photo.metadata.cameraData.model || "Camera"}}
</div>
<div class="details-sub">
<div class="col-sm-3" *ngIf="photo.metadata.cameraData.ISO">ISO{{photo.metadata.cameraData.ISO}}</div>
<div class="col-sm-3" *ngIf="photo.metadata.cameraData.fStop">f/{{photo.metadata.cameraData.fStop}}</div>
<div class="col-sm-3" *ngIf="photo.metadata.cameraData.exposure">
{{toFraction(photo.metadata.cameraData.exposure)}}s
</div>
<div class="col-sm-3" *ngIf="photo.metadata.cameraData.focalLength">
{{photo.metadata.cameraData.focalLength}}mm
</div>
<div class="col-sm-12" *ngIf="photo.metadata.cameraData.lens">{{photo.metadata.cameraData.lens}}</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-2">
<span class="details-icon glyphicon glyphicon-map-marker"></span>
</div>
<div class="col-sm-10">
<div class="details-main">
{{getPositionText() || "Position"}}
</div>
<div class="details-sub">
<div class="col-sm-12"
*ngIf="hasGPS()">
{{photo.metadata.positionData.GPSData.latitude.toFixed(3)}},
{{photo.metadata.positionData.GPSData.longitude.toFixed(3)}}
</div>
</div>
</div>
</div>
<div id="map" *ngIf="hasGPS()">
<agm-map
[disableDefaultUI]="true"
[zoomControl]="false"
[streetViewControl]="false"
[zoom]="5"
[latitude]="photo.metadata.positionData.GPSData.latitude"
[longitude]="photo.metadata.positionData.GPSData.longitude">
<agm-marker
[latitude]="photo.metadata.positionData.GPSData.latitude"
[longitude]="photo.metadata.positionData.GPSData.longitude">
</agm-marker>
</agm-map>
</div>
</div>

View File

@ -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;
}
}

View File

@ -1,89 +1,96 @@
.lightbox { .lightbox {
position: fixed; /* Stay in place */ position: fixed; /* Stay in place */
z-index: 1100; /* Sit on top */ z-index: 1100; /* Sit on top */
left: 0; left: 0;
top: 0; top: 0;
width: 100%; /* Full width */ width: 100%; /* Full width */
height: 100%; /* Full height */ height: 100%; /* Full height */
overflow: hidden; overflow: hidden;
display: flex; /* add */ display: flex; /* add */
justify-content: center; /* add to align horizontal */ justify-content: center; /* add to align horizontal */
align-items: center; /* add to align vertical */ align-items: center; /* add to align vertical */
cursor: pointer; cursor: pointer;
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
} }
gallery-lightbox-photo { gallery-lightbox-photo {
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
overflow: hidden; overflow: hidden;
} }
.blackCanvas{ .blackCanvas {
position: fixed; /* Stay in place */ position: fixed; /* Stay in place */
z-index: 1099; /* Sit on top */ z-index: 1099; /* Sit on top */
left: 0; left: 0;
top: 0; top: 0;
width: 100%; /* Full width */ width: 100%; /* Full width */
height: 100%; /* Full height */ height: 100%; /* Full height */
background-color: black; background-color: black;
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
} }
.navigation-arrow { .navigation-arrow {
width: 30%; width: 30%;
height: 100%; height: 100%;
position: static; position: static;
display: inline-block; display: inline-block;
padding: 15px; padding: 15px;
cursor: pointer; cursor: pointer;
font-size: x-large; font-size: x-large;
} }
.navigation-arrow span { .navigation-arrow span {
top: 43%; top: 43%;
} }
#controllers-container { #controllers-container {
z-index: 1100; z-index: 1100;
width: 100%; width: 100%;
height: 100%; height: 100%;
left: 0; left: 0;
top: 0; top: 0;
position: fixed;; position: fixed;
color: white; color: white;
transition: all 0.3s ease-in-out;
} }
#rightArrow { #rightArrow {
float: right; float: right;
text-align: right; text-align: right;
} }
#controls { #controls {
top: 0; top: 0;
height: initial; height: initial;
text-align: right; text-align: right;
width: 100%; width: 100%;
padding: 5px; padding: 5px;
font-size: large; font-size: large;
} }
#controls span { #controls span {
margin-left: 6px; margin-left: 6px;
margin-right: 6px; margin-right: 6px;
color: white; color: white;
cursor: pointer; cursor: pointer;
} }
.highlight { .highlight {
opacity: 0.4; opacity: 0.4;
transition: opacity .2s ease-out; transition: opacity .2s ease-out;
-moz-transition: opacity .2s ease-out; -moz-transition: opacity .2s ease-out;
-webkit-transition: opacity .2s ease-out; -webkit-transition: opacity .2s ease-out;
-o-transition: opacity .2s ease-out; -o-transition: opacity .2s ease-out;
} }
.highlight:hover { .highlight:hover {
opacity: 1.0; opacity: 1.0;
} }
info-panel {
z-index: 1100; /* Sit on top */
position: fixed;
height: 100vh;
right: 0;
transition: all 0.3s ease-in-out;
}

View File

@ -1,47 +1,54 @@
<div [hidden]="!visible" #root> <div [hidden]="!visible" #root>
<div class="blackCanvas" <div class="blackCanvas"
[style.opacity]="blackCanvasOpacity"> [style.opacity]="blackCanvasOpacity">
</div>
<div class="lightbox"
[style.width.px]="lightboxDimension.width"
[style.height.px]="lightboxDimension.height"
[style.top.px]="lightboxDimension.top"
[style.left.px]="lightboxDimension.left">
<gallery-lightbox-photo [gridPhoto]="activePhoto ? activePhoto.gridPhoto : null"
[style.top.px]="photoDimension.top"
[style.left.px]="photoDimension.left"
[style.width.px]="photoDimension.width"
[style.height.px]="photoDimension.height"
[style.transition]="transition">
</gallery-lightbox-photo>
</div>
<div id="controllers-container" #controls
[style.width.px]="contentWidth">
<div id="controls">
<a *ngIf="activePhoto" [href]="activePhoto.gridPhoto.getPhotoPath()"
[download]="activePhoto.gridPhoto.photo.name"><span class="glyphicon glyphicon-download-alt highlight"
title="download"></span></a>
<span class="glyphicon glyphicon-info-sign highlight" (click)="toggleInfoPanel()" title="info"></span>
<span class=" glyphicon glyphicon-resize-small highlight"
*ngIf="fullScreenService.isFullScreenEnabled()"
(click)="fullScreenService.exitFullScreen()" title="toggle fullscreen"></span>
<span class="glyphicon glyphicon-fullscreen highlight"
*ngIf="!fullScreenService.isFullScreenEnabled()"
(click)="fullScreenService.showFullScreen(root)" title="toggle fullscreen"></span>
<span class="glyphicon glyphicon-remove highlight" (click)="hide()" title="close"></span>
</div> </div>
<div class="lightbox" <div class="navigation-arrow highlight" *ngIf="navigation.hasPrev" title="key: left arrow" id="leftArrow"
[style.width.px]="lightboxDimension.width" (click)="prevImage()"><span
[style.height.px]="lightboxDimension.height" class="glyphicon glyphicon-chevron-left"></span></div>
[style.top.px]="lightboxDimension.top" <div class="navigation-arrow highlight" *ngIf="navigation.hasNext" title="key: right arrow" id="rightArrow"
[style.left.px]="lightboxDimension.left"> (click)="nextImage()"><span
<gallery-lightbox-photo [gridPhoto]="activePhoto ? activePhoto.gridPhoto : null" class="glyphicon glyphicon-chevron-right"></span></div>
[style.top.px]="photoDimension.top" </div>
[style.left.px]="photoDimension.left" <info-panel *ngIf="activePhoto && infoPanelVisible"
[style.width.px]="photoDimension.width" id="info-panel"
[style.height.px]="photoDimension.height" [style.width.px]="infoPanelWidth"
[style.transition]="transition"> [photo]="activePhoto.gridPhoto.photo">
</gallery-lightbox-photo>
</div>
</info-panel>
<div id="controllers-container" #controls>
<div id="controls">
<a *ngIf="activePhoto" [href]="activePhoto.gridPhoto.getPhotoPath()"
[download]="activePhoto.gridPhoto.photo.name"><span class="glyphicon glyphicon-download-alt highlight"
title="download"></span></a>
<span class="glyphicon glyphicon-info-sign highlight" title="info"></span>
<span class=" glyphicon glyphicon-resize-small highlight"
*ngIf="fullScreenService.isFullScreenEnabled()"
(click)="fullScreenService.exitFullScreen()" title="toggle fullscreen"></span>
<span class="glyphicon glyphicon-fullscreen highlight"
*ngIf="!fullScreenService.isFullScreenEnabled()"
(click)="fullScreenService.showFullScreen(root)" title="toggle fullscreen"></span>
<span class="glyphicon glyphicon-remove highlight" (click)="hide()" title="close"></span>
</div>
<div class="navigation-arrow highlight" *ngIf="navigation.hasPrev" title="key: left arrow" id="leftArrow"
(click)="prevImage()"><span
class="glyphicon glyphicon-chevron-left"></span></div>
<div class="navigation-arrow highlight" *ngIf="navigation.hasNext" title="key: right arrow" id="rightArrow"
(click)="nextImage()"><span
class="glyphicon glyphicon-chevron-right"></span></div>
</div>
</div> </div>

View File

@ -4,6 +4,7 @@ import {
ElementRef, ElementRef,
EventEmitter, EventEmitter,
HostListener, HostListener,
OnDestroy,
Output, Output,
QueryList, QueryList,
ViewChild ViewChild
@ -20,8 +21,9 @@ import {Subscription} from "rxjs";
styleUrls: ['./lightbox.gallery.component.css'], styleUrls: ['./lightbox.gallery.component.css'],
templateUrl: './lightbox.gallery.component.html', templateUrl: './lightbox.gallery.component.html',
}) })
export class GalleryLightboxComponent { export class GalleryLightboxComponent implements OnDestroy {
@Output('onLastElement') onLastElement = new EventEmitter(); @Output('onLastElement') onLastElement = new EventEmitter();
@ViewChild("root") elementRef: ElementRef;
public navigation = {hasPrev: true, hasNext: true}; public navigation = {hasPrev: true, hasNext: true};
public photoDimension: Dimension = <Dimension>{top: 0, left: 0, width: 0, height: 0}; public photoDimension: Dimension = <Dimension>{top: 0, left: 0, width: 0, height: 0};
@ -35,14 +37,23 @@ export class GalleryLightboxComponent {
public visible = false; public visible = false;
private changeSubscription: Subscription = null; 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) { 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']) @HostListener('window:resize', ['$event'])
onResize() { onResize() {
if (this.activePhoto) { if (this.activePhoto) {
@ -130,11 +141,13 @@ export class GalleryLightboxComponent {
}; };
this.blackCanvasOpacity = 1.0; this.blackCanvasOpacity = 1.0;
this.showPhoto(this.gridPhotoQL.toArray().indexOf(selectedPhoto)); this.showPhoto(this.gridPhotoQL.toArray().indexOf(selectedPhoto));
this.contentWidth = this.getScreenWidth();
}, 0); }, 0);
} }
public hide() { public hide() {
this.enableAnimation(); this.enableAnimation();
this.hideInfoPanel();
this.fullScreenService.exitFullScreen(); this.fullScreenService.exitFullScreen();
this.lightboxDimension = this.activePhoto.getDimension(); 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 = <Dimension>{
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() { private enableAnimation() {
this.transition = null; this.transition = null;
} }
@ -214,7 +273,7 @@ export class GalleryLightboxComponent {
} }
private getScreenWidth() { private getScreenWidth() {
return window.innerWidth; return Math.max(window.innerWidth - this.infoPanelWidth, 0);
} }
private getScreenHeight() { private getScreenHeight() {