1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2025-02-05 13:25:08 +02: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
* 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`

View File

@ -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 = <UserDTO>{name: "", role: UserRoles.Admin};
req.session.user = <UserDTO>{name: "Admin", role: UserRoles.Admin};
return next();
}
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 {
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<number> = [];
readyThumbnails: Array<number> = [];
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<string>;
@Column("string")
keywords: Array<string>;
@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;
}*/
}*/

View File

@ -32,7 +32,7 @@ export class DiskMangerWorker {
private static loadPhotoMetadata(fullPath: string): Promise<PhotoMetadata> {
return new Promise<PhotoMetadata>((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 = <CameraMetadata> {

View File

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

View File

@ -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,

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 {
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;
}
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 class="blackCanvas"
[style.opacity]="blackCanvasOpacity">
<div class="blackCanvas"
[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 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 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>
<info-panel *ngIf="activePhoto && infoPanelVisible"
id="info-panel"
[style.width.px]="infoPanelWidth"
[photo]="activePhoto.gridPhoto.photo">
<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>
</info-panel>
</div>

View File

@ -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 = <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 = <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() {
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() {