1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2025-01-10 04:07:35 +02:00

Refactoring gallery to use observable for soring #287

This commit is contained in:
Patrik J. Braun 2022-02-11 19:05:51 +01:00
parent fc385d9617
commit 169d59fb8e
8 changed files with 314 additions and 260 deletions

View File

@ -114,6 +114,8 @@ import {BlogService} from './ui/gallery/blog/blog.service';
import {PhotoFilterPipe} from './pipes/PhotoFilterPipe'; import {PhotoFilterPipe} from './pipes/PhotoFilterPipe';
import {PreviewSettingsComponent} from './ui/settings/preview/preview.settings.component'; import {PreviewSettingsComponent} from './ui/settings/preview/preview.settings.component';
import {GallerySearchFieldComponent} from './ui/gallery/search/search-field/search-field.gallery.component'; import {GallerySearchFieldComponent} from './ui/gallery/search/search-field/search-field.gallery.component';
import {GalleryFilterComponent} from './ui/gallery/filter/filter.gallery.component';
import {GallerySortingService} from './ui/gallery/navigator/sorting.service';
@Injectable() @Injectable()
export class MyHammerConfig extends HammerGestureConfig { export class MyHammerConfig extends HammerGestureConfig {
@ -213,6 +215,7 @@ Marker.prototype.options.icon = iconDefault;
GallerySearchQueryBuilderComponent, GallerySearchQueryBuilderComponent,
GalleryShareComponent, GalleryShareComponent,
GalleryNavigatorComponent, GalleryNavigatorComponent,
GalleryFilterComponent,
GalleryPhotoComponent, GalleryPhotoComponent,
AdminComponent, AdminComponent,
InfoPanelLightboxComponent, InfoPanelLightboxComponent,
@ -270,6 +273,7 @@ Marker.prototype.options.icon = iconDefault;
AlbumsService, AlbumsService,
GalleryCacheService, GalleryCacheService,
GalleryService, GalleryService,
GallerySortingService,
MapService, MapService,
BlogService, BlogService,
SearchQueryParserService, SearchQueryParserService,

View File

@ -41,18 +41,18 @@
</div> </div>
<!-- Its safe to hand over both as only one should have a value (search result or dir listing)--> <!-- Its safe to hand over both as only one should have a value (search result or dir listing)-->
<app-gallery-navbar [searchResult]="ContentWrapper.searchResult" <app-gallery-navbar></app-gallery-navbar>
[directory]="ContentWrapper.directory"></app-gallery-navbar>
<app-gallery-directories class="directories" [directories]="directories"></app-gallery-directories> <app-gallery-directories class="directories"
[directories]="(directoryContent | async)?.directories || []"></app-gallery-directories>
<div class="blog-map-row"> <div class="blog-map-row">
<div class="blog-wrapper" <div class="blog-wrapper"
[style.width]="blogOpen ? '100%' : 'calc(100% - 100px)'" [style.width]="blogOpen ? '100%' : 'calc(100% - 100px)'"
*ngIf="config.Client.MetaFile.markdown && Content.metaFile && (Content.metaFile | mdFiles).length>0"> *ngIf="config.Client.MetaFile.markdown && (directoryContent | async)?.metaFile && ((directoryContent | async).metaFile | mdFiles).length>0">
<app-gallery-blog [collapsed]="!blogOpen" <app-gallery-blog [collapsed]="!blogOpen"
[mdFiles]="Content.metaFile | mdFiles"></app-gallery-blog> [mdFiles]="(directoryContent | async).metaFile | mdFiles"></app-gallery-blog>
<button class="btn btn-blog-details" (click)="blogOpen=!blogOpen"><span <button class="btn btn-blog-details" (click)="blogOpen=!blogOpen"><span
@ -60,10 +60,10 @@
</button> </button>
</div> </div>
<app-gallery-map *ngIf="isPhotoWithLocation && mapEnabled" <app-gallery-map *ngIf="isPhotoWithLocation && mapEnabled"
[photos]="Content.media | photosOnly" [photos]="(directoryContent | async).media | photosOnly"
[gpxFiles]="Content.metaFile | gpxFiles"></app-gallery-map> [gpxFiles]="(directoryContent | async).metaFile | gpxFiles"></app-gallery-map>
</div> </div>
<app-gallery-grid [media]="Content.media" <app-gallery-grid [mediaObs]="mediaObs"
[lightbox]="lightbox"></app-gallery-grid> [lightbox]="lightbox"></app-gallery-grid>
</ng-container> </ng-container>

View File

@ -1,7 +1,7 @@
import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {AuthenticationService} from '../../model/network/authentication.service'; import {AuthenticationService} from '../../model/network/authentication.service';
import {ActivatedRoute, Params, Router} from '@angular/router'; import {ActivatedRoute, Params, Router} from '@angular/router';
import {ContentWrapperWithError, GalleryService} from './gallery.service'; import {ContentWrapperWithError, DirectoryContent, GalleryService} from './gallery.service';
import {GalleryGridComponent} from './grid/grid.gallery.component'; import {GalleryGridComponent} from './grid/grid.gallery.component';
import {Config} from '../../../../common/config/public/Config'; import {Config} from '../../../../common/config/public/Config';
import {ParentDirectoryDTO, SubDirectoryDTO} from '../../../../common/entities/DirectoryDTO'; import {ParentDirectoryDTO, SubDirectoryDTO} from '../../../../common/entities/DirectoryDTO';
@ -12,11 +12,12 @@ import {UserRoles} from '../../../../common/entities/UserDTO';
import {interval, Observable, Subscription} from 'rxjs'; import {interval, Observable, Subscription} from 'rxjs';
import {ContentWrapper} from '../../../../common/entities/ConentWrapper'; import {ContentWrapper} from '../../../../common/entities/ConentWrapper';
import {PageHelper} from '../../model/page.helper'; import {PageHelper} from '../../model/page.helper';
import {SortingMethods} from '../../../../common/entities/SortingMethods';
import {PhotoDTO} from '../../../../common/entities/PhotoDTO'; import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
import {QueryParams} from '../../../../common/QueryParams'; import {QueryParams} from '../../../../common/QueryParams';
import {SeededRandomService} from '../../model/seededRandom.service'; import {map, take} from 'rxjs/operators';
import {take} from 'rxjs/operators'; import {GallerySortingService} from './navigator/sorting.service';
import {Media} from './Media';
import {MediaDTO} from '../../../../common/entities/MediaDTO';
@Component({ @Component({
selector: 'app-gallery', selector: 'app-gallery',
@ -37,6 +38,8 @@ export class GalleryComponent implements OnInit, OnDestroy {
public isPhotoWithLocation = false; public isPhotoWithLocation = false;
public countDown: { day: number, hour: number, minute: number, second: number } = null; public countDown: { day: number, hour: number, minute: number, second: number } = null;
public readonly mapEnabled: boolean; public readonly mapEnabled: boolean;
public readonly directoryContent: Observable<DirectoryContent>;
public readonly mediaObs: Observable<MediaDTO[]>;
private $counter: Observable<number>; private $counter: Observable<number>;
private subscription: { [key: string]: Subscription } = { private subscription: { [key: string]: Subscription } = {
content: null, content: null,
@ -44,7 +47,6 @@ export class GalleryComponent implements OnInit, OnDestroy {
timer: null, timer: null,
sorting: null sorting: null
}; };
private collator = new Intl.Collator(undefined, {numeric: true});
constructor(public galleryService: GalleryService, constructor(public galleryService: GalleryService,
private authService: AuthenticationService, private authService: AuthenticationService,
@ -52,15 +54,13 @@ export class GalleryComponent implements OnInit, OnDestroy {
private shareService: ShareService, private shareService: ShareService,
private route: ActivatedRoute, private route: ActivatedRoute,
private navigation: NavigationService, private navigation: NavigationService,
private rndService: SeededRandomService) { private sortingService: GallerySortingService) {
this.mapEnabled = Config.Client.Map.enabled; this.mapEnabled = Config.Client.Map.enabled;
this.directoryContent = this.sortingService.applySorting(this.galleryService.directoryContent);
this.mediaObs = this.directoryContent.pipe(map(c => c?.media));
PageHelper.showScrollY(); PageHelper.showScrollY();
} }
get Content(): SearchResultDTO | ParentDirectoryDTO {
const cont = (this.ContentWrapper.searchResult || this.ContentWrapper.directory);
return cont ? cont : {} as any;
}
get ContentWrapper(): ContentWrapperWithError { get ContentWrapper(): ContentWrapperWithError {
return this.galleryService.content.value; return this.galleryService.content.value;
@ -119,11 +119,11 @@ export class GalleryComponent implements OnInit, OnDestroy {
this.$counter = interval(1000); this.$counter = interval(1000);
this.subscription.timer = this.$counter.subscribe((x): void => this.updateTimer(x)); this.subscription.timer = this.$counter.subscribe((x): void => this.updateTimer(x));
} }
/*
this.subscription.sorting = this.galleryService.sorting.subscribe((): void => { this.subscription.sorting = this.galleryService.sorting.subscribe((): void => {
this.sortDirectories(); this.sortDirectories();
}); });
*/
} }
private onRoute = async (params: Params): Promise<void> => { private onRoute = async (params: Params): Promise<void> => {
@ -155,7 +155,7 @@ export class GalleryComponent implements OnInit, OnDestroy {
media: [] media: []
}) as ParentDirectoryDTO | SearchResultDTO; }) as ParentDirectoryDTO | SearchResultDTO;
this.directories = tmp.directories; this.directories = tmp.directories;
this.sortDirectories(); // this.sortDirectories();
this.isPhotoWithLocation = false; this.isPhotoWithLocation = false;
for (const media of tmp.media as PhotoDTO[]) { for (const media of tmp.media as PhotoDTO[]) {
@ -169,50 +169,4 @@ export class GalleryComponent implements OnInit, OnDestroy {
} }
} }
}; };
private sortDirectories(): void {
if (!this.directories) {
return;
}
switch (this.galleryService.sorting.value) {
case SortingMethods.ascRating: // directories does not have rating
case SortingMethods.ascName:
this.directories.sort((a, b) => this.collator.compare(a.name, b.name));
break;
case SortingMethods.ascDate:
if (Config.Client.Other.enableDirectorySortingByDate === true) {
this.directories.sort((a, b) => a.lastModified - b.lastModified);
break;
}
this.directories.sort((a, b) => this.collator.compare(a.name, b.name));
break;
case SortingMethods.descRating: // directories does not have rating
case SortingMethods.descName:
this.directories.sort((a, b) => this.collator.compare(b.name, a.name));
break;
case SortingMethods.descDate:
if (Config.Client.Other.enableDirectorySortingByDate === true) {
this.directories.sort((a, b) => b.lastModified - a.lastModified);
break;
}
this.directories.sort((a, b) => this.collator.compare(b.name, a.name));
break;
case SortingMethods.random:
this.rndService.setSeed(this.directories.length);
this.directories.sort((a, b): number => {
if (a.name.toLowerCase() < b.name.toLowerCase()) {
return 1;
}
if (a.name.toLowerCase() > b.name.toLowerCase()) {
return -1;
}
return 0;
}).sort((): number => {
return this.rndService.get() - 0.5;
});
break;
}
}
} }

View File

@ -1,24 +1,25 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {NetworkService} from '../../model/network/network.service'; import {NetworkService} from '../../model/network/network.service';
import {ContentWrapper} from '../../../../common/entities/ConentWrapper'; import {ContentWrapper} from '../../../../common/entities/ConentWrapper';
import {DirectoryDTOUtils, ParentDirectoryDTO} from '../../../../common/entities/DirectoryDTO'; import {DirectoryDTOUtils, ParentDirectoryDTO, SubDirectoryDTO} from '../../../../common/entities/DirectoryDTO';
import {GalleryCacheService} from './cache.gallery.service'; import {GalleryCacheService} from './cache.gallery.service';
import {BehaviorSubject} from 'rxjs'; import {BehaviorSubject, Observable} from 'rxjs';
import {Config} from '../../../../common/config/public/Config'; import {Config} from '../../../../common/config/public/Config';
import {ShareService} from './share.service'; import {ShareService} from './share.service';
import {NavigationService} from '../../model/navigation.service'; import {NavigationService} from '../../model/navigation.service';
import {SortingMethods} from '../../../../common/entities/SortingMethods';
import {QueryParams} from '../../../../common/QueryParams'; import {QueryParams} from '../../../../common/QueryParams';
import {PG2ConfMap} from '../../../../common/PG2ConfMap';
import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO'; import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO';
import {ErrorCodes} from '../../../../common/entities/Error'; import {ErrorCodes} from '../../../../common/entities/Error';
import {map} from 'rxjs/operators';
import {MediaDTO} from '../../../../common/entities/MediaDTO';
import {FileDTO} from '../../../../common/entities/FileDTO';
@Injectable() @Injectable()
export class GalleryService { export class GalleryService {
public content: BehaviorSubject<ContentWrapperWithError>; public content: BehaviorSubject<ContentWrapperWithError>;
public sorting: BehaviorSubject<SortingMethods>; public directoryContent: Observable<DirectoryContent>;
lastRequest: { directory: string } = { lastRequest: { directory: string } = {
directory: null directory: null
}; };
@ -31,42 +32,14 @@ export class GalleryService {
private shareService: ShareService, private shareService: ShareService,
private navigationService: NavigationService) { private navigationService: NavigationService) {
this.content = new BehaviorSubject<ContentWrapperWithError>(new ContentWrapperWithError()); this.content = new BehaviorSubject<ContentWrapperWithError>(new ContentWrapperWithError());
this.sorting = new BehaviorSubject<SortingMethods>(Config.Client.Other.defaultPhotoSortingMethod); this.directoryContent = this.content.pipe(map(c =>
} c.directory ? c.directory : c.searchResult
));
getDefaultSorting(directory: ParentDirectoryDTO): SortingMethods {
if (directory && directory.metaFile) {
for (const file in PG2ConfMap.sorting) {
if (directory.metaFile.some(f => f.name === file)) {
return (PG2ConfMap.sorting as any)[file];
}
}
}
return Config.Client.Other.defaultPhotoSortingMethod;
}
setSorting(sorting: SortingMethods): void {
this.sorting.next(sorting);
if (this.content.value.directory) {
if (sorting !== this.getDefaultSorting(this.content.value.directory)) {
this.galleryCacheService.setSorting(this.content.value.directory, sorting);
} else {
this.galleryCacheService.removeSorting(this.content.value.directory);
}
}
} }
setContent(content: ContentWrapperWithError): void { setContent(content: ContentWrapperWithError): void {
this.content.next(content); this.content.next(content);
if (content.directory) {
const sort = this.galleryCacheService.getSorting(content.directory);
if (sort !== null) {
this.sorting.next(sort);
} else {
this.sorting.next(this.getDefaultSorting(content.directory));
}
}
} }
@ -161,3 +134,9 @@ export class GalleryService {
export class ContentWrapperWithError extends ContentWrapper { export class ContentWrapperWithError extends ContentWrapper {
public error: string; public error: string;
} }
export interface DirectoryContent {
directories: SubDirectoryDTO[];
media: MediaDTO[];
metaFile: FileDTO[];
}

View File

@ -5,14 +5,12 @@ import {
ElementRef, ElementRef,
HostListener, HostListener,
Input, Input,
OnChanges,
OnDestroy, OnDestroy,
OnInit, OnInit,
QueryList, QueryList,
ViewChild, ViewChild,
ViewChildren ViewChildren
} from '@angular/core'; } from '@angular/core';
import {PhotoDTO} from '../../../../../common/entities/PhotoDTO';
import {GridRowBuilder} from './GridRowBuilder'; import {GridRowBuilder} from './GridRowBuilder';
import {GalleryLightboxComponent} from '../lightbox/lightbox.gallery.component'; import {GalleryLightboxComponent} from '../lightbox/lightbox.gallery.component';
import {GridMedia} from './GridMedia'; import {GridMedia} from './GridMedia';
@ -20,37 +18,34 @@ import {GalleryPhotoComponent} from './photo/photo.grid.gallery.component';
import {OverlayService} from '../overlay.service'; import {OverlayService} from '../overlay.service';
import {Config} from '../../../../../common/config/public/Config'; import {Config} from '../../../../../common/config/public/Config';
import {PageHelper} from '../../../model/page.helper'; import {PageHelper} from '../../../model/page.helper';
import {Subscription} from 'rxjs'; import {Observable, Subscription} from 'rxjs';
import {ActivatedRoute, Params, Router} from '@angular/router'; import {ActivatedRoute, Params, Router} from '@angular/router';
import {QueryService} from '../../../model/query.service'; import {QueryService} from '../../../model/query.service';
import {GalleryService} from '../gallery.service'; import {GalleryService} from '../gallery.service';
import {SortingMethods} from '../../../../../common/entities/SortingMethods';
import {MediaDTO, MediaDTOUtils} from '../../../../../common/entities/MediaDTO'; import {MediaDTO, MediaDTOUtils} from '../../../../../common/entities/MediaDTO';
import {QueryParams} from '../../../../../common/QueryParams'; import {QueryParams} from '../../../../../common/QueryParams';
import {SeededRandomService} from '../../../model/seededRandom.service';
@Component({ @Component({
selector: 'app-gallery-grid', selector: 'app-gallery-grid',
templateUrl: './grid.gallery.component.html', templateUrl: './grid.gallery.component.html',
styleUrls: ['./grid.gallery.component.css'], styleUrls: ['./grid.gallery.component.css'],
}) })
export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, OnDestroy { export class GalleryGridComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('gridContainer', {static: false}) gridContainer: ElementRef; @ViewChild('gridContainer', {static: false}) gridContainer: ElementRef;
@ViewChildren(GalleryPhotoComponent) gridPhotoQL: QueryList<GalleryPhotoComponent>; @ViewChildren(GalleryPhotoComponent) gridPhotoQL: QueryList<GalleryPhotoComponent>;
@Input() media: MediaDTO[]; @Input() mediaObs: Observable<MediaDTO[]>;
@Input() lightbox: GalleryLightboxComponent; @Input() lightbox: GalleryLightboxComponent;
media: MediaDTO[];
photosToRender: GridMedia[] = []; photosToRender: GridMedia[] = [];
containerWidth = 0; containerWidth = 0;
screenHeight = 0; screenHeight = 0;
public IMAGE_MARGIN = 2; public IMAGE_MARGIN = 2;
isAfterViewInit = false; isAfterViewInit = false;
subscriptions: { subscriptions: {
route: Subscription, route: Subscription
sorting: Subscription
} = { } = {
route: null, route: null
sorting: null
}; };
delayedRenderUpToPhoto: string = null; delayedRenderUpToPhoto: string = null;
private scrollListenerPhotos: GalleryPhotoComponent[] = []; private scrollListenerPhotos: GalleryPhotoComponent[] = [];
@ -66,8 +61,8 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O
public queryService: QueryService, public queryService: QueryService,
private router: Router, private router: Router,
public galleryService: GalleryService, public galleryService: GalleryService,
private route: ActivatedRoute, private route: ActivatedRoute) {
private rndService: SeededRandomService) {
} }
ngOnInit(): void { ngOnInit(): void {
@ -81,19 +76,17 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O
this.renderUpToMedia(params[QueryParams.gallery.photo]); this.renderUpToMedia(params[QueryParams.gallery.photo]);
} }
}); });
this.subscriptions.sorting = this.galleryService.sorting.subscribe((): void => { this.mediaObs.subscribe((m) => {
this.clearRenderedPhotos(); this.media = m || [];
this.sortPhotos(); this.onChange();
this.renderPhotos();
}); });
} }
ngOnChanges(): void { onChange = () => {
if (this.isAfterViewInit === false) { if (this.isAfterViewInit === false) {
return; return;
} }
this.updateContainerDimensions(); this.updateContainerDimensions();
this.sortPhotos();
this.mergeNewPhotos(); this.mergeNewPhotos();
this.helperTime = window.setTimeout((): void => { this.helperTime = window.setTimeout((): void => {
this.renderPhotos(); this.renderPhotos();
@ -101,7 +94,8 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O
this.renderUpToMedia(this.delayedRenderUpToPhoto); this.renderUpToMedia(this.delayedRenderUpToPhoto);
} }
}, 0); }, 0);
} };
ngOnDestroy(): void { ngOnDestroy(): void {
@ -112,10 +106,6 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O
this.subscriptions.route.unsubscribe(); this.subscriptions.route.unsubscribe();
this.subscriptions.route = null; this.subscriptions.route = null;
} }
if (this.subscriptions.sorting !== null) {
this.subscriptions.sorting.unsubscribe();
this.subscriptions.sorting = null;
}
} }
@HostListener('window:resize') @HostListener('window:resize')
@ -129,7 +119,6 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O
if (this.updateContainerDimensions() === false) { if (this.updateContainerDimensions() === false) {
return; return;
} }
this.sortPhotos();
this.renderPhotos(renderedIndex); this.renderPhotos(renderedIndex);
} }
@ -137,7 +126,6 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O
this.router.navigate([], {queryParams: this.queryService.getParams(media)}); this.router.navigate([], {queryParams: this.queryService.getParams(media)});
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.lightbox.setGridPhotoQL(this.gridPhotoQL); this.lightbox.setGridPhotoQL(this.gridPhotoQL);
@ -148,7 +136,6 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O
} }
this.updateContainerDimensions(); this.updateContainerDimensions();
this.sortPhotos();
this.clearRenderedPhotos(); this.clearRenderedPhotos();
this.helperTime = window.setTimeout((): void => { this.helperTime = window.setTimeout((): void => {
this.renderPhotos(); this.renderPhotos();
@ -193,6 +180,7 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O
@HostListener('window:scroll') @HostListener('window:scroll')
onScroll(): void { onScroll(): void {
if (!this.onScrollFired && if (!this.onScrollFired &&
this.media &&
// should we trigger this at all? // should we trigger this at all?
(this.renderedPhotoIndex < this.media.length || this.scrollListenerPhotos.length > 0)) { (this.renderedPhotoIndex < this.media.length || this.scrollListenerPhotos.length > 0)) {
window.requestAnimationFrame((): void => { window.requestAnimationFrame((): void => {
@ -238,51 +226,6 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O
this.changeDetector.detectChanges(); this.changeDetector.detectChanges();
} }
private collator = new Intl.Collator(undefined, {numeric: true});
private sortPhotos(): void {
switch (this.galleryService.sorting.value) {
case SortingMethods.ascName:
this.media.sort((a: PhotoDTO, b: PhotoDTO) => this.collator.compare(a.name, b.name));
break;
case SortingMethods.descName:
this.media.sort((a: PhotoDTO, b: PhotoDTO) => this.collator.compare(b.name, a.name));
break;
case SortingMethods.ascDate:
this.media.sort((a: PhotoDTO, b: PhotoDTO): number => {
return a.metadata.creationDate - b.metadata.creationDate;
});
break;
case SortingMethods.descDate:
this.media.sort((a: PhotoDTO, b: PhotoDTO): number => {
return b.metadata.creationDate - a.metadata.creationDate;
});
break;
case SortingMethods.ascRating:
this.media.sort((a: PhotoDTO, b: PhotoDTO) => (a.metadata.rating || 0) - (b.metadata.rating || 0));
break;
case SortingMethods.descRating:
this.media.sort((a: PhotoDTO, b: PhotoDTO) => (b.metadata.rating || 0) - (a.metadata.rating || 0));
break;
case SortingMethods.random:
this.rndService.setSeed(this.media.length);
this.media.sort((a: PhotoDTO, b: PhotoDTO): number => {
if (a.name.toLowerCase() < b.name.toLowerCase()) {
return -1;
}
if (a.name.toLowerCase() > b.name.toLowerCase()) {
return 1;
}
return 0;
}).sort((): number => {
return this.rndService.get() - 0.5;
});
break;
}
}
// TODO: This is deprecated, // TODO: This is deprecated,
// we do not post update galleries anymore since the preview member in the DriectoryDTO // we do not post update galleries anymore since the preview member in the DriectoryDTO
private mergeNewPhotos(): void { private mergeNewPhotos(): void {
@ -331,6 +274,9 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O
} }
private renderPhotos(numberOfPhotos: number = 0): void { private renderPhotos(numberOfPhotos: number = 0): void {
if (!this.media) {
return;
}
if (this.containerWidth === 0 || if (this.containerWidth === 0 ||
this.renderedPhotoIndex >= this.media.length || this.renderedPhotoIndex >= this.media.length ||
!this.shouldRenderMore()) { !this.shouldRenderMore()) {

View File

@ -1,29 +1,29 @@
<nav class="nav-container" aria-label="breadcrumb"> <nav class="nav-container" aria-label="breadcrumb">
<ol *ngIf="directory" id="directory-path" class="breadcrumb"> <ol *ngIf="isDirectory | async" id="directory-path" class="breadcrumb">
<li *ngFor="let path of routes" class="breadcrumb-item"> <li *ngFor="let path of Routes | async" class="breadcrumb-item">
<a *ngIf="path.route" [routerLink]="['/gallery',path.route]" <a *ngIf="path.route" [routerLink]="['/gallery',path.route]"
[queryParams]="queryService.getParams()">{{path.name}}</a> [queryParams]="queryService.getParams()">{{path.name}}</a>
<ng-container *ngIf="!path.route">{{path.name}}</ng-container> <ng-container *ngIf="!path.route">{{path.name}}</ng-container>
</li> </li>
</ol> </ol>
<ol *ngIf="searchResult" class="breadcrumb"> <ol *ngIf="!(isDirectory | async)" class="breadcrumb">
<li class="active"> <li class="active">
<ng-container i18n>Searching for:</ng-container> <ng-container i18n>Searching for:</ng-container>
<strong> {{searchResult.searchQuery | searchQuery}}</strong> <strong> {{(wrappedContent | async).searchQuery | searchQuery}}</strong>
</li> </li>
</ol> </ol>
<div class="right-side"> <div class="right-side">
<ng-container *ngIf="ItemCount > 0 && config.Client.Other.NavBar.showItemCount"> <ng-container *ngIf="(ItemCount | async) > 0 && config.Client.Other.NavBar.showItemCount">
<div class="photos-count"> <div class="photos-count">
{{ItemCount}} <span i18n>items</span> {{ItemCount | async}} <span i18n>items</span>
</div> </div>
<div class="divider">&nbsp;</div> <div class="divider">&nbsp;</div>
</ng-container> </ng-container>
<ng-container *ngIf="config.Client.Other.enableDownloadZip && directory && ItemCount > 0"> <ng-container *ngIf="config.Client.Other.enableDownloadZip && (isDirectory | async) && (ItemCount | async) > 0">
<a [href]="getDownloadZipLink()" <a [href]="getDownloadZipLink() | async"
class="btn btn-download"> class="btn btn-download">
<span class="oi oi-data-transfer-download" <span class="oi oi-data-transfer-download"
title="download" i18n-title></span> title="download" i18n-title></span>
@ -31,10 +31,10 @@
<div class="divider">&nbsp;</div> <div class="divider">&nbsp;</div>
</ng-container> </ng-container>
<ng-container *ngIf="config.Client.Other.enableDirectoryFlattening && directory"> <ng-container *ngIf="config.Client.Other.enableDirectoryFlattening && (isDirectory | async)">
<a <a
[routerLink]="['/search', getDirectoryFlattenSearchQuery()]" [routerLink]="['/search', getDirectoryFlattenSearchQuery() | async]"
class="btn btn-download"> class="btn btn-download">
<span class="oi oi-fork" <span class="oi oi-fork"
title="Flatten directory" i18n-title></span> title="Flatten directory" i18n-title></span>
</a> </a>
@ -43,9 +43,9 @@
<div class="btn-group" dropdown placement="bottom right"> <div class="btn-group" dropdown placement="bottom right">
<button id="button-alignment" dropdownToggle type="button" <button id="button-alignment" dropdownToggle type="button"
class="btn btn-secondary dropdown-toggle" class="btn btn-secondary dropdown-toggle"
[ngClass]="{'btn-secondary':galleryService.sorting.value !== DefaultSorting}" [ngClass]="{'btn-secondary':sortingService.sorting.value !== (DefaultSorting | async)}"
aria-controls="dropdown-alignment" aria-controls="dropdown-alignment"
[innerHTML]="galleryService.sorting.value| iconizeSorting"> [innerHTML]="sortingService.sorting.value | iconizeSorting">
</button> </button>
<ul id="dropdown-alignment" *dropdownMenu class="dropdown-menu dropdown-menu-right" <ul id="dropdown-alignment" *dropdownMenu class="dropdown-menu dropdown-menu-right"
role="menu" aria-labelledby="button-alignment"> role="menu" aria-labelledby="button-alignment">
@ -62,4 +62,3 @@
</div> </div>
</nav> </nav>

View File

@ -1,15 +1,16 @@
import {Component, Input, OnChanges} from '@angular/core'; import {Component} from '@angular/core';
import {ParentDirectoryDTO} from '../../../../../common/entities/DirectoryDTO';
import {RouterLink} from '@angular/router'; import {RouterLink} from '@angular/router';
import {UserDTOUtils} from '../../../../../common/entities/UserDTO'; import {UserDTOUtils} from '../../../../../common/entities/UserDTO';
import {AuthenticationService} from '../../../model/network/authentication.service'; import {AuthenticationService} from '../../../model/network/authentication.service';
import {QueryService} from '../../../model/query.service'; import {QueryService} from '../../../model/query.service';
import {GalleryService} from '../gallery.service'; import {ContentWrapperWithError, DirectoryContent, GalleryService} from '../gallery.service';
import {Utils} from '../../../../../common/Utils'; import {Utils} from '../../../../../common/Utils';
import {SortingMethods} from '../../../../../common/entities/SortingMethods'; import {SortingMethods} from '../../../../../common/entities/SortingMethods';
import {Config} from '../../../../../common/config/public/Config'; import {Config} from '../../../../../common/config/public/Config';
import {SearchResultDTO} from '../../../../../common/entities/SearchResultDTO';
import {SearchQueryTypes, TextSearch, TextSearchQueryMatchTypes} from '../../../../../common/entities/SearchQueryDTO'; import {SearchQueryTypes, TextSearch, TextSearchQueryMatchTypes} from '../../../../../common/entities/SearchQueryDTO';
import {Observable} from 'rxjs';
import {map} from 'rxjs/operators';
import {GallerySortingService} from './sorting.service';
@Component({ @Component({
selector: 'app-gallery-navbar', selector: 'app-gallery-navbar',
@ -17,103 +18,118 @@ import {SearchQueryTypes, TextSearch, TextSearchQueryMatchTypes} from '../../../
templateUrl: './navigator.gallery.component.html', templateUrl: './navigator.gallery.component.html',
providers: [RouterLink], providers: [RouterLink],
}) })
export class GalleryNavigatorComponent implements OnChanges { export class GalleryNavigatorComponent {
@Input() directory: ParentDirectoryDTO;
@Input() searchResult: SearchResultDTO;
routes: NavigatorPath[] = [];
SortingMethods = SortingMethods; SortingMethods = SortingMethods;
sortingMethodsType: { key: number; value: string }[] = []; sortingMethodsType: { key: number; value: string }[] = [];
config = Config; readonly config = Config;
DefaultSorting = Config.Client.Other.defaultPhotoSortingMethod; // DefaultSorting = Config.Client.Other.defaultPhotoSortingMethod;
readonly SearchQueryTypes = SearchQueryTypes; readonly SearchQueryTypes = SearchQueryTypes;
wrappedContent: Observable<ContentWrapperWithError>;
public directoryContent: Observable<DirectoryContent>;
private readonly RootFolderName: string; private readonly RootFolderName: string;
constructor(private authService: AuthenticationService, constructor(private authService: AuthenticationService,
public queryService: QueryService, public queryService: QueryService,
public galleryService: GalleryService) { public galleryService: GalleryService,
public sortingService: GallerySortingService) {
this.sortingMethodsType = Utils.enumToArray(SortingMethods); this.sortingMethodsType = Utils.enumToArray(SortingMethods);
this.RootFolderName = $localize`Images`; this.RootFolderName = $localize`Images`;
this.wrappedContent = this.galleryService.content;
this.directoryContent = this.wrappedContent.pipe(map(c => c.directory ? c.directory : c.searchResult));
} }
get ItemCount(): number { get isDirectory(): Observable<boolean> {
return this.directory ? this.directory.mediaCount : this.searchResult.media.length; return this.wrappedContent.pipe(map(c => !!c.directory));
} }
ngOnChanges(): void { get ItemCount(): Observable<number> {
this.getPath(); return this.wrappedContent.pipe(map(c => c.directory ? c.directory.mediaCount : (c.searchResult ? c.searchResult.media.length : 0)));
this.DefaultSorting = this.galleryService.getDefaultSorting(this.directory);
} }
getPath(): any { get Routes(): Observable<NavigatorPath[]> {
if (!this.directory) { return this.wrappedContent.pipe(map((c) => {
return []; if (!c.directory) {
} return [];
const path = this.directory.path.replace(new RegExp('\\\\', 'g'), '/');
const dirs = path.split('/');
dirs.push(this.directory.name);
// removing empty strings
for (let i = 0; i < dirs.length; i++) {
if (!dirs[i] || 0 === dirs[i].length || '.' === dirs[i]) {
dirs.splice(i, 1);
i--;
} }
}
const user = this.authService.user.value; const path = c.directory.path.replace(new RegExp('\\\\', 'g'), '/');
const arr: NavigatorPath[] = [];
// create root link const dirs = path.split('/');
if (dirs.length === 0) { dirs.push(c.directory.name);
arr.push({name: this.RootFolderName, route: null});
} else {
arr.push({name: this.RootFolderName, route: UserDTOUtils.isDirectoryPathAvailable('/', user.permissions) ? '/' : null});
}
// create rest navigation // removing empty strings
dirs.forEach((name, index) => { for (let i = 0; i < dirs.length; i++) {
const route = dirs.slice(0, dirs.indexOf(name) + 1).join('/'); if (!dirs[i] || 0 === dirs[i].length || '.' === dirs[i]) {
if (dirs.length - 1 === index) { dirs.splice(i, 1);
arr.push({name, route: null}); i--;
}
}
const user = this.authService.user.value;
const arr: NavigatorPath[] = [];
// create root link
if (dirs.length === 0) {
arr.push({name: this.RootFolderName, route: null});
} else { } else {
arr.push({name, route: UserDTOUtils.isDirectoryPathAvailable(route, user.permissions) ? route : null}); arr.push({name: this.RootFolderName, route: UserDTOUtils.isDirectoryPathAvailable('/', user.permissions) ? '/' : null});
} }
});
// create rest navigation
dirs.forEach((name, index) => {
const route = dirs.slice(0, dirs.indexOf(name) + 1).join('/');
if (dirs.length - 1 === index) {
arr.push({name, route: null});
} else {
arr.push({name, route: UserDTOUtils.isDirectoryPathAvailable(route, user.permissions) ? route : null});
}
});
this.routes = arr; return arr;
}));
}
get DefaultSorting(): Observable<SortingMethods> {
return this.wrappedContent.pipe(map(c =>
this.sortingService.getDefaultSorting(c.directory)
));
} }
setSorting(sorting: SortingMethods): void { setSorting(sorting: SortingMethods): void {
this.galleryService.setSorting(sorting); this.sortingService.setSorting(sorting);
} }
getDownloadZipLink(): string { getDownloadZipLink(): Observable<string> {
let queryParams = ''; return this.wrappedContent.pipe(map((c) => {
Object.entries(this.queryService.getParams()).forEach(e => { if (!c.directory) {
queryParams += e[0] + '=' + e[1]; return null;
}); }
return Utils.concatUrls(Config.Client.urlBase, let queryParams = '';
'/api/gallery/zip/', Object.entries(this.queryService.getParams()).forEach(e => {
this.getDirectoryPath(), '?' + queryParams); queryParams += e[0] + '=' + e[1];
});
return Utils.concatUrls(Config.Client.urlBase,
'/api/gallery/zip/',
c.directory.path, c.directory.name, '?' + queryParams);
}));
} }
getDirectoryPath(): string { getDirectoryFlattenSearchQuery(): Observable<string> {
return Utils.concatUrls(this.directory.path, this.directory.name); return this.wrappedContent.pipe(map((c) => {
if (!c.directory) {
return null;
}
return JSON.stringify({
type: SearchQueryTypes.directory,
matchType: TextSearchQueryMatchTypes.like,
text: Utils.concatUrls('./', c.directory.path, c.directory.name)
} as TextSearch);
}));
} }
getDirectoryFlattenSearchQuery(): string {
return JSON.stringify({
type: SearchQueryTypes.directory,
matchType: TextSearchQueryMatchTypes.like,
text: Utils.concatUrls('./', this.directory.path, this.directory.name)
} as TextSearch);
}
} }
interface NavigatorPath { interface NavigatorPath {

View File

@ -0,0 +1,156 @@
import {Injectable} from '@angular/core';
import {NetworkService} from '../../../model/network/network.service';
import {ParentDirectoryDTO} from '../../../../../common/entities/DirectoryDTO';
import {GalleryCacheService} from '../cache.gallery.service';
import {BehaviorSubject, Observable} from 'rxjs';
import {Config} from '../../../../../common/config/public/Config';
import {SortingMethods} from '../../../../../common/entities/SortingMethods';
import {PG2ConfMap} from '../../../../../common/PG2ConfMap';
import {DirectoryContent, GalleryService} from '../gallery.service';
import {PhotoDTO} from '../../../../../common/entities/PhotoDTO';
import {map, mergeMap} from 'rxjs/operators';
import {SeededRandomService} from '../../../model/seededRandom.service';
@Injectable()
export class GallerySortingService {
public sorting: BehaviorSubject<SortingMethods>;
private collator = new Intl.Collator(undefined, {numeric: true});
constructor(private networkService: NetworkService,
private galleryCacheService: GalleryCacheService,
private galleryService: GalleryService,
private rndService: SeededRandomService) {
this.sorting = new BehaviorSubject<SortingMethods>(Config.Client.Other.defaultPhotoSortingMethod);
this.galleryService.content.subscribe((c) => {
if (c.directory) {
const sort = this.galleryCacheService.getSorting(c.directory);
if (sort !== null) {
this.sorting.next(sort);
} else {
this.sorting.next(this.getDefaultSorting(c.directory));
}
}
});
}
getDefaultSorting(directory: ParentDirectoryDTO): SortingMethods {
if (directory && directory.metaFile) {
for (const file in PG2ConfMap.sorting) {
if (directory.metaFile.some(f => f.name === file)) {
return (PG2ConfMap.sorting as any)[file];
}
}
}
return Config.Client.Other.defaultPhotoSortingMethod;
}
setSorting(sorting: SortingMethods): void {
this.sorting.next(sorting);
if (this.galleryService.content.value.directory) {
if (sorting !== this.getDefaultSorting(this.galleryService.content.value.directory)) {
this.galleryCacheService.setSorting(this.galleryService.content.value.directory, sorting);
} else {
this.galleryCacheService.removeSorting(this.galleryService.content.value.directory);
}
}
}
public applySorting(directoryContent: Observable<DirectoryContent>): Observable<DirectoryContent> {
return directoryContent.pipe(mergeMap((c) => {
return this.sorting.pipe(map((sorting: SortingMethods) => {
if (!c) {
return c;
}
if (c.directories) {
switch (sorting) {
case SortingMethods.ascRating: // directories do not have rating
case SortingMethods.ascName:
c.directories.sort((a, b) => this.collator.compare(a.name, b.name));
break;
case SortingMethods.ascDate:
if (Config.Client.Other.enableDirectorySortingByDate === true) {
c.directories.sort((a, b) => a.lastModified - b.lastModified);
break;
}
c.directories.sort((a, b) => this.collator.compare(a.name, b.name));
break;
case SortingMethods.descRating: // directories do not have rating
case SortingMethods.descName:
c.directories.sort((a, b) => this.collator.compare(b.name, a.name));
break;
case SortingMethods.descDate:
if (Config.Client.Other.enableDirectorySortingByDate === true) {
c.directories.sort((a, b) => b.lastModified - a.lastModified);
break;
}
c.directories.sort((a, b) => this.collator.compare(b.name, a.name));
break;
case SortingMethods.random:
this.rndService.setSeed(c.directories.length);
c.directories.sort((a, b): number => {
if (a.name.toLowerCase() < b.name.toLowerCase()) {
return 1;
}
if (a.name.toLowerCase() > b.name.toLowerCase()) {
return -1;
}
return 0;
}).sort((): number => {
return this.rndService.get() - 0.5;
});
break;
}
}
if (c.media) {
switch (sorting) {
case SortingMethods.ascName:
c.media.sort((a: PhotoDTO, b: PhotoDTO) => this.collator.compare(a.name, b.name));
break;
case SortingMethods.descName:
c.media.sort((a: PhotoDTO, b: PhotoDTO) => this.collator.compare(b.name, a.name));
break;
case SortingMethods.ascDate:
c.media.sort((a: PhotoDTO, b: PhotoDTO): number => {
return a.metadata.creationDate - b.metadata.creationDate;
});
break;
case SortingMethods.descDate:
c.media.sort((a: PhotoDTO, b: PhotoDTO): number => {
return b.metadata.creationDate - a.metadata.creationDate;
});
break;
case SortingMethods.ascRating:
c.media.sort((a: PhotoDTO, b: PhotoDTO) => (a.metadata.rating || 0) - (b.metadata.rating || 0));
break;
case SortingMethods.descRating:
c.media.sort((a: PhotoDTO, b: PhotoDTO) => (b.metadata.rating || 0) - (a.metadata.rating || 0));
break;
case SortingMethods.random:
this.rndService.setSeed(c.media.length);
c.media.sort((a: PhotoDTO, b: PhotoDTO): number => {
if (a.name.toLowerCase() < b.name.toLowerCase()) {
return -1;
}
if (a.name.toLowerCase() > b.name.toLowerCase()) {
return 1;
}
return 0;
}).sort((): number => {
return this.rndService.get() - 0.5;
});
break;
}
}
return c;
}));
}));
}
}