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

refactoring lightbox

This commit is contained in:
Patrik J. Braun 2019-07-14 19:31:34 +02:00
parent 9e6247ae57
commit 0542f0b556
9 changed files with 855 additions and 537 deletions

View File

@ -54,8 +54,8 @@ export class PersonManager implements IPersonManager {
this.samplePhotos[name] = media;
}
return this.samplePhotos[name];
return this.samplePhotos[name];
}
async loadAll(): Promise<void> {

View File

@ -79,6 +79,7 @@ import {FacesService} from './ui/faces/faces.service';
import {FaceComponent} from './ui/faces/face/face.component';
import {VersionService} from './model/version.service';
import { DirectoriesComponent } from './ui/gallery/directories/directories.component';
import {ControlsLightboxComponent} from './ui/gallery/lightbox/controls/controls.lightbox.gallery.component';
@Injectable()
@ -162,6 +163,7 @@ export function translationsFactory(locale: string) {
GalleryPhotoComponent,
AdminComponent,
InfoPanelLightboxComponent,
ControlsLightboxComponent,
RandomQueryBuilderGalleryComponent,
// Face
FaceComponent,

View File

@ -2,7 +2,7 @@
<div body class="container">
<ng-template [ngIf]="renderedDirGroups">
<div class="alert alert-secondary" role="alert" *ngIf=" duplicateCount.photos >0" i18n>
<div class="alert alert-secondary" role="alert" *ngIf=" duplicateCount.photos >0">
Listing <strong>{{duplicateCount.pairs}}</strong> duplicates ({{duplicateCount.photos}} photos).
</div>
<div class="alert alert-secondary" role="alert" *ngIf=" duplicateCount.photos ==0" i18n>

View File

@ -0,0 +1,242 @@
.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;
cursor: pointer;
}
app-gallery-lightbox-photo {
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;
-o-transition: all 0.3s ease-in-out;
-ms-transition: all 0.3s ease-in-out;
-webkit-transition: all 0.3s ease-in-out;
-moz-transition: all 0.3s ease-in-out;
}
.navigation-arrow {
width: 30px;
min-width: 60px;
height: 100px;
position: absolute;
top: calc(50% - 50px);
display: inline-block;
padding: 15px;
cursor: pointer;
font-size: xx-large;
left: 0;
}
.navigation-arrow span {
top: 20px;
}
#controllers-container {
z-index: 1100;
width: 100%;
height: 100%;
left: 0;
top: 0;
opacity: 1.0;
position: fixed;
color: white;
transition: width 0.3s ease-in-out, opacity 1s;
-webkit-transition: width 0.3s ease-in-out, opacity 1s;
-o-transition: width 0.3s ease-in-out, opacity 1s;
-ms-transition: width 0.3s ease-in-out, opacity 1s;
-moz-transition: width 0.3s ease-in-out, opacity 1s;
}
#controllers-container.dim-controls {
opacity: 0.1;
}
#controllers-container.dim-controls-video {
opacity: 0;
}
#swipeable-container {
height: 100%;
}
#rightArrow {
right: 0;
left: auto;
text-align: right;
}
.controls {
height: initial;
text-align: right;
width: 100%;
padding: 5px;
font-size: large;
}
.controls .control-button {
margin-left: 0.2em;
margin-right: 0.2em;
display: inline-block;
padding: 0 0.5rem;
font-size: 1.5rem;
color: white;
cursor: pointer;
}
.controls .button-disabled {
color: #888;
}
.controls-top {
top: 0;
}
.controls-caption {
opacity: 0.5;
top: 0;
position: absolute;
height: initial;
text-align: left;
width: 75%;
padding: 5px 13px;
font-size: 1.5rem;
}
.controls-playback {
padding-right: 15px;
bottom: 0;
position: absolute;
}
.controls-video {
padding-right: 15px;
bottom: 0;
position: absolute;
}
.controls-video .oi,
.controls-video input {
color: white;
cursor: pointer;
}
.controls-big-play span {
font-size: 20vh;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.controls-video input[type=range] {
padding: 0;
margin-top: 6px;
}
.controls-video .oi {
text-align: center;
max-width: 45px;
margin-left: 10px;
}
.highlight {
opacity: 0.5;
transition: opacity .2s ease-out;
-moz-transition: opacity .2s ease-out;
-webkit-transition: opacity .2s ease-out;
-o-transition: opacity .2s ease-out;
-ms-transition: opacity .2s ease-out;
}
.highlight:hover {
opacity: 1.0;
}
@-webkit-keyframes blink {
0% {
opacity: 0.5;
}
25% {
opacity: 1.0;
}
75% {
opacity: 1.0;
}
100% {
opacity: 0.5;
}
}
.button-active {
animation: blink 3s ease-in-out infinite;
}
app-info-panel {
z-index: 1100; /* Sit on top */
position: fixed;
height: 100vh;
max-width: 100vw;
right: 0;
top: 0;
transition: all 0.3s ease-in-out;
-o-transition: all 0.3s ease-in-out;
-ms-transition: all 0.3s ease-in-out;
-moz-transition: all 0.3s ease-in-out;
-webkit-transition: all 0.3s ease-in-out;
}
.controls-zoom {
bottom: 0;
position: absolute;
margin-bottom: 10px;
margin-right: 0;
margin-left: 0;
}
.zoom-progress-wrapper {
margin-top: 3px;
height: 11px;
border: 1px solid white;
padding: 0;
}
input[type="range"].zoom-progress {
height: 11px;
margin-top: 3px;
border: 1px solid white;
background: transparent;
padding: 0;
}
input[type="range"].zoom-progress::-webkit-slider-runnable-track {
background: transparent;
}
input[type="range"].zoom-progress::-moz-range-track {
background: transparent;
}
.controls-zoom .oi {
cursor: pointer;
}

View File

@ -0,0 +1,125 @@
<div id="controllers-container" #root>
<div class="controls-caption" *ngIf="Title">{{Title}}</div>
<div class="controls controls-top">
<a class="highlight control-button"
*ngIf="activePhoto"
[href]="activePhoto.gridPhoto.getPhotoPath()"
[download]="activePhoto.gridPhoto.media.name">
<span class="oi oi-data-transfer-download"
title="download" i18n-title></span>
</a>
<div class=" highlight control-button" (click)="toggleInfoPanel.emit()"
title="info key: i" i18n-title>
<span class="oi oi-info"></span>
</div>
<div *ngIf="fullScreenService.isFullScreenEnabled()"
class=" highlight control-button"
(click)="toggleFullScreen.emit()"
title="toggle fullscreen, key: f" i18n-title>
<span class="oi oi-fullscreen-exit">
</span>
</div>
<div *ngIf="!fullScreenService.isFullScreenEnabled()"
class="highlight control-button"
(click)="toggleFullScreen.emit(true)"
title="toggle fullscreen, key: f" i18n-title>
<span class="oi oi-fullscreen-enter">
</span>
</div>
<div class="highlight control-button"
(click)="closed.emit()"
title="close, key: Escape" i18n-title>
<span class="oi oi-x">
</span>
</div>
</div>
<div id="swipeable-container"
(swipeleft)="zoom == 1 && nextPhoto.emit()"
(swiperight)="zoom == 1 && previousPhoto.emit()"
(swipeup)="zoom == 1 && closed.emit()"
(tap)="tap($event)"
(pan)="pan($event)"
(wheel)="wheel($event)"
(click)="mediaElement.playPause()">
</div>
<div class="navigation-arrow highlight"
*ngIf="navigation.hasPrev && zoom == 1" title="key: left arrow" id="leftArrow" i18n-title
(click)="previousPhoto.emit()"><span
class="oi oi-chevron-left"></span></div>
<div class="navigation-arrow highlight"
*ngIf="navigation.hasNext && zoom == 1" title="key: right arrow" id="rightArrow" i18n-title
(click)="nextPhoto.emit()"><span
class="oi oi-chevron-right"></span></div>
<div class="controls controls-zoom row" *ngIf="Zoom > 1">
<div class="col-1 col-md-4">
<span (click)="zoomOut()" i18n-title title="Zoom out, key: '-'"
class="oi oi-zoom-out float-right"></span>
</div>
<input type="range"
[(ngModel)]="Zoom" min="1" [max]="MAX_ZOOM" step="0.1"
value="1" class="col-10 col-md-4 zoom-progress">
<div class="col-1 col-md-4">
<span (click)="zoomIn()" i18n-title title="Zoom in, key: '+'"
class="oi oi-zoom-in float-left"></span>
</div>
</div>
<div class="controls controls-playback"
*ngIf="zoom == 1 && activePhoto && activePhoto.gridPhoto.isPhoto()">
<span class="oi oi-media-pause highlight control-button"
[ngClass]="playBackState == PlayBackStates.Paused ? 'button-disabled':''"
(click)="pause()"
title="pause"></span>
<span
class="oi oi-media-play highlight control-button"
[ngClass]="playBackState == PlayBackStates.Play ? 'button-active':''"
(click)="play()"
title="auto play"></span>
<span class="oi oi-media-skip-forward highlight control-button"
[ngClass]="playBackState == PlayBackStates.FastForward ? 'button-active':''"
(click)="fastForward()"
title="fast auto play"></span>
</div>
<div class="controls controls-big-play"
*ngIf="activePhoto && activePhoto.gridPhoto.isVideo() && mediaElement.Paused">
<span class="oi oi-media-play"></span>
</div>
<div class="controls controls-video row" *ngIf="activePhoto && activePhoto.gridPhoto.isVideo()">
<span class="oi col-1"
[ngClass]="!mediaElement.Paused ? 'oi-media-pause':'oi-media-play'"
(click)="mediaElement.playPause()"></span>
<input type="range" [(ngModel)]="mediaElement.VideoProgress"
min="0" max="100" step="0.1" class="col video-progress">
<span class="oi col-1"
[ngClass]="mediaElement.Muted ? 'oi-volume-off':'oi-volume-high'"
(click)="mediaElement.mute()"></span>
<input type="range"
[(ngModel)]="mediaElement.VideoVolume" min="0" max="1" step="0.1"
value="1" class="col-2 col-md-1 volume">
</div>
<!-- <div class="faces"
*ngIf="activePhoto">
<div
[style.top.%]="face.box.top / activePhoto.gridPhoto.Photo.metadata.size.height*100"
[style.left.%]="face.box.left / activePhoto.gridPhoto.Photo.metadata.size.width*100"
[style.height.%]="face.box.height / activePhoto.gridPhoto.Photo.metadata.size.height*100"
[style.width.%]="face.box.width / activePhoto.gridPhoto.Photo.metadata.size.width*100"
style="background-color: red"
*ngFor="let face of activePhoto.gridPhoto.Photo.metadata.faces">
</div>
</div> -->
</div>

View File

@ -0,0 +1,343 @@
import {Component, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output} from '@angular/core';
import {MediaDTO} from '../../../../../../common/entities/MediaDTO';
import {FullScreenService} from '../../fullscreen.service';
import {GalleryPhotoComponent} from '../../grid/photo/photo.grid.gallery.component';
import {Observable, Subscription, timer} from 'rxjs';
import {filter} from 'rxjs/operators';
import {PhotoDTO} from '../../../../../../common/entities/PhotoDTO';
import {GalleryLightboxMediaComponent} from '../media/media.lightbox.gallery.component';
export enum PlayBackStates {
Paused = 1,
Play = 2,
FastForward = 3
}
@Component({
selector: 'app-lightbox-controls',
styleUrls: ['./controls.lightbox.gallery.component.css', './inputrange.css'],
templateUrl: './controls.lightbox.gallery.component.html',
})
export class ControlsLightboxComponent implements OnDestroy, OnInit {
readonly MAX_ZOOM = 10;
@Output() closed = new EventEmitter();
@Output() toggleInfoPanel = new EventEmitter();
@Output() toggleFullScreen = new EventEmitter();
@Output() nextPhoto = new EventEmitter();
@Output() previousPhoto = new EventEmitter();
@Input() navigation = {hasPrev: true, hasNext: true};
@Input() activePhoto: GalleryPhotoComponent;
@Input() mediaElement: GalleryLightboxMediaComponent;
@Input() photoFrameDim = {width: 1, height: 1, aspect: 1};
public zoom = 1;
public playBackState: PlayBackStates = PlayBackStates.Paused;
public PlayBackStates = PlayBackStates;
public controllersDimmed = false;
public controllersAlwaysOn = false;
public controllersVisible = true;
visibilityTimer: number = null;
// delayedMediaShow: string = null;
public drag = {x: 0, y: 0};
private timer: Observable<number>;
private timerSub: Subscription;
private prevDrag = {x: 0, y: 0};
private prevZoom = 1;
constructor(public fullScreenService: FullScreenService) {
}
public get Zoom(): number {
return this.zoom;
}
public set Zoom(zoom: number) {
if (!this.activePhoto || this.activePhoto.gridPhoto.isVideo()) {
return;
}
if (zoom < 1) {
zoom = 1;
}
if (zoom > this.MAX_ZOOM) {
zoom = this.MAX_ZOOM;
}
if (this.zoom === zoom) {
return;
}
this.pause();
this.drag.x = this.drag.x / this.zoom * zoom;
this.drag.y = this.drag.y / this.zoom * zoom;
this.prevDrag.x = this.drag.x;
this.prevDrag.y = this.drag.y;
this.zoom = zoom;
this.checkZoomAndDrag();
}
get Title(): string {
if (!this.activePhoto) {
return null;
}
return (<PhotoDTO>this.activePhoto.gridPhoto.media).metadata.caption;
}
ngOnInit(): void {
this.timer = timer(1000, 2000);
}
ngOnDestroy(): void {
this.pause();
if (this.visibilityTimer != null) {
clearTimeout(this.visibilityTimer);
}
}
pan($event: { deltaY: number, deltaX: number, isFinal: boolean }) {
if (!this.activePhoto || this.activePhoto.gridPhoto.isVideo()) {
return;
}
if (this.zoom === 1) {
return;
}
this.drag.x = this.prevDrag.x + $event.deltaX;
this.drag.y = this.prevDrag.y + $event.deltaY;
this.checkZoomAndDrag();
if ($event.isFinal) {
this.prevDrag = {
x: this.drag.x,
y: this.drag.y,
};
}
}
wheel($event: { deltaY: number }) {
if (!this.activePhoto || this.activePhoto.gridPhoto.isVideo()) {
return;
}
if ($event.deltaY < 0) {
this.zoomIn();
} else {
this.zoomOut();
}
}
@HostListener('pinch', ['$event'])
pinch($event: { scale: number }) {
if (!this.activePhoto || this.activePhoto.gridPhoto.isVideo()) {
return;
}
this.showControls();
this.Zoom = this.prevZoom * $event.scale;
}
@HostListener('pinchend', ['$event'])
pinchend($event: { scale: number }) {
if (!this.activePhoto || this.activePhoto.gridPhoto.isVideo()) {
return;
}
this.showControls();
this.Zoom = this.prevZoom * $event.scale;
this.prevZoom = this.zoom;
}
tap($event: any) {
if (!this.activePhoto || this.activePhoto.gridPhoto.isVideo()) {
return;
}
if ($event.tapCount < 2) {
return;
}
this.showControls();
if (this.zoom > 1) {
this.Zoom = 1;
this.prevZoom = this.zoom;
return;
} else {
this.Zoom = 5;
this.prevZoom = this.zoom;
return;
}
}
zoomIn() {
this.showControls();
this.Zoom = this.zoom + this.zoom / 10;
}
zoomOut() {
this.showControls();
this.Zoom = this.zoom - this.zoom / 10;
}
@HostListener('window:keydown', ['$event'])
onKeyPress(e: KeyboardEvent) {
const event: KeyboardEvent = window.event ? <any>window.event : e;
switch (event.key) {
case 'ArrowLeft':
if (this.navigation.hasPrev) {
this.previousPhoto.emit();
}
break;
case 'ArrowRight':
if (this.navigation.hasNext) {
this.nextPhoto.emit();
}
break;
case 'i':
case 'I':
this.toggleInfoPanel.emit();
break;
case 'f':
case 'F':
this.toggleFullScreen.emit();
break;
case '-':
this.zoomOut();
break;
case '+':
this.zoomIn();
break;
case 'c':
case 'C':
this.controllersAlwaysOn = !this.controllersAlwaysOn;
break;
case 'Escape': // escape
this.closed.emit();
break;
case ' ': // space
if (this.activePhoto && this.activePhoto.gridPhoto.isVideo()) {
this.mediaElement.playPause();
}
break;
}
}
public play() {
this.pause();
this.timerSub = this.timer.pipe(filter(t => t % 2 === 0)).subscribe(() => {
if (this.mediaElement.imageLoadFinished === false) {
return;
}
// do not skip video if its playing
if (this.activePhoto && this.activePhoto.gridPhoto.isVideo() &&
!this.mediaElement.Paused) {
return;
}
this.nextPhoto.emit();
});
this.playBackState = PlayBackStates.Play;
}
public fastForward() {
this.pause();
this.timerSub = this.timer.subscribe(() => {
if (this.mediaElement.imageLoadFinished === false) {
return;
}
if (this.activePhoto && this.activePhoto.gridPhoto.isVideo() &&
!this.mediaElement.Paused) {
return;
}
this.nextPhoto.emit();
});
this.playBackState = PlayBackStates.FastForward;
}
@HostListener('mousemove')
onMouseMove() {
this.showControls();
}
public pause() {
if (this.timerSub != null) {
this.timerSub.unsubscribe();
}
this.playBackState = PlayBackStates.Paused;
}
resetZoom() {
this.Zoom = 1;
}
onResize() {
this.checkZoomAndDrag();
}
private checkZoomAndDrag() {
const fixDrag = (drag: { x: number, y: number }) => {
if (this.zoom === 1) {
drag.y = 0;
drag.x = 0;
return;
}
if (!this.activePhoto) {
return;
}
const photoAspect = MediaDTO.calcRotatedAspectRatio(this.activePhoto.gridPhoto.media);
const widthFilled = photoAspect > this.photoFrameDim.aspect;
const divWidth = this.photoFrameDim.width;
const divHeight = this.photoFrameDim.height;
const size = {
width: (widthFilled ? divWidth : divHeight * photoAspect) * this.zoom,
height: (widthFilled ? divWidth / photoAspect : divHeight) * this.zoom
};
const widthDrag = Math.abs(divWidth - size.width) / 2;
const heightDrag = Math.abs(divHeight - size.height) / 2;
if (divWidth > size.width) {
drag.x = 0;
}
if (divHeight > size.height) {
drag.y = 0;
}
if (drag.x < -widthDrag) {
drag.x = -widthDrag;
}
if (drag.x > widthDrag) {
drag.x = widthDrag;
}
if (drag.y < -heightDrag) {
drag.y = -heightDrag;
}
if (drag.y > heightDrag) {
drag.y = heightDrag;
}
};
if (this.zoom < 1) {
this.zoom = 1;
}
if (this.zoom > this.MAX_ZOOM) {
this.zoom = this.MAX_ZOOM;
}
fixDrag(this.drag);
fixDrag(this.prevDrag);
}
private showControls() {
this.controllersDimmed = false;
if (this.visibilityTimer != null) {
clearTimeout(this.visibilityTimer);
}
this.visibilityTimer = window.setTimeout(this.hideControls, 2000);
}
private hideControls = () => {
this.controllersDimmed = true;
};
}

View File

@ -7,128 +7,25 @@
<div class="lightbox" #lightbox>
<app-gallery-lightbox-media [gridMedia]="activePhoto ? activePhoto.gridPhoto : null"
[loadMedia]="!animating"
[zoom]="zoom"
[drag]="drag"
[windowAspect]="getWindowAspectRatio()"
[zoom]="controls ? controls.Zoom : 1"
[drag]="controls ? controls.drag : {x:0,y:0}"
[windowAspect]="photoFrameDim.aspect"
#photo>
</app-gallery-lightbox-media>
<app-lightbox-controls
#controls
[activePhoto]="activePhoto"
(closed)="hide()"
[navigation]="navigation"
(nextPhoto)="nextImage()"
(previousPhoto)="prevImage()"
(toggleInfoPanel)="toggleInfoPanel()"
(toggleFullScreen)="toggleFullscreen()"
[photoFrameDim]="photoFrameDim"
[mediaElement]="mediaElement">
</app-lightbox-controls>
</div>
<div
*ngIf="controllersVisible"
id="controllers-container"
#controls
[style.width.px]="getPhotoFrameWidth()"
[ngClass]="(controllersDimmed && !controllersAlwaysOn) ? (activePhoto && activePhoto.gridPhoto.isVideo() ? 'dim-controls-video' :'dim-controls'): ''">
<div class="controls-caption" *ngIf="Title">{{Title}}</div>
<div class="controls controls-top">
<a *ngIf="activePhoto"
class="highlight control-button"
[href]="activePhoto.gridPhoto.getPhotoPath()"
[download]="activePhoto.gridPhoto.media.name">
<span class="oi oi-data-transfer-download"
title="download" i18n-title></span>
</a>
<div class=" highlight control-button" (click)="toggleInfoPanel()"
title="info key: i" i18n-title>
<span class="oi oi-info"></span>
</div>
<div *ngIf="fullScreenService.isFullScreenEnabled()"
class=" highlight control-button"
(click)="fullScreenService.exitFullScreen()"
title="toggle fullscreen, key: f" i18n-title>
<span class="oi oi-fullscreen-exit">
</span>
</div>
<div *ngIf="!fullScreenService.isFullScreenEnabled()"
class="highlight control-button"
(click)="fullScreenService.showFullScreen(root)"
title="toggle fullscreen, key: f" i18n-title>
<span class="oi oi-fullscreen-enter">
</span>
</div>
<div class="highlight control-button"
(click)="hide()"
title="close, key: Escape" i18n-title>
<span class="oi oi-x">
</span>
</div>
</div>
<div id="swipeable-container"
(swipeleft)="zoom == 1 && nextImage()"
(swiperight)="zoom == 1 && prevImage()"
(swipeup)="zoom == 1 && hide()"
(tap)="tap($event)"
(pan)="pan($event)"
(wheel)="wheel($event)"
(click)="mediaElement.playPause()">
</div>
<div class="navigation-arrow highlight"
*ngIf="navigation.hasPrev && zoom == 1" title="key: left arrow" id="leftArrow" i18n-title
(click)="prevImage()"><span
class="oi oi-chevron-left"></span></div>
<div class="navigation-arrow highlight"
*ngIf="navigation.hasNext && zoom == 1" title="key: right arrow" id="rightArrow" i18n-title
(click)="nextImage()"><span
class="oi oi-chevron-right"></span></div>
<div class="controls controls-zoom row" *ngIf="zoom > 1">
<div class="col-1 col-md-4">
<span (click)="zoomOut()" i18n-title title="Zoom out, key: '-'"
class="oi oi-zoom-out float-right"></span>
</div>
<input type="range"
[(ngModel)]="Zoom" min="1" [max]="MAX_ZOOM" step="0.1"
value="1" class="col-10 col-md-4 zoom-progress">
<div class="col-1 col-md-4">
<span (click)="zoomIn()" i18n-title title="Zoom in, key: '+'"
class="oi oi-zoom-in float-left"></span>
</div>
</div>
<div class="controls controls-playback"
*ngIf="zoom == 1 && activePhoto && activePhoto.gridPhoto.isPhoto()">
<span class="oi oi-media-pause highlight control-button"
[ngClass]="playBackState == PlayBackStates.Paused ? 'button-disabled':''"
(click)="pause()"
title="pause"></span>
<span
class="oi oi-media-play highlight control-button"
[ngClass]="playBackState == PlayBackStates.Play ? 'button-active':''"
(click)="play()"
title="auto play"></span>
<span class="oi oi-media-skip-forward highlight control-button"
[ngClass]="playBackState == PlayBackStates.FastForward ? 'button-active':''"
(click)="fastForward()"
title="fast auto play"></span>
</div>
<div class="controls controls-big-play"
*ngIf="activePhoto && activePhoto.gridPhoto.isVideo() && mediaElement.Paused">
<span class="oi oi-media-play"></span>
</div>
<div class="controls controls-video row" *ngIf="activePhoto && activePhoto.gridPhoto.isVideo()">
<span class="oi col-1"
[ngClass]="!mediaElement.Paused ? 'oi-media-pause':'oi-media-play'"
(click)="mediaElement.playPause()"></span>
<input type="range" [(ngModel)]="mediaElement.VideoProgress"
min="0" max="100" step="0.1" class="col video-progress">
<span class="oi col-1"
[ngClass]="mediaElement.Muted ? 'oi-volume-off':'oi-volume-high'"
(click)="mediaElement.mute()"></span>
<input type="range"
[(ngModel)]="mediaElement.VideoVolume" min="0" max="1" step="0.1"
value="1" class="col-2 col-md-1 volume">
</div>
</div>
<app-info-panel *ngIf="activePhoto && infoPanelVisible"
id="info-panel"
[style.width.px]="infoPanelWidth"

View File

@ -1,21 +1,11 @@
import {
ChangeDetectorRef,
Component,
ElementRef,
HostListener,
OnDestroy,
OnInit,
QueryList,
ViewChild
} from '@angular/core';
import {ChangeDetectorRef, Component, ElementRef, HostListener, OnDestroy, OnInit, QueryList, ViewChild} from '@angular/core';
import {GalleryPhotoComponent} from '../grid/photo/photo.grid.gallery.component';
import {Dimension} from '../../../model/IRenderable';
import {FullScreenService} from '../fullscreen.service';
import {OverlayService} from '../overlay.service';
import {animate, AnimationBuilder, AnimationPlayer, style} from '@angular/animations';
import {GalleryLightboxMediaComponent} from './media/media.lightbox.gallery.component';
import {Observable, Subscription, timer} from 'rxjs';
import {filter} from 'rxjs/operators';
import {Subscription} from 'rxjs';
import {ActivatedRoute, Params, Router} from '@angular/router';
import {PageHelper} from '../../../model/page.helper';
import {QueryService} from '../../../model/query.service';
@ -23,6 +13,7 @@ import {MediaDTO} from '../../../../../common/entities/MediaDTO';
import {QueryParams} from '../../../../../common/QueryParams';
import {GalleryService} from '../gallery.service';
import {PhotoDTO} from '../../../../../common/entities/PhotoDTO';
import {ControlsLightboxComponent} from './controls/controls.lightbox.gallery.component';
export enum LightboxStates {
Open = 1,
@ -30,48 +21,22 @@ export enum LightboxStates {
Closed = 3
}
export enum PlayBackStates {
Paused = 1,
Play = 2,
FastForward = 3
}
@Component({
selector: 'app-gallery-lightbox',
styleUrls: ['./lightbox.gallery.component.css', './inputrange.css'],
styleUrls: ['./lightbox.gallery.component.css'],
templateUrl: './lightbox.gallery.component.html'
})
export class GalleryLightboxComponent implements OnDestroy, OnInit {
readonly MAX_ZOOM = 10;
@ViewChild('photo') mediaElement: GalleryLightboxMediaComponent;
@ViewChild('controls') controls: ControlsLightboxComponent;
@ViewChild('lightbox') lightboxElement: ElementRef;
@ViewChild('root') root: ElementRef;
public navigation = {hasPrev: true, hasNext: true};
public blackCanvasOpacity = 0;
private activePhotoId: number = null;
public activePhoto: GalleryPhotoComponent;
private gridPhotoQL: QueryList<GalleryPhotoComponent>;
public status: LightboxStates = LightboxStates.Closed;
private subscription: {
photosChange: Subscription,
route: Subscription
} = {
photosChange: null,
route: null
};
private timer: Observable<number>;
private timerSub: Subscription;
public playBackState: PlayBackStates = PlayBackStates.Paused;
public PlayBackStates = PlayBackStates;
public controllersDimmed = false;
public controllersAlwaysOn = false;
public controllersVisible = true;
public infoPanelVisible = false;
public infoPanelWidth = 0;
public animating = false;
@ -79,10 +44,17 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
iPvisibilityTimer: number = null;
visibilityTimer: number = null;
delayedMediaShow: string = null;
public zoom = 1;
public drag = {x: 0, y: 0};
private prevDrag = {x: 0, y: 0};
private prevZoom = 1;
public photoFrameDim = {width: 1, height: 1, aspect: 1};
private activePhotoId: number = null;
private gridPhotoQL: QueryList<GalleryPhotoComponent>;
private subscription: {
photosChange: Subscription,
route: Subscription
} = {
photosChange: null,
route: null
};
constructor(public fullScreenService: FullScreenService,
private changeDetector: ChangeDetectorRef,
@ -94,8 +66,24 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
private route: ActivatedRoute) {
}
get Title(): string {
if (!this.activePhoto) {
return null;
}
return (<PhotoDTO>this.activePhoto.gridPhoto.media).metadata.caption;
}
public toggleFullscreen(): void {
if (this.fullScreenService.isFullScreenEnabled()) {
this.fullScreenService.exitFullScreen();
} else {
this.fullScreenService.showFullScreen(this.root.nativeElement);
}
}
ngOnInit(): void {
this.timer = timer(1000, 2000);
this.updatePhotoFrameDim();
this.subscription.route = this.route.queryParams.subscribe((params: Params) => {
if (params[QueryParams.gallery.photo] && params[QueryParams.gallery.photo] !== '') {
if (!this.gridPhotoQL) {
@ -109,9 +97,10 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
});
}
ngOnDestroy(): void {
this.pause();
if (this.controls) {
this.controls.pause();
}
if (this.subscription.photosChange != null) {
this.subscription.photosChange.unsubscribe();
}
@ -132,7 +121,9 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
return;
}
this.Zoom = 1;
if (this.controls) {
this.controls.resetZoom();
}
const photo = this.gridPhotoQL.find(i => this.queryService.getMediaStringId(i.gridPhoto.media) === photoStringId);
if (!photo) {
return this.delayedMediaShow = photoStringId;
@ -164,165 +155,6 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
}
}
pan($event: any) {
if (!this.activePhoto || this.activePhoto.gridPhoto.isVideo()) {
return;
}
if (this.zoom === 1) {
return;
}
this.drag.x = this.prevDrag.x + $event.deltaX;
this.drag.y = this.prevDrag.y + $event.deltaY;
this.checkZoomAndDrag();
if ($event.isFinal) {
this.prevDrag = {
x: this.drag.x,
y: this.drag.y,
};
}
}
wheel($event: any) {
if (!this.activePhoto || this.activePhoto.gridPhoto.isVideo()) {
return;
}
if ($event.deltaY < 0) {
this.zoomIn();
} else {
this.zoomOut();
}
}
@HostListener('pinch', ['$event'])
pinch($event: any) {
if (!this.activePhoto || this.activePhoto.gridPhoto.isVideo()) {
return;
}
this.showControls();
this.Zoom = this.prevZoom * $event.scale;
}
@HostListener('pinchend', ['$event'])
pinchend($event: any) {
if (!this.activePhoto || this.activePhoto.gridPhoto.isVideo()) {
return;
}
this.showControls();
this.Zoom = this.prevZoom * $event.scale;
this.prevZoom = this.zoom;
}
tap($event: any) {
if (!this.activePhoto || this.activePhoto.gridPhoto.isVideo()) {
return;
}
if ($event.tapCount < 2) {
return;
}
this.showControls();
if (this.zoom > 1) {
this.Zoom = 1;
this.prevZoom = this.zoom;
return;
} else {
this.Zoom = 5;
this.prevZoom = this.zoom;
return;
}
}
zoomIn() {
this.showControls();
this.Zoom = this.zoom + this.zoom / 10;
}
zoomOut() {
this.showControls();
this.Zoom = this.zoom - this.zoom / 10;
}
public get Zoom(): number {
return this.zoom;
}
public set Zoom(zoom: number) {
if (!this.activePhoto || this.activePhoto.gridPhoto.isVideo()) {
return;
}
if (zoom < 1) {
zoom = 1;
}
if (zoom > this.MAX_ZOOM) {
zoom = this.MAX_ZOOM;
}
if (this.zoom === zoom) {
return;
}
this.pause();
this.drag.x = this.drag.x / this.zoom * zoom;
this.drag.y = this.drag.y / this.zoom * zoom;
this.prevDrag.x = this.drag.x;
this.prevDrag.y = this.drag.y;
this.zoom = zoom;
this.checkZoomAndDrag();
}
private checkZoomAndDrag() {
const fixDrag = (drag: { x: number, y: number }) => {
if (this.zoom === 1) {
drag.y = 0;
drag.x = 0;
return;
}
if (!this.activePhoto) {
return;
}
const photoAspect = MediaDTO.calcRotatedAspectRatio(this.activePhoto.gridPhoto.media);
const widthFilled = photoAspect > this.getWindowAspectRatio();
const divWidth = this.getPhotoFrameWidth();
const divHeight = this.getPhotoFrameHeight();
const size = {
width: (widthFilled ? divWidth : divHeight * photoAspect) * this.zoom,
height: (widthFilled ? divWidth / photoAspect : divHeight) * this.zoom
};
const widthDrag = Math.abs(divWidth - size.width) / 2;
const heightDrag = Math.abs(divHeight - size.height) / 2;
if (divWidth > size.width) {
drag.x = 0;
}
if (divHeight > size.height) {
drag.y = 0;
}
if (drag.x < -widthDrag) {
drag.x = -widthDrag;
}
if (drag.x > widthDrag) {
drag.x = widthDrag;
}
if (drag.y < -heightDrag) {
drag.y = -heightDrag;
}
if (drag.y > heightDrag) {
drag.y = heightDrag;
}
};
if (this.zoom < 1) {
this.zoom = 1;
}
if (this.zoom > this.MAX_ZOOM) {
this.zoom = this.MAX_ZOOM;
}
fixDrag(this.drag);
fixDrag(this.prevDrag);
}
//noinspection JSUnusedGlobalSymbols
@HostListener('window:resize', ['$event'])
onResize() {
@ -330,37 +162,30 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
this.animateLightbox();
this.updateActivePhoto(this.activePhotoId);
}
this.updatePhotoFrameDim();
}
public nextImage() {
if (this.activePhotoId + 1 < this.gridPhotoQL.length) {
this.navigateToPhoto(this.activePhotoId + 1);
} else {
this.navigateToPhoto(0);
}
}
public prevImage() {
this.pause();
if (this.controls) {
this.controls.pause();
}
if (this.activePhotoId > 0) {
this.navigateToPhoto(this.activePhotoId - 1);
}
}
private navigateToPhoto(photoIndex: number) {
this.router.navigate([],
{queryParams: this.queryService.getParams(this.gridPhotoQL.toArray()[photoIndex].gridPhoto.media)}).catch(console.error);
}
private showPhoto(photoIndex: number, resize: boolean = true) {
this.activePhoto = null;
this.changeDetector.detectChanges();
this.updateActivePhoto(photoIndex, resize);
}
public showLigthbox(photo: MediaDTO) {
this.Zoom = 1;
this.controllersVisible = true;
this.showControls();
if (this.controls) {
this.controls.resetZoom();
}
this.status = LightboxStates.Open;
const selectedPhoto = this.findPhotoComponent(photo);
if (selectedPhoto === null) {
@ -378,8 +203,8 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
<Dimension>{
top: 0,
left: 0,
width: this.getPhotoFrameWidth(),
height: this.getPhotoFrameHeight()
width: this.photoFrameDim.width,
height: this.photoFrameDim.height
});
@ -391,99 +216,11 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
this.showPhoto(this.gridPhotoQL.toArray().indexOf(selectedPhoto), false);
}
@HostListener('window:keydown', ['$event'])
onKeyPress(e: KeyboardEvent) {
if (this.status !== LightboxStates.Open) {
return;
}
const event: KeyboardEvent = window.event ? <any>window.event : e;
switch (event.key) {
case 'ArrowLeft':
if (this.activePhotoId > 0) {
this.prevImage();
}
break;
case 'ArrowRight':
if (this.activePhotoId < this.gridPhotoQL.length - 1) {
this.nextImage();
}
break;
case 'i':
case 'I':
if (this.isInfoPanelAnimating()) {
return;
}
if (this.infoPanelVisible) {
this.hideInfoPanel(true);
} else {
this.showInfoPanel();
}
break;
case 'f':
case 'F':
if (this.fullScreenService.isFullScreenEnabled()) {
this.fullScreenService.exitFullScreen();
} else {
this.fullScreenService.showFullScreen(this.root.nativeElement);
}
break;
case '-':
this.zoomOut();
break;
case '+':
this.zoomIn();
break;
case 'c':
case 'C':
this.controllersAlwaysOn = !this.controllersAlwaysOn;
break;
case 'Escape': // escape
this.hide();
break;
case ' ': // space
if (this.activePhoto && this.activePhoto.gridPhoto.isVideo()) {
this.mediaElement.playPause();
}
break;
}
}
public hide() {
this.router.navigate([],
{queryParams: this.queryService.getParams()}).catch(console.error);
}
private hideLightbox() {
this.Zoom = 1;
this.controllersVisible = false;
this.status = LightboxStates.Closing;
this.fullScreenService.exitFullScreen();
this.pause();
this.animating = true;
const lightboxDimension = this.activePhoto.getDimension();
lightboxDimension.top -= PageHelper.ScrollY;
this.blackCanvasOpacity = 0;
this.animatePhoto(this.calcLightBoxPhotoDimension(this.activePhoto.gridPhoto.media), this.activePhoto.getDimension());
this.animateLightbox(<Dimension>{
top: 0,
left: 0,
width: this.getPhotoFrameWidth(),
height: this.getPhotoFrameHeight()
}, lightboxDimension).onDone(() => {
this.status = LightboxStates.Closed;
this.activePhoto = null;
this.activePhotoId = null;
this.overlayService.hideOverlay();
});
this.hideInfoPanel(false);
}
animatePhoto(from: Dimension, to: Dimension = from): AnimationPlayer {
const elem = this._builder.build([
style(Dimension.toString(from)),
@ -492,14 +229,15 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
])
.create(this.mediaElement.elementRef.nativeElement);
elem.play();
return elem;
}
animateLightbox(from: Dimension = <Dimension>{
top: 0,
left: 0,
width: this.getPhotoFrameWidth(),
height: this.getPhotoFrameHeight()
width: this.photoFrameDim.width,
height: this.photoFrameDim.height
}, to: Dimension = from): AnimationPlayer {
const elem = this._builder.build([
style(Dimension.toString(from)),
@ -511,10 +249,8 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
return elem;
}
public toggleInfoPanel() {
if (this.infoPanelWidth !== 400) {
this.showInfoPanel();
} else {
@ -527,11 +263,12 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
this.iPvisibilityTimer = window.setTimeout(() => {
this.iPvisibilityTimer = null;
this.infoPanelVisible = false;
this.checkZoomAndDrag();
// this.controls.onResize();
}, 1000);
const starPhotoPos = this.calcLightBoxPhotoDimension(this.activePhoto.gridPhoto.media);
this.infoPanelWidth = 0;
this.updatePhotoFrameDim();
const endPhotoPos = this.calcLightBoxPhotoDimension(this.activePhoto.gridPhoto.media);
if (_animate) {
this.animatePhoto(starPhotoPos, endPhotoPos);
@ -540,37 +277,18 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
this.animateLightbox(<Dimension>{
top: 0,
left: 0,
width: Math.max(this.getPhotoFrameWidth() - 400, 0),
height: this.getPhotoFrameHeight()
width: Math.max(this.photoFrameDim.width - 400, 0),
height: this.photoFrameDim.height
},
<Dimension>{
top: 0,
left: 0,
width: this.getPhotoFrameWidth(),
height: this.getPhotoFrameHeight()
width: this.photoFrameDim.width,
height: this.photoFrameDim.height
});
}
}
public play() {
this.pause();
this.timerSub = this.timer.pipe(filter(t => t % 2 === 0)).subscribe(() => {
if (this.mediaElement.imageLoadFinished === false) {
return;
}
// do not skip video if its playing
if (this.activePhoto && this.activePhoto.gridPhoto.isVideo() &&
!this.mediaElement.Paused) {
return;
}
if (this.navigation.hasNext) {
this.nextImage();
} else {
this.navigateToPhoto(0);
}
});
this.playBackState = PlayBackStates.Play;
}
isInfoPanelAnimating(): boolean {
return this.iPvisibilityTimer != null;
@ -581,57 +299,85 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
const starPhotoPos = this.calcLightBoxPhotoDimension(this.activePhoto.gridPhoto.media);
this.infoPanelWidth = 400;
this.updatePhotoFrameDim();
const endPhotoPos = this.calcLightBoxPhotoDimension(this.activePhoto.gridPhoto.media);
this.animatePhoto(starPhotoPos, endPhotoPos);
this.animateLightbox(<Dimension>{
top: 0,
left: 0,
width: this.getPhotoFrameWidth() + 400,
height: this.getPhotoFrameHeight()
width: this.photoFrameDim.width + 400,
height: this.photoFrameDim.height
},
<Dimension>{
top: 0,
left: 0,
width: this.getPhotoFrameWidth(),
height: this.getPhotoFrameHeight()
width: this.photoFrameDim.width,
height: this.photoFrameDim.height
});
if (this.iPvisibilityTimer != null) {
clearTimeout(this.iPvisibilityTimer);
}
this.checkZoomAndDrag();
if (this.controls) {
this.controls.resetZoom();
}
}
public fastForward() {
this.pause();
this.timerSub = this.timer.subscribe(() => {
if (this.mediaElement.imageLoadFinished === false) {
return;
}
if (this.activePhoto && this.activePhoto.gridPhoto.isVideo() &&
!this.mediaElement.Paused) {
return;
}
if (this.navigation.hasNext) {
this.nextImage();
} else {
this.navigateToPhoto(0);
}
public isVisible(): boolean {
return this.status !== LightboxStates.Closed;
}
private updatePhotoFrameDim = () => {
this.photoFrameDim.width = Math.max(window.innerWidth - this.infoPanelWidth, 0);
this.photoFrameDim.height = window.innerHeight;
this.photoFrameDim.aspect = Math.round(this.photoFrameDim.width / this.photoFrameDim.height * 100) / 100;
};
private navigateToPhoto(photoIndex: number) {
this.router.navigate([],
{queryParams: this.queryService.getParams(this.gridPhotoQL.toArray()[photoIndex].gridPhoto.media)}).catch(console.error);
}
private showPhoto(photoIndex: number, resize: boolean = true) {
this.activePhoto = null;
this.changeDetector.detectChanges();
this.updateActivePhoto(photoIndex, resize);
}
private hideLightbox() {
if (this.controls) {
this.controls.resetZoom();
}
this.status = LightboxStates.Closing;
this.fullScreenService.exitFullScreen();
if (this.controls) {
this.controls.pause();
}
this.animating = true;
const lightboxDimension = this.activePhoto.getDimension();
lightboxDimension.top -= PageHelper.ScrollY;
this.blackCanvasOpacity = 0;
this.animatePhoto(this.calcLightBoxPhotoDimension(this.activePhoto.gridPhoto.media), this.activePhoto.getDimension());
this.animateLightbox(<Dimension>{
top: 0,
left: 0,
width: this.photoFrameDim.width,
height: this.photoFrameDim.height
}, lightboxDimension).onDone(() => {
this.status = LightboxStates.Closed;
this.activePhoto = null;
this.activePhotoId = null;
this.overlayService.hideOverlay();
});
this.playBackState = PlayBackStates.FastForward;
}
public getPhotoFrameWidth(): number {
return Math.max(window.innerWidth - this.infoPanelWidth, 0);
}
this.hideInfoPanel(false);
public getPhotoFrameHeight(): number {
return window.innerHeight;
}
public getWindowAspectRatio(): number {
return Math.round(this.getPhotoFrameWidth() / this.getPhotoFrameHeight() * 100) / 100;
}
private updateActivePhoto(photoIndex: number, resize: boolean = true) {
@ -653,38 +399,13 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
const to = this.activePhoto.getDimension();
// if target image out of screen -> scroll to there
if (PageHelper.ScrollY > to.top || PageHelper.ScrollY + this.getPhotoFrameHeight() < to.top) {
if (PageHelper.ScrollY > to.top || PageHelper.ScrollY + this.photoFrameDim.height < to.top) {
PageHelper.ScrollY = to.top;
}
}
@HostListener('mousemove')
onMouseMove() {
this.showControls();
}
private showControls() {
this.controllersDimmed = false;
if (this.visibilityTimer != null) {
clearTimeout(this.visibilityTimer);
}
this.visibilityTimer = window.setTimeout(this.hideControls, 2000);
}
private hideControls = () => {
this.controllersDimmed = true;
};
public pause() {
if (this.timerSub != null) {
this.timerSub.unsubscribe();
}
this.playBackState = PlayBackStates.Paused;
}
private findPhotoComponent(media: MediaDTO): GalleryPhotoComponent {
const galleryPhotoComponents = this.gridPhotoQL.toArray();
for (let i = 0; i < galleryPhotoComponents.length; i++) {
@ -699,30 +420,18 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
let width = 0;
let height = 0;
const photoAspect = photo.metadata.size.width / photo.metadata.size.height;
const windowAspect = this.getPhotoFrameWidth() / this.getPhotoFrameHeight();
const windowAspect = this.photoFrameDim.aspect;
if (photoAspect < windowAspect) {
width = Math.round(photo.metadata.size.width * (this.getPhotoFrameHeight() / photo.metadata.size.height));
height = this.getPhotoFrameHeight();
width = Math.round(photo.metadata.size.width * (this.photoFrameDim.height / photo.metadata.size.height));
height = this.photoFrameDim.height;
} else {
width = this.getPhotoFrameWidth();
height = Math.round(photo.metadata.size.height * (this.getPhotoFrameWidth() / photo.metadata.size.width));
width = this.photoFrameDim.width;
height = Math.round(photo.metadata.size.height * (this.photoFrameDim.width / photo.metadata.size.width));
}
const top = (this.getPhotoFrameHeight() / 2 - height / 2);
const left = (this.getPhotoFrameWidth() / 2 - width / 2);
const top = (this.photoFrameDim.height / 2 - height / 2);
const left = (this.photoFrameDim.width / 2 - width / 2);
return <Dimension>{top: top, left: left, width: width, height: height};
}
public isVisible(): boolean {
return this.status !== LightboxStates.Closed;
}
get Title(): string {
if (!this.activePhoto) {
return null;
}
return (<PhotoDTO>this.activePhoto.gridPhoto.media).metadata.caption;
}
}