ObsiViewer/src/services/markdown-calendar.service.ts

139 lines
4.3 KiB
TypeScript

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of, shareReplay, finalize, map } from 'rxjs';
import { FileMetadata } from '../types';
@Injectable({
providedIn: 'root',
})
export class MarkdownCalendarService {
private readonly http = inject(HttpClient);
private metadataCache: FileMetadata[] | null = null;
private metadataRequest$: Observable<FileMetadata[]> | null = null;
private readonly parsedDateCache = new Map<string, number | null>();
getFilesMetadata(forceRefresh = false): Observable<FileMetadata[]> {
if (!forceRefresh && this.metadataCache) {
return of(this.metadataCache);
}
if (!forceRefresh && this.metadataRequest$) {
return this.metadataRequest$;
}
const request$ = this.http
.get<FileMetadata[]>('/api/files/metadata')
.pipe(
map((metadata) => {
this.metadataCache = metadata;
this.metadataRequest$ = null;
this.parsedDateCache.clear();
return metadata;
}),
finalize(() => {
this.metadataRequest$ = null;
}),
shareReplay({ bufferSize: 1, refCount: true })
);
this.metadataRequest$ = request$;
return request$;
}
searchFilesByDate(date: Date): Observable<FileMetadata[]> {
const start = this.startOfDay(date);
const end = this.endOfDay(date);
return this.getFilesMetadata().pipe(map((metadata) => this.filterByRange(metadata, start, end)));
}
searchFilesByDateRange(startDate: Date, endDate: Date): Observable<FileMetadata[]> {
const rangeStart = this.startOfDay(startDate);
const rangeEnd = this.endOfDay(endDate);
return this.getFilesMetadata().pipe(map((metadata) => this.filterByRange(metadata, rangeStart, rangeEnd)));
}
groupMetadataByDay(metadata: FileMetadata[]): Map<string, { created: FileMetadata[]; updated: FileMetadata[] }> {
const map = new Map<string, { created: FileMetadata[]; updated: FileMetadata[] }>();
for (const file of metadata) {
const createdKey = this.normalizeDateKey(file.createdAt);
const updatedKey = this.normalizeDateKey(file.updatedAt);
if (createdKey) {
const bucket = map.get(createdKey) ?? { created: [], updated: [] };
bucket.created = this.appendUnique(bucket.created, file);
map.set(createdKey, bucket);
}
if (updatedKey) {
const bucket = map.get(updatedKey) ?? { created: [], updated: [] };
bucket.updated = this.appendUnique(bucket.updated, file);
map.set(updatedKey, bucket);
}
}
return map;
}
private normalizeDateKey(value: string | undefined | null): string | null {
if (!value) {
return null;
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return null;
}
date.setHours(0, 0, 0, 0);
return date.toDateString();
}
private appendUnique(list: FileMetadata[], file: FileMetadata): FileMetadata[] {
return list.some((item) => item.id === file.id) ? list : list.concat(file);
}
private filterByRange(metadata: FileMetadata[], start: Date, end: Date): FileMetadata[] {
const startTime = start.getTime();
const endTime = end.getTime();
return metadata.filter((file) => this.matchesRange(file, startTime, endTime));
}
private matchesRange(file: FileMetadata, startTime: number, endTime: number): boolean {
const created = this.getTimeValue(file.createdAt);
if (created !== null && created >= startTime && created <= endTime) {
return true;
}
const updated = this.getTimeValue(file.updatedAt);
return updated !== null && updated >= startTime && updated <= endTime;
}
private getTimeValue(value: string | undefined | null): number | null {
if (!value) {
return null;
}
if (this.parsedDateCache.has(value)) {
return this.parsedDateCache.get(value) ?? null;
}
const time = Date.parse(value);
const normalized = Number.isNaN(time) ? null : time;
this.parsedDateCache.set(value, normalized);
return normalized;
}
private startOfDay(date: Date): Date {
const start = new Date(date);
start.setHours(0, 0, 0, 0);
return start;
}
private endOfDay(date: Date): Date {
const end = new Date(date);
end.setHours(23, 59, 59, 999);
return end;
}
}