139 lines
4.3 KiB
TypeScript
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;
|
|
}
|
|
}
|