mirror of
https://github.com/xuthus83/pigallery2.git
synced 2025-01-14 14:43:17 +08:00
Implementing grouping
This commit is contained in:
parent
0cf0e733b4
commit
0a1fb29c9f
@ -177,6 +177,7 @@ import {
|
||||
} from '@ng-icons/ionicons';
|
||||
import {SortingMethodIconComponent} from './ui/sorting-method-icon/sorting-method-icon.component';
|
||||
import {SafeHtmlPipe} from './pipes/SafeHTMLPipe';
|
||||
import {DatePipe} from '@angular/common';
|
||||
|
||||
@Injectable()
|
||||
export class MyHammerConfig extends HammerGestureConfig {
|
||||
@ -190,7 +191,7 @@ export class MyHammerConfig extends HammerGestureConfig {
|
||||
|
||||
export class CustomUrlSerializer implements UrlSerializer {
|
||||
private defaultUrlSerializer: DefaultUrlSerializer =
|
||||
new DefaultUrlSerializer();
|
||||
new DefaultUrlSerializer();
|
||||
|
||||
parse(url: string): UrlTree {
|
||||
// Encode parentheses
|
||||
@ -201,9 +202,9 @@ export class CustomUrlSerializer implements UrlSerializer {
|
||||
|
||||
serialize(tree: UrlTree): string {
|
||||
return this.defaultUrlSerializer
|
||||
.serialize(tree)
|
||||
.replace(/%28/g, '(')
|
||||
.replace(/%29/g, ')');
|
||||
.serialize(tree)
|
||||
.replace(/%28/g, '(')
|
||||
.replace(/%29/g, ')');
|
||||
}
|
||||
}
|
||||
|
||||
@ -361,6 +362,7 @@ Marker.prototype.options.icon = MarkerFactory.defIcon;
|
||||
CookieService,
|
||||
GPXFilesFilterPipe,
|
||||
MDFilesFilterPipe,
|
||||
DatePipe
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
|
@ -1,15 +1,20 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { MediaDTO, MediaDTOUtils } from '../../../common/entities/MediaDTO';
|
||||
import { PhotoDTO } from '../../../common/entities/PhotoDTO';
|
||||
import {MediaGroup} from '../ui/gallery/navigator/sorting.service';
|
||||
|
||||
@Pipe({ name: 'photosOnly' })
|
||||
export class PhotoFilterPipe implements PipeTransform {
|
||||
transform(media: MediaDTO[]): PhotoDTO[] | null {
|
||||
if (!media) {
|
||||
transform(mediaGroups: MediaGroup[]): PhotoDTO[] | null {
|
||||
if (!mediaGroups) {
|
||||
return null;
|
||||
}
|
||||
return media.filter((m: MediaDTO): boolean =>
|
||||
MediaDTOUtils.isPhoto(m)
|
||||
) as PhotoDTO[];
|
||||
const ret = [];
|
||||
for(let i = 0; i < mediaGroups.length; ++i){
|
||||
ret.push(...mediaGroups[i].media.filter((m: MediaDTO): boolean =>
|
||||
MediaDTOUtils.isPhoto(m)
|
||||
) as PhotoDTO[])
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
@ -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,124 @@ export class FilterService {
|
||||
}
|
||||
|
||||
public applyFilters(
|
||||
directoryContent: Observable<DirectoryContent>
|
||||
directoryContent: Observable<DirectoryContent>
|
||||
): Observable<DirectoryContent> {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
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;
|
||||
}
|
||||
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
|
||||
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
// If th e number of photos did not change, the filters are not active
|
||||
afilters.areFiltersActive = c.media.length !== dirContent.media.length;
|
||||
return c;
|
||||
})
|
||||
);
|
||||
})
|
||||
|
||||
// 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;
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -55,10 +55,10 @@
|
||||
class="rounded"
|
||||
[class.rounded-start-0]="ShowMarkDown"
|
||||
*ngIf="ShowMap"
|
||||
[photos]="directoryContent?.media | photosOnly"
|
||||
[photos]="directoryContent?.mediaGroups | photosOnly"
|
||||
[gpxFiles]="directoryContent?.metaFile | gpxFiles"></app-gallery-map>
|
||||
</div>
|
||||
<app-gallery-grid [media]="directoryContent?.media"
|
||||
<app-gallery-grid [mediaGroups]="directoryContent?.mediaGroups"
|
||||
[lightbox]="lightbox"></app-gallery-grid>
|
||||
|
||||
</ng-container>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
|
||||
import {AuthenticationService} from '../../model/network/authentication.service';
|
||||
import {ActivatedRoute, Params, Router} from '@angular/router';
|
||||
import {ContentService, ContentWrapperWithError, DirectoryContent,} from './content.service';
|
||||
import {ContentService, ContentWrapperWithError,} from './content.service';
|
||||
import {GalleryGridComponent} from './grid/grid.gallery.component';
|
||||
import {Config} from '../../../../common/config/public/Config';
|
||||
import {ShareService} from './share.service';
|
||||
@ -12,7 +12,7 @@ import {PageHelper} from '../../model/page.helper';
|
||||
import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
|
||||
import {QueryParams} from '../../../../common/QueryParams';
|
||||
import {take} from 'rxjs/operators';
|
||||
import {GallerySortingService} from './navigator/sorting.service';
|
||||
import {GallerySortingService, GroupedDirectoryContent} from './navigator/sorting.service';
|
||||
import {MediaDTO} from '../../../../common/entities/MediaDTO';
|
||||
import {FilterService} from './filter/filter.service';
|
||||
import {PiTitleService} from '../../model/pi-title.service';
|
||||
@ -42,7 +42,7 @@ export class GalleryComponent implements OnInit, OnDestroy {
|
||||
second: number;
|
||||
} = null;
|
||||
public readonly mapEnabled: boolean;
|
||||
public directoryContent: DirectoryContent;
|
||||
public directoryContent: GroupedDirectoryContent;
|
||||
public readonly mediaObs: Observable<MediaDTO[]>;
|
||||
private $counter: Observable<number>;
|
||||
private subscription: { [key: string]: Subscription } = {
|
||||
@ -53,17 +53,17 @@ export class GalleryComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
|
||||
constructor(
|
||||
public galleryService: ContentService,
|
||||
private authService: AuthenticationService,
|
||||
private router: Router,
|
||||
private shareService: ShareService,
|
||||
private route: ActivatedRoute,
|
||||
private navigation: NavigationService,
|
||||
private filterService: FilterService,
|
||||
private sortingService: GallerySortingService,
|
||||
private piTitleService: PiTitleService,
|
||||
private gpxFilesFilterPipe: GPXFilesFilterPipe,
|
||||
private mdFilesFilterPipe:MDFilesFilterPipe,
|
||||
public galleryService: ContentService,
|
||||
private authService: AuthenticationService,
|
||||
private router: Router,
|
||||
private shareService: ShareService,
|
||||
private route: ActivatedRoute,
|
||||
private navigation: NavigationService,
|
||||
private filterService: FilterService,
|
||||
private sortingService: GallerySortingService,
|
||||
private piTitleService: PiTitleService,
|
||||
private gpxFilesFilterPipe: GPXFilesFilterPipe,
|
||||
private mdFilesFilterPipe: MDFilesFilterPipe,
|
||||
) {
|
||||
this.mapEnabled = Config.Map.enabled;
|
||||
PageHelper.showScrollY();
|
||||
@ -79,17 +79,17 @@ export class GalleryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
// if the timer is longer than 10 years, just do not show it
|
||||
if (
|
||||
(this.shareService.sharingSubject.value.expires - Date.now()) /
|
||||
1000 /
|
||||
86400 /
|
||||
365 >
|
||||
10
|
||||
(this.shareService.sharingSubject.value.expires - Date.now()) /
|
||||
1000 /
|
||||
86400 /
|
||||
365 >
|
||||
10
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
t = Math.floor(
|
||||
(this.shareService.sharingSubject.value.expires - Date.now()) / 1000
|
||||
(this.shareService.sharingSubject.value.expires - Date.now()) / 1000
|
||||
);
|
||||
this.countDown = {} as any;
|
||||
this.countDown.day = Math.floor(t / 86400);
|
||||
@ -119,33 +119,33 @@ export class GalleryComponent implements OnInit, OnDestroy {
|
||||
async ngOnInit(): Promise<boolean> {
|
||||
await this.shareService.wait();
|
||||
if (
|
||||
!this.authService.isAuthenticated() &&
|
||||
(!this.shareService.isSharing() ||
|
||||
(this.shareService.isSharing() &&
|
||||
Config.Sharing.passwordProtected === true))
|
||||
!this.authService.isAuthenticated() &&
|
||||
(!this.shareService.isSharing() ||
|
||||
(this.shareService.isSharing() &&
|
||||
Config.Sharing.passwordProtected === true))
|
||||
) {
|
||||
return this.navigation.toLogin();
|
||||
}
|
||||
this.showSearchBar = this.authService.canSearch();
|
||||
this.showShare =
|
||||
Config.Sharing.enabled &&
|
||||
this.authService.isAuthorized(UserRoles.User);
|
||||
Config.Sharing.enabled &&
|
||||
this.authService.isAuthorized(UserRoles.User);
|
||||
this.showRandomPhotoBuilder =
|
||||
Config.RandomPhoto.enabled &&
|
||||
this.authService.isAuthorized(UserRoles.User);
|
||||
Config.RandomPhoto.enabled &&
|
||||
this.authService.isAuthorized(UserRoles.User);
|
||||
this.subscription.content = this.sortingService
|
||||
.applySorting(
|
||||
this.filterService.applyFilters(this.galleryService.directoryContent)
|
||||
)
|
||||
.subscribe((dc: DirectoryContent) => {
|
||||
this.onContentChange(dc);
|
||||
});
|
||||
.applySorting(
|
||||
this.filterService.applyFilters(this.galleryService.directoryContent)
|
||||
)
|
||||
.subscribe((dc: GroupedDirectoryContent) => {
|
||||
this.onContentChange(dc);
|
||||
});
|
||||
this.subscription.route = this.route.params.subscribe(this.onRoute);
|
||||
|
||||
if (this.shareService.isSharing()) {
|
||||
this.$counter = interval(1000);
|
||||
this.subscription.timer = this.$counter.subscribe((x): void =>
|
||||
this.updateTimer(x)
|
||||
this.updateTimer(x)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -159,18 +159,18 @@ export class GalleryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
if (
|
||||
params[QueryParams.gallery.sharingKey_params] &&
|
||||
params[QueryParams.gallery.sharingKey_params] !== ''
|
||||
params[QueryParams.gallery.sharingKey_params] &&
|
||||
params[QueryParams.gallery.sharingKey_params] !== ''
|
||||
) {
|
||||
const sharing = await this.shareService.currentSharing
|
||||
.pipe(take(1))
|
||||
.toPromise();
|
||||
.pipe(take(1))
|
||||
.toPromise();
|
||||
const qParams: { [key: string]: any } = {};
|
||||
qParams[QueryParams.gallery.sharingKey_query] =
|
||||
this.shareService.getSharingKey();
|
||||
this.shareService.getSharingKey();
|
||||
this.router
|
||||
.navigate(['/gallery', sharing.path], {queryParams: qParams})
|
||||
.catch(console.error);
|
||||
.navigate(['/gallery', sharing.path], {queryParams: qParams})
|
||||
.catch(console.error);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -181,22 +181,21 @@ export class GalleryComponent implements OnInit, OnDestroy {
|
||||
this.galleryService.loadDirectory(directoryName);
|
||||
};
|
||||
|
||||
private onContentChange = (content: DirectoryContent): void => {
|
||||
private onContentChange = (content: GroupedDirectoryContent): void => {
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
this.directoryContent = content;
|
||||
|
||||
// enforce change detection on grid
|
||||
this.directoryContent.media = this.directoryContent.media?.slice();
|
||||
this.directoryContent.mediaGroups = this.directoryContent.mediaGroups?.slice();
|
||||
this.isPhotoWithLocation = false;
|
||||
|
||||
for (const media of content.media as PhotoDTO[]) {
|
||||
for (const mediaGroup of content.mediaGroups) {
|
||||
|
||||
if (
|
||||
media.metadata &&
|
||||
media.metadata.positionData &&
|
||||
media.metadata.positionData.GPSData &&
|
||||
media.metadata.positionData.GPSData.longitude
|
||||
mediaGroup.media
|
||||
.findIndex((m: PhotoDTO) => !!m.metadata?.positionData?.GPSData?.longitude) !== -1
|
||||
) {
|
||||
this.isPhotoWithLocation = true;
|
||||
break;
|
||||
@ -204,11 +203,11 @@ export class GalleryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
};
|
||||
|
||||
get ShowMarkDown():boolean{
|
||||
return this.config.MetaFile.markdown && this.directoryContent?.metaFile && this.mdFilesFilterPipe.transform(this.directoryContent.metaFile).length>0;
|
||||
get ShowMarkDown(): boolean {
|
||||
return this.config.MetaFile.markdown && this.directoryContent?.metaFile && this.mdFilesFilterPipe.transform(this.directoryContent.metaFile).length > 0;
|
||||
}
|
||||
|
||||
get ShowMap():boolean{
|
||||
get ShowMap(): boolean {
|
||||
return (this.isPhotoWithLocation || this.gpxFilesFilterPipe.transform(this.directoryContent?.metaFile)?.length > 0) && this.mapEnabled;
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,7 @@
|
||||
import { Media } from '../Media';
|
||||
import {
|
||||
MediaDTO,
|
||||
MediaDTOUtils,
|
||||
} from '../../../../../common/entities/MediaDTO';
|
||||
import { PhotoDTO } from '../../../../../common/entities/PhotoDTO';
|
||||
import { VideoDTO } from '../../../../../common/entities/VideoDTO';
|
||||
import {Media} from '../Media';
|
||||
import {MediaDTO, MediaDTOUtils,} from '../../../../../common/entities/MediaDTO';
|
||||
import {PhotoDTO} from '../../../../../common/entities/PhotoDTO';
|
||||
import {VideoDTO} from '../../../../../common/entities/VideoDTO';
|
||||
|
||||
export class GridMedia extends Media {
|
||||
constructor(
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { MediaDTO } from '../../../../../common/entities/MediaDTO';
|
||||
import {MediaDTO} from '../../../../../common/entities/MediaDTO';
|
||||
|
||||
export class GridRowBuilder {
|
||||
private photoRow: MediaDTO[] = [];
|
||||
@ -6,15 +6,15 @@ export class GridRowBuilder {
|
||||
private photoIndex = 0; // index of the last pushed media to the photoRow
|
||||
|
||||
constructor(
|
||||
private photos: MediaDTO[],
|
||||
private startIndex: number,
|
||||
private photoMargin: number,
|
||||
private containerWidth: number
|
||||
private photos: MediaDTO[],
|
||||
private startIndex: number,
|
||||
private photoMargin: number,
|
||||
private containerWidth: number
|
||||
) {
|
||||
this.photoIndex = startIndex;
|
||||
if (this.containerWidth <= 0) {
|
||||
throw new Error(
|
||||
'container width cant be <=0, got:' + this.containerWidth
|
||||
'container width cant be <=0, got:' + this.containerWidth
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -60,14 +60,15 @@ export class GridRowBuilder {
|
||||
width += (size.width / size.height) || 1; // summing up aspect ratios, NaN should be treated as square photo
|
||||
}
|
||||
const height =
|
||||
(this.containerWidth -
|
||||
this.photoRow.length * (this.photoMargin * 2) -
|
||||
1) /
|
||||
width; // cant be equal -> width-1
|
||||
(this.containerWidth -
|
||||
this.photoRow.length * (this.photoMargin * 2) -
|
||||
1) /
|
||||
width; // cant be equal -> width-1
|
||||
|
||||
return height + this.photoMargin * 2;
|
||||
}
|
||||
|
||||
|
||||
private addPhoto(): boolean {
|
||||
if (this.photoIndex + 1 > this.photos.length) {
|
||||
return false;
|
||||
|
@ -3,8 +3,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div {
|
||||
/*display: block;*/
|
||||
.media-grid {
|
||||
line-height: normal;
|
||||
font-size: 0;
|
||||
}
|
||||
|
@ -1,11 +1,19 @@
|
||||
<div #gridContainer [style.width]="renderDelayTimer ? containerWidth+'px' : ''">
|
||||
<app-gallery-grid-photo
|
||||
*ngFor="let gridPhoto of photosToRender"
|
||||
(click)="photoClicked(gridPhoto.media)"
|
||||
[gridMedia]="gridPhoto"
|
||||
[style.width.px]="gridPhoto.renderWidth"
|
||||
[style.height.px]="gridPhoto.renderHeight"
|
||||
[style.margin-left.px]="IMAGE_MARGIN"
|
||||
[style.margin-right.px]="IMAGE_MARGIN">
|
||||
</app-gallery-grid-photo>
|
||||
<ng-container *ngIf="mediaToRender?.length > 0">
|
||||
<ng-container *ngFor="let group of mediaToRender">
|
||||
<div *ngIf="group.name" class="mt-4 mb-3"><h6 class="ms-2">{{group.name}}</h6></div>
|
||||
<div class="media-grid">
|
||||
<app-gallery-grid-photo
|
||||
*ngFor="let gridPhoto of group.media"
|
||||
(click)="photoClicked(gridPhoto.media)"
|
||||
[gridMedia]="gridPhoto"
|
||||
[style.width.px]="gridPhoto.renderWidth"
|
||||
[style.height.px]="gridPhoto.renderHeight"
|
||||
[style.margin-left.px]="IMAGE_MARGIN"
|
||||
[style.margin-right.px]="IMAGE_MARGIN">
|
||||
</app-gallery-grid-photo>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
|
@ -25,6 +25,8 @@ import {QueryService} from '../../../model/query.service';
|
||||
import {ContentService} from '../content.service';
|
||||
import {MediaDTO, MediaDTOUtils,} from '../../../../../common/entities/MediaDTO';
|
||||
import {QueryParams} from '../../../../../common/QueryParams';
|
||||
import {MediaGroup} from '../navigator/sorting.service';
|
||||
import {SimpleChanges} from '../../../../../../node_modules/@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-gallery-grid',
|
||||
@ -32,13 +34,13 @@ import {QueryParams} from '../../../../../common/QueryParams';
|
||||
styleUrls: ['./grid.gallery.component.css'],
|
||||
})
|
||||
export class GalleryGridComponent
|
||||
implements OnInit, OnChanges, AfterViewInit, OnDestroy {
|
||||
implements OnInit, OnChanges, AfterViewInit, OnDestroy {
|
||||
@ViewChild('gridContainer', {static: false}) gridContainer: ElementRef;
|
||||
@ViewChildren(GalleryPhotoComponent)
|
||||
gridPhotoQL: QueryList<GalleryPhotoComponent>;
|
||||
@Input() lightbox: GalleryLightboxComponent;
|
||||
@Input() media: MediaDTO[];
|
||||
photosToRender: GridMedia[] = [];
|
||||
@Input() mediaGroups: MediaGroup[];
|
||||
mediaToRender: GridMediaGroup[] = [];
|
||||
containerWidth = 0;
|
||||
screenHeight = 0;
|
||||
public IMAGE_MARGIN = 2;
|
||||
@ -56,37 +58,37 @@ export class GalleryGridComponent
|
||||
private onScrollFired = false;
|
||||
private helperTime: number = null;
|
||||
public renderDelayTimer: number = null; // delays render on resize
|
||||
private renderedPhotoIndex = 0;
|
||||
|
||||
constructor(
|
||||
private overlayService: OverlayService,
|
||||
private changeDetector: ChangeDetectorRef,
|
||||
public queryService: QueryService,
|
||||
private router: Router,
|
||||
public galleryService: ContentService,
|
||||
private route: ActivatedRoute
|
||||
private overlayService: OverlayService,
|
||||
private changeDetector: ChangeDetectorRef,
|
||||
public queryService: QueryService,
|
||||
private router: Router,
|
||||
public galleryService: ContentService,
|
||||
private route: ActivatedRoute
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log(changes);
|
||||
this.onChange();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.subscriptions.route = this.route.queryParams.subscribe(
|
||||
(params: Params): void => {
|
||||
if (
|
||||
params[QueryParams.gallery.photo] &&
|
||||
params[QueryParams.gallery.photo] !== ''
|
||||
) {
|
||||
this.delayedRenderUpToPhoto = params[QueryParams.gallery.photo];
|
||||
if (!this.media || this.media.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.renderUpToMedia(params[QueryParams.gallery.photo]);
|
||||
(params: Params): void => {
|
||||
if (
|
||||
params[QueryParams.gallery.photo] &&
|
||||
params[QueryParams.gallery.photo] !== ''
|
||||
) {
|
||||
this.delayedRenderUpToPhoto = params[QueryParams.gallery.photo];
|
||||
if (!this.mediaGroups?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.renderUpToMedia(params[QueryParams.gallery.photo]);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -95,6 +97,7 @@ export class GalleryGridComponent
|
||||
return;
|
||||
}
|
||||
this.updateContainerDimensions();
|
||||
this.mergeNewPhotos();
|
||||
this.helperTime = window.setTimeout((): void => {
|
||||
this.renderPhotos();
|
||||
if (this.delayedRenderUpToPhoto) {
|
||||
@ -128,13 +131,13 @@ export class GalleryGridComponent
|
||||
}
|
||||
this.renderDelayTimer = window.setTimeout(() => {
|
||||
this.renderDelayTimer = null;
|
||||
// render the same amount of images on resize
|
||||
const renderedIndex = this.renderedPhotoIndex;
|
||||
// render the same amount of images after resize
|
||||
const renderedCount = this.mediaToRender.reduce((c, mg) => c + mg.media.length, 0);
|
||||
// do not rerender if container is not changes
|
||||
if (this.updateContainerDimensions() === false) {
|
||||
return;
|
||||
}
|
||||
this.renderPhotos(renderedIndex);
|
||||
this.renderPhotos(renderedCount);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
@ -150,7 +153,7 @@ export class GalleryGridComponent
|
||||
if (Config.Gallery.enableOnScrollThumbnailPrioritising === true) {
|
||||
this.gridPhotoQL.changes.subscribe((): void => {
|
||||
this.scrollListenerPhotos = this.gridPhotoQL.filter(
|
||||
(pc): boolean => pc.ScrollListener
|
||||
(pc): boolean => pc.ScrollListener
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -166,22 +169,78 @@ export class GalleryGridComponent
|
||||
this.isAfterViewInit = true;
|
||||
}
|
||||
|
||||
|
||||
// TODO: This is deprecated,
|
||||
// we do not post update galleries anymore since the preview member in the DriectoryDTO
|
||||
private mergeNewPhotos(): void {
|
||||
// merge new data with old one
|
||||
const lastSameIndex = {groups: 0, media: 0};
|
||||
let lastRowId = 0;
|
||||
let diffFound = false;
|
||||
for (let i = 0; i < this.mediaGroups.length && i < this.mediaToRender.length; ++i) {
|
||||
for (let j = 0; j < this.mediaGroups[i].media.length && j < this.mediaToRender[i].media.length; ++j) {
|
||||
const media = this.mediaGroups[i].media[j];
|
||||
const gridMedia = this.mediaToRender[i].media[j];
|
||||
// If a media changed the whole row has to be removed
|
||||
if (gridMedia.rowId !== lastRowId) {
|
||||
lastSameIndex.groups = i;
|
||||
lastSameIndex.media = j;
|
||||
lastRowId = gridMedia.rowId;
|
||||
}
|
||||
if (gridMedia.equals(media) === false) {
|
||||
diffFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// delete last row if the length of the two are not equal
|
||||
if (!diffFound && this.mediaGroups[i].media.length < this.mediaToRender[i].media.length) {
|
||||
lastRowId = this.mediaToRender[i].media[this.mediaToRender[i].media.length].rowId;
|
||||
for (let j = this.mediaToRender[i].media.length - 2; j >= 0; --j) {
|
||||
const gridMedia = this.mediaToRender[i].media[j];
|
||||
if (gridMedia.rowId !== lastRowId) {
|
||||
lastSameIndex.groups = i;
|
||||
lastSameIndex.media = j + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// if all the same
|
||||
if (diffFound) {
|
||||
if (lastSameIndex.media == 0 && lastSameIndex.groups == 0) {
|
||||
this.clearRenderedPhotos();
|
||||
return;
|
||||
}
|
||||
this.mediaToRender.splice(lastSameIndex.groups, this.mediaToRender.length - lastSameIndex.groups);
|
||||
const media = this.mediaToRender[lastSameIndex.groups].media;
|
||||
media.splice(lastSameIndex.media, media.length - lastSameIndex.media);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public renderARow(): number {
|
||||
if (
|
||||
this.renderedPhotoIndex >= this.media.length ||
|
||||
this.containerWidth === 0
|
||||
!this.isMoreToRender() ||
|
||||
this.containerWidth === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// step group
|
||||
if (this.mediaToRender.length == 0 ||
|
||||
this.mediaToRender[this.mediaToRender.length - 1].media.length >=
|
||||
this.mediaGroups[this.mediaToRender.length - 1].media.length) {
|
||||
this.mediaToRender.push({name: this.mediaGroups[this.mediaToRender.length].name, media: []});
|
||||
}
|
||||
|
||||
let maxRowHeight = this.getMaxRowHeight();
|
||||
const minRowHeight = this.screenHeight / this.MAX_ROW_COUNT;
|
||||
|
||||
const photoRowBuilder = new GridRowBuilder(
|
||||
this.media,
|
||||
this.renderedPhotoIndex,
|
||||
this.IMAGE_MARGIN,
|
||||
this.containerWidth - this.overlayService.getPhantomScrollbarWidth()
|
||||
this.mediaGroups[this.mediaToRender.length - 1].media,
|
||||
this.mediaToRender[this.mediaToRender.length - 1].media.length,
|
||||
this.IMAGE_MARGIN,
|
||||
this.containerWidth - this.overlayService.getPhantomScrollbarWidth()
|
||||
);
|
||||
|
||||
photoRowBuilder.addPhotos(this.TARGET_COL_COUNT);
|
||||
@ -191,40 +250,43 @@ export class GalleryGridComponent
|
||||
if (photoRowBuilder.getPhotoRow().length > 1) {
|
||||
maxRowHeight *= 1.2;
|
||||
}
|
||||
const rowHeight = Math.min(photoRowBuilder.calcRowHeight(), maxRowHeight);
|
||||
const noFullRow = photoRowBuilder.calcRowHeight() > maxRowHeight;
|
||||
// if the row is not full, make it average sized
|
||||
const rowHeight = noFullRow ? (minRowHeight + maxRowHeight) / 2 :
|
||||
Math.min(photoRowBuilder.calcRowHeight(), maxRowHeight);
|
||||
const imageHeight = rowHeight - this.IMAGE_MARGIN * 2;
|
||||
|
||||
photoRowBuilder.getPhotoRow().forEach((photo): void => {
|
||||
const imageWidth = imageHeight * MediaDTOUtils.calcAspectRatio(photo);
|
||||
this.photosToRender.push(
|
||||
new GridMedia(photo, imageWidth, imageHeight, this.renderedPhotoIndex)
|
||||
photoRowBuilder.getPhotoRow().forEach((media): void => {
|
||||
const imageWidth = imageHeight * MediaDTOUtils.calcAspectRatio(media);
|
||||
this.mediaToRender[this.mediaToRender.length - 1].media.push(
|
||||
new GridMedia(media, imageWidth, imageHeight, this.mediaToRender[this.mediaToRender.length - 1].media.length)
|
||||
);
|
||||
});
|
||||
|
||||
this.renderedPhotoIndex += photoRowBuilder.getPhotoRow().length;
|
||||
//this.renderedPhotoIndex += photoRowBuilder.getPhotoRow().length;
|
||||
return rowHeight;
|
||||
}
|
||||
|
||||
@HostListener('window:scroll')
|
||||
onScroll(): void {
|
||||
if (
|
||||
!this.onScrollFired &&
|
||||
this.media &&
|
||||
// should we trigger this at all?
|
||||
(this.renderedPhotoIndex < this.media.length ||
|
||||
this.scrollListenerPhotos.length > 0)
|
||||
!this.onScrollFired &&
|
||||
this.mediaGroups &&
|
||||
// should we trigger this at all?
|
||||
(this.isMoreToRender() ||
|
||||
this.scrollListenerPhotos.length > 0)
|
||||
) {
|
||||
window.requestAnimationFrame((): void => {
|
||||
this.renderPhotos();
|
||||
|
||||
if (Config.Gallery.enableOnScrollThumbnailPrioritising === true) {
|
||||
this.scrollListenerPhotos.forEach(
|
||||
(pc: GalleryPhotoComponent): void => {
|
||||
pc.onScroll();
|
||||
}
|
||||
(pc: GalleryPhotoComponent): void => {
|
||||
pc.onScroll();
|
||||
}
|
||||
);
|
||||
this.scrollListenerPhotos = this.scrollListenerPhotos.filter(
|
||||
(pc): boolean => pc.ScrollListener
|
||||
(pc): boolean => pc.ScrollListener
|
||||
);
|
||||
}
|
||||
|
||||
@ -242,28 +304,36 @@ export class GalleryGridComponent
|
||||
* Makes sure that the photo with the given mediaString is visible on the screen
|
||||
*/
|
||||
private renderUpToMedia(mediaStringId: string): void {
|
||||
const index = this.media.findIndex(
|
||||
let groupIndex = -1;
|
||||
let mediaIndex = -1;
|
||||
for (let i = 0; i < this.mediaGroups.length; ++i) {
|
||||
mediaIndex = this.mediaGroups[i].media.findIndex(
|
||||
(p): boolean => this.queryService.getMediaStringId(p) === mediaStringId
|
||||
);
|
||||
if (index === -1) {
|
||||
);
|
||||
if (mediaIndex !== -1) {
|
||||
groupIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (groupIndex === -1) {
|
||||
this.router.navigate([], {queryParams: this.queryService.getParams()});
|
||||
return;
|
||||
}
|
||||
// Make sure that at leas one more photo is rendered
|
||||
// Make sure that at leas one more row is rendered
|
||||
// It is possible that only the last few pixels of a photo is visible,
|
||||
// so not required to render more, but the scrollbar does not trigger more photos to render
|
||||
// (on ligthbox navigation)
|
||||
// (on lightbox navigation)
|
||||
while (
|
||||
this.renderedPhotoIndex - 1 < index + 1 &&
|
||||
this.renderARow() !== null
|
||||
// eslint-disable-next-line no-empty
|
||||
) {
|
||||
(this.mediaToRender.length - 1 < groupIndex &&
|
||||
this.mediaToRender[this.mediaToRender.length - 1].media.length < mediaIndex) &&
|
||||
this.renderARow() !== null
|
||||
// eslint-disable-next-line no-empty
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
private clearRenderedPhotos(): void {
|
||||
this.photosToRender = [];
|
||||
this.renderedPhotoIndex = 0;
|
||||
this.mediaToRender = [];
|
||||
this.changeDetector.detectChanges();
|
||||
}
|
||||
|
||||
@ -276,24 +346,24 @@ export class GalleryGridComponent
|
||||
private shouldRenderMore(offset = 0): boolean {
|
||||
const bottomOffset = this.getMaxRowHeight() * 2;
|
||||
return (
|
||||
Config.Gallery.enableOnScrollRendering === false ||
|
||||
PageHelper.ScrollY >=
|
||||
document.body.clientHeight +
|
||||
offset -
|
||||
window.innerHeight -
|
||||
bottomOffset ||
|
||||
(document.body.clientHeight + offset) * 0.85 < window.innerHeight
|
||||
Config.Gallery.enableOnScrollRendering === false ||
|
||||
PageHelper.ScrollY >=
|
||||
document.body.clientHeight +
|
||||
offset -
|
||||
window.innerHeight -
|
||||
bottomOffset ||
|
||||
(document.body.clientHeight + offset) * 0.85 < window.innerHeight
|
||||
);
|
||||
}
|
||||
|
||||
private renderPhotos(numberOfPhotos = 0): void {
|
||||
if (!this.media) {
|
||||
if (!this.mediaGroups) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this.containerWidth === 0 ||
|
||||
this.renderedPhotoIndex >= this.media.length ||
|
||||
!this.shouldRenderMore()
|
||||
this.containerWidth === 0 ||
|
||||
!this.isMoreToRender() ||
|
||||
!this.shouldRenderMore()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@ -301,10 +371,10 @@ export class GalleryGridComponent
|
||||
let renderedContentHeight = 0;
|
||||
|
||||
while (
|
||||
this.renderedPhotoIndex < this.media.length &&
|
||||
(this.shouldRenderMore(renderedContentHeight) === true ||
|
||||
this.renderedPhotoIndex < numberOfPhotos)
|
||||
) {
|
||||
this.isMoreToRender() &&
|
||||
(this.shouldRenderMore(renderedContentHeight) === true ||
|
||||
this.getNumberOfRenderedMedia() < numberOfPhotos)
|
||||
) {
|
||||
const ret = this.renderARow();
|
||||
if (ret === null) {
|
||||
throw new Error('Grid media rendering failed');
|
||||
@ -313,6 +383,15 @@ export class GalleryGridComponent
|
||||
}
|
||||
}
|
||||
|
||||
private isMoreToRender() {
|
||||
return this.mediaToRender.length < this.mediaGroups.length ||
|
||||
(this.mediaToRender[this.mediaToRender.length - 1]?.media.length || 0) < this.mediaGroups[this.mediaToRender.length - 1]?.media.length;
|
||||
}
|
||||
|
||||
getNumberOfRenderedMedia() {
|
||||
return this.mediaToRender.reduce((c, mg) => c + mg.media.length, 0);
|
||||
}
|
||||
|
||||
private updateContainerDimensions(): boolean {
|
||||
if (!this.gridContainer) {
|
||||
return false;
|
||||
@ -322,9 +401,9 @@ export class GalleryGridComponent
|
||||
PageHelper.showScrollY();
|
||||
// if the width changed a bit or the height changed a lot
|
||||
if (
|
||||
this.containerWidth !== this.gridContainer.nativeElement.parentElement.clientWidth ||
|
||||
this.screenHeight < window.innerHeight * 0.75 ||
|
||||
this.screenHeight > window.innerHeight * 1.25
|
||||
this.containerWidth !== this.gridContainer.nativeElement.parentElement.clientWidth ||
|
||||
this.screenHeight < window.innerHeight * 0.75 ||
|
||||
this.screenHeight > window.innerHeight * 1.25
|
||||
) {
|
||||
this.screenHeight = window.innerHeight;
|
||||
this.containerWidth = this.gridContainer.nativeElement.parentElement.clientWidth;
|
||||
@ -341,3 +420,8 @@ export class GalleryGridComponent
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
interface GridMediaGroup {
|
||||
media: GridMedia[];
|
||||
name: string;
|
||||
}
|
||||
|
@ -11,9 +11,6 @@
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#sorting-dropdown .dropdown-item.row {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.25rem 0.25rem;
|
||||
@ -46,8 +43,9 @@ app-gallery-filter {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.sorting-item {
|
||||
.sorting-grouping-item {
|
||||
cursor: pointer;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
|
||||
|
@ -61,17 +61,37 @@
|
||||
aria-controls="sorting-dropdown">
|
||||
<app-sorting-method-icon [method]="sortingService.sorting.value"></app-sorting-method-icon>
|
||||
</button>
|
||||
<ul id="sorting-dropdown" *dropdownMenu class="dropdown-menu dropdown-menu-right"
|
||||
role="menu" aria-labelledby="button-alignment">
|
||||
<li class="row dropdown-item sorting-item" role="menuitem"
|
||||
*ngFor="let type of sortingMethodsType"
|
||||
(click)="setSorting(type.key)">
|
||||
<div class="col-3">
|
||||
<app-sorting-method-icon [method]="type.key"></app-sorting-method-icon>
|
||||
<div id="sorting-dropdown" *dropdownMenu class="dropdown-menu dropdown-menu-right"
|
||||
role="menu" aria-labelledby="button-alignment">
|
||||
<div class="row flex-nowrap">
|
||||
<div class="col-6">
|
||||
Sorting
|
||||
<div class="row">
|
||||
<div class="dropdown-item sorting-grouping-item" role="menuitem"
|
||||
*ngFor="let type of sortingMethodsType"
|
||||
(click)="setSorting(type.key)">
|
||||
<div class="me-2 d-inline-block">
|
||||
<app-sorting-method-icon [method]="type.key"></app-sorting-method-icon>
|
||||
</div>
|
||||
<div class="d-inline-block">{{type.key | stringifySorting}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-9">{{type.key | stringifySorting}}</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="col-6">
|
||||
Grouping
|
||||
<div class="row">
|
||||
<div class="dropdown-item sorting-grouping-item" role="menuitem"
|
||||
*ngFor="let type of sortingMethodsType"
|
||||
(click)="setGrouping(type.key)">
|
||||
<div class="me-2 d-inline-block">
|
||||
<app-sorting-method-icon [method]="type.key"></app-sorting-method-icon>
|
||||
</div>
|
||||
<div class="d-inline-block">{{type.key | stringifySorting}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
@ -145,6 +145,10 @@ export class GalleryNavigatorComponent {
|
||||
|
||||
setSorting(sorting: SortingMethods): void {
|
||||
this.sortingService.setSorting(sorting);
|
||||
this.sortingService.setGrouping(sorting);
|
||||
}
|
||||
setGrouping(grouping: SortingMethods): void {
|
||||
this.sortingService.setGrouping(grouping);
|
||||
}
|
||||
|
||||
getDownloadZipLink(): string {
|
||||
@ -215,6 +219,7 @@ export class GalleryNavigatorComponent {
|
||||
}
|
||||
this.lastScroll.any = scrollPosition;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface NavigatorPath {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {DatePipe} from '@angular/common';
|
||||
import {NetworkService} from '../../../model/network/network.service';
|
||||
import {GalleryCacheService} from '../cache.gallery.service';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
@ -10,21 +11,29 @@ import {PhotoDTO} from '../../../../../common/entities/PhotoDTO';
|
||||
import {map, switchMap} from 'rxjs/operators';
|
||||
import {SeededRandomService} from '../../../model/seededRandom.service';
|
||||
import {ContentWrapper} from '../../../../../common/entities/ConentWrapper';
|
||||
import {SubDirectoryDTO} from '../../../../../common/entities/DirectoryDTO';
|
||||
import {MediaDTO} from '../../../../../common/entities/MediaDTO';
|
||||
import {FileDTO} from '../../../../../common/entities/FileDTO';
|
||||
|
||||
@Injectable()
|
||||
export class GallerySortingService {
|
||||
public sorting: BehaviorSubject<SortingMethods>;
|
||||
public grouping: BehaviorSubject<SortingMethods>;
|
||||
private collator = new Intl.Collator(undefined, {numeric: true});
|
||||
|
||||
constructor(
|
||||
private networkService: NetworkService,
|
||||
private galleryCacheService: GalleryCacheService,
|
||||
private galleryService: ContentService,
|
||||
private rndService: SeededRandomService
|
||||
private rndService: SeededRandomService,
|
||||
private datePipe: DatePipe
|
||||
) {
|
||||
this.sorting = new BehaviorSubject<SortingMethods>(
|
||||
Config.Gallery.defaultPhotoSortingMethod
|
||||
);
|
||||
this.grouping = new BehaviorSubject<SortingMethods>(
|
||||
SortingMethods.ascDate // TODO: move to config
|
||||
);
|
||||
this.galleryService.content.subscribe((c) => {
|
||||
if (c) {
|
||||
const sort = this.galleryCacheService.getSorting(c);
|
||||
@ -70,146 +79,196 @@ export class GallerySortingService {
|
||||
}
|
||||
}
|
||||
|
||||
setGrouping(grouping: SortingMethods): void {
|
||||
this.grouping.next(grouping);
|
||||
}
|
||||
|
||||
private sortMedia(sorting: SortingMethods, media: MediaDTO[]): void {
|
||||
if (!media) {
|
||||
return;
|
||||
}
|
||||
switch (sorting) {
|
||||
case SortingMethods.ascName:
|
||||
media.sort((a: PhotoDTO, b: PhotoDTO) =>
|
||||
this.collator.compare(a.name, b.name)
|
||||
);
|
||||
break;
|
||||
case SortingMethods.descName:
|
||||
media.sort((a: PhotoDTO, b: PhotoDTO) =>
|
||||
this.collator.compare(b.name, a.name)
|
||||
);
|
||||
break;
|
||||
case SortingMethods.ascDate:
|
||||
media.sort((a: PhotoDTO, b: PhotoDTO): number => {
|
||||
return a.metadata.creationDate - b.metadata.creationDate;
|
||||
});
|
||||
break;
|
||||
case SortingMethods.descDate:
|
||||
media.sort((a: PhotoDTO, b: PhotoDTO): number => {
|
||||
return b.metadata.creationDate - a.metadata.creationDate;
|
||||
});
|
||||
break;
|
||||
case SortingMethods.ascRating:
|
||||
media.sort(
|
||||
(a: PhotoDTO, b: PhotoDTO) =>
|
||||
(a.metadata.rating || 0) - (b.metadata.rating || 0)
|
||||
);
|
||||
break;
|
||||
case SortingMethods.descRating:
|
||||
media.sort(
|
||||
(a: PhotoDTO, b: PhotoDTO) =>
|
||||
(b.metadata.rating || 0) - (a.metadata.rating || 0)
|
||||
);
|
||||
break;
|
||||
case SortingMethods.ascPersonCount:
|
||||
media.sort(
|
||||
(a: PhotoDTO, b: PhotoDTO) =>
|
||||
(a.metadata?.faces?.length || 0) - (b.metadata?.faces?.length || 0)
|
||||
);
|
||||
break;
|
||||
case SortingMethods.descPersonCount:
|
||||
media.sort(
|
||||
(a: PhotoDTO, b: PhotoDTO) =>
|
||||
(b.metadata?.faces?.length || 0) - (a.metadata?.faces?.length || 0)
|
||||
);
|
||||
break;
|
||||
case SortingMethods.random:
|
||||
this.rndService.setSeed(media.length);
|
||||
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;
|
||||
}
|
||||
|
||||
public applySorting(
|
||||
directoryContent: Observable<DirectoryContent>
|
||||
): Observable<DirectoryContent> {
|
||||
): Observable<GroupedDirectoryContent> {
|
||||
return directoryContent.pipe(
|
||||
switchMap((dirContent) => {
|
||||
return this.sorting.pipe(
|
||||
map((sorting: SortingMethods) => {
|
||||
if (!dirContent) {
|
||||
return dirContent;
|
||||
}
|
||||
const c = {
|
||||
media: dirContent.media,
|
||||
directories: dirContent.directories,
|
||||
metaFile: dirContent.metaFile,
|
||||
};
|
||||
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.Gallery.enableDirectorySortingByDate === true
|
||||
) {
|
||||
c.directories.sort(
|
||||
(a, b) => a.lastModified - b.lastModified
|
||||
);
|
||||
break;
|
||||
return this.grouping.pipe(
|
||||
switchMap((grouping) => {
|
||||
return this.sorting.pipe(
|
||||
map((sorting) => {
|
||||
if (!dirContent) {
|
||||
return null;
|
||||
}
|
||||
const c: GroupedDirectoryContent = {
|
||||
mediaGroups: [],
|
||||
directories: dirContent.directories,
|
||||
metaFile: dirContent.metaFile,
|
||||
};
|
||||
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.Gallery.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.Gallery.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;
|
||||
}
|
||||
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.Gallery.enableDirectorySortingByDate === true
|
||||
) {
|
||||
c.directories.sort(
|
||||
(a, b) => b.lastModified - a.lastModified
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// group
|
||||
if (dirContent.media) {
|
||||
const mCopy = dirContent.media;
|
||||
this.sortMedia(grouping, mCopy);
|
||||
let groupFN = (m: MediaDTO) => '';
|
||||
switch (grouping) {
|
||||
case SortingMethods.ascDate:
|
||||
case SortingMethods.descDate:
|
||||
groupFN = (m: MediaDTO) => this.datePipe.transform(m.metadata.creationDate, 'longDate');
|
||||
break;
|
||||
case SortingMethods.ascName:
|
||||
case SortingMethods.descName:
|
||||
groupFN = (m: MediaDTO) => m.name.at(0).toLowerCase();
|
||||
break;
|
||||
case SortingMethods.descRating:
|
||||
case SortingMethods.ascRating:
|
||||
groupFN = (m: MediaDTO) => ((m as PhotoDTO).metadata.rating || 0).toString();
|
||||
break;
|
||||
case SortingMethods.descPersonCount:
|
||||
case SortingMethods.ascPersonCount:
|
||||
groupFN = (m: MediaDTO) => ((m as PhotoDTO).metadata.faces || []).length.toString();
|
||||
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;
|
||||
}
|
||||
}
|
||||
c.mediaGroups = [];
|
||||
for (const m of mCopy) {
|
||||
const k = groupFN(m);
|
||||
if (c.mediaGroups.length == 0 || c.mediaGroups[c.mediaGroups.length - 1].name != k) {
|
||||
c.mediaGroups.push({name: k, media: []});
|
||||
}
|
||||
c.mediaGroups[c.mediaGroups.length - 1].media.push(m);
|
||||
}
|
||||
c.mediaGroups;
|
||||
}
|
||||
|
||||
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.ascPersonCount:
|
||||
c.media.sort(
|
||||
(a: PhotoDTO, b: PhotoDTO) =>
|
||||
(a.metadata?.faces?.length || 0) - (b.metadata?.faces?.length || 0)
|
||||
);
|
||||
break;
|
||||
case SortingMethods.descPersonCount:
|
||||
c.media.sort(
|
||||
(a: PhotoDTO, b: PhotoDTO) =>
|
||||
(b.metadata?.faces?.length || 0) - (a.metadata?.faces?.length || 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;
|
||||
}
|
||||
}
|
||||
// sort groups
|
||||
for (let i = 0; i < c.mediaGroups.length; ++i) {
|
||||
this.sortMedia(sorting, c.mediaGroups[i].media);
|
||||
}
|
||||
|
||||
return c;
|
||||
return c;
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
@ -217,4 +276,15 @@ export class GallerySortingService {
|
||||
}
|
||||
}
|
||||
|
||||
export interface MediaGroup {
|
||||
name: string;
|
||||
media: MediaDTO[];
|
||||
}
|
||||
|
||||
export interface GroupedDirectoryContent {
|
||||
directories: SubDirectoryDTO[];
|
||||
mediaGroups: MediaGroup[];
|
||||
metaFile: FileDTO[];
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user