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

Merge remote-tracking branch 'origin/master'

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

View File

@ -90,6 +90,26 @@ export class Utils {
return true;
}
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

@ -3,19 +3,17 @@ 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 {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">
<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]="collapsed ? '77px':''">
<ng-container *ngFor="let md of markdowns; let i = index">
<div class="card-body" style="min-height: 77px" [style.height]="!open ? '77px':''">
<ng-container *ngFor="let md of markdowns; let last = last">
<markdown
*ngIf="!collapsed"
[data]="md">
*ngIf="open"
[data]="md.text">
</markdown>
<span *ngIf="!open" class="text-preview">
<markdown
[inline]="true"
[data]="md.text">
</markdown>
<span *ngIf="collapsed" class="text-preview">
{{md}}
</span>
<hr *ngIf="i != markdowns.length-1">
<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>
</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 {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

@ -2,12 +2,93 @@ 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"
<app-gallery-blog
[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>
*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,6 +53,7 @@ export class GalleryComponent implements OnInit, OnDestroy {
};
constructor(
public contentLoader: ContentLoaderService,
public galleryService: ContentService,
private authService: AuthenticationService,
private router: Router,
@ -70,7 +71,7 @@ export class GalleryComponent implements OnInit, OnDestroy {
}
get ContentWrapper(): ContentWrapperWithError {
return this.galleryService.content.value;
return this.contentLoader.content.value;
}
updateTimer(t: number): void {
@ -133,10 +134,7 @@ export class GalleryComponent implements OnInit, OnDestroy {
this.showRandomPhotoBuilder =
Config.RandomPhoto.enabled &&
this.authService.isAuthorized(UserRoles.User);
this.subscription.content = this.sortingService
.applySorting(
this.filterService.applyFilters(this.galleryService.directoryContent)
)
this.subscription.content = this.galleryService.sortedFilteredContent
.subscribe((dc: GroupedDirectoryContent) => {
this.onContentChange(dc);
});
@ -153,7 +151,7 @@ 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;
}
@ -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 => {

View File

@ -3,13 +3,24 @@
<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 *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
*ngFor="let gridPhoto of group.media"

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
) {
@ -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]; // 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,