From ed56de452315fd1533226d70544cde07bfdace05 Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Sun, 3 Sep 2023 18:35:57 +0200 Subject: [PATCH] Implenenting groupped markdowns #711 --- demo/images/index.md | 7 + src/common/Utils.ts | 20 +++ src/frontend/app/app.module.ts | 2 + src/frontend/app/model/query.service.ts | 24 ++- .../gallery/blog/blog.gallery.component.css | 17 ++ .../gallery/blog/blog.gallery.component.html | 37 +++-- .../ui/gallery/blog/blog.gallery.component.ts | 37 +++-- .../app/ui/gallery/blog/blog.service.ts | 97 +++++++++++- .../app/ui/gallery/cache.gallery.service.ts | 2 +- .../app/ui/gallery/content.service.ts | 135 ++-------------- .../app/ui/gallery/contentLoader.service.ts | 145 ++++++++++++++++++ .../app/ui/gallery/filter/filter.service.ts | 2 +- .../app/ui/gallery/gallery.component.css | 21 --- .../app/ui/gallery/gallery.component.html | 15 +- .../app/ui/gallery/gallery.component.ts | 92 ++++++----- .../gallery/grid/grid.gallery.component.html | 23 ++- .../ui/gallery/grid/grid.gallery.component.ts | 11 +- ...info-panel.lightbox.gallery.component.html | 2 +- .../info-panel.lightbox.gallery.component.ts | 3 +- .../navigator.gallery.component.html | 2 +- .../navigator/navigator.gallery.component.ts | 20 +-- .../ui/gallery/navigator/sorting.service.ts | 80 ++++++---- .../random-query-builder.gallery.component.ts | 5 +- .../gallery/share/share.gallery.component.ts | 3 +- 24 files changed, 492 insertions(+), 310 deletions(-) create mode 100644 src/frontend/app/ui/gallery/contentLoader.service.ts diff --git a/demo/images/index.md b/demo/images/index.md index 42fb551f..585d0cba 100644 --- a/demo/images/index.md +++ b/demo/images/index.md @@ -83,4 +83,11 @@ Start numbering with offset: 57. foo 1. bar + +## Day 1 + +You can tag section in the `*.md` files with ``, like: `` 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. diff --git a/src/common/Utils.ts b/src/common/Utils.ts index d164cace..ceef8811 100644 --- a/src/common/Utils.ts +++ b/src/common/Utils.ts @@ -90,6 +90,26 @@ export class Utils { 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 { const postFixes = ['B', 'KB', 'MB', 'GB', 'TB']; let index = 0; diff --git a/src/frontend/app/app.module.ts b/src/frontend/app/app.module.ts index ac7e6a85..77ce8de1 100644 --- a/src/frontend/app/app.module.ts +++ b/src/frontend/app/app.module.ts @@ -183,6 +183,7 @@ import {ParseIntPipe} from './pipes/ParseIntPipe'; import { SortingMethodSettingsEntryComponent } from './ui/settings/template/settings-entry/sorting-method/sorting-method.settings-entry.component'; +import {ContentLoaderService} from './ui/gallery/contentLoader.service'; @Injectable() export class MyHammerConfig extends HammerGestureConfig { @@ -344,6 +345,7 @@ Marker.prototype.options.icon = MarkerFactory.defIcon; AlbumsService, GalleryCacheService, ContentService, + ContentLoaderService, FilterService, GallerySortingService, MapService, diff --git a/src/frontend/app/model/query.service.ts b/src/frontend/app/model/query.service.ts index 56e581d3..53aaf199 100644 --- a/src/frontend/app/model/query.service.ts +++ b/src/frontend/app/model/query.service.ts @@ -1,21 +1,19 @@ -import { Injectable } from '@angular/core'; -import { ShareService } from '../ui/gallery/share.service'; -import { MediaDTO } from '../../../common/entities/MediaDTO'; -import { QueryParams } from '../../../common/QueryParams'; -import { Utils } from '../../../common/Utils'; -import { ContentService } from '../ui/gallery/content.service'; -import { Config } from '../../../common/config/public/Config'; -import { - ParentDirectoryDTO, - SubDirectoryDTO, -} from '../../../common/entities/DirectoryDTO'; +import {Injectable} from '@angular/core'; +import {ShareService} from '../ui/gallery/share.service'; +import {MediaDTO} from '../../../common/entities/MediaDTO'; +import {QueryParams} from '../../../common/QueryParams'; +import {Utils} from '../../../common/Utils'; +import {Config} from '../../../common/config/public/Config'; +import {ParentDirectoryDTO, SubDirectoryDTO,} from '../../../common/entities/DirectoryDTO'; +import {ContentLoaderService} from '../ui/gallery/contentLoader.service'; @Injectable() export class QueryService { constructor( private shareService: ShareService, - private galleryService: ContentService - ) {} + private galleryService: ContentLoaderService + ) { + } getMediaStringId(media: MediaDTO): string { if (this.galleryService.isSearchResult()) { diff --git a/src/frontend/app/ui/gallery/blog/blog.gallery.component.css b/src/frontend/app/ui/gallery/blog/blog.gallery.component.css index 44b11b6c..8262532d 100644 --- a/src/frontend/app/ui/gallery/blog/blog.gallery.component.css +++ b/src/frontend/app/ui/gallery/blog/blog.gallery.component.css @@ -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 { + opacity: 0.8; + position: relative; +} + +.blog:hover { + opacity: 1; } .card-body { diff --git a/src/frontend/app/ui/gallery/blog/blog.gallery.component.html b/src/frontend/app/ui/gallery/blog/blog.gallery.component.html index 37a0788c..b07a892c 100644 --- a/src/frontend/app/ui/gallery/blog/blog.gallery.component.html +++ b/src/frontend/app/ui/gallery/blog/blog.gallery.component.html @@ -1,16 +1,25 @@ -
-
-
- - - - - {{md}} - -
-
+ +
+
+
+ + + + + + + +
+
+
+ +
-
+ diff --git a/src/frontend/app/ui/gallery/blog/blog.gallery.component.ts b/src/frontend/app/ui/gallery/blog/blog.gallery.component.ts index d44112c8..d8804935 100644 --- a/src/frontend/app/ui/gallery/blog/blog.gallery.component.ts +++ b/src/frontend/app/ui/gallery/blog/blog.gallery.component.ts @@ -1,7 +1,8 @@ -import { Component, Input } from '@angular/core'; -import { FileDTO } from '../../../../../common/entities/FileDTO'; -import { BlogService } from './blog.service'; -import { OnChanges } from '../../../../../../node_modules/@angular/core'; +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {BlogService, GroupedMarkdown} from './blog.service'; +import {OnChanges} from '../../../../../../node_modules/@angular/core'; +import {Utils} from '../../../../../common/Utils'; +import {map, Observable} from 'rxjs'; @Component({ selector: 'app-gallery-blog', @@ -9,22 +10,30 @@ import { OnChanges } from '../../../../../../node_modules/@angular/core'; styleUrls: ['./blog.gallery.component.css'], }) export class GalleryBlogComponent implements OnChanges { - @Input() mdFiles: FileDTO[]; - @Input() collapsed: boolean; - markdowns: string[] = []; + @Input() open: boolean; + @Input() date: Date; + @Output() openChange = new EventEmitter(); + public markdowns: string[] = []; + mkObservable: Observable; - constructor(public blogService: BlogService) {} + constructor(public blogService: BlogService) { + } 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 { - this.markdowns = []; - for (const f of this.mdFiles) { - this.markdowns.push(await this.blogService.getMarkDown(f)); - } + + toggleCollapsed(): void { + this.open = !this.open; + this.openChange.emit(this.open); } } diff --git a/src/frontend/app/ui/gallery/blog/blog.service.ts b/src/frontend/app/ui/gallery/blog/blog.service.ts index 7f42cb04..93e01377 100644 --- a/src/frontend/app/ui/gallery/blog/blog.service.ts +++ b/src/frontend/app/ui/gallery/blog/blog.service.ts @@ -1,13 +1,94 @@ -import { Injectable } from '@angular/core'; -import { NetworkService } from '../../../model/network/network.service'; -import { FileDTO } from '../../../../../common/entities/FileDTO'; -import { Utils } from '../../../../../common/Utils'; +import {Injectable} from '@angular/core'; +import {NetworkService} from '../../../model/network/network.service'; +import {FileDTO} from '../../../../../common/entities/FileDTO'; +import {Utils} from '../../../../../common/Utils'; +import {ContentService} from '../content.service'; +import {mergeMap, Observable} from 'rxjs'; +import {MDFilesFilterPipe} from '../../../pipes/MDFilesFilterPipe'; @Injectable() export class BlogService { cache: { [key: string]: Promise | string } = {}; + public groupedMarkdowns: Observable; - 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 { + const markdown = await this.getMarkDown(file); + + if (dates.length == 0) { + return [{ + text: markdown, + file: file + }]; + } + + dates.sort(); + + const splitterRgx = new RegExp(//, '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 { const filePath = Utils.concatUrls( @@ -27,3 +108,9 @@ export class BlogService { } } + +export interface GroupedMarkdown { + date?: number; + text: string; + file: FileDTO; +} diff --git a/src/frontend/app/ui/gallery/cache.gallery.service.ts b/src/frontend/app/ui/gallery/cache.gallery.service.ts index 2b82b693..810a96a5 100644 --- a/src/frontend/app/ui/gallery/cache.gallery.service.ts +++ b/src/frontend/app/ui/gallery/cache.gallery.service.ts @@ -8,7 +8,7 @@ import {GroupingMethod, SortingMethod} from '../../../../common/entities/Sorting import {VersionService} from '../../model/version.service'; import {SearchQueryDTO, SearchQueryTypes,} from '../../../../common/entities/SearchQueryDTO'; import {ContentWrapper} from '../../../../common/entities/ConentWrapper'; -import {ContentWrapperWithError} from './content.service'; +import {ContentWrapperWithError} from './contentLoader.service'; import {ThemeModes} from '../../../../common/config/public/ClientConfig'; interface CacheItem { diff --git a/src/frontend/app/ui/gallery/content.service.ts b/src/frontend/app/ui/gallery/content.service.ts index bd9ca59a..e6680e06 100644 --- a/src/frontend/app/ui/gallery/content.service.ts +++ b/src/frontend/app/ui/gallery/content.service.ts @@ -1,148 +1,35 @@ import {Injectable} from '@angular/core'; import {NetworkService} from '../../model/network/network.service'; import {ContentWrapper} from '../../../../common/entities/ConentWrapper'; -import { - ParentDirectoryDTO, - SubDirectoryDTO, -} from '../../../../common/entities/DirectoryDTO'; +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 {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO'; import {ErrorCodes} from '../../../../common/entities/Error'; import {map} from 'rxjs/operators'; import {MediaDTO} from '../../../../common/entities/MediaDTO'; 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() export class ContentService { - public content: BehaviorSubject; - public directoryContent: Observable; - lastRequest: { directory: string } = { - directory: null, - }; - private lastDirectory: ParentDirectoryDTO; - private searchId: any; - private ongoingSearch: string = null; + public sortedFilteredContent: Observable; constructor( - private networkService: NetworkService, - private galleryCacheService: GalleryCacheService, - private shareService: ShareService, - private navigationService: NavigationService + private contentLoaderService: ContentLoaderService, + private sortingService: GallerySortingService, + private filterService: FilterService ) { - this.content = new BehaviorSubject( - new ContentWrapperWithError() - ); - 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 { - - // 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( - '/gallery/content/' + encodeURIComponent(directoryName), - params + this.sortedFilteredContent = this.sortingService + .applySorting( + this.filterService.applyFilters(this.contentLoaderService.originalContent) ); - 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 { - 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('/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[]; } diff --git a/src/frontend/app/ui/gallery/contentLoader.service.ts b/src/frontend/app/ui/gallery/contentLoader.service.ts new file mode 100644 index 00000000..9aaa9c5b --- /dev/null +++ b/src/frontend/app/ui/gallery/contentLoader.service.ts @@ -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; + public originalContent: Observable; + public sortedFilteredContent: Observable; + 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( + 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 { + + // 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( + '/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 { + 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('/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[]; +} diff --git a/src/frontend/app/ui/gallery/filter/filter.service.ts b/src/frontend/app/ui/gallery/filter/filter.service.ts index 26924daa..2bbfec9f 100644 --- a/src/frontend/app/ui/gallery/filter/filter.service.ts +++ b/src/frontend/app/ui/gallery/filter/filter.service.ts @@ -1,7 +1,7 @@ import {Injectable} from '@angular/core'; import {BehaviorSubject, Observable} from 'rxjs'; import {PhotoDTO} from '../../../../../common/entities/PhotoDTO'; -import {DirectoryContent} from '../content.service'; +import {DirectoryContent} from '../contentLoader.service'; import {map, switchMap} from 'rxjs/operators'; export enum FilterRenderType { diff --git a/src/frontend/app/ui/gallery/gallery.component.css b/src/frontend/app/ui/gallery/gallery.component.css index f7bbb9b7..13cb8c1b 100644 --- a/src/frontend/app/ui/gallery/gallery.component.css +++ b/src/frontend/app/ui/gallery/gallery.component.css @@ -3,15 +3,6 @@ padding: 0; } -.blog-wrapper { - opacity: 0.8; - display: flex; - position: relative; -} - -.blog-wrapper:hover { - opacity: 1; -} .blog-map-row { width: 100%; @@ -22,18 +13,6 @@ 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 { float: left; diff --git a/src/frontend/app/ui/gallery/gallery.component.html b/src/frontend/app/ui/gallery/gallery.component.html index e638f645..7ac74036 100644 --- a/src/frontend/app/ui/gallery/gallery.component.html +++ b/src/frontend/app/ui/gallery/gallery.component.html @@ -40,17 +40,10 @@ [directories]="directoryContent?.directories || []">
-
- - - - -
+ ; private $counter: Observable; private subscription: { [key: string]: Subscription } = { content: null, @@ -53,24 +53,25 @@ export class GalleryComponent implements OnInit, OnDestroy { }; constructor( - public galleryService: ContentService, - private authService: AuthenticationService, - private router: Router, - private shareService: ShareService, - private route: ActivatedRoute, - private navigation: NavigationService, - private filterService: FilterService, - private sortingService: GallerySortingService, - private piTitleService: PiTitleService, - private gpxFilesFilterPipe: GPXFilesFilterPipe, - private mdFilesFilterPipe: MDFilesFilterPipe, + public contentLoader: ContentLoaderService, + public galleryService: ContentService, + private authService: AuthenticationService, + private router: Router, + private shareService: ShareService, + private route: ActivatedRoute, + private navigation: NavigationService, + private filterService: FilterService, + private sortingService: GallerySortingService, + private piTitleService: PiTitleService, + private gpxFilesFilterPipe: GPXFilesFilterPipe, + private mdFilesFilterPipe: MDFilesFilterPipe, ) { this.mapEnabled = Config.Map.enabled; PageHelper.showScrollY(); } get ContentWrapper(): ContentWrapperWithError { - return this.galleryService.content.value; + return this.contentLoader.content.value; } 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 ( - (this.shareService.sharingSubject.value.expires - Date.now()) / - 1000 / - 86400 / - 365 > - 10 + (this.shareService.sharingSubject.value.expires - Date.now()) / + 1000 / + 86400 / + 365 > + 10 ) { return; } t = Math.floor( - (this.shareService.sharingSubject.value.expires - Date.now()) / 1000 + (this.shareService.sharingSubject.value.expires - Date.now()) / 1000 ); this.countDown = {} as any; this.countDown.day = Math.floor(t / 86400); @@ -119,33 +120,30 @@ export class GalleryComponent implements OnInit, OnDestroy { async ngOnInit(): Promise { await this.shareService.wait(); if ( - !this.authService.isAuthenticated() && - (!this.shareService.isSharing() || - (this.shareService.isSharing() && - Config.Sharing.passwordProtected === true)) + !this.authService.isAuthenticated() && + (!this.shareService.isSharing() || + (this.shareService.isSharing() && + Config.Sharing.passwordProtected === true)) ) { return this.navigation.toLogin(); } this.showSearchBar = this.authService.canSearch(); this.showShare = - Config.Sharing.enabled && - this.authService.isAuthorized(UserRoles.User); + Config.Sharing.enabled && + this.authService.isAuthorized(UserRoles.User); this.showRandomPhotoBuilder = - Config.RandomPhoto.enabled && - this.authService.isAuthorized(UserRoles.User); - this.subscription.content = this.sortingService - .applySorting( - this.filterService.applyFilters(this.galleryService.directoryContent) - ) - .subscribe((dc: GroupedDirectoryContent) => { - this.onContentChange(dc); - }); + Config.RandomPhoto.enabled && + this.authService.isAuthorized(UserRoles.User); + this.subscription.content = this.galleryService.sortedFilteredContent + .subscribe((dc: GroupedDirectoryContent) => { + this.onContentChange(dc); + }); this.subscription.route = this.route.params.subscribe(this.onRoute); if (this.shareService.isSharing()) { this.$counter = interval(1000); this.subscription.timer = this.$counter.subscribe((x): void => - this.updateTimer(x) + this.updateTimer(x) ); } } @@ -153,24 +151,24 @@ export class GalleryComponent implements OnInit, OnDestroy { private onRoute = async (params: Params): Promise => { const searchQuery = params[QueryParams.gallery.search.query]; if (searchQuery) { - this.galleryService.search(searchQuery).catch(console.error); + this.contentLoader.search(searchQuery).catch(console.error); this.piTitleService.setSearchTitle(searchQuery); return; } if ( - params[QueryParams.gallery.sharingKey_params] && - params[QueryParams.gallery.sharingKey_params] !== '' + params[QueryParams.gallery.sharingKey_params] && + params[QueryParams.gallery.sharingKey_params] !== '' ) { const sharing = await this.shareService.currentSharing - .pipe(take(1)) - .toPromise(); + .pipe(take(1)) + .toPromise(); const qParams: { [key: string]: any } = {}; qParams[QueryParams.gallery.sharingKey_query] = - this.shareService.getSharingKey(); + this.shareService.getSharingKey(); this.router - .navigate(['/gallery', sharing.path], {queryParams: qParams}) - .catch(console.error); + .navigate(['/gallery', sharing.path], {queryParams: qParams}) + .catch(console.error); return; } @@ -178,7 +176,7 @@ export class GalleryComponent implements OnInit, OnDestroy { directoryName = directoryName || ''; this.piTitleService.setDirectoryTitle(directoryName); - this.galleryService.loadDirectory(directoryName); + this.contentLoader.loadDirectory(directoryName); }; private onContentChange = (content: GroupedDirectoryContent): void => { @@ -194,8 +192,8 @@ export class GalleryComponent implements OnInit, OnDestroy { for (const mediaGroup of content.mediaGroups) { if ( - mediaGroup.media - .findIndex((m: PhotoDTO) => !!m.metadata?.positionData?.GPSData?.longitude) !== -1 + mediaGroup.media + .findIndex((m: PhotoDTO) => !!m.metadata?.positionData?.GPSData?.longitude) !== -1 ) { this.isPhotoWithLocation = true; break; diff --git a/src/frontend/app/ui/gallery/grid/grid.gallery.component.html b/src/frontend/app/ui/gallery/grid/grid.gallery.component.html index 3cd486f2..718b991b 100644 --- a/src/frontend/app/ui/gallery/grid/grid.gallery.component.html +++ b/src/frontend/app/ui/gallery/grid/grid.gallery.component.html @@ -2,13 +2,24 @@ - -
- -
-
{{group.name}}
-
{{group.name}}
+ +
+
+ +
+
+
+
{{group.name}} + +
+
+
{{group.name}}
+
+ + +
= 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(); @@ -453,4 +455,5 @@ export class GalleryGridComponent interface GridMediaGroup { media: GridMedia[]; name: string; + date?: Date; } diff --git a/src/frontend/app/ui/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.html b/src/frontend/app/ui/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.html index 1a68b44a..02a1fcd1 100644 --- a/src/frontend/app/ui/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.html +++ b/src/frontend/app/ui/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.html @@ -4,7 +4,7 @@
-
+
diff --git a/src/frontend/app/ui/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.ts b/src/frontend/app/ui/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.ts index d628a87c..0e898ec7 100644 --- a/src/frontend/app/ui/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.ts +++ b/src/frontend/app/ui/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.ts @@ -11,6 +11,7 @@ import {AuthenticationService} from '../../../../model/network/authentication.se import {LatLngLiteral, marker, Marker, TileLayer, tileLayer} from 'leaflet'; import {ContentService} from '../../content.service'; import {ThemeService} from '../../../../model/theme.service'; +import { ContentLoaderService } from '../../contentLoader.service'; @Component({ selector: 'app-info-panel', @@ -31,7 +32,7 @@ export class InfoPanelLightboxComponent implements OnInit, OnChanges { constructor( public queryService: QueryService, - public galleryService: ContentService, + public contentLoaderService: ContentLoaderService, public mapService: MapService, private authService: AuthenticationService, private themeService: ThemeService diff --git a/src/frontend/app/ui/gallery/navigator/navigator.gallery.component.html b/src/frontend/app/ui/gallery/navigator/navigator.gallery.component.html index ff07397d..bfea1046 100644 --- a/src/frontend/app/ui/gallery/navigator/navigator.gallery.component.html +++ b/src/frontend/app/ui/gallery/navigator/navigator.gallery.component.html @@ -13,7 +13,7 @@ diff --git a/src/frontend/app/ui/gallery/navigator/navigator.gallery.component.ts b/src/frontend/app/ui/gallery/navigator/navigator.gallery.component.ts index c795ab71..e8124aae 100644 --- a/src/frontend/app/ui/gallery/navigator/navigator.gallery.component.ts +++ b/src/frontend/app/ui/gallery/navigator/navigator.gallery.component.ts @@ -4,7 +4,6 @@ import {DomSanitizer} from '@angular/platform-browser'; import {UserDTOUtils} from '../../../../../common/entities/UserDTO'; import {AuthenticationService} from '../../../model/network/authentication.service'; import {QueryService} from '../../../model/query.service'; -import {ContentService, ContentWrapperWithError, DirectoryContent,} from '../content.service'; import {Utils} from '../../../../../common/Utils'; import {GroupByTypes, GroupingMethod, SortByDirectionalTypes, SortByTypes} from '../../../../../common/entities/SortingMethods'; import {Config} from '../../../../../common/config/public/Config'; @@ -15,6 +14,7 @@ import {GallerySortingService} from './sorting.service'; import {PageHelper} from '../../../model/page.helper'; import {BsDropdownDirective} from 'ngx-bootstrap/dropdown'; import {FilterService} from '../filter/filter.service'; +import {ContentLoaderService, ContentWrapperWithError, DirectoryContent} from '../contentLoader.service'; @Component({ selector: 'app-gallery-navbar', @@ -47,7 +47,7 @@ export class GalleryNavigatorComponent { constructor( public authService: AuthenticationService, public queryService: QueryService, - public galleryService: ContentService, + public contentLoaderService: ContentLoaderService, public filterService: FilterService, public sortingService: GallerySortingService, private router: Router, @@ -57,11 +57,11 @@ export class GalleryNavigatorComponent { // can't group by random this.groupingByTypes = Utils.enumToArray(GroupByTypes); this.RootFolderName = $localize`Home`; - this.wrappedContent = this.galleryService.content; + this.wrappedContent = this.contentLoaderService.content; this.directoryContent = this.wrappedContent.pipe( map((c) => (c.directory ? c.directory : c.searchResult)) ); - this.routes = this.galleryService.content.pipe( + this.routes = this.contentLoaderService.content.pipe( map((c) => { this.parentPath = null; if (!c.directory) { @@ -124,15 +124,15 @@ export class GalleryNavigatorComponent { } get isDirectory(): boolean { - return !!this.galleryService.content.value.directory; + return !!this.contentLoaderService.content.value.directory; } get isSearch(): boolean { - return !!this.galleryService.content.value.searchResult; + return !!this.contentLoaderService.content.value.searchResult; } get ItemCount(): number { - const c = this.galleryService.content.value; + const c = this.contentLoaderService.content.value; return c.directory ? c.directory.mediaCount : c.searchResult @@ -142,7 +142,7 @@ export class GalleryNavigatorComponent { isDefaultSortingAndGrouping(): boolean { return this.sortingService.isDefaultSortingAndGrouping( - this.galleryService.content.value + this.contentLoaderService.content.value ); } @@ -193,7 +193,7 @@ export class GalleryNavigatorComponent { getDownloadZipLink(): string { - const c = this.galleryService.content.value; + const c = this.contentLoaderService.content.value; if (!c.directory) { return null; } @@ -212,7 +212,7 @@ export class GalleryNavigatorComponent { } getDirectoryFlattenSearchQuery(): string { - const c = this.galleryService.content.value; + const c = this.contentLoaderService.content.value; if (!c.directory) { return null; } diff --git a/src/frontend/app/ui/gallery/navigator/sorting.service.ts b/src/frontend/app/ui/gallery/navigator/sorting.service.ts index e89182bb..fa1a00ec 100644 --- a/src/frontend/app/ui/gallery/navigator/sorting.service.ts +++ b/src/frontend/app/ui/gallery/navigator/sorting.service.ts @@ -4,9 +4,8 @@ import {NetworkService} from '../../../model/network/network.service'; import {GalleryCacheService} from '../cache.gallery.service'; import {BehaviorSubject, Observable} from 'rxjs'; 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 {ContentService, DirectoryContent} from '../content.service'; import {PhotoDTO} from '../../../../../common/entities/PhotoDTO'; import {map, switchMap} from 'rxjs/operators'; import {SeededRandomService} from '../../../model/seededRandom.service'; @@ -14,6 +13,8 @@ import {ContentWrapper} from '../../../../../common/entities/ConentWrapper'; import {SubDirectoryDTO} from '../../../../../common/entities/DirectoryDTO'; import {MediaDTO} from '../../../../../common/entities/MediaDTO'; import {FileDTO} from '../../../../../common/entities/FileDTO'; +import {Utils} from '../../../../../common/Utils'; +import {ContentLoaderService, DirectoryContent} from '../contentLoader.service'; @Injectable() export class GallerySortingService { @@ -24,7 +25,7 @@ export class GallerySortingService { constructor( private networkService: NetworkService, private galleryCacheService: GalleryCacheService, - private galleryService: ContentService, + private galleryService: ContentLoaderService, private rndService: SeededRandomService, private datePipe: DatePipe ) { @@ -176,6 +177,38 @@ export class GallerySortingService { 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( directoryContent: Observable ): Observable { @@ -243,36 +276,10 @@ export class GallerySortingService { if (dirContent.media) { const mCopy = dirContent.media; this.sortMedia(grouping, mCopy); - let groupFN = (m: MediaDTO) => ''; - 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; - } + const groupFN = this.getGroupByNameFn(grouping); + c.mediaGroups = []; + for (const m of mCopy) { const k = groupFN(m); 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; + } + + 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 @@ -300,6 +313,7 @@ export class GallerySortingService { export interface MediaGroup { name: string; + date?: Date; // used for blog. It allows to chop off blog to smaller pieces media: MediaDTO[]; } diff --git a/src/frontend/app/ui/gallery/random-query-builder/random-query-builder.gallery.component.ts b/src/frontend/app/ui/gallery/random-query-builder/random-query-builder.gallery.component.ts index e6a99add..753c3515 100644 --- a/src/frontend/app/ui/gallery/random-query-builder/random-query-builder.gallery.component.ts +++ b/src/frontend/app/ui/gallery/random-query-builder/random-query-builder.gallery.component.ts @@ -15,6 +15,7 @@ import { import { ActivatedRoute, Params } from '@angular/router'; import { QueryParams } from '../../../../../common/QueryParams'; import { SearchQueryParserService } from '../search/search-query-parser.service'; +import {ContentLoaderService} from '../contentLoader.service'; @Component({ selector: 'app-gallery-random-query-builder', @@ -36,7 +37,7 @@ export class RandomQueryBuilderGalleryComponent implements OnInit, OnDestroy { private readonly subscription: Subscription = null; constructor( - public galleryService: ContentService, + public contentLoaderService: ContentLoaderService, private notification: NotificationService, private searchQueryParserService: SearchQueryParserService, private route: ActivatedRoute, @@ -65,7 +66,7 @@ export class RandomQueryBuilderGalleryComponent implements OnInit, OnDestroy { } ngOnInit(): void { - this.contentSubscription = this.galleryService.content.subscribe( + this.contentSubscription = this.contentLoaderService.content.subscribe( (content: ContentWrapper) => { this.enabled = !!content.directory; if (!this.enabled) { diff --git a/src/frontend/app/ui/gallery/share/share.gallery.component.ts b/src/frontend/app/ui/gallery/share/share.gallery.component.ts index 89f874ed..374f213c 100644 --- a/src/frontend/app/ui/gallery/share/share.gallery.component.ts +++ b/src/frontend/app/ui/gallery/share/share.gallery.component.ts @@ -12,6 +12,7 @@ import {Subscription} from 'rxjs'; import {UserRoles} from '../../../../../common/entities/UserDTO'; import {AuthenticationService} from '../../../model/network/authentication.service'; import {ClipboardService} from 'ngx-clipboard'; +import {ContentLoaderService} from '../contentLoader.service'; @Component({ selector: 'app-gallery-share', @@ -51,7 +52,7 @@ export class GalleryShareComponent implements OnInit, OnDestroy { constructor( public sharingService: ShareService, - public galleryService: ContentService, + public galleryService: ContentLoaderService, private notification: NotificationService, private modalService: BsModalService, public authService: AuthenticationService,