chore: update Angular build cache and TypeScript definitions
This commit is contained in:
parent
53bef252d9
commit
f68440656e
2
.angular/cache/20.3.2/app/.tsbuildinfo
vendored
2
.angular/cache/20.3.2/app/.tsbuildinfo
vendored
File diff suppressed because one or more lines are too long
@ -20,6 +20,7 @@
|
|||||||
"angular-calendar": "^0.32.0",
|
"angular-calendar": "^0.32.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"chokidar": "^4.0.3",
|
||||||
"highlight.js": "^11.10.0",
|
"highlight.js": "^11.10.0",
|
||||||
"mermaid": "^11.12.0",
|
"mermaid": "^11.12.0",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
|
@ -4,6 +4,7 @@ import express from 'express';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import chokidar from 'chokidar';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
@ -15,6 +16,43 @@ const rootDir = path.resolve(__dirname, '..');
|
|||||||
const distDir = path.join(rootDir, 'dist');
|
const distDir = path.join(rootDir, 'dist');
|
||||||
const vaultDir = path.join(rootDir, 'vault');
|
const vaultDir = path.join(rootDir, 'vault');
|
||||||
|
|
||||||
|
const vaultEventClients = new Set();
|
||||||
|
|
||||||
|
const registerVaultEventClient = (res) => {
|
||||||
|
const heartbeat = setInterval(() => {
|
||||||
|
try {
|
||||||
|
res.write(':keepalive\n\n');
|
||||||
|
} catch {
|
||||||
|
// Write failures will be handled by the close handler.
|
||||||
|
}
|
||||||
|
}, 20000);
|
||||||
|
|
||||||
|
const client = { res, heartbeat };
|
||||||
|
vaultEventClients.add(client);
|
||||||
|
return client;
|
||||||
|
};
|
||||||
|
|
||||||
|
const unregisterVaultEventClient = (client) => {
|
||||||
|
clearInterval(client.heartbeat);
|
||||||
|
vaultEventClients.delete(client);
|
||||||
|
};
|
||||||
|
|
||||||
|
const broadcastVaultEvent = (payload) => {
|
||||||
|
if (!vaultEventClients.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = `data: ${JSON.stringify(payload)}\n\n`;
|
||||||
|
for (const client of [...vaultEventClients]) {
|
||||||
|
try {
|
||||||
|
client.res.write(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to notify vault event client:', error);
|
||||||
|
unregisterVaultEventClient(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const isMarkdownFile = (entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.md');
|
const isMarkdownFile = (entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.md');
|
||||||
|
|
||||||
const normalizeString = (value) => {
|
const normalizeString = (value) => {
|
||||||
@ -138,6 +176,44 @@ const isDateWithinRange = (target, start, end) => {
|
|||||||
return targetTime >= start.getTime() && targetTime <= end.getTime();
|
return targetTime >= start.getTime() && targetTime <= end.getTime();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const vaultWatcher = chokidar.watch(vaultDir, {
|
||||||
|
persistent: true,
|
||||||
|
ignoreInitial: true,
|
||||||
|
awaitWriteFinish: {
|
||||||
|
stabilityThreshold: 250,
|
||||||
|
pollInterval: 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const watchedVaultEvents = ['add', 'change', 'unlink', 'addDir', 'unlinkDir'];
|
||||||
|
|
||||||
|
watchedVaultEvents.forEach((eventName) => {
|
||||||
|
vaultWatcher.on(eventName, (changedPath) => {
|
||||||
|
const relativePath = path.relative(vaultDir, changedPath).replace(/\\/g, '/');
|
||||||
|
broadcastVaultEvent({
|
||||||
|
event: eventName,
|
||||||
|
path: relativePath,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
vaultWatcher.on('ready', () => {
|
||||||
|
broadcastVaultEvent({
|
||||||
|
event: 'ready',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
vaultWatcher.on('error', (error) => {
|
||||||
|
console.error('Vault watcher error:', error);
|
||||||
|
broadcastVaultEvent({
|
||||||
|
event: 'error',
|
||||||
|
message: typeof error?.message === 'string' ? error.message : 'Unknown watcher error',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Vérifier si le répertoire dist existe
|
// Vérifier si le répertoire dist existe
|
||||||
if (!fs.existsSync(distDir)) {
|
if (!fs.existsSync(distDir)) {
|
||||||
console.warn(`Warning: build directory not found at ${distDir}. Did you run \`npm run build\`?`);
|
console.warn(`Warning: build directory not found at ${distDir}. Did you run \`npm run build\`?`);
|
||||||
@ -151,6 +227,29 @@ app.get('/api/health', (req, res) => {
|
|||||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/api/vault/events', (req, res) => {
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
Connection: 'keep-alive',
|
||||||
|
'X-Accel-Buffering': 'no',
|
||||||
|
});
|
||||||
|
|
||||||
|
res.flushHeaders?.();
|
||||||
|
res.write(
|
||||||
|
`data: ${JSON.stringify({
|
||||||
|
event: 'connected',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
})}\n\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const client = registerVaultEventClient(res);
|
||||||
|
|
||||||
|
req.on('close', () => {
|
||||||
|
unregisterVaultEventClient(client);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// API endpoint pour les données de la voûte (contenu réel)
|
// API endpoint pour les données de la voûte (contenu réel)
|
||||||
app.get('/api/vault', (req, res) => {
|
app.get('/api/vault', (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
@ -681,54 +681,56 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:host ::ng-deep .md-heading-1 {
|
:host ::ng-deep .md-heading-1 {
|
||||||
font-size: clamp(2.8rem, 4vw, 3.5rem);
|
font-size: clamp(2.6rem, 3.6vw, 3.1rem);
|
||||||
margin-top: 3.5rem;
|
margin-top: 0.55rem;
|
||||||
margin-bottom: 1.4rem;
|
margin-bottom: 0.55rem;
|
||||||
border-bottom-width: 3px;
|
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host ::ng-deep .md-heading-2 {
|
:host ::ng-deep .md-heading-2 {
|
||||||
font-size: clamp(2.2rem, 3.2vw, 2.8rem);
|
font-size: clamp(2.1rem, 3vw, 2.5rem);
|
||||||
margin-top: 3rem;
|
margin-top: 0.35rem;
|
||||||
margin-bottom: 1.2rem;
|
margin-bottom: 0.35rem;
|
||||||
border-bottom-width: 2px;
|
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host ::ng-deep .md-heading-3 {
|
:host ::ng-deep .md-heading-3 {
|
||||||
font-size: clamp(1.8rem, 2.8vw, 2.2rem);
|
font-size: clamp(1.7rem, 2.6vw, 2rem);
|
||||||
margin-top: 2.6rem;
|
margin-top: 0.3rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 0.3rem;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host ::ng-deep .md-heading-4 {
|
:host ::ng-deep .md-heading-4 {
|
||||||
font-size: clamp(1.5rem, 2.2vw, 1.8rem);
|
font-size: clamp(1.45rem, 2.1vw, 1.7rem);
|
||||||
margin-top: 2.4rem;
|
margin-top: 0.25rem;
|
||||||
margin-bottom: 0.9rem;
|
margin-bottom: 0.25rem;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host ::ng-deep .md-heading-5 {
|
:host ::ng-deep .md-heading-5 {
|
||||||
font-size: clamp(1.3rem, 1.8vw, 1.4rem);
|
font-size: clamp(1.25rem, 1.7vw, 1.35rem);
|
||||||
margin-top: 2.2rem;
|
margin-top: 0.2rem;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.22rem;
|
||||||
font-weight: 600;
|
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host ::ng-deep .md-heading-6 {
|
:host ::ng-deep .md-heading-6 {
|
||||||
font-size: clamp(1.1rem, 1.6vw, 1.2rem);
|
font-size: clamp(1.1rem, 1.5vw, 1.2rem);
|
||||||
margin-top: 2rem;
|
margin-top: 0.2rem;
|
||||||
margin-bottom: 0.6rem;
|
margin-bottom: 0.2rem;
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.01em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .note-content-area p {
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
:host-context(.dark) ::ng-deep .md-heading-1 {
|
:host-context(.dark) ::ng-deep .md-heading-1 {
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
text-transform: uppercase;
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -740,6 +742,10 @@
|
|||||||
color: #5eead4;
|
color: #5eead4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .prose :where(p, li, blockquote):not(:where(.not-prose *)) {
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
:host ::ng-deep .metadata-panel {
|
:host ::ng-deep .metadata-panel {
|
||||||
margin-bottom: 2.2rem;
|
margin-bottom: 2.2rem;
|
||||||
padding: 0.2rem 0 0.2rem 0.8rem;
|
padding: 0.2rem 0 0.2rem 0.8rem;
|
||||||
|
@ -49,47 +49,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Navigation mobile -->
|
|
||||||
<nav class="sticky bottom-0 z-30 flex w-full items-center justify-around gap-2 border-t border-obs-l-border bg-obs-l-bg-main/95 px-2 py-2 backdrop-blur-xs dark:border-obs-d-border dark:bg-obs-d-bg-main/95 lg:hidden">
|
|
||||||
<button
|
|
||||||
(click)="setView('files'); toggleSidebarTo(true)"
|
|
||||||
class="flex flex-1 flex-col items-center gap-1 rounded-lg px-3 py-2 text-xs font-semibold text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary"
|
|
||||||
[class.text-obs-l-text-main]="activeView() === 'files'"
|
|
||||||
[class.dark:text-obs-d-text-main]="activeView() === 'files'"
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg>
|
|
||||||
Fichiers
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
(click)="setView('search'); toggleSidebarTo(true)"
|
|
||||||
class="flex flex-1 flex-col items-center gap-1 rounded-lg px-3 py-2 text-xs font-semibold text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary"
|
|
||||||
[class.text-obs-l-text-main]="activeView() === 'search'"
|
|
||||||
[class.dark:text-obs-d-text-main]="activeView() === 'search'"
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
|
||||||
Recherche
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
(click)="setView('tags'); toggleSidebarTo(true)"
|
|
||||||
class="flex flex-1 flex-col items-center gap-1 rounded-lg px-3 py-2 text-xs font-semibold text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary"
|
|
||||||
[class.text-obs-l-text-main]="activeView() === 'tags'"
|
|
||||||
[class.dark:text-obs-d-text-main]="activeView() === 'tags'"
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-5 5a2 2 0 01-2.828 0l-7-7A2 2 0 013 8V3z" /></svg>
|
|
||||||
Tags
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
(click)="setView('calendar'); toggleSidebarTo(true)"
|
|
||||||
class="flex flex-1 flex-col items-center gap-1 rounded-lg px-3 py-2 text-xs font-semibold text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary"
|
|
||||||
[class.text-obs-l-text-main]="activeView() === 'calendar'"
|
|
||||||
[class.dark:text-obs-d-text-main]="activeView() === 'calendar'"
|
|
||||||
[attr.aria-pressed]="activeView() === 'calendar'"
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2h-1.5a1.5 1.5 0 01-3 0h-5a1.5 1.5 0 01-3 0H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
|
||||||
Agenda
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
@if (isDesktop() || isSidebarOpen()) {
|
@if (isDesktop() || isSidebarOpen()) {
|
||||||
<aside
|
<aside
|
||||||
class="fixed inset-y-0 left-0 z-40 flex h-screen w-[min(320px,85vw)] -translate-x-full transform flex-col border-r border-obs-l-border bg-obs-l-bg-secondary shadow-xl transition-transform duration-200 ease-in-out dark:border-obs-d-border dark:bg-obs-d-bg-secondary lg:static lg:h-auto lg:w-auto lg:translate-x-0 lg:shadow-none"
|
class="fixed inset-y-0 left-0 z-40 flex h-screen w-[min(320px,85vw)] -translate-x-full transform flex-col border-r border-obs-l-border bg-obs-l-bg-secondary shadow-xl transition-transform duration-200 ease-in-out dark:border-obs-d-border dark:bg-obs-d-bg-secondary lg:static lg:h-auto lg:w-auto lg:translate-x-0 lg:shadow-none"
|
||||||
|
@ -390,6 +390,10 @@ export class AppComponent implements OnDestroy {
|
|||||||
|
|
||||||
this.vaultService.ensureFolderOpen(note.originalPath);
|
this.vaultService.ensureFolderOpen(note.originalPath);
|
||||||
this.selectedNoteId.set(note.id);
|
this.selectedNoteId.set(note.id);
|
||||||
|
|
||||||
|
if (!this.isDesktopView() && this.activeView() === 'search') {
|
||||||
|
this.isSidebarOpen.set(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTagClick(tagName: string): void {
|
handleTagClick(tagName: string): void {
|
||||||
|
@ -20,7 +20,7 @@ import { adapterFactory } from 'angular-calendar/date-adapters/date-fns';
|
|||||||
import { MarkdownCalendarService } from '../../services/markdown-calendar.service';
|
import { MarkdownCalendarService } from '../../services/markdown-calendar.service';
|
||||||
import { FileMetadata } from '../../types';
|
import { FileMetadata } from '../../types';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { Observable, Subject } from 'rxjs';
|
import { Observable, Subject, Subscription } from 'rxjs';
|
||||||
|
|
||||||
interface DayBucket {
|
interface DayBucket {
|
||||||
created: FileMetadata[];
|
created: FileMetadata[];
|
||||||
@ -77,6 +77,7 @@ export class MarkdownCalendarComponent {
|
|||||||
|
|
||||||
private dragAnchor: Date | null = null;
|
private dragAnchor: Date | null = null;
|
||||||
private searchRequestId = 0;
|
private searchRequestId = 0;
|
||||||
|
private searchSubscription: Subscription | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.loadMetadata();
|
this.loadMetadata();
|
||||||
@ -193,12 +194,13 @@ export class MarkdownCalendarComponent {
|
|||||||
|
|
||||||
private runSearch(source$: Observable<FileMetadata[]>): void {
|
private runSearch(source$: Observable<FileMetadata[]>): void {
|
||||||
const token = ++this.searchRequestId;
|
const token = ++this.searchRequestId;
|
||||||
|
this.searchSubscription?.unsubscribe();
|
||||||
this.isSearching.set(true);
|
this.isSearching.set(true);
|
||||||
this.searchError.set(null);
|
this.searchError.set(null);
|
||||||
this.searchStateChange.emit('loading');
|
this.searchStateChange.emit('loading');
|
||||||
this.searchErrorChange.emit(null);
|
this.searchErrorChange.emit(null);
|
||||||
|
|
||||||
source$.subscribe({
|
this.searchSubscription = source$.subscribe({
|
||||||
next: (files: FileMetadata[]) => {
|
next: (files: FileMetadata[]) => {
|
||||||
if (token !== this.searchRequestId) {
|
if (token !== this.searchRequestId) {
|
||||||
return;
|
return;
|
||||||
|
@ -24,7 +24,7 @@ interface MetadataEntry {
|
|||||||
imports: [CommonModule],
|
imports: [CommonModule],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: `
|
template: `
|
||||||
<div class="p-8 prose prose-lg dark:prose-invert max-w-none">
|
<div class="p-8 prose prose-lg dark:prose-invert max-w-none prose-p:leading-[1] prose-li:leading-[1] prose-blockquote:leading-[1]">
|
||||||
<div class="!mb-6 pb-2 border-b border-obs-l-border dark:border-obs-d-border">
|
<div class="!mb-6 pb-2 border-b border-obs-l-border dark:border-obs-d-border">
|
||||||
<h1 class="!text-4xl !font-bold !mb-3">{{ note().title }}</h1>
|
<h1 class="!text-4xl !font-bold !mb-3">{{ note().title }}</h1>
|
||||||
@if (note().tags.length > 0) {
|
@if (note().tags.length > 0) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, of, shareReplay, finalize, map } from 'rxjs';
|
||||||
import { FileMetadata } from '../types';
|
import { FileMetadata } from '../types';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@ -9,23 +9,48 @@ import { FileMetadata } from '../types';
|
|||||||
export class MarkdownCalendarService {
|
export class MarkdownCalendarService {
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
getFilesMetadata(): Observable<FileMetadata[]> {
|
private metadataCache: FileMetadata[] | null = null;
|
||||||
return this.http.get<FileMetadata[]>('/api/files/metadata');
|
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[]> {
|
searchFilesByDate(date: Date): Observable<FileMetadata[]> {
|
||||||
return this.http.get<FileMetadata[]>('/api/files/by-date', {
|
const start = this.startOfDay(date);
|
||||||
params: { date: date.toISOString() },
|
const end = this.endOfDay(date);
|
||||||
});
|
return this.getFilesMetadata().pipe(map((metadata) => this.filterByRange(metadata, start, end)));
|
||||||
}
|
}
|
||||||
|
|
||||||
searchFilesByDateRange(startDate: Date, endDate: Date): Observable<FileMetadata[]> {
|
searchFilesByDateRange(startDate: Date, endDate: Date): Observable<FileMetadata[]> {
|
||||||
return this.http.get<FileMetadata[]>('/api/files/by-date-range', {
|
const rangeStart = this.startOfDay(startDate);
|
||||||
params: {
|
const rangeEnd = this.endOfDay(endDate);
|
||||||
start: startDate.toISOString(),
|
return this.getFilesMetadata().pipe(map((metadata) => this.filterByRange(metadata, rangeStart, rangeEnd)));
|
||||||
end: endDate.toISOString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
groupMetadataByDay(metadata: FileMetadata[]): Map<string, { created: FileMetadata[]; updated: FileMetadata[] }> {
|
groupMetadataByDay(metadata: FileMetadata[]): Map<string, { created: FileMetadata[]; updated: FileMetadata[] }> {
|
||||||
@ -66,4 +91,48 @@ export class MarkdownCalendarService {
|
|||||||
private appendUnique(list: FileMetadata[], file: FileMetadata): FileMetadata[] {
|
private appendUnique(list: FileMetadata[], file: FileMetadata): FileMetadata[] {
|
||||||
return list.some((item) => item.id === file.id) ? list : list.concat(file);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
102
src/services/vault-events.service.ts
Normal file
102
src/services/vault-events.service.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { Injectable, NgZone, OnDestroy } from '@angular/core';
|
||||||
|
import { Observable, Subject } from 'rxjs';
|
||||||
|
|
||||||
|
export interface VaultEventPayload {
|
||||||
|
event: string;
|
||||||
|
path?: string;
|
||||||
|
timestamp: number;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class VaultEventsService implements OnDestroy {
|
||||||
|
private readonly eventsSubject = new Subject<VaultEventPayload>();
|
||||||
|
private eventSource: EventSource | null = null;
|
||||||
|
private reconnectTimer: number | null = null;
|
||||||
|
private readonly reconnectDelayMs = 5000;
|
||||||
|
|
||||||
|
constructor(private readonly zone: NgZone) {}
|
||||||
|
|
||||||
|
events$(): Observable<VaultEventPayload> {
|
||||||
|
this.ensureConnection();
|
||||||
|
return this.eventsSubject.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureConnection(): void {
|
||||||
|
if (this.eventSource || typeof window === 'undefined' || typeof EventSource === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.zone.runOutsideAngular(() => {
|
||||||
|
const source = new EventSource('/api/vault/events');
|
||||||
|
this.eventSource = source;
|
||||||
|
|
||||||
|
source.onmessage = (event: MessageEvent<string>) => {
|
||||||
|
this.handleMessage(event.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
source.onerror = () => {
|
||||||
|
// Let the browser attempt to reconnect automatically, but make sure we clean up references.
|
||||||
|
this.scheduleReconnect();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMessage(rawData: string): void {
|
||||||
|
if (!rawData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload: VaultEventPayload | null = null;
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(rawData) as VaultEventPayload;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse vault event payload:', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!payload || typeof payload.event !== 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.zone.run(() => {
|
||||||
|
this.eventsSubject.next(payload!);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleReconnect(): void {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.eventSource) {
|
||||||
|
this.eventSource.close();
|
||||||
|
this.eventSource = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.reconnectTimer !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reconnectTimer = window.setTimeout(() => {
|
||||||
|
this.reconnectTimer = null;
|
||||||
|
this.ensureConnection();
|
||||||
|
}, this.reconnectDelayMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.reconnectTimer !== null && typeof window !== 'undefined') {
|
||||||
|
window.clearTimeout(this.reconnectTimer);
|
||||||
|
this.reconnectTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.eventSource) {
|
||||||
|
this.eventSource.close();
|
||||||
|
this.eventSource = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventsSubject.complete();
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import { Injectable, signal, computed } from '@angular/core';
|
import { Injectable, signal, computed, OnDestroy } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Note, VaultNode, GraphData, TagInfo, VaultFolder } from '../types';
|
import { Note, VaultNode, GraphData, TagInfo, VaultFolder } from '../types';
|
||||||
|
import { VaultEventsService, VaultEventPayload } from './vault-events.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
interface VaultApiNote {
|
interface VaultApiNote {
|
||||||
id: string;
|
id: string;
|
||||||
@ -22,7 +24,7 @@ interface VaultApiResponse {
|
|||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class VaultService {
|
export class VaultService implements OnDestroy {
|
||||||
private notesMap = signal<Map<string, Note>>(new Map());
|
private notesMap = signal<Map<string, Note>>(new Map());
|
||||||
private openFolderPaths = signal(new Set<string>());
|
private openFolderPaths = signal(new Set<string>());
|
||||||
private initialVaultName = this.resolveVaultName();
|
private initialVaultName = this.resolveVaultName();
|
||||||
@ -123,8 +125,22 @@ export class VaultService {
|
|||||||
.sort((a, b) => b.count - a.count);
|
.sort((a, b) => b.count - a.count);
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(private http: HttpClient) {
|
private vaultEventsSubscription: Subscription | null = null;
|
||||||
|
private refreshTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
constructor(private http: HttpClient, private vaultEvents: VaultEventsService) {
|
||||||
this.refreshNotes();
|
this.refreshNotes();
|
||||||
|
this.observeVaultEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.vaultEventsSubscription?.unsubscribe();
|
||||||
|
this.vaultEventsSubscription = null;
|
||||||
|
|
||||||
|
if (this.refreshTimeoutId !== null) {
|
||||||
|
clearTimeout(this.refreshTimeoutId);
|
||||||
|
this.refreshTimeoutId = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getNoteById(id: string): Note | undefined {
|
getNoteById(id: string): Note | undefined {
|
||||||
@ -167,6 +183,52 @@ export class VaultService {
|
|||||||
this.refreshNotes();
|
this.refreshNotes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private observeVaultEvents(): void {
|
||||||
|
this.vaultEventsSubscription = this.vaultEvents.events$().subscribe({
|
||||||
|
next: (event) => this.handleVaultEvent(event),
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Vault events stream error:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleVaultEvent(event: VaultEventPayload): void {
|
||||||
|
if (!event || typeof event.event !== 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.event) {
|
||||||
|
case 'add':
|
||||||
|
case 'change':
|
||||||
|
case 'unlink':
|
||||||
|
case 'addDir':
|
||||||
|
case 'unlinkDir':
|
||||||
|
this.scheduleRefresh();
|
||||||
|
break;
|
||||||
|
case 'ready':
|
||||||
|
case 'connected':
|
||||||
|
// Initial ready/connected events can trigger a refresh to ensure state is up-to-date.
|
||||||
|
this.scheduleRefresh();
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
console.error('Vault watcher reported error:', event.message ?? 'Unknown watcher error');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleRefresh(): void {
|
||||||
|
if (this.refreshTimeoutId !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.refreshTimeoutId = setTimeout(() => {
|
||||||
|
this.refreshTimeoutId = null;
|
||||||
|
this.refreshNotes();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
private refreshNotes() {
|
private refreshNotes() {
|
||||||
this.http.get<VaultApiResponse>('/api/vault').subscribe({
|
this.http.get<VaultApiResponse>('/api/vault').subscribe({
|
||||||
next: ({ notes }) => {
|
next: ({ notes }) => {
|
||||||
|
@ -4,5 +4,9 @@ NomDeVoute: IT
|
|||||||
Description: Page d'accueil de la voute IT
|
Description: Page d'accueil de la voute IT
|
||||||
tags: [home, accueil]
|
tags: [home, accueil]
|
||||||
---
|
---
|
||||||
|
## TEST
|
||||||
|
bonjour
|
||||||
|
|
||||||
# Page d'accueil
|
### allo
|
||||||
|
bonjour
|
||||||
|
alloooo
|
||||||
|
@ -68,6 +68,10 @@ Citation en ligne : « > Ceci est une citation »
|
|||||||
- [ ] Tâche à faire
|
- [ ] Tâche à faire
|
||||||
- [X] Tâche terminée
|
- [X] Tâche terminée
|
||||||
|
|
||||||
|
## Images
|
||||||
|
|
||||||
|
![[Voute_IT.png]]
|
||||||
|
|
||||||
## Liens et images
|
## Liens et images
|
||||||
|
|
||||||
[Lien vers le site officiel d'Obsidian](https://obsidian.md)
|
[Lien vers le site officiel d'Obsidian](https://obsidian.md)
|
||||||
@ -209,9 +213,5 @@ Le Markdown peut inclure des notes de bas de page[^1].
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
## Sections horizontales
|
## Sections horizontales
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Fin de la page de test.
|
Fin de la page de test.
|
||||||
|
|
||||||
[^1]: Ceci est un exemple de note de bas de page.
|
[^1]: Ceci est un exemple de note de bas de page.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user