From 5a852dc4431d4a1228c3fc0433963923a01a0580 Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Sun, 3 Sep 2023 15:20:24 +0200 Subject: [PATCH 1/3] Add more file sizes for groupping #706 --- src/frontend/app/ui/gallery/navigator/sorting.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/app/ui/gallery/navigator/sorting.service.ts b/src/frontend/app/ui/gallery/navigator/sorting.service.ts index d72f994e..e89182bb 100644 --- a/src/frontend/app/ui/gallery/navigator/sorting.service.ts +++ b/src/frontend/app/ui/gallery/navigator/sorting.service.ts @@ -255,7 +255,7 @@ export class GallerySortingService { groupFN = (m: MediaDTO) => ((m as PhotoDTO).metadata.rating || 0).toString(); break; case SortByTypes.FileSize: { - const groups = [0.5, 1, 2, 5, 10, 15, 20, 30, 50]; // MBs + 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); From ed56de452315fd1533226d70544cde07bfdace05 Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Sun, 3 Sep 2023 18:35:57 +0200 Subject: [PATCH 2/3] 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, From 1055ec19d49fa15a15d1df80983027c00ad9f8f8 Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Sun, 3 Sep 2023 18:36:19 +0200 Subject: [PATCH 3/3] typo --- demo/images/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/images/index.md b/demo/images/index.md index 585d0cba..4bee4d4d 100644 --- a/demo/images/index.md +++ b/demo/images/index.md @@ -87,7 +87,7 @@ Start numbering with offset: ## 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. +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.