diff --git a/src/frontend/app/ui/gallery/filter/filter.service.ts b/src/frontend/app/ui/gallery/filter/filter.service.ts index 2bbfec9f..5fd7d984 100644 --- a/src/frontend/app/ui/gallery/filter/filter.service.ts +++ b/src/frontend/app/ui/gallery/filter/filter.service.ts @@ -2,7 +2,7 @@ import {Injectable} from '@angular/core'; import {BehaviorSubject, Observable} from 'rxjs'; import {PhotoDTO} from '../../../../../common/entities/PhotoDTO'; import {DirectoryContent} from '../contentLoader.service'; -import {map, switchMap} from 'rxjs/operators'; +import {debounceTime, map, switchMap} from 'rxjs/operators'; export enum FilterRenderType { enum = 1, @@ -39,19 +39,19 @@ export class FilterService { { name: $localize`Faces`, mapFn: (m: PhotoDTO): string[] => - m.metadata.faces - ? m.metadata.faces.map((f) => f.name) - : ['<' + $localize`no face` + '>'], + m.metadata.faces + ? m.metadata.faces.map((f) => f.name) + : ['<' + $localize`no face` + '>'], renderType: FilterRenderType.enum, isArrayValue: true, }, { name: $localize`Faces groups`, mapFn: (m: PhotoDTO): string => - m.metadata.faces - ?.map((f) => f.name) - .sort() - .join(', '), + m.metadata.faces + ?.map((f) => f.name) + .sort() + .join(', '), renderType: FilterRenderType.enum, isArrayValue: false, }, @@ -124,18 +124,18 @@ export class FilterService { private getStatistic(prefiltered: DirectoryContent): { date: Date, endDate: Date, dateStr: string, count: number, max: number }[] { if (!prefiltered || - !prefiltered.media || - prefiltered.media.length === 0) { + !prefiltered.media || + prefiltered.media.length === 0) { return []; } const ret: { date: Date, endDate: Date, dateStr: string, count: number, max: number }[] = []; const minDate = prefiltered.media.reduce( - (p, curr) => Math.min(p, curr.metadata.creationDate), - Number.MAX_VALUE - 1 + (p, curr) => Math.min(p, curr.metadata.creationDate), + Number.MAX_VALUE - 1 ); const maxDate = prefiltered.media.reduce( - (p, curr) => Math.max(p, curr.metadata.creationDate), - Number.MIN_VALUE + 1 + (p, curr) => Math.max(p, curr.metadata.creationDate), + Number.MIN_VALUE + 1 ); const diff = (maxDate - minDate) / 1000; const H = 60 * 60; @@ -221,124 +221,125 @@ export class FilterService { } public applyFilters( - directoryContent: Observable + directoryContent: Observable ): Observable { return directoryContent.pipe( - switchMap((dirContent: DirectoryContent) => { - this.statistic = this.getStatistic(dirContent); - this.resetFilters(false); - return this.activeFilters.pipe( - map((afilters) => { - if (!dirContent || !dirContent.media || (!afilters.filtersVisible && !afilters.areFiltersActive)) { - return dirContent; + debounceTime(1), + switchMap((dirContent: DirectoryContent) => { + this.statistic = this.getStatistic(dirContent); + this.resetFilters(false); + return this.activeFilters.pipe( + map((afilters) => { + if (!dirContent || !dirContent.media || (!afilters.filtersVisible && !afilters.areFiltersActive)) { + return dirContent; + } + + // clone, so the original won't get overwritten + const c = { + media: dirContent.media, + directories: dirContent.directories, + metaFile: dirContent.metaFile, + }; + + /* Date Selector */ + if (c.media.length > 0) { + // Update date filter range + afilters.dateFilter.minDate = c.media.reduce( + (p, curr) => Math.min(p, curr.metadata.creationDate), + Number.MAX_VALUE - 1 + ); + afilters.dateFilter.maxDate = c.media.reduce( + (p, curr) => Math.max(p, curr.metadata.creationDate), + Number.MIN_VALUE + 1 + ); + // Add a few sec padding + afilters.dateFilter.minDate -= (afilters.dateFilter.minDate % 1000) + 1000; + afilters.dateFilter.maxDate += (afilters.dateFilter.maxDate % 1000) + 1000; + + if (afilters.dateFilter.minFilter === Number.MIN_VALUE) { + afilters.dateFilter.minFilter = afilters.dateFilter.minDate; + } + if (afilters.dateFilter.maxFilter === Number.MAX_VALUE) { + afilters.dateFilter.maxFilter = afilters.dateFilter.maxDate; + } + + // Apply Date filter + c.media = c.media.filter( + (m) => + m.metadata.creationDate >= afilters.dateFilter.minFilter && + m.metadata.creationDate <= afilters.dateFilter.maxFilter + ); + } else { + afilters.dateFilter.minDate = Number.MIN_VALUE; + afilters.dateFilter.maxDate = Number.MAX_VALUE; + afilters.dateFilter.minFilter = Number.MIN_VALUE; + afilters.dateFilter.maxFilter = Number.MAX_VALUE; + } + + // filters + for (const f of afilters.selectedFilters) { + + /* Update filter options */ + const valueMap: { [key: string]: any } = {}; + f.options.forEach((o) => { + valueMap[o.name] = o; + o.count = 0; // reset count so unknown option can be removed at the end + }); + + if (f.filter.isArrayValue) { + c.media.forEach((m) => { + (f.filter.mapFn(m as PhotoDTO) as string[])?.forEach((v) => { + valueMap[v] = valueMap[v] || { + name: v, + count: 0, + selected: true, + }; + valueMap[v].count++; + }); + }); + } else { + c.media.forEach((m) => { + const key = f.filter.mapFn(m as PhotoDTO) as string; + valueMap[key] = valueMap[key] || { + name: key, + count: 0, + selected: true, + }; + valueMap[key].count++; + }); + } + + f.options = Object.values(valueMap) + .filter((o) => o.count > 0) + .sort((a, b) => b.count - a.count); + + /* Apply filters */ + f.options.forEach((opt) => { + if (opt.selected) { + return; } - - // clone, so the original won't get overwritten - const c = { - media: dirContent.media, - directories: dirContent.directories, - metaFile: dirContent.metaFile, - }; - - /* Date Selector */ - if (c.media.length > 0) { - // Update date filter range - afilters.dateFilter.minDate = c.media.reduce( - (p, curr) => Math.min(p, curr.metadata.creationDate), - Number.MAX_VALUE - 1 - ); - afilters.dateFilter.maxDate = c.media.reduce( - (p, curr) => Math.max(p, curr.metadata.creationDate), - Number.MIN_VALUE + 1 - ); - // Add a few sec padding - afilters.dateFilter.minDate -= (afilters.dateFilter.minDate % 1000) + 1000; - afilters.dateFilter.maxDate += (afilters.dateFilter.maxDate % 1000) + 1000; - - if (afilters.dateFilter.minFilter === Number.MIN_VALUE) { - afilters.dateFilter.minFilter = afilters.dateFilter.minDate; - } - if (afilters.dateFilter.maxFilter === Number.MAX_VALUE) { - afilters.dateFilter.maxFilter = afilters.dateFilter.maxDate; - } - - // Apply Date filter - c.media = c.media.filter( - (m) => - m.metadata.creationDate >= afilters.dateFilter.minFilter && - m.metadata.creationDate <= afilters.dateFilter.maxFilter - ); + if (f.filter.isArrayValue) { + c.media = c.media.filter((m) => { + const mapped = f.filter.mapFn(m as PhotoDTO) as string[]; + if (!mapped) { + return true; + } + return mapped.indexOf(opt.name) === -1; + }); } else { - afilters.dateFilter.minDate = Number.MIN_VALUE; - afilters.dateFilter.maxDate = Number.MAX_VALUE; - afilters.dateFilter.minFilter = Number.MIN_VALUE; - afilters.dateFilter.maxFilter = Number.MAX_VALUE; + c.media = c.media.filter( + (m) => + (f.filter.mapFn(m as PhotoDTO) as string) !== opt.name + ); } - - // filters - for (const f of afilters.selectedFilters) { - - /* Update filter options */ - const valueMap: { [key: string]: any } = {}; - f.options.forEach((o) => { - valueMap[o.name] = o; - o.count = 0; // reset count so unknown option can be removed at the end - }); - - if (f.filter.isArrayValue) { - c.media.forEach((m) => { - (f.filter.mapFn(m as PhotoDTO) as string[])?.forEach((v) => { - valueMap[v] = valueMap[v] || { - name: v, - count: 0, - selected: true, - }; - valueMap[v].count++; - }); - }); - } else { - c.media.forEach((m) => { - const key = f.filter.mapFn(m as PhotoDTO) as string; - valueMap[key] = valueMap[key] || { - name: key, - count: 0, - selected: true, - }; - valueMap[key].count++; - }); - } - - f.options = Object.values(valueMap) - .filter((o) => o.count > 0) - .sort((a, b) => b.count - a.count); - - /* Apply filters */ - f.options.forEach((opt) => { - if (opt.selected) { - return; - } - if (f.filter.isArrayValue) { - c.media = c.media.filter((m) => { - const mapped = f.filter.mapFn(m as PhotoDTO) as string[]; - if (!mapped) { - return true; - } - return mapped.indexOf(opt.name) === -1; - }); - } else { - c.media = c.media.filter( - (m) => - (f.filter.mapFn(m as PhotoDTO) as string) !== opt.name - ); - } - }); - } - // If the number of photos did not change, the filters are not active - afilters.areFiltersActive = c.media.length !== dirContent.media.length; - return c; - }) - ); - }) + }); + } + // If the number of photos did not change, the filters are not active + afilters.areFiltersActive = c.media.length !== dirContent.media.length; + return c; + }) + ); + }) ); } diff --git a/src/frontend/app/ui/gallery/grid/grid.gallery.component.ts b/src/frontend/app/ui/gallery/grid/grid.gallery.component.ts index 30479c28..fa93b692 100644 --- a/src/frontend/app/ui/gallery/grid/grid.gallery.component.ts +++ b/src/frontend/app/ui/gallery/grid/grid.gallery.component.ts @@ -385,6 +385,9 @@ export class GalleryGridComponent * Makes sure that the photo with the given mediaString is visible on the screen */ private renderUpToMedia(mediaStringId: string): void { + if (!this.mediaGroups) { + return; + } let groupIndex = -1; let mediaIndex = -1; for (let i = 0; i < this.mediaGroups.length; ++i) { diff --git a/src/frontend/app/ui/gallery/lightbox/lightbox.gallery.component.ts b/src/frontend/app/ui/gallery/lightbox/lightbox.gallery.component.ts index 214c09c6..261c1e9c 100644 --- a/src/frontend/app/ui/gallery/lightbox/lightbox.gallery.component.ts +++ b/src/frontend/app/ui/gallery/lightbox/lightbox.gallery.component.ts @@ -67,14 +67,14 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit { }; constructor( - public fullScreenService: FullScreenService, - private changeDetector: ChangeDetectorRef, - private overlayService: OverlayService, - private builder: AnimationBuilder, - private router: Router, - private queryService: QueryService, - private route: ActivatedRoute, - private piTitleService: PiTitleService + public fullScreenService: FullScreenService, + private changeDetector: ChangeDetectorRef, + private overlayService: OverlayService, + private builder: AnimationBuilder, + private router: Router, + private queryService: QueryService, + private route: ActivatedRoute, + private piTitleService: PiTitleService ) { } @@ -104,22 +104,23 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit { this.infoPanelMaxWidth = 1000; this.updatePhotoFrameDim(); this.subscription.route = this.route.queryParams.subscribe( - (params: Params) => { - if ( - params[QueryParams.gallery.photo] && - params[QueryParams.gallery.photo] !== '' - ) { - this.delayedMediaShow = params[QueryParams.gallery.photo] - // photos are not yet available to show - if (!this.gridPhotoQL) { - return; - } - this.onNavigateTo(params[QueryParams.gallery.photo]); - } else if (this.status === LightboxStates.Open) { - this.delayedMediaShow = null; - this.hideLightbox(); + (params: Params) => { + this.delayedMediaShow = null; + if ( + params[QueryParams.gallery.photo] && + params[QueryParams.gallery.photo] !== '' + ) { + this.delayedMediaShow = params[QueryParams.gallery.photo]; + // photos are not yet available to show + if (!this.gridPhotoQL) { + return; } + this.onNavigateTo(params[QueryParams.gallery.photo]); + } else if (this.status === LightboxStates.Open) { + this.delayedMediaShow = null; + this.hideLightbox(); } + } ); } @@ -144,9 +145,9 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit { onNavigateTo(photoStringId: string): string { if ( - this.activePhoto && - this.queryService.getMediaStringId(this.activePhoto.gridMedia.media) === - photoStringId + this.activePhoto && + this.queryService.getMediaStringId(this.activePhoto.gridMedia.media) === + photoStringId ) { return; } @@ -155,8 +156,8 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit { this.controls.resetZoom(); } const photo = this.gridPhotoQL.find( - (i): boolean => - this.queryService.getMediaStringId(i.gridMedia.media) === photoStringId + (i): boolean => + this.queryService.getMediaStringId(i.gridMedia.media) === photoStringId ); if (!photo) { return (this.delayedMediaShow = photoStringId); @@ -175,17 +176,17 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit { } this.gridPhotoQL = value; this.subscription.photosChange = this.gridPhotoQL.changes.subscribe( - (): void => { - if ( - this.activePhotoId != null && - this.gridPhotoQL.length > this.activePhotoId - ) { - this.updateActivePhoto(this.activePhotoId); - } - if (this.delayedMediaShow) { - this.onNavigateTo(this.delayedMediaShow); - } + (): void => { + if ( + this.activePhotoId != null && + this.gridPhotoQL.length > this.activePhotoId + ) { + this.updateActivePhoto(this.activePhotoId); } + if (this.delayedMediaShow) { + this.onNavigateTo(this.delayedMediaShow); + } + } ); if (this.delayedMediaShow) { @@ -233,8 +234,8 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit { lightboxDimension.top -= PageHelper.ScrollY; this.animating = true; this.animatePhoto( - selectedPhoto.getDimension(), - this.calcLightBoxPhotoDimension(selectedPhoto.gridMedia.media) + selectedPhoto.getDimension(), + this.calcLightBoxPhotoDimension(selectedPhoto.gridMedia.media) ).onDone((): void => { this.animating = false; this.status = LightboxStates.Open; @@ -257,41 +258,41 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit { public hide(): void { this.router - .navigate([], {queryParams: this.queryService.getParams()}) - .then(() => { - this.piTitleService.setLastNonMedia(); - }) - .catch(console.error); + .navigate([], {queryParams: this.queryService.getParams()}) + .then(() => { + this.piTitleService.setLastNonMedia(); + }) + .catch(console.error); } animatePhoto(from: Dimension, to: Dimension = from): AnimationPlayer { const elem = this.builder - .build([ - style(DimensionUtils.toString(from)), - animate('0.2s ease-in-out', style(DimensionUtils.toString(to))), - ]) - .create(this.mediaElement.elementRef.nativeElement); + .build([ + style(DimensionUtils.toString(from)), + animate('0.2s ease-in-out', style(DimensionUtils.toString(to))), + ]) + .create(this.mediaElement.elementRef.nativeElement); elem.play(); return elem; } animateLightbox( - from: Dimension = { - top: 0, - left: 0, - width: this.photoFrameDim.width, - height: this.photoFrameDim.height, - } as Dimension, - to: Dimension = from + from: Dimension = { + top: 0, + left: 0, + width: this.photoFrameDim.width, + height: this.photoFrameDim.height, + } as Dimension, + to: Dimension = from ): AnimationPlayer { const elem = this.builder - .build([ - style(DimensionUtils.toString(from)), - animate('0.2s ease-in-out', style(DimensionUtils.toString(to))), - ]) - .create(this.lightboxElement.nativeElement); + .build([ + style(DimensionUtils.toString(from)), + animate('0.2s ease-in-out', style(DimensionUtils.toString(to))), + ]) + .create(this.lightboxElement.nativeElement); elem.play(); return elem; } @@ -311,30 +312,30 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit { }, 1000); const starPhotoPos = this.calcLightBoxPhotoDimension( - this.activePhoto.gridMedia.media + this.activePhoto.gridMedia.media ); this.infoPanelWidth = 0; this.updatePhotoFrameDim(); const endPhotoPos = this.calcLightBoxPhotoDimension( - this.activePhoto.gridMedia.media + this.activePhoto.gridMedia.media ); if (enableAnimate) { this.animatePhoto(starPhotoPos, endPhotoPos); } if (enableAnimate) { this.animateLightbox( - { - top: 0, - left: 0, - width: Math.max(this.photoFrameDim.width - this.infoPanelMaxWidth, 0), - height: this.photoFrameDim.height, - } as Dimension, - { - top: 0, - left: 0, - width: this.photoFrameDim.width, - height: this.photoFrameDim.height, - } as Dimension + { + top: 0, + left: 0, + width: Math.max(this.photoFrameDim.width - this.infoPanelMaxWidth, 0), + height: this.photoFrameDim.height, + } as Dimension, + { + top: 0, + left: 0, + width: this.photoFrameDim.width, + height: this.photoFrameDim.height, + } as Dimension ); } } @@ -348,27 +349,27 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit { this.infoPanelVisible = true; const starPhotoPos = this.calcLightBoxPhotoDimension( - this.activePhoto.gridMedia.media + this.activePhoto.gridMedia.media ); this.infoPanelWidth = this.infoPanelMaxWidth; this.updatePhotoFrameDim(); const endPhotoPos = this.calcLightBoxPhotoDimension( - this.activePhoto.gridMedia.media + this.activePhoto.gridMedia.media ); this.animatePhoto(starPhotoPos, endPhotoPos); this.animateLightbox( - { - top: 0, - left: 0, - width: this.photoFrameDim.width + this.infoPanelMaxWidth, - height: this.photoFrameDim.height, - } as Dimension, - { - top: 0, - left: 0, - width: this.photoFrameDim.width, - height: this.photoFrameDim.height, - } as Dimension + { + top: 0, + left: 0, + width: this.photoFrameDim.width + this.infoPanelMaxWidth, + height: this.photoFrameDim.height, + } as Dimension, + { + top: 0, + left: 0, + width: this.photoFrameDim.width, + height: this.photoFrameDim.height, + } as Dimension ); if (this.iPvisibilityTimer != null) { clearTimeout(this.iPvisibilityTimer); @@ -394,28 +395,28 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit { private updatePhotoFrameDim = (): void => { this.photoFrameDim = { width: Math.max( - window.innerWidth - this.infoPanelWidth, - 0 + window.innerWidth - this.infoPanelWidth, + 0 ), height: window.innerHeight, aspect: 0 }; this.photoFrameDim.aspect = - Math.round((this.photoFrameDim.width / this.photoFrameDim.height) * 100) / - 100; + Math.round((this.photoFrameDim.width / this.photoFrameDim.height) * 100) / + 100; }; private navigateToPhoto(photoIndex: number): void { this.router - .navigate([], { - queryParams: this.queryService.getParams( - this.gridPhotoQL.get(photoIndex).gridMedia.media - ), - }) - .then(() => { - this.piTitleService.setMediaTitle(this.gridPhotoQL.get(photoIndex).gridMedia); - }) - .catch(console.error); + .navigate([], { + queryParams: this.queryService.getParams( + this.gridPhotoQL.get(photoIndex).gridMedia.media + ), + }) + .then(() => { + this.piTitleService.setMediaTitle(this.gridPhotoQL.get(photoIndex).gridMedia); + }) + .catch(console.error); } @@ -442,17 +443,17 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit { this.blackCanvasOpacity = 0; this.animatePhoto( - this.calcLightBoxPhotoDimension(this.activePhoto.gridMedia.media), - this.activePhoto.getDimension() + this.calcLightBoxPhotoDimension(this.activePhoto.gridMedia.media), + this.activePhoto.getDimension() ); this.animateLightbox( - { - top: 0, - left: 0, - width: this.photoFrameDim.width, - height: this.photoFrameDim.height, - } as Dimension, - lightboxDimension + { + top: 0, + left: 0, + width: this.photoFrameDim.width, + height: this.photoFrameDim.height, + } as Dimension, + lightboxDimension ).onDone((): void => { this.status = LightboxStates.Closed; this.activePhoto = null; @@ -473,7 +474,7 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit { if (resize) { this.animatePhoto( - this.calcLightBoxPhotoDimension(this.activePhoto.gridMedia.media) + this.calcLightBoxPhotoDimension(this.activePhoto.gridMedia.media) ); } this.navigation.hasPrev = photoIndex > 0; @@ -483,8 +484,8 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit { // if target image out of screen -> scroll to there if ( - PageHelper.ScrollY > to.top || - PageHelper.ScrollY + this.photoFrameDim.height < to.top + PageHelper.ScrollY > to.top || + PageHelper.ScrollY + this.photoFrameDim.height < to.top ) { PageHelper.ScrollY = to.top; } @@ -507,15 +508,15 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit { const windowAspect = this.photoFrameDim.aspect; if (photoAspect < windowAspect) { width = Math.round( - photo.metadata.size.width * - (this.photoFrameDim.height / photo.metadata.size.height) + photo.metadata.size.width * + (this.photoFrameDim.height / photo.metadata.size.height) ); height = this.photoFrameDim.height; } else { width = this.photoFrameDim.width; height = Math.round( - photo.metadata.size.height * - (this.photoFrameDim.width / photo.metadata.size.width) + photo.metadata.size.height * + (this.photoFrameDim.width / photo.metadata.size.width) ); } const top = this.photoFrameDim.height / 2 - height / 2;