From 7be20d05e0b0cb67bdaf205493cfdbe40959a970 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Tue, 4 Nov 2025 13:14:51 -0500 Subject: [PATCH] feat: enhance note list views with rich metadata display - Added timestamp, author, and description fields to both compact and detailed view modes - Implemented thumbnail support in detailed view with automatic image extraction from note content - Improved auto-selection behavior when filters change using reactive signals for more reliable note navigation --- src/app/features/list/notes-list.component.ts | 92 ++++++++++++++-- .../list/paginated-notes-list.component.ts | 97 +++++++++++++++-- .../app-shell-nimbus.component.ts | 102 ++++++++++++++---- vault/.test/test.md | 27 +++-- vault/Allo-3/Stargate Atlantis.md | 2 +- ...pléments Alimentaires Un Guide Général.md | 17 +-- 6 files changed, 272 insertions(+), 65 deletions(-) diff --git a/src/app/features/list/notes-list.component.ts b/src/app/features/list/notes-list.component.ts index ca4cd74..f0d7222 100644 --- a/src/app/features/list/notes-list.component.ts +++ b/src/app/features/list/notes-list.component.ts @@ -192,21 +192,26 @@ import { AIToolsService } from '../../services/ai-tools.service';
{{ n.title }}
{{ n.filePath }}
+
🕓 {{ formatDateTime(ts) }}
-
- - {{ typeIcon(n) }} -
-
{{ n.title }}
-
{{ n.filePath }}
-
- Status: {{ n.frontmatter.status }} - {{ formatDate(n.mtime) }} +
+
+ + {{ typeIcon(n) }} +
+
{{ n.title }}
+
{{ n.filePath }}
+
✍️ {{ a }}
+
{{ d }}
+
🕓 {{ formatDateTime(ts) }}
+
+ +
@@ -1049,6 +1054,75 @@ export class NotesListComponent { } } + getUpdatedTimestamp(n: Note): number | null { + try { + const m = Number(n.mtime || 0); + if (m && !Number.isNaN(m)) return m; + } catch {} + const parse = (s?: string) => { + if (!s) return 0; + const t = Date.parse(s); + return Number.isFinite(t) ? t : 0; + }; + const u = parse(n.updatedAt); + if (u) return u; + const c = parse(n.createdAt); + return c || null; + } + + formatDateTime(ts: number): string { + try { + const d = new Date(ts); + return d.toLocaleString('fr-FR', { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }); + } catch { return ''; } + } + + getAuthor(n: Note): string | null { + const fm = (n.frontmatter || {}) as any; + const v = (fm.author ?? fm.auteur ?? n.author) as any; + return (typeof v === 'string' && v.trim().length > 0) ? v : null; + } + + getDescription(n: Note): string | null { + const fm = (n.frontmatter || {}) as any; + const v = (fm.description ?? fm.desc) as any; + return (typeof v === 'string' && v.trim().length > 0) ? v : null; + } + + private extractFirstImageFromContent(n: Note): string | null { + const content = (n.rawContent ?? n.content ?? '').toString(); + if (!content) return null; + try { + const embed = content.match(/!\[\[(.*?)\]\]/); + if (embed && embed[1]) { + const name = embed[1].trim(); + if (name) { + const notePath = (n.filePath || n.originalPath || '').replace(/\\/g, '/'); + return `/api/attachments/resolve?name=${encodeURIComponent(name)}¬e=${encodeURIComponent(notePath)}`; + } + } + const md = content.match(/!\[[^\]]*\]\(([^\)\s]+)(?:\s+\"[^\"]*\")?\)/); + if (md && md[1]) { + const p = md[1].trim(); + if (/^(data:|https?:)/i.test(p)) return p; + if (p.startsWith('/')) return `/vault/${encodeURI(p.replace(/^\/+/, ''))}`; + const notePath = (n.filePath || n.originalPath || '').replace(/\\/g, '/'); + return `/api/attachments/resolve?name=${encodeURIComponent(p)}¬e=${encodeURIComponent(notePath)}`; + } + } catch {} + return null; + } + + getThumbnailSrc(n: Note): string | null { + try { + const kind = this.fileTypes.getViewerType(n.filePath, n.rawContent ?? n.content); + if (kind === 'image') { + return `/vault/${encodeURI(n.filePath)}`; + } + } catch {} + return this.extractFirstImageFromContent(n); + } + // ============ Multiple Selection Methods ============ /** diff --git a/src/app/features/list/paginated-notes-list.component.ts b/src/app/features/list/paginated-notes-list.component.ts index deb8b5a..772d012 100644 --- a/src/app/features/list/paginated-notes-list.component.ts +++ b/src/app/features/list/paginated-notes-list.component.ts @@ -138,16 +138,25 @@ import { takeUntil } from 'rxjs/operators';
{{ note.title }}
{{ note.filePath }}
+
🕓 {{ formatDateTime(ts) }}
-
- - {{ typeIcon(note.filePath) }} -
-
{{ note.title }}
-
{{ note.filePath }}
+
+
+ + {{ typeIcon(note.filePath) }} +
+
{{ note.title }}
+
{{ note.filePath }}
+
✍️ {{ a }}
+
{{ d }}
+
🕓 {{ formatDateTime(ts) }}
+
+
+
+
@@ -1030,6 +1039,82 @@ export class PaginatedNotesListComponent implements OnInit, OnDestroy { } catch { return '📎'; } } + // Metadata helpers used by Comfortable/Detailed modes + getUpdatedTimestampById(meta: NoteMetadata): number | null { + try { + const full = this.getFullNoteById(meta.id) as any; + const m = Number(full?.mtime || 0); + if (m && !Number.isNaN(m)) return m; + } catch {} + const parse = (s?: string) => { + if (!s) return 0; + const t = Date.parse(s); + return Number.isFinite(t) ? t : 0; + }; + const u = parse(meta.updatedAt); + if (u) return u; + const c = parse(meta.createdAt); + return c || null; + } + + formatDateTime(ts: number): string { + try { + const d = new Date(ts); + return d.toLocaleString('fr-FR', { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }); + } catch { return ''; } + } + + getAuthorById(id: string): string | null { + const full = this.getFullNoteById(id) as any; + const fm = (full?.frontmatter || {}) as any; + const v = (fm.author ?? fm.auteur ?? full?.author) as any; + return (typeof v === 'string' && v.trim().length > 0) ? v : null; + } + + getDescriptionById(id: string): string | null { + const full = this.getFullNoteById(id) as any; + const fm = (full?.frontmatter || {}) as any; + const v = (fm.description ?? fm.desc) as any; + return (typeof v === 'string' && v.trim().length > 0) ? v : null; + } + + private extractFirstImageFromFull(full: any): string | null { + if (!full) return null; + const content = String(full.rawContent ?? full.content ?? ''); + if (!content) return null; + try { + const embed = content.match(/!\[\[(.*?)\]\]/); + if (embed && embed[1]) { + const name = embed[1].trim(); + if (name) { + const notePath = String(full.filePath || full.originalPath || '').replace(/\\/g, '/'); + return `/api/attachments/resolve?name=${encodeURIComponent(name)}¬e=${encodeURIComponent(notePath)}`; + } + } + const md = content.match(/!\[[^\]]*\]\(([^\)\s]+)(?:\s+\"[^\"]*\")?\)/); + if (md && md[1]) { + const p = md[1].trim(); + if (/^(data:|https?:)/i.test(p)) return p; + if (p.startsWith('/')) return `/vault/${encodeURI(p.replace(/^\/+/, ''))}`; + const notePath = String(full.filePath || full.originalPath || '').replace(/\\/g, '/'); + return `/api/attachments/resolve?name=${encodeURIComponent(p)}¬e=${encodeURIComponent(notePath)}`; + } + } catch {} + return null; + } + + getThumbnailSrcById(meta: NoteMetadata): string | null { + try { + const full = this.getFullNoteById(meta.id) as any; + const path = full?.filePath || meta.filePath || ''; + const kind = this.fileTypes.getViewerType(path, full?.rawContent ?? full?.content ?? ''); + if (kind === 'image') { + return `/vault/${encodeURI(path)}`; + } + return this.extractFirstImageFromFull(full); + } catch { return null; } + } + // Sort/View menus toggleSortMenu(): void { this.sortMenuOpen.set(!this.sortMenuOpen()); this.viewModeMenuOpen.set(false); } toggleViewModeMenu(): void { this.viewModeMenuOpen.set(!this.viewModeMenuOpen()); this.sortMenuOpen.set(false); } diff --git a/src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts b/src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts index 7bef1c3..3b44727 100644 --- a/src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts +++ b/src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, HostListener, Input, Output, inject, effect, AfterViewInit, ViewChild, ElementRef } from '@angular/core'; +import { Component, EventEmitter, HostListener, Input, Output, inject, effect, signal, computed, AfterViewInit, ViewChild, ElementRef } from '@angular/core'; import { CommonModule } from '@angular/common'; import { UiModeService } from '../../shared/services/ui-mode.service'; import { ResponsiveService } from '../../shared/services/responsive.service'; @@ -431,6 +431,22 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { quickLinkFilter: 'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | 'bookmarks' | null = null; private suppressNextNoteSelection = false; + // Signaux pour tracker les filtres et déclencher la sélection automatique + private filterStateSignal = signal<{ + tag: string | null; + folder: string | null; + quick: string | null; + search: string; + }>({ + tag: null, + folder: null, + quick: null, + search: '' + }); + + // Signal pour forcer un recalcul de la liste filtrée + private filterChangeCounter = signal(0); + // --- URL State <-> Layout sync --- private mapUrlQuickToInternal(q: string | null): AppShellNimbusLayoutComponent['quickLinkFilter'] { switch ((q || '').toLowerCase()) { @@ -525,7 +541,7 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { this.tagFilter = norm || null; this.folderFilter = null; this.quickLinkFilter = null; - if (!hasNote) this.autoSelectFirstNote(); + if (!hasNote) this.notifyFilterChange(); if (!this.responsive.isDesktop()) this.mobileNav.setActiveTab('list'); } // Auto-open tags flyout when tag filter is active @@ -538,7 +554,7 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { this.folderFilter = folder || null; this.tagFilter = null; this.quickLinkFilter = null; - if (!hasNote) this.autoSelectFirstNote(); + if (!hasNote) this.notifyFilterChange(); if (!this.responsive.isDesktop()) this.mobileNav.setActiveTab('list'); } // Auto-open folders flyout when folder filter is active @@ -552,12 +568,12 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { this.quickLinkFilter = internal; this.folderFilter = null; this.tagFilter = null; - this.autoSelectFirstNote(); + this.notifyFilterChange(); if (!this.responsive.isDesktop()) { this.mobileNav.setActiveTab('list'); } } else { - this.autoSelectFirstNote(); + this.notifyFilterChange(); } // Auto-open quick flyout when quick filter is active if (this.hoveredFlyout !== 'quick') { @@ -571,7 +587,7 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { this.folderFilter = null; this.tagFilter = null; this.quickLinkFilter = null; - this.autoSelectFirstNote(); + this.notifyFilterChange(); } this.suppressNextNoteSelection = false; // Close any open flyout when no filters @@ -589,11 +605,58 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { }); }); + // Effect pour sélection automatique quand les filtres changent + private _autoSelectEffect = effect(() => { + // Écouter le signal de changement de filtre + const filterState = this.filterStateSignal(); + const counter = this.filterChangeCounter(); + + // Éviter l'exécution lors de l'initialisation + if (counter === 0) return; + + console.log('🎯 Auto-select effect triggered:', { filterState, counter }); + + // Utiliser queueMicrotask pour s'assurer que la liste est mise à jour + queueMicrotask(() => { + this.autoSelectFirstNote(true); + }); + }); + + // Méthode helper pour notifier les changements de filtres + private notifyFilterChange() { + const currentState = { + tag: this.tagFilter, + folder: this.folderFilter, + quick: this.quickLinkFilter, + search: this.listQuery + }; + + console.log('🔔 Notifying filter change:', currentState); + + // Mettre à jour le signal d'état + this.filterStateSignal.set(currentState); + + // Incrémenter le compteur pour déclencher l'effect + this.filterChangeCounter.update(c => c + 1); + } + // Auto-select first note when filters change - private autoSelectFirstNote() { + private autoSelectFirstNote(forceSelection = false) { const filteredNotes = this.getFilteredNotes(); - if (filteredNotes.length > 0 && filteredNotes[0].id !== this.selectedNoteId) { - this.noteSelected.emit(filteredNotes[0].id); + console.log('🎯 autoSelectFirstNote called:', { + forceSelection, + notesCount: filteredNotes.length, + firstNoteId: filteredNotes[0]?.id, + selectedNoteId: this.selectedNoteId + }); + + if (filteredNotes.length > 0) { + const firstNote = filteredNotes[0]; + // Forcer la sélection si demandé, ou si la note est différente + if (forceSelection || firstNote.id !== this.selectedNoteId) { + console.log('✅ Emitting noteSelected for:', firstNote.title, firstNote.id); + this.noteSelected.emit(firstNote.id); + } } } @@ -685,8 +748,8 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { onQueryChange(query: string) { this.listQuery = query; - // Auto-select first note on any list change including search updates - this.autoSelectFirstNote(); + // Notifier le changement pour déclencher la sélection automatique + this.notifyFilterChange(); // Sync URL search term this.urlState.updateSearch(query); } @@ -747,6 +810,7 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { onClearFolderFromList() { this.folderFilter = null; + this.notifyFilterChange(); this.urlState.clearFolderFilter(); } @@ -807,7 +871,8 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { this.listQuery = ''; try { this.filters.clearKinds(); } catch {} try { this.filters.clearTags(); } catch {} - this.autoSelectFirstNote(); + // Notifier le changement pour déclencher la sélection automatique + this.notifyFilterChange(); if (this.responsive.isMobile() || this.responsive.isTablet()) { this.mobileNav.setActiveTab('list'); } @@ -829,7 +894,8 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { this.listQuery = ''; try { this.filters.clearKinds(); } catch {} try { this.filters.clearTags(); } catch {} - this.autoSelectFirstNote(); + // Notifier le changement pour déclencher la sélection automatique + this.notifyFilterChange(); this.mobileNav.setActiveTab('list'); this.mobileNav.sidebarOpen.set(false); if (path) { @@ -987,8 +1053,8 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { this.urlState.setQuickWithMarkdown(label); } } - // Auto-select first note after filter changes - this.autoSelectFirstNote(); + // Notifier le changement pour déclencher la sélection automatique + this.notifyFilterChange(); this.suppressNextNoteSelection = false; } @@ -1000,8 +1066,8 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { // Clear other filters and search to focus on tag results this.quickLinkFilter = null; this.listQuery = ''; - // Auto-select first note after filter changes - this.autoSelectFirstNote(); + // Notifier le changement pour déclencher la sélection automatique + this.notifyFilterChange(); // Ensure the list is visible: exit fullscreen if active if (this.noteFullScreen) { this.noteFullScreen = false; @@ -1104,6 +1170,6 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { this.tagFilter = null; this.quickLinkFilter = null; this.listQuery = ''; - this.autoSelectFirstNote(); + this.notifyFilterChange(); } } diff --git a/vault/.test/test.md b/vault/.test/test.md index 1a87b4f..ac9a1b5 100644 --- a/vault/.test/test.md +++ b/vault/.test/test.md @@ -1,13 +1,10 @@ --- -titre: test -auteur: Bruno Charest -creation_date: 2025-09-25T07:45:20-04:00 -modification_date: 2025-11-02T12:07:38-04:00 -catégorie: "" -tags: [] -aliases: - - "" -status: en-cours +titre: "test" +auteur: "Bruno Charest" +creation_date: "2025-09-25T07:45:20-04:00" +modification_date: "2025-11-02T12:07:38-04:00" +tags: [""] +status: "en-cours" publish: true favoris: false template: true @@ -15,14 +12,14 @@ task: true archive: true draft: true private: true -first_name: Bruno -birth_date: 2025-06-18 -email: bruno.charest@gmail.com +first_name: "Bruno" +birth_date: "2025-06-18" +email: "bruno.charest@gmail.com" number: "12345" todo: false -url: https://google.com -image: https://images.unsplash.com/photo-1675789652575-0a5d2425b6c2?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80 -color: "#64748B" +url: "https://google.com" +image: "https://images.unsplash.com/photo-1675789652575-0a5d2425b6c2?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80" +color: "#22C55E" --- # Test 1 Markdown diff --git a/vault/Allo-3/Stargate Atlantis.md b/vault/Allo-3/Stargate Atlantis.md index dd5d76c..61ec8da 100644 --- a/vault/Allo-3/Stargate Atlantis.md +++ b/vault/Allo-3/Stargate Atlantis.md @@ -9,7 +9,7 @@ publish: false favoris: true template: false task: false -archive: false +archive: true draft: false private: false description: "Stargate Atlantis: une expédition militaire et scientifique découvre la cité mythique d'Atlantis dans la galaxie de Pégase et affronte les Wraiths." diff --git a/vault/tata/Les Compléments Alimentaires Un Guide Général.md b/vault/tata/Les Compléments Alimentaires Un Guide Général.md index 289bb94..6c8f204 100644 --- a/vault/tata/Les Compléments Alimentaires Un Guide Général.md +++ b/vault/tata/Les Compléments Alimentaires Un Guide Général.md @@ -12,24 +12,9 @@ archive: false draft: true private: false titre: "" -Les Compléments Alimentaires: "Un Guide Général" -catégorie: "" readOnly: false description: "Les Compléments Alimentaires : Un Guide Général Dans notre quête constante de bien-être et de..." -tags: - - supplments - - tag2 - - configuration - - accueil - - home - - markdown - - bruno - - tag4 - - tag3 - - test - - tag1 - - test2 - - tagtag +color: "#00AEEF" --- ## Les Compléments Alimentaires : Un Guide Général