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

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	demo/images/index.md
This commit is contained in:
Patrik J. Braun 2023-09-03 18:38:31 +02:00
commit 383de854dd
23 changed files with 485 additions and 310 deletions

View File

@ -90,6 +90,26 @@ export class Utils {
return true; return true;
} }
static toIsoString(d: number | Date) {
if (!(d instanceof Date)) {
d = new Date(d);
}
return d.getUTCFullYear() + '-' + d.getUTCMonth() + '-' + d.getUTCDate();
}
static makeUTCMidnight(d: number | Date) {
if (!(d instanceof Date)) {
d = new Date(d);
}
d.setUTCHours(0);
d.setUTCMinutes(0);
d.setUTCSeconds(0);
d.setUTCMilliseconds(0);
return d;
}
static renderDataSize(size: number): string { static renderDataSize(size: number): string {
const postFixes = ['B', 'KB', 'MB', 'GB', 'TB']; const postFixes = ['B', 'KB', 'MB', 'GB', 'TB'];
let index = 0; let index = 0;

View File

@ -183,6 +183,7 @@ import {ParseIntPipe} from './pipes/ParseIntPipe';
import { import {
SortingMethodSettingsEntryComponent SortingMethodSettingsEntryComponent
} from './ui/settings/template/settings-entry/sorting-method/sorting-method.settings-entry.component'; } from './ui/settings/template/settings-entry/sorting-method/sorting-method.settings-entry.component';
import {ContentLoaderService} from './ui/gallery/contentLoader.service';
@Injectable() @Injectable()
export class MyHammerConfig extends HammerGestureConfig { export class MyHammerConfig extends HammerGestureConfig {
@ -344,6 +345,7 @@ Marker.prototype.options.icon = MarkerFactory.defIcon;
AlbumsService, AlbumsService,
GalleryCacheService, GalleryCacheService,
ContentService, ContentService,
ContentLoaderService,
FilterService, FilterService,
GallerySortingService, GallerySortingService,
MapService, MapService,

View File

@ -1,21 +1,19 @@
import { Injectable } from '@angular/core'; import {Injectable} from '@angular/core';
import { ShareService } from '../ui/gallery/share.service'; import {ShareService} from '../ui/gallery/share.service';
import { MediaDTO } from '../../../common/entities/MediaDTO'; import {MediaDTO} from '../../../common/entities/MediaDTO';
import { QueryParams } from '../../../common/QueryParams'; import {QueryParams} from '../../../common/QueryParams';
import { Utils } from '../../../common/Utils'; import {Utils} from '../../../common/Utils';
import { ContentService } from '../ui/gallery/content.service'; import {Config} from '../../../common/config/public/Config';
import { Config } from '../../../common/config/public/Config'; import {ParentDirectoryDTO, SubDirectoryDTO,} from '../../../common/entities/DirectoryDTO';
import { import {ContentLoaderService} from '../ui/gallery/contentLoader.service';
ParentDirectoryDTO,
SubDirectoryDTO,
} from '../../../common/entities/DirectoryDTO';
@Injectable() @Injectable()
export class QueryService { export class QueryService {
constructor( constructor(
private shareService: ShareService, private shareService: ShareService,
private galleryService: ContentService private galleryService: ContentLoaderService
) {} ) {
}
getMediaStringId(media: MediaDTO): string { getMediaStringId(media: MediaDTO): string {
if (this.galleryService.isSearchResult()) { if (this.galleryService.isSearchResult()) {

View File

@ -1,4 +1,21 @@
.btn-blog-details {
position: absolute;
bottom: 0;
border: 0;
width: 100%;
}
.btn-blog-details:hover {
background-image: linear-gradient(transparent, rgba(var(--bs-body-color-rgb), 0.5));
}
.blog { .blog {
opacity: 0.8;
position: relative;
}
.blog:hover {
opacity: 1;
} }
.card-body { .card-body {

View File

@ -1,16 +1,25 @@
<div class="blog"> <ng-container *ngIf="mkObservable | async as markdowns">
<div class="card"> <div class="blog" *ngIf="markdowns.length > 0">
<div class="card-body" style="min-height: 77px" [style.height]="collapsed ? '77px':''"> <div class="card">
<ng-container *ngFor="let md of markdowns; let i = index"> <div class="card-body" style="min-height: 77px" [style.height]="!open ? '77px':''">
<markdown <ng-container *ngFor="let md of markdowns; let last = last">
*ngIf="!collapsed" <markdown
[data]="md"> *ngIf="open"
</markdown> [data]="md.text">
<span *ngIf="collapsed" class="text-preview"> </markdown>
{{md}} <span *ngIf="!open" class="text-preview">
</span> <markdown
<hr *ngIf="i != markdowns.length-1"> [inline]="true"
</ng-container> [data]="md.text">
</markdown>
</span>
<hr *ngIf="!last">
</ng-container>
</div>
</div> </div>
<button class="btn btn-blog-details text-body" (click)="toggleCollapsed()">
<ng-icon [name]="open ? 'ionChevronUpOutline' : 'ionChevronDownOutline'"></ng-icon>
</button>
</div> </div>
</div> </ng-container>

View File

@ -1,7 +1,8 @@
import { Component, Input } from '@angular/core'; import {Component, EventEmitter, Input, Output} from '@angular/core';
import { FileDTO } from '../../../../../common/entities/FileDTO'; import {BlogService, GroupedMarkdown} from './blog.service';
import { BlogService } from './blog.service'; import {OnChanges} from '../../../../../../node_modules/@angular/core';
import { OnChanges } from '../../../../../../node_modules/@angular/core'; import {Utils} from '../../../../../common/Utils';
import {map, Observable} from 'rxjs';
@Component({ @Component({
selector: 'app-gallery-blog', selector: 'app-gallery-blog',
@ -9,22 +10,30 @@ import { OnChanges } from '../../../../../../node_modules/@angular/core';
styleUrls: ['./blog.gallery.component.css'], styleUrls: ['./blog.gallery.component.css'],
}) })
export class GalleryBlogComponent implements OnChanges { export class GalleryBlogComponent implements OnChanges {
@Input() mdFiles: FileDTO[]; @Input() open: boolean;
@Input() collapsed: boolean; @Input() date: Date;
markdowns: string[] = []; @Output() openChange = new EventEmitter<boolean>();
public markdowns: string[] = [];
mkObservable: Observable<GroupedMarkdown[]>;
constructor(public blogService: BlogService) {} constructor(public blogService: BlogService) {
}
ngOnChanges(): void { ngOnChanges(): void {
this.loadMarkdown().catch(console.error); const utcDate = this.date ? this.date.getTime() : undefined;
this.mkObservable = this.blogService.groupedMarkdowns.pipe(map(gm => {
if (!this.date) {
return gm.filter(g => !g.date);
}
return gm.filter(g => g.date == utcDate);
}));
} }
async loadMarkdown(): Promise<void> {
this.markdowns = []; toggleCollapsed(): void {
for (const f of this.mdFiles) { this.open = !this.open;
this.markdowns.push(await this.blogService.getMarkDown(f)); this.openChange.emit(this.open);
}
} }
} }

View File

@ -1,13 +1,94 @@
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 { FileDTO } from '../../../../../common/entities/FileDTO'; import {FileDTO} from '../../../../../common/entities/FileDTO';
import { Utils } from '../../../../../common/Utils'; import {Utils} from '../../../../../common/Utils';
import {ContentService} from '../content.service';
import {mergeMap, Observable} from 'rxjs';
import {MDFilesFilterPipe} from '../../../pipes/MDFilesFilterPipe';
@Injectable() @Injectable()
export class BlogService { export class BlogService {
cache: { [key: string]: Promise<string> | string } = {}; cache: { [key: string]: Promise<string> | string } = {};
public groupedMarkdowns: Observable<GroupedMarkdown[]>;
constructor(private networkService: NetworkService) {} constructor(private networkService: NetworkService,
private galleryService: ContentService,
private mdFilesFilterPipe: MDFilesFilterPipe) {
this.groupedMarkdowns = this.galleryService.sortedFilteredContent.pipe(
mergeMap(async content => {
if (!content) {
return [];
}
const dates = content.mediaGroups.map(g => g.date)
.filter(d => !!d).map(d => d.getTime());
const files = this.mdFilesFilterPipe.transform(content.metaFile)
.map(f => this.splitMarkDown(f, dates));
return (await Promise.all(files)).flat();
}));
}
private async splitMarkDown(file: FileDTO, dates: number[]): Promise<GroupedMarkdown[]> {
const markdown = await this.getMarkDown(file);
if (dates.length == 0) {
return [{
text: markdown,
file: file
}];
}
dates.sort();
const splitterRgx = new RegExp(/<!--\s*@pg-date:?\s*\d{4}-\d{1,2}-\d{1,2}\s*-->/, 'gi');
const dateRgx = new RegExp(/\d{4}-\d{1,2}-\d{1,2}/);
const ret: GroupedMarkdown[] = [];
const matches = Array.from(markdown.matchAll(splitterRgx));
if (matches.length == 0) {
return [{
text: markdown,
file: file
}];
}
ret.push({
text: markdown.substring(0, matches[0].index),
file: file
});
for (let i = 0; i < matches.length; ++i) {
const matchedStr = matches[i][0];
// get UTC midnight date
const dateNum = Utils.makeUTCMidnight(new Date(matchedStr.match(dateRgx)[0])).getTime();
let groupDate = dates.find((d, i) => i > dates.length - 1 ? false : dates[i + 1] > dateNum); //dates are sorted
// cant find the date. put to the last group (as it was later)
if (groupDate === undefined) {
groupDate = dates[dates.length - 1];
}
const text = i + 1 >= matches.length ? markdown.substring(matches[i].index) : markdown.substring(matches[i].index, matches[i + 1].index);
// if it would be in the same group. Concatenate it
const sameGroup = ret.find(g => g.date == groupDate);
if (sameGroup) {
sameGroup.text += text;
continue;
}
ret.push({
date: groupDate,
text: text,
file: file
});
}
return ret;
}
public getMarkDown(file: FileDTO): Promise<string> { public getMarkDown(file: FileDTO): Promise<string> {
const filePath = Utils.concatUrls( const filePath = Utils.concatUrls(
@ -27,3 +108,9 @@ export class BlogService {
} }
} }
export interface GroupedMarkdown {
date?: number;
text: string;
file: FileDTO;
}

View File

@ -8,7 +8,7 @@ import {GroupingMethod, SortingMethod} from '../../../../common/entities/Sorting
import {VersionService} from '../../model/version.service'; import {VersionService} from '../../model/version.service';
import {SearchQueryDTO, SearchQueryTypes,} from '../../../../common/entities/SearchQueryDTO'; import {SearchQueryDTO, SearchQueryTypes,} from '../../../../common/entities/SearchQueryDTO';
import {ContentWrapper} from '../../../../common/entities/ConentWrapper'; import {ContentWrapper} from '../../../../common/entities/ConentWrapper';
import {ContentWrapperWithError} from './content.service'; import {ContentWrapperWithError} from './contentLoader.service';
import {ThemeModes} from '../../../../common/config/public/ClientConfig'; import {ThemeModes} from '../../../../common/config/public/ClientConfig';
interface CacheItem<T> { interface CacheItem<T> {

View File

@ -1,148 +1,35 @@
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 { import {SubDirectoryDTO,} from '../../../../common/entities/DirectoryDTO';
ParentDirectoryDTO,
SubDirectoryDTO,
} from '../../../../common/entities/DirectoryDTO';
import {GalleryCacheService} from './cache.gallery.service'; import {GalleryCacheService} from './cache.gallery.service';
import {BehaviorSubject, Observable} 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 {QueryParams} from '../../../../common/QueryParams'; import {QueryParams} from '../../../../common/QueryParams';
import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO';
import {ErrorCodes} from '../../../../common/entities/Error'; import {ErrorCodes} from '../../../../common/entities/Error';
import {map} from 'rxjs/operators'; import {map} from 'rxjs/operators';
import {MediaDTO} from '../../../../common/entities/MediaDTO'; import {MediaDTO} from '../../../../common/entities/MediaDTO';
import {FileDTO} from '../../../../common/entities/FileDTO'; import {FileDTO} from '../../../../common/entities/FileDTO';
import {GallerySortingService, GroupedDirectoryContent} from './navigator/sorting.service';
import {FilterService} from './filter/filter.service';
import {ContentLoaderService} from './contentLoader.service';
@Injectable() @Injectable()
export class ContentService { export class ContentService {
public content: BehaviorSubject<ContentWrapperWithError>; public sortedFilteredContent: Observable<GroupedDirectoryContent>;
public directoryContent: Observable<DirectoryContent>;
lastRequest: { directory: string } = {
directory: null,
};
private lastDirectory: ParentDirectoryDTO;
private searchId: any;
private ongoingSearch: string = null;
constructor( constructor(
private networkService: NetworkService, private contentLoaderService: ContentLoaderService,
private galleryCacheService: GalleryCacheService, private sortingService: GallerySortingService,
private shareService: ShareService, private filterService: FilterService
private navigationService: NavigationService
) { ) {
this.content = new BehaviorSubject<ContentWrapperWithError>( this.sortedFilteredContent = this.sortingService
new ContentWrapperWithError() .applySorting(
); this.filterService.applyFilters(this.contentLoaderService.originalContent)
this.directoryContent = this.content.pipe(
map((c) => (c.directory ? c.directory : c.searchResult))
);
}
setContent(content: ContentWrapperWithError): void {
this.content.next(content);
}
public async loadDirectory(directoryName: string): Promise<void> {
// load from cache
const cw = this.galleryCacheService.getDirectory(directoryName);
ContentWrapper.unpack(cw);
this.setContent(cw);
this.lastRequest.directory = directoryName;
// prepare server request
const params: { [key: string]: any } = {};
if (Config.Sharing.enabled === true) {
if (this.shareService.isSharing()) {
params[QueryParams.gallery.sharingKey_query] =
this.shareService.getSharingKey();
}
}
if (
cw.directory &&
cw.directory.lastModified &&
cw.directory.lastScanned &&
!cw.directory.isPartial
) {
params[QueryParams.gallery.knownLastModified] =
cw.directory.lastModified;
params[QueryParams.gallery.knownLastScanned] =
cw.directory.lastScanned;
}
try {
const cw = await this.networkService.getJson<ContentWrapperWithError>(
'/gallery/content/' + encodeURIComponent(directoryName),
params
); );
if (!cw || cw.notModified === true) {
return;
}
this.galleryCacheService.setDirectory(cw); // save it before adding references
if (this.lastRequest.directory !== directoryName) {
return;
}
ContentWrapper.unpack(cw);
this.lastDirectory = cw.directory;
this.setContent(cw);
} catch (e) {
console.error(e);
this.navigationService.toGallery().catch(console.error);
}
} }
public async search(query: string): Promise<void> {
if (this.searchId != null) {
clearTimeout(this.searchId);
}
this.ongoingSearch = query;
this.setContent(new ContentWrapperWithError());
let cw = this.galleryCacheService.getSearch(JSON.parse(query));
if (!cw || cw.searchResult == null) {
try {
cw = await this.networkService.getJson<ContentWrapperWithError>('/search/' + query);
this.galleryCacheService.setSearch(cw);
} catch (e) {
if (e.code === ErrorCodes.LocationLookUp_ERROR) {
cw.error = 'Cannot find location: ' + e.message;
} else {
throw e;
}
}
}
if (this.ongoingSearch !== query) {
return;
}
ContentWrapper.unpack(cw);
this.setContent(cw);
}
isSearchResult(): boolean {
return !!this.content.value.searchResult;
}
}
export class ContentWrapperWithError extends ContentWrapper {
public error?: string;
}
export interface DirectoryContent {
directories: SubDirectoryDTO[];
media: MediaDTO[];
metaFile: FileDTO[];
} }

View File

@ -0,0 +1,145 @@
import {Injectable} from '@angular/core';
import {NetworkService} from '../../model/network/network.service';
import {ContentWrapper} from '../../../../common/entities/ConentWrapper';
import {SubDirectoryDTO,} from '../../../../common/entities/DirectoryDTO';
import {GalleryCacheService} from './cache.gallery.service';
import {BehaviorSubject, Observable} from 'rxjs';
import {Config} from '../../../../common/config/public/Config';
import {ShareService} from './share.service';
import {NavigationService} from '../../model/navigation.service';
import {QueryParams} from '../../../../common/QueryParams';
import {ErrorCodes} from '../../../../common/entities/Error';
import {map} from 'rxjs/operators';
import {MediaDTO} from '../../../../common/entities/MediaDTO';
import {FileDTO} from '../../../../common/entities/FileDTO';
import {GroupedDirectoryContent} from './navigator/sorting.service';
@Injectable()
export class ContentLoaderService {
public content: BehaviorSubject<ContentWrapperWithError>;
public originalContent: Observable<DirectoryContent>;
public sortedFilteredContent: Observable<GroupedDirectoryContent>;
lastRequest: { directory: string } = {
directory: null,
};
private searchId: any;
private ongoingSearch: string = null;
constructor(
private networkService: NetworkService,
private galleryCacheService: GalleryCacheService,
private shareService: ShareService,
private navigationService: NavigationService,
) {
this.content = new BehaviorSubject<ContentWrapperWithError>(
new ContentWrapperWithError()
);
this.originalContent = this.content.pipe(
map((c) => (c.directory ? c.directory : c.searchResult))
);
}
setContent(content: ContentWrapperWithError): void {
this.content.next(content);
}
public async loadDirectory(directoryName: string): Promise<void> {
// load from cache
const cw = this.galleryCacheService.getDirectory(directoryName);
ContentWrapper.unpack(cw);
this.setContent(cw);
this.lastRequest.directory = directoryName;
// prepare server request
const params: { [key: string]: any } = {};
if (Config.Sharing.enabled === true) {
if (this.shareService.isSharing()) {
params[QueryParams.gallery.sharingKey_query] =
this.shareService.getSharingKey();
}
}
if (
cw.directory &&
cw.directory.lastModified &&
cw.directory.lastScanned &&
!cw.directory.isPartial
) {
params[QueryParams.gallery.knownLastModified] =
cw.directory.lastModified;
params[QueryParams.gallery.knownLastScanned] =
cw.directory.lastScanned;
}
try {
const cw = await this.networkService.getJson<ContentWrapperWithError>(
'/gallery/content/' + encodeURIComponent(directoryName),
params
);
if (!cw || cw.notModified === true) {
return;
}
this.galleryCacheService.setDirectory(cw); // save it before adding references
if (this.lastRequest.directory !== directoryName) {
return;
}
ContentWrapper.unpack(cw);
this.setContent(cw);
} catch (e) {
console.error(e);
this.navigationService.toGallery().catch(console.error);
}
}
public async search(query: string): Promise<void> {
if (this.searchId != null) {
clearTimeout(this.searchId);
}
this.ongoingSearch = query;
this.setContent(new ContentWrapperWithError());
let cw = this.galleryCacheService.getSearch(JSON.parse(query));
if (!cw || cw.searchResult == null) {
try {
cw = await this.networkService.getJson<ContentWrapperWithError>('/search/' + query);
this.galleryCacheService.setSearch(cw);
} catch (e) {
if (e.code === ErrorCodes.LocationLookUp_ERROR) {
cw.error = 'Cannot find location: ' + e.message;
} else {
throw e;
}
}
}
if (this.ongoingSearch !== query) {
return;
}
ContentWrapper.unpack(cw);
this.setContent(cw);
}
isSearchResult(): boolean {
return !!this.content.value.searchResult;
}
}
export class ContentWrapperWithError extends ContentWrapper {
public error?: string;
}
export interface DirectoryContent {
directories: SubDirectoryDTO[];
media: MediaDTO[];
metaFile: FileDTO[];
}

View File

@ -1,7 +1,7 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {BehaviorSubject, Observable} from 'rxjs'; import {BehaviorSubject, Observable} from 'rxjs';
import {PhotoDTO} from '../../../../../common/entities/PhotoDTO'; import {PhotoDTO} from '../../../../../common/entities/PhotoDTO';
import {DirectoryContent} from '../content.service'; import {DirectoryContent} from '../contentLoader.service';
import {map, switchMap} from 'rxjs/operators'; import {map, switchMap} from 'rxjs/operators';
export enum FilterRenderType { export enum FilterRenderType {

View File

@ -3,15 +3,6 @@
padding: 0; padding: 0;
} }
.blog-wrapper {
opacity: 0.8;
display: flex;
position: relative;
}
.blog-wrapper:hover {
opacity: 1;
}
.blog-map-row { .blog-map-row {
width: 100%; width: 100%;
@ -22,18 +13,6 @@
min-height: 80px; min-height: 80px;
} }
.btn-blog-details {
width: calc(100% - 5px);
position: absolute;
bottom: 0;
margin-left: 2px;
margin-right: 2px;
border: 0;
}
.btn-blog-details:hover {
background-image: linear-gradient(transparent, rgba(var(--bs-body-color-rgb),0.5));
}
app-gallery-blog { app-gallery-blog {
float: left; float: left;

View File

@ -40,17 +40,10 @@
[directories]="directoryContent?.directories || []"></app-gallery-directories> [directories]="directoryContent?.directories || []"></app-gallery-directories>
<div class="blog-map-row" *ngIf="ShowMarkDown || ShowMap"> <div class="blog-map-row" *ngIf="ShowMarkDown || ShowMap">
<div class="blog-wrapper" <app-gallery-blog
[style.width]="blogOpen ? '100%' : 'calc(100% - 100px)'" [style.width]="blogOpen ? '100%' : 'calc(100% - 100px)'"
*ngIf="ShowMarkDown"> *ngIf="ShowMarkDown"
<app-gallery-blog [collapsed]="!blogOpen" [(open)]="blogOpen"></app-gallery-blog>
[mdFiles]="directoryContent.metaFile | mdFiles"></app-gallery-blog>
<button class="btn btn-blog-details text-body" (click)="blogOpen=!blogOpen">
<ng-icon [name]="blogOpen ? 'ionChevronUpOutline' : 'ionChevronDownOutline'"></ng-icon>
</button>
</div>
<app-gallery-map <app-gallery-map
class="rounded" class="rounded"
[class.rounded-start-0]="ShowMarkDown" [class.rounded-start-0]="ShowMarkDown"

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 {ContentService, ContentWrapperWithError,} from './content.service'; import {ContentService} from './content.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 {ShareService} from './share.service'; import {ShareService} from './share.service';
@ -18,6 +18,7 @@ import {FilterService} from './filter/filter.service';
import {PiTitleService} from '../../model/pi-title.service'; import {PiTitleService} from '../../model/pi-title.service';
import {GPXFilesFilterPipe} from '../../pipes/GPXFilesFilterPipe'; import {GPXFilesFilterPipe} from '../../pipes/GPXFilesFilterPipe';
import {MDFilesFilterPipe} from '../../pipes/MDFilesFilterPipe'; import {MDFilesFilterPipe} from '../../pipes/MDFilesFilterPipe';
import { ContentLoaderService,ContentWrapperWithError } from './contentLoader.service';
@Component({ @Component({
selector: 'app-gallery', selector: 'app-gallery',
@ -43,7 +44,6 @@ export class GalleryComponent implements OnInit, OnDestroy {
} = null; } = null;
public readonly mapEnabled: boolean; public readonly mapEnabled: boolean;
public directoryContent: GroupedDirectoryContent; public directoryContent: GroupedDirectoryContent;
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,
@ -53,24 +53,25 @@ export class GalleryComponent implements OnInit, OnDestroy {
}; };
constructor( constructor(
public galleryService: ContentService, public contentLoader: ContentLoaderService,
private authService: AuthenticationService, public galleryService: ContentService,
private router: Router, private authService: AuthenticationService,
private shareService: ShareService, private router: Router,
private route: ActivatedRoute, private shareService: ShareService,
private navigation: NavigationService, private route: ActivatedRoute,
private filterService: FilterService, private navigation: NavigationService,
private sortingService: GallerySortingService, private filterService: FilterService,
private piTitleService: PiTitleService, private sortingService: GallerySortingService,
private gpxFilesFilterPipe: GPXFilesFilterPipe, private piTitleService: PiTitleService,
private mdFilesFilterPipe: MDFilesFilterPipe, private gpxFilesFilterPipe: GPXFilesFilterPipe,
private mdFilesFilterPipe: MDFilesFilterPipe,
) { ) {
this.mapEnabled = Config.Map.enabled; this.mapEnabled = Config.Map.enabled;
PageHelper.showScrollY(); PageHelper.showScrollY();
} }
get ContentWrapper(): ContentWrapperWithError { get ContentWrapper(): ContentWrapperWithError {
return this.galleryService.content.value; return this.contentLoader.content.value;
} }
updateTimer(t: number): void { updateTimer(t: number): void {
@ -79,17 +80,17 @@ export class GalleryComponent implements OnInit, OnDestroy {
} }
// if the timer is longer than 10 years, just do not show it // if the timer is longer than 10 years, just do not show it
if ( if (
(this.shareService.sharingSubject.value.expires - Date.now()) / (this.shareService.sharingSubject.value.expires - Date.now()) /
1000 / 1000 /
86400 / 86400 /
365 > 365 >
10 10
) { ) {
return; return;
} }
t = Math.floor( 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 = {} as any;
this.countDown.day = Math.floor(t / 86400); this.countDown.day = Math.floor(t / 86400);
@ -119,33 +120,30 @@ export class GalleryComponent implements OnInit, OnDestroy {
async ngOnInit(): Promise<boolean> { async ngOnInit(): Promise<boolean> {
await this.shareService.wait(); await this.shareService.wait();
if ( if (
!this.authService.isAuthenticated() && !this.authService.isAuthenticated() &&
(!this.shareService.isSharing() || (!this.shareService.isSharing() ||
(this.shareService.isSharing() && (this.shareService.isSharing() &&
Config.Sharing.passwordProtected === true)) Config.Sharing.passwordProtected === true))
) { ) {
return this.navigation.toLogin(); return this.navigation.toLogin();
} }
this.showSearchBar = this.authService.canSearch(); this.showSearchBar = this.authService.canSearch();
this.showShare = this.showShare =
Config.Sharing.enabled && Config.Sharing.enabled &&
this.authService.isAuthorized(UserRoles.User); this.authService.isAuthorized(UserRoles.User);
this.showRandomPhotoBuilder = this.showRandomPhotoBuilder =
Config.RandomPhoto.enabled && Config.RandomPhoto.enabled &&
this.authService.isAuthorized(UserRoles.User); this.authService.isAuthorized(UserRoles.User);
this.subscription.content = this.sortingService this.subscription.content = this.galleryService.sortedFilteredContent
.applySorting( .subscribe((dc: GroupedDirectoryContent) => {
this.filterService.applyFilters(this.galleryService.directoryContent) this.onContentChange(dc);
) });
.subscribe((dc: GroupedDirectoryContent) => {
this.onContentChange(dc);
});
this.subscription.route = this.route.params.subscribe(this.onRoute); this.subscription.route = this.route.params.subscribe(this.onRoute);
if (this.shareService.isSharing()) { if (this.shareService.isSharing()) {
this.$counter = interval(1000); this.$counter = interval(1000);
this.subscription.timer = this.$counter.subscribe((x): void => this.subscription.timer = this.$counter.subscribe((x): void =>
this.updateTimer(x) this.updateTimer(x)
); );
} }
} }
@ -153,24 +151,24 @@ export class GalleryComponent implements OnInit, OnDestroy {
private onRoute = async (params: Params): Promise<void> => { private onRoute = async (params: Params): Promise<void> => {
const searchQuery = params[QueryParams.gallery.search.query]; const searchQuery = params[QueryParams.gallery.search.query];
if (searchQuery) { if (searchQuery) {
this.galleryService.search(searchQuery).catch(console.error); this.contentLoader.search(searchQuery).catch(console.error);
this.piTitleService.setSearchTitle(searchQuery); this.piTitleService.setSearchTitle(searchQuery);
return; return;
} }
if ( 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 const sharing = await this.shareService.currentSharing
.pipe(take(1)) .pipe(take(1))
.toPromise(); .toPromise();
const qParams: { [key: string]: any } = {}; const qParams: { [key: string]: any } = {};
qParams[QueryParams.gallery.sharingKey_query] = qParams[QueryParams.gallery.sharingKey_query] =
this.shareService.getSharingKey(); this.shareService.getSharingKey();
this.router this.router
.navigate(['/gallery', sharing.path], {queryParams: qParams}) .navigate(['/gallery', sharing.path], {queryParams: qParams})
.catch(console.error); .catch(console.error);
return; return;
} }
@ -178,7 +176,7 @@ export class GalleryComponent implements OnInit, OnDestroy {
directoryName = directoryName || ''; directoryName = directoryName || '';
this.piTitleService.setDirectoryTitle(directoryName); this.piTitleService.setDirectoryTitle(directoryName);
this.galleryService.loadDirectory(directoryName); this.contentLoader.loadDirectory(directoryName);
}; };
private onContentChange = (content: GroupedDirectoryContent): void => { private onContentChange = (content: GroupedDirectoryContent): void => {
@ -194,8 +192,8 @@ export class GalleryComponent implements OnInit, OnDestroy {
for (const mediaGroup of content.mediaGroups) { for (const mediaGroup of content.mediaGroups) {
if ( if (
mediaGroup.media mediaGroup.media
.findIndex((m: PhotoDTO) => !!m.metadata?.positionData?.GPSData?.longitude) !== -1 .findIndex((m: PhotoDTO) => !!m.metadata?.positionData?.GPSData?.longitude) !== -1
) { ) {
this.isPhotoWithLocation = true; this.isPhotoWithLocation = true;
break; break;

View File

@ -2,13 +2,24 @@
<ng-container *ngIf="mediaToRender?.length > 0"> <ng-container *ngIf="mediaToRender?.length > 0">
<ng-container *ngFor="let group of mediaToRender"> <ng-container *ngFor="let group of mediaToRender">
<ng-container *ngIf="group.name"> <ng-container *ngIf="group.name">
<ng-container [ngSwitch]="sortingService.grouping.value.method"> <ng-container [ngSwitch]="sortingService.grouping.value.method">
<div *ngSwitchCase="GroupByTypes.Rating" class="mt-4 mb-3"><h6 class="ms-2"> <div *ngSwitchCase="GroupByTypes.Rating" class="mt-4 mb-3">
<ng-icon *ngFor="let i of [0,1,2,3,4]" [name]="(i < (group.name | parseInt)) ? 'ionStar' : 'ionStarOutline'"></ng-icon> <h6 class="ms-2">
</h6></div> <ng-icon *ngFor="let i of [0,1,2,3,4]"
<div *ngSwitchCase="GroupByTypes.PersonCount" class="mt-4 mb-3"><h6 class="ms-2">{{group.name}} <ng-icon class="ms-1" name="ionPeopleOutline"></ng-icon></h6></div> [name]="(i < (group.name | parseInt)) ? 'ionStar' : 'ionStarOutline'"></ng-icon>
<div *ngSwitchDefault class="mt-4 mb-3"><h6 class="ms-2">{{group.name}}</h6></div> </h6>
</div>
<div *ngSwitchCase="GroupByTypes.PersonCount" class="mt-4 mb-3">
<h6 class="ms-2">{{group.name}}
<ng-icon class="ms-1" name="ionPeopleOutline"></ng-icon>
</h6>
</div>
<div *ngSwitchDefault class="mt-4 mb-3"><h6 class="ms-2">{{group.name}}</h6></div>
</ng-container>
</ng-container> </ng-container>
<ng-container *ngIf="group.date">
<app-gallery-blog [date]="group.date" [open]="false"></app-gallery-blog>
</ng-container> </ng-container>
<div class="media-grid"> <div class="media-grid">
<app-gallery-grid-photo <app-gallery-grid-photo

View File

@ -22,7 +22,6 @@ import {PageHelper} from '../../../model/page.helper';
import {Subscription} from 'rxjs'; import {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 {ContentService} from '../content.service';
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 {GallerySortingService, MediaGroup} from '../navigator/sorting.service'; import {GallerySortingService, MediaGroup} from '../navigator/sorting.service';
@ -65,7 +64,6 @@ export class GalleryGridComponent
private changeDetector: ChangeDetectorRef, private changeDetector: ChangeDetectorRef,
public queryService: QueryService, public queryService: QueryService,
private router: Router, private router: Router,
public galleryService: ContentService,
public sortingService: GallerySortingService, public sortingService: GallerySortingService,
private route: ActivatedRoute private route: ActivatedRoute
) { ) {
@ -191,7 +189,7 @@ export class GalleryGridComponent
for (; i < this.mediaGroups.length && i < this.mediaToRender.length; ++i) { for (; i < this.mediaGroups.length && i < this.mediaToRender.length; ++i) {
if (diffFound) { if (diffFound) {
break; break;
} }
@ -259,7 +257,11 @@ export class GalleryGridComponent
if (this.mediaToRender.length == 0 || if (this.mediaToRender.length == 0 ||
this.mediaToRender[this.mediaToRender.length - 1].media.length >= this.mediaToRender[this.mediaToRender.length - 1].media.length >=
this.mediaGroups[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: []}); this.mediaToRender.push({
name: this.mediaGroups[this.mediaToRender.length].name,
date: this.mediaGroups[this.mediaToRender.length].date,
media: []
} as GridMediaGroup);
} }
let maxRowHeight = this.getMaxRowHeight(); let maxRowHeight = this.getMaxRowHeight();
@ -453,4 +455,5 @@ export class GalleryGridComponent
interface GridMediaGroup { interface GridMediaGroup {
media: GridMedia[]; media: GridMedia[];
name: string; name: string;
date?: Date;
} }

View File

@ -4,7 +4,7 @@
<button type="button" class="btn-close" (click)="close()" aria-label="Close"> <button type="button" class="btn-close" (click)="close()" aria-label="Close">
</button> </button>
</div> </div>
<div class="row" *ngIf="galleryService.isSearchResult()"> <div class="row" *ngIf="contentLoaderService.isSearchResult()">
<div class="col-1 ps-0"> <div class="col-1 ps-0">
<ng-icon class="details-icon" name="ionFolderOutline"></ng-icon> <ng-icon class="details-icon" name="ionFolderOutline"></ng-icon>
</div> </div>

View File

@ -11,6 +11,7 @@ import {AuthenticationService} from '../../../../model/network/authentication.se
import {LatLngLiteral, marker, Marker, TileLayer, tileLayer} from 'leaflet'; import {LatLngLiteral, marker, Marker, TileLayer, tileLayer} from 'leaflet';
import {ContentService} from '../../content.service'; import {ContentService} from '../../content.service';
import {ThemeService} from '../../../../model/theme.service'; import {ThemeService} from '../../../../model/theme.service';
import { ContentLoaderService } from '../../contentLoader.service';
@Component({ @Component({
selector: 'app-info-panel', selector: 'app-info-panel',
@ -31,7 +32,7 @@ export class InfoPanelLightboxComponent implements OnInit, OnChanges {
constructor( constructor(
public queryService: QueryService, public queryService: QueryService,
public galleryService: ContentService, public contentLoaderService: ContentLoaderService,
public mapService: MapService, public mapService: MapService,
private authService: AuthenticationService, private authService: AuthenticationService,
private themeService: ThemeService private themeService: ThemeService

View File

@ -13,7 +13,7 @@
<ol *ngIf="isSearch" class="mb-0 mt-1 breadcrumb"> <ol *ngIf="isSearch" class="mb-0 mt-1 breadcrumb">
<li class="active"> <li class="active">
<ng-container i18n>Searching for:</ng-container> <ng-container i18n>Searching for:</ng-container>
<strong> {{galleryService.content.value?.searchResult?.searchQuery | searchQuery}}</strong> <strong> {{contentLoaderService.content.value?.searchResult?.searchQuery | searchQuery}}</strong>
</li> </li>
</ol> </ol>

View File

@ -4,7 +4,6 @@ import {DomSanitizer} from '@angular/platform-browser';
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 {ContentService, ContentWrapperWithError, DirectoryContent,} from '../content.service';
import {Utils} from '../../../../../common/Utils'; import {Utils} from '../../../../../common/Utils';
import {GroupByTypes, GroupingMethod, SortByDirectionalTypes, SortByTypes} from '../../../../../common/entities/SortingMethods'; import {GroupByTypes, GroupingMethod, SortByDirectionalTypes, SortByTypes} from '../../../../../common/entities/SortingMethods';
import {Config} from '../../../../../common/config/public/Config'; import {Config} from '../../../../../common/config/public/Config';
@ -15,6 +14,7 @@ import {GallerySortingService} from './sorting.service';
import {PageHelper} from '../../../model/page.helper'; import {PageHelper} from '../../../model/page.helper';
import {BsDropdownDirective} from 'ngx-bootstrap/dropdown'; import {BsDropdownDirective} from 'ngx-bootstrap/dropdown';
import {FilterService} from '../filter/filter.service'; import {FilterService} from '../filter/filter.service';
import {ContentLoaderService, ContentWrapperWithError, DirectoryContent} from '../contentLoader.service';
@Component({ @Component({
selector: 'app-gallery-navbar', selector: 'app-gallery-navbar',
@ -47,7 +47,7 @@ export class GalleryNavigatorComponent {
constructor( constructor(
public authService: AuthenticationService, public authService: AuthenticationService,
public queryService: QueryService, public queryService: QueryService,
public galleryService: ContentService, public contentLoaderService: ContentLoaderService,
public filterService: FilterService, public filterService: FilterService,
public sortingService: GallerySortingService, public sortingService: GallerySortingService,
private router: Router, private router: Router,
@ -57,11 +57,11 @@ export class GalleryNavigatorComponent {
// can't group by random // can't group by random
this.groupingByTypes = Utils.enumToArray(GroupByTypes); this.groupingByTypes = Utils.enumToArray(GroupByTypes);
this.RootFolderName = $localize`Home`; this.RootFolderName = $localize`Home`;
this.wrappedContent = this.galleryService.content; this.wrappedContent = this.contentLoaderService.content;
this.directoryContent = this.wrappedContent.pipe( this.directoryContent = this.wrappedContent.pipe(
map((c) => (c.directory ? c.directory : c.searchResult)) map((c) => (c.directory ? c.directory : c.searchResult))
); );
this.routes = this.galleryService.content.pipe( this.routes = this.contentLoaderService.content.pipe(
map((c) => { map((c) => {
this.parentPath = null; this.parentPath = null;
if (!c.directory) { if (!c.directory) {
@ -124,15 +124,15 @@ export class GalleryNavigatorComponent {
} }
get isDirectory(): boolean { get isDirectory(): boolean {
return !!this.galleryService.content.value.directory; return !!this.contentLoaderService.content.value.directory;
} }
get isSearch(): boolean { get isSearch(): boolean {
return !!this.galleryService.content.value.searchResult; return !!this.contentLoaderService.content.value.searchResult;
} }
get ItemCount(): number { get ItemCount(): number {
const c = this.galleryService.content.value; const c = this.contentLoaderService.content.value;
return c.directory return c.directory
? c.directory.mediaCount ? c.directory.mediaCount
: c.searchResult : c.searchResult
@ -142,7 +142,7 @@ export class GalleryNavigatorComponent {
isDefaultSortingAndGrouping(): boolean { isDefaultSortingAndGrouping(): boolean {
return this.sortingService.isDefaultSortingAndGrouping( return this.sortingService.isDefaultSortingAndGrouping(
this.galleryService.content.value this.contentLoaderService.content.value
); );
} }
@ -193,7 +193,7 @@ export class GalleryNavigatorComponent {
getDownloadZipLink(): string { getDownloadZipLink(): string {
const c = this.galleryService.content.value; const c = this.contentLoaderService.content.value;
if (!c.directory) { if (!c.directory) {
return null; return null;
} }
@ -212,7 +212,7 @@ export class GalleryNavigatorComponent {
} }
getDirectoryFlattenSearchQuery(): string { getDirectoryFlattenSearchQuery(): string {
const c = this.galleryService.content.value; const c = this.contentLoaderService.content.value;
if (!c.directory) { if (!c.directory) {
return null; return null;
} }

View File

@ -4,9 +4,8 @@ import {NetworkService} from '../../../model/network/network.service';
import {GalleryCacheService} from '../cache.gallery.service'; import {GalleryCacheService} from '../cache.gallery.service';
import {BehaviorSubject, Observable} from 'rxjs'; import {BehaviorSubject, Observable} from 'rxjs';
import {Config} from '../../../../../common/config/public/Config'; import {Config} from '../../../../../common/config/public/Config';
import {GroupingMethod, SortByTypes, SortingMethod} from '../../../../../common/entities/SortingMethods'; import {GroupByTypes, GroupingMethod, SortByTypes, SortingMethod} from '../../../../../common/entities/SortingMethods';
import {PG2ConfMap} from '../../../../../common/PG2ConfMap'; import {PG2ConfMap} from '../../../../../common/PG2ConfMap';
import {ContentService, DirectoryContent} from '../content.service';
import {PhotoDTO} from '../../../../../common/entities/PhotoDTO'; import {PhotoDTO} from '../../../../../common/entities/PhotoDTO';
import {map, switchMap} from 'rxjs/operators'; import {map, switchMap} from 'rxjs/operators';
import {SeededRandomService} from '../../../model/seededRandom.service'; import {SeededRandomService} from '../../../model/seededRandom.service';
@ -14,6 +13,8 @@ import {ContentWrapper} from '../../../../../common/entities/ConentWrapper';
import {SubDirectoryDTO} from '../../../../../common/entities/DirectoryDTO'; import {SubDirectoryDTO} from '../../../../../common/entities/DirectoryDTO';
import {MediaDTO} from '../../../../../common/entities/MediaDTO'; import {MediaDTO} from '../../../../../common/entities/MediaDTO';
import {FileDTO} from '../../../../../common/entities/FileDTO'; import {FileDTO} from '../../../../../common/entities/FileDTO';
import {Utils} from '../../../../../common/Utils';
import {ContentLoaderService, DirectoryContent} from '../contentLoader.service';
@Injectable() @Injectable()
export class GallerySortingService { export class GallerySortingService {
@ -24,7 +25,7 @@ export class GallerySortingService {
constructor( constructor(
private networkService: NetworkService, private networkService: NetworkService,
private galleryCacheService: GalleryCacheService, private galleryCacheService: GalleryCacheService,
private galleryService: ContentService, private galleryService: ContentLoaderService,
private rndService: SeededRandomService, private rndService: SeededRandomService,
private datePipe: DatePipe private datePipe: DatePipe
) { ) {
@ -176,6 +177,38 @@ export class GallerySortingService {
return; return;
} }
private getGroupByNameFn(grouping: GroupingMethod) {
switch (grouping.method) {
case SortByTypes.Date:
return (m: MediaDTO) => this.datePipe.transform(m.metadata.creationDate, 'longDate', 'UTC');
case SortByTypes.Name:
return (m: MediaDTO) => m.name.at(0).toUpperCase();
case SortByTypes.Rating:
return (m: MediaDTO) => ((m as PhotoDTO).metadata.rating || 0).toString();
case SortByTypes.FileSize: {
const groups = [0.5, 1, 2, 5, 10, 15, 20, 30, 50, 100, 200, 500, 1000]; // MBs
return (m: MediaDTO) => {
const mbites = ((m as PhotoDTO).metadata.fileSize || 0) / 1024 / 1024;
const i = groups.findIndex((s) => s > mbites);
if (i == -1) {
return '>' + groups[groups.length - 1] + ' MB';
} else if (i == 0) {
return '<' + groups[0] + ' MB';
}
return groups[i - 1] + ' - ' + groups[i] + ' MB';
};
}
case SortByTypes.PersonCount:
return (m: MediaDTO) => ((m as PhotoDTO).metadata.faces || []).length.toString();
}
return (m: MediaDTO) => '';
}
public applySorting( public applySorting(
directoryContent: Observable<DirectoryContent> directoryContent: Observable<DirectoryContent>
): Observable<GroupedDirectoryContent> { ): Observable<GroupedDirectoryContent> {
@ -243,36 +276,10 @@ export class GallerySortingService {
if (dirContent.media) { if (dirContent.media) {
const mCopy = dirContent.media; const mCopy = dirContent.media;
this.sortMedia(grouping, mCopy); this.sortMedia(grouping, mCopy);
let groupFN = (m: MediaDTO) => ''; const groupFN = this.getGroupByNameFn(grouping);
switch (grouping.method) {
case SortByTypes.Date:
groupFN = (m: MediaDTO) => this.datePipe.transform(m.metadata.creationDate, 'longDate');
break;
case SortByTypes.Name:
groupFN = (m: MediaDTO) => m.name.at(0).toUpperCase();
break;
case SortByTypes.Rating:
groupFN = (m: MediaDTO) => ((m as PhotoDTO).metadata.rating || 0).toString();
break;
case SortByTypes.FileSize: {
const groups = [0.5, 1, 2, 5, 10, 15, 20, 30, 50]; // MBs
groupFN = (m: MediaDTO) => {
const mbites = ((m as PhotoDTO).metadata.fileSize || 0) / 1024 / 1024;
const i = groups.findIndex((s) => s > mbites);
if (i == -1) {
return '>' + groups[groups.length - 1] + ' MB';
} else if (i == 0) {
return '<' + groups[0] + ' MB';
}
return groups[i - 1] + ' - ' + groups[i] + ' MB';
};
}
break;
case SortByTypes.PersonCount:
groupFN = (m: MediaDTO) => ((m as PhotoDTO).metadata.faces || []).length.toString();
break;
}
c.mediaGroups = []; c.mediaGroups = [];
for (const m of mCopy) { for (const m of mCopy) {
const k = groupFN(m); const k = groupFN(m);
if (c.mediaGroups.length == 0 || c.mediaGroups[c.mediaGroups.length - 1].name != k) { if (c.mediaGroups.length == 0 || c.mediaGroups[c.mediaGroups.length - 1].name != k) {
@ -280,7 +287,13 @@ export class GallerySortingService {
} }
c.mediaGroups[c.mediaGroups.length - 1].media.push(m); c.mediaGroups[c.mediaGroups.length - 1].media.push(m);
} }
c.mediaGroups; }
if (grouping.method === GroupByTypes.Date) {
// We do not need the youngest as we group by day. All photos are from the same day
c.mediaGroups.forEach(g => {
g.date = Utils.makeUTCMidnight(new Date(g.media?.[0]?.metadata?.creationDate));
});
} }
// sort groups // sort groups
@ -300,6 +313,7 @@ export class GallerySortingService {
export interface MediaGroup { export interface MediaGroup {
name: string; name: string;
date?: Date; // used for blog. It allows to chop off blog to smaller pieces
media: MediaDTO[]; media: MediaDTO[];
} }

View File

@ -15,6 +15,7 @@ import {
import { ActivatedRoute, Params } from '@angular/router'; import { ActivatedRoute, Params } from '@angular/router';
import { QueryParams } from '../../../../../common/QueryParams'; import { QueryParams } from '../../../../../common/QueryParams';
import { SearchQueryParserService } from '../search/search-query-parser.service'; import { SearchQueryParserService } from '../search/search-query-parser.service';
import {ContentLoaderService} from '../contentLoader.service';
@Component({ @Component({
selector: 'app-gallery-random-query-builder', selector: 'app-gallery-random-query-builder',
@ -36,7 +37,7 @@ export class RandomQueryBuilderGalleryComponent implements OnInit, OnDestroy {
private readonly subscription: Subscription = null; private readonly subscription: Subscription = null;
constructor( constructor(
public galleryService: ContentService, public contentLoaderService: ContentLoaderService,
private notification: NotificationService, private notification: NotificationService,
private searchQueryParserService: SearchQueryParserService, private searchQueryParserService: SearchQueryParserService,
private route: ActivatedRoute, private route: ActivatedRoute,
@ -65,7 +66,7 @@ export class RandomQueryBuilderGalleryComponent implements OnInit, OnDestroy {
} }
ngOnInit(): void { ngOnInit(): void {
this.contentSubscription = this.galleryService.content.subscribe( this.contentSubscription = this.contentLoaderService.content.subscribe(
(content: ContentWrapper) => { (content: ContentWrapper) => {
this.enabled = !!content.directory; this.enabled = !!content.directory;
if (!this.enabled) { if (!this.enabled) {

View File

@ -12,6 +12,7 @@ import {Subscription} from 'rxjs';
import {UserRoles} from '../../../../../common/entities/UserDTO'; import {UserRoles} from '../../../../../common/entities/UserDTO';
import {AuthenticationService} from '../../../model/network/authentication.service'; import {AuthenticationService} from '../../../model/network/authentication.service';
import {ClipboardService} from 'ngx-clipboard'; import {ClipboardService} from 'ngx-clipboard';
import {ContentLoaderService} from '../contentLoader.service';
@Component({ @Component({
selector: 'app-gallery-share', selector: 'app-gallery-share',
@ -51,7 +52,7 @@ export class GalleryShareComponent implements OnInit, OnDestroy {
constructor( constructor(
public sharingService: ShareService, public sharingService: ShareService,
public galleryService: ContentService, public galleryService: ContentLoaderService,
private notification: NotificationService, private notification: NotificationService,
private modalService: BsModalService, private modalService: BsModalService,
public authService: AuthenticationService, public authService: AuthenticationService,