1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2025-01-24 05:17:16 +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
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;
}
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;

View File

@ -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,

View File

@ -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()) {

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 {
opacity: 0.8;
position: relative;
}
.blog:hover {
opacity: 1;
}
.card-body {

View File

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

View File

@ -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<boolean>();
public markdowns: string[] = [];
mkObservable: Observable<GroupedMarkdown[]>;
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<void> {
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);
}
}

View File

@ -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> | 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> {
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 {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<T> {

View File

@ -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<ContentWrapperWithError>;
public directoryContent: Observable<DirectoryContent>;
lastRequest: { directory: string } = {
directory: null,
};
private lastDirectory: ParentDirectoryDTO;
private searchId: any;
private ongoingSearch: string = null;
public sortedFilteredContent: Observable<GroupedDirectoryContent>;
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<ContentWrapperWithError>(
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<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
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<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 {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 {

View File

@ -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;

View File

@ -40,17 +40,10 @@
[directories]="directoryContent?.directories || []"></app-gallery-directories>
<div class="blog-map-row" *ngIf="ShowMarkDown || ShowMap">
<div class="blog-wrapper"
[style.width]="blogOpen ? '100%' : 'calc(100% - 100px)'"
*ngIf="ShowMarkDown">
<app-gallery-blog [collapsed]="!blogOpen"
[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-blog
[style.width]="blogOpen ? '100%' : 'calc(100% - 100px)'"
*ngIf="ShowMarkDown"
[(open)]="blogOpen"></app-gallery-blog>
<app-gallery-map
class="rounded"
[class.rounded-start-0]="ShowMarkDown"

View File

@ -1,7 +1,7 @@
import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {AuthenticationService} from '../../model/network/authentication.service';
import {ActivatedRoute, Params, Router} from '@angular/router';
import {ContentService, ContentWrapperWithError,} from './content.service';
import {ContentService} from './content.service';
import {GalleryGridComponent} from './grid/grid.gallery.component';
import {Config} from '../../../../common/config/public/Config';
import {ShareService} from './share.service';
@ -18,6 +18,7 @@ import {FilterService} from './filter/filter.service';
import {PiTitleService} from '../../model/pi-title.service';
import {GPXFilesFilterPipe} from '../../pipes/GPXFilesFilterPipe';
import {MDFilesFilterPipe} from '../../pipes/MDFilesFilterPipe';
import { ContentLoaderService,ContentWrapperWithError } from './contentLoader.service';
@Component({
selector: 'app-gallery',
@ -43,7 +44,6 @@ export class GalleryComponent implements OnInit, OnDestroy {
} = null;
public readonly mapEnabled: boolean;
public directoryContent: GroupedDirectoryContent;
public readonly mediaObs: Observable<MediaDTO[]>;
private $counter: Observable<number>;
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<boolean> {
await this.shareService.wait();
if (
!this.authService.isAuthenticated() &&
(!this.shareService.isSharing() ||
(this.shareService.isSharing() &&
Config.Sharing.passwordProtected === true))
!this.authService.isAuthenticated() &&
(!this.shareService.isSharing() ||
(this.shareService.isSharing() &&
Config.Sharing.passwordProtected === true))
) {
return this.navigation.toLogin();
}
this.showSearchBar = this.authService.canSearch();
this.showShare =
Config.Sharing.enabled &&
this.authService.isAuthorized(UserRoles.User);
Config.Sharing.enabled &&
this.authService.isAuthorized(UserRoles.User);
this.showRandomPhotoBuilder =
Config.RandomPhoto.enabled &&
this.authService.isAuthorized(UserRoles.User);
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<void> => {
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;

View File

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

View File

@ -22,7 +22,6 @@ import {PageHelper} from '../../../model/page.helper';
import {Subscription} from 'rxjs';
import {ActivatedRoute, Params, Router} from '@angular/router';
import {QueryService} from '../../../model/query.service';
import {ContentService} from '../content.service';
import {MediaDTO, MediaDTOUtils,} from '../../../../../common/entities/MediaDTO';
import {QueryParams} from '../../../../../common/QueryParams';
import {GallerySortingService, MediaGroup} from '../navigator/sorting.service';
@ -65,7 +64,6 @@ export class GalleryGridComponent
private changeDetector: ChangeDetectorRef,
public queryService: QueryService,
private router: Router,
public galleryService: ContentService,
public sortingService: GallerySortingService,
private route: ActivatedRoute
) {
@ -191,7 +189,7 @@ export class GalleryGridComponent
for (; i < this.mediaGroups.length && i < this.mediaToRender.length; ++i) {
if (diffFound) {
if (diffFound) {
break;
}
@ -259,7 +257,11 @@ export class GalleryGridComponent
if (this.mediaToRender.length == 0 ||
this.mediaToRender[this.mediaToRender.length - 1].media.length >=
this.mediaGroups[this.mediaToRender.length - 1].media.length) {
this.mediaToRender.push({name: this.mediaGroups[this.mediaToRender.length].name, media: []});
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;
}

View File

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

View File

@ -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

View File

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

View File

@ -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;
}

View File

@ -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<DirectoryContent>
): Observable<GroupedDirectoryContent> {
@ -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[];
}

View File

@ -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) {

View File

@ -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,