1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2025-02-01 13:17:55 +02:00

Implenenting groupped markdowns #711

This commit is contained in:
Patrik J. Braun 2023-09-03 18:35:57 +02:00
parent 5a852dc443
commit ed56de4523
24 changed files with 492 additions and 310 deletions

View File

@ -83,4 +83,11 @@ Start numbering with offset:
57. foo 57. foo
1. bar 1. bar
<!-- @pg-date 2015-06-12 -->
## Day 1
You can tag section in the `*.md` files with `<!-- @pg-date <ISO_DATE> -->`, like: `<!-- @pg-date 2015-06-12 -->` to attach them to a date.
Then if you group by date, they will show up at the assigned day.
That mart of the markdown will be removed from the mail markdown at the top and shown only at that day.

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, 100, 200, 500, 1000]; // 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,