feat: add quick links filtering and frontmatter enrichment on file load

This commit is contained in:
2025-10-19 20:47:18 -04:00
parent 8cccf83f9a
commit 0f7610bed1
51 changed files with 4078 additions and 647 deletions
+23 -2
View File
@@ -50,6 +50,7 @@ export class NotesListComponent {
folderFilter = input<string | null>(null); // like "folder/subfolder"
query = input<string>('');
tagFilter = input<string | null>(null);
quickLinkFilter = input<'favoris' | 'template' | 'task' | 'draft' | 'archive' | null>(null);
@Output() openNote = new EventEmitter<string>();
@Output() queryChange = new EventEmitter<string>();
@@ -61,18 +62,38 @@ export class NotesListComponent {
filtered = computed(() => {
const q = (this.q() || '').toLowerCase().trim();
const folder = (this.folderFilter() || '').toLowerCase();
const folder = (this.folderFilter() || '').toLowerCase().replace(/^\/+|\/+$/g, '');
const tag = (this.tagFilter() || '').toLowerCase();
const quickLink = this.quickLinkFilter();
let list = this.notes();
if (folder) {
list = list.filter(n => (n.originalPath || '').toLowerCase().startsWith(folder));
if (folder === '.trash') {
// All files anywhere under .trash (including subfolders)
list = list.filter(n => {
const filePath = (n.filePath || n.originalPath || '').toLowerCase().replace(/\\/g, '/');
return filePath.includes('/.trash/');
});
} else {
list = list.filter(n => {
const originalPath = (n.originalPath || '').toLowerCase().replace(/^\/+|\/+$/g, '');
return originalPath === folder || originalPath.startsWith(folder + '/');
});
}
}
if (tag) {
list = list.filter(n => Array.isArray(n.tags) && n.tags.some(t => (t || '').toLowerCase() === tag));
}
// Apply Quick Link filter (favoris, template, task)
if (quickLink) {
list = list.filter(n => {
const frontmatter = n.frontmatter || {};
return frontmatter[quickLink] === true;
});
}
// Apply query if present
if (q) {
list = list.filter(n => {
@@ -1,26 +1,71 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { Component, EventEmitter, Output, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { VaultService } from '../../../services/vault.service';
import { BadgeCountComponent } from '../../shared/ui/badge-count.component';
interface QuickLinkCountsUi {
all: number;
favorites: number;
templates: number;
tasks: number;
drafts: number;
archive: number;
}
@Component({
selector: 'app-quick-links',
standalone: true,
imports: [CommonModule],
imports: [CommonModule, BadgeCountComponent],
template: `
<div class="p-3">
<ul class="text-sm">
<li><button (click)="select('all')" class="w-full text-left px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2"><span>🗂️</span> <span>All pages</span></button></li>
<li><button (click)="select('favorites')" class="w-full text-left px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2"><span>❤️</span> <span>Favorites</span></button></li>
<li><button (click)="select('templates')" class="w-full text-left px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2"><span>📑</span> <span>Templates</span></button></li>
<li><button (click)="select('import')" class="w-full text-left px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2"><span>⬇️</span> <span>Import</span></button></li>
<li><button (click)="select('tasks')" class="w-full text-left px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2"><span>🗒️</span> <span>Tasks</span></button></li>
<li><button (click)="select('chat')" class="w-full text-left px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2"><span>💬</span> <span>Chat</span></button></li>
<li><button (click)="select('activity')" class="w-full text-left px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2"><span>🕒</span> <span>Activity Panel</span></button></li>
<li><button (click)="select('portal')" class="w-full text-left px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2"><span>📰</span> <span>Portal</span></button></li>
<ul class="text-sm space-y-1">
<li>
<button (click)="select('all')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left">
<span class="flex items-center gap-2"><span>🗂️</span> <span>All pages</span></span>
<app-badge-count class="ml-auto" [count]="counts().all" color="slate"></app-badge-count>
</button>
</li>
<li>
<button (click)="select('favorites')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left">
<span class="flex items-center gap-2"><span>❤️</span> <span>Favorites</span></span>
<app-badge-count class="ml-auto" [count]="counts().favorites" color="rose"></app-badge-count>
</button>
</li>
<li>
<button (click)="select('templates')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left">
<span class="flex items-center gap-2"><span>📑</span> <span>Templates</span></span>
<app-badge-count class="ml-auto" [count]="counts().templates" color="amber"></app-badge-count>
</button>
</li>
<li>
<button (click)="select('tasks')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left">
<span class="flex items-center gap-2"><span>🗒️</span> <span>Tasks</span></span>
<app-badge-count class="ml-auto" [count]="counts().tasks" color="indigo"></app-badge-count>
</button>
</li>
<li>
<button (click)="select('drafts')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left">
<span class="flex items-center gap-2"><span>📝</span> <span>Drafts</span></span>
<app-badge-count class="ml-auto" [count]="counts().drafts" color="emerald"></app-badge-count>
</button>
</li>
<li>
<button (click)="select('archive')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left">
<span class="flex items-center gap-2"><span>🗃️</span> <span>Archive</span></span>
<app-badge-count class="ml-auto" [count]="counts().archive" color="stone"></app-badge-count>
</button>
</li>
</ul>
</div>
`
})
export class QuickLinksComponent {
private vault = inject(VaultService);
@Output() quickLinkSelected = new EventEmitter<string>();
select(id: string) { this.quickLinkSelected.emit(id); }
counts = () => this.vault.counts() as QuickLinkCountsUi;
select(id: string) {
this.quickLinkSelected.emit(id);
}
}
@@ -6,11 +6,13 @@ import { QuickLinksComponent } from '../quick-links/quick-links.component';
import { ScrollableOverlayDirective } from '../../shared/overlay-scrollbar/scrollable-overlay.directive';
import type { VaultNode, TagInfo } from '../../../types';
import { environment } from '../../../environments/environment';
import { VaultService } from '../../../services/vault.service';
import { TrashExplorerComponent } from '../../layout/sidebar/trash/trash-explorer.component';
@Component({
selector: 'app-sidebar-drawer',
standalone: true,
imports: [CommonModule, FileExplorerComponent, QuickLinksComponent, ScrollableOverlayDirective],
imports: [CommonModule, FileExplorerComponent, QuickLinksComponent, ScrollableOverlayDirective, TrashExplorerComponent],
template: `
<aside class="fixed left-0 top-0 bottom-0 w-80 max-w-[80vw] bg-white dark:bg-gray-900 shadow-2xl z-40 transform transition-all duration-300 ease-out flex flex-col"
[class.-translate-x-full]="!mobileNav.sidebarOpen()"
@@ -90,11 +92,18 @@ import { environment } from '../../../environments/environment';
<!-- Trash accordion -->
<section class="border-b border-gray-200 dark:border-gray-800">
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 transition-colors"
(click)="open.trash = !open.trash">
<span>Trash</span>
(click)="open.trash = !open.trash; onFolder('.trash')">
<span class="flex items-center gap-2">Trash</span>
<span class="text-xs text-gray-500 transition-transform duration-200" [class.rotate-90]="!open.trash">{{ open.trash ? '▾' : '▸' }}</span>
</button>
<div *ngIf="open.trash" class="px-3 py-3 text-sm text-gray-500 dark:text-gray-400">Empty</div>
<div *ngIf="open.trash" class="px-1 py-2">
<ng-container *ngIf="trashHasContent(); else emptyTrash">
<app-trash-explorer (folderSelected)="onFolder($event)"></app-trash-explorer>
</ng-container>
<ng-template #emptyTrash>
<div class="px-3 py-2 text-slate-400 text-sm">La corbeille est vide</div>
</ng-template>
</div>
</section>
</div>
@@ -116,6 +125,7 @@ import { environment } from '../../../environments/environment';
export class AppSidebarDrawerComponent {
mobileNav = inject(MobileNavService);
env = environment;
private vault = inject(VaultService);
@Input() nodes: VaultNode[] = [];
@Input() selectedNoteId: string | null = null;
@@ -154,4 +164,9 @@ export class AppSidebarDrawerComponent {
this.markdownPlaygroundSelected.emit();
this.mobileNav.sidebarOpen.set(false);
}
trashNotes = () => this.vault.trashNotes();
trashCount = () => this.vault.counts().trash;
trashHasContent = () => (this.vault.trashTree() || []).length > 0;
trackNoteId = (_: number, n: { id: string }) => n.id;
}
@@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Component, EventEmitter, Input, Output, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { FileExplorerComponent } from '../../../components/file-explorer/file-explorer.component';
@@ -6,6 +6,7 @@ import { QuickLinksComponent } from '../quick-links/quick-links.component';
import { ScrollableOverlayDirective } from '../../shared/overlay-scrollbar/scrollable-overlay.directive';
import type { VaultNode, TagInfo } from '../../../types';
import { environment } from '../../../environments/environment';
import { VaultService } from '../../../services/vault.service';
@Component({
selector: 'app-nimbus-sidebar',
@@ -58,7 +59,13 @@ import { environment } from '../../../environments/environment';
<span class="text-xs text-gray-500">{{ open.folders ? '▾' : '▸' }}</span>
</button>
<div *ngIf="open.folders" class="px-1 py-1">
<app-file-explorer [nodes]="effectiveFileTree" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" (folderSelected)="folderSelected.emit($event)" (fileSelected)="fileSelected.emit($event)"></app-file-explorer>
<app-file-explorer
[nodes]="effectiveFileTree"
[selectedNoteId]="selectedNoteId"
[foldersOnly]="true"
(folderSelected)="folderSelected.emit($event)"
(fileSelected)="fileSelected.emit($event)">
</app-file-explorer>
</div>
</section>
@@ -85,11 +92,25 @@ import { environment } from '../../../environments/environment';
<!-- Trash accordion -->
<section class="border-b border-gray-200 dark:border-gray-800">
<button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800"
(click)="open.trash = !open.trash">
<span>Trash</span>
(click)="open.trash = !open.trash; folderSelected.emit('.trash')">
<span class="flex items-center gap-2">Trash</span>
<span class="text-xs text-gray-500">{{ open.trash ? '▾' : '▸' }}</span>
</button>
<div *ngIf="open.trash" class="px-3 py-3 text-sm text-gray-500 dark:text-gray-400">Empty</div>
<div *ngIf="open.trash" class="px-1 py-2">
<ng-container *ngIf="trashHasContent(); else emptyTrash">
<app-file-explorer
[nodes]="vault.trashTree()"
[selectedNoteId]="selectedNoteId"
[foldersOnly]="true"
[useTrashCounts]="true"
(folderSelected)="folderSelected.emit($event)"
(fileSelected)="fileSelected.emit($event)">
</app-file-explorer>
</ng-container>
<ng-template #emptyTrash>
<div class="px-3 py-2 text-slate-400 text-sm">La corbeille est vide</div>
</ng-template>
</div>
</section>
</div>
@@ -113,10 +134,16 @@ export class NimbusSidebarComponent {
env = environment;
open = { quick: true, folders: true, tags: false, trash: false, tests: true };
private vault = inject(VaultService);
onQuickLink(id: string) { this.quickLinkSelected.emit(id); }
onMarkdownPlaygroundClick(): void {
this.markdownPlaygroundSelected.emit();
}
trashNotes = () => this.vault.trashNotes();
trashCount = () => this.vault.counts().trash;
trashHasContent = () => (this.vault.trashTree() || []).length > 0;
trackNoteId = (_: number, n: { id: string }) => n.id;
}
@@ -107,6 +107,7 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
[notes]="vault.allNotes()"
[folderFilter]="folderFilter"
[tagFilter]="tagFilter"
[quickLinkFilter]="quickLinkFilter"
[query]="listQuery"
(openNote)="onOpenNote($event)"
(queryChange)="listQuery = $event"
@@ -161,7 +162,7 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
<app-file-explorer [nodes]="effectiveFileTree" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" (folderSelected)="onFolderSelected($event)" (fileSelected)="onOpenNote($event)"></app-file-explorer>
</div>
<div [hidden]="mobileNav.activeTab() !== 'list'" class="h-full overflow-y-auto" appScrollableOverlay>
<app-notes-list [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [query]="listQuery" (queryChange)="listQuery=$event" (openNote)="onOpenNote($event)"></app-notes-list>
<app-notes-list [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" (queryChange)="listQuery=$event" (openNote)="onOpenNote($event)"></app-notes-list>
</div>
<div [hidden]="mobileNav.activeTab() !== 'page'" class="note-content-area h-full overflow-y-auto px-3 py-4" appScrollableOverlay>
<app-note-viewer [note]="selectedNote || null" [noteHtmlContent]="renderedNoteContent" [allNotes]="vault.allNotes()" (noteLinkClicked)="noteSelected.emit($event)" (tagClicked)="tagClicked.emit($event)" (wikiLinkActivated)="wikiLinkActivated.emit($event)" [fullScreenActive]="noteFullScreen" (fullScreenRequested)="toggleNoteFullScreen()" (legacyRequested)="ui.toggleUIMode()" (showToc)="mobileNav.toggleToc()" (directoryClicked)="onFolderSelected($event)" [tocOpen]="mobileNav.tocOpen()"></app-note-viewer>
@@ -185,7 +186,7 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
@if (mobileNav.activeTab() === 'list') {
<div class="h-full flex flex-col overflow-hidden animate-fadeIn">
<app-notes-list class="flex-1" [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [query]="listQuery" (queryChange)="listQuery=$event" (openNote)="onNoteSelectedMobile($event)"></app-notes-list>
<app-notes-list class="flex-1" [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" (queryChange)="listQuery=$event" (openNote)="onNoteSelectedMobile($event)"></app-notes-list>
</div>
}
@@ -260,6 +261,7 @@ export class AppShellNimbusLayoutComponent {
hoveredFlyout: 'quick' | 'folders' | 'tags' | 'trash' | null = null;
private flyoutCloseTimer: any = null;
tagFilter: string | null = null;
quickLinkFilter: 'favoris' | 'template' | 'task' | 'draft' | 'archive' | null = null;
toggleNoteFullScreen(): void {
this.noteFullScreen = !this.noteFullScreen;
@@ -316,11 +318,61 @@ export class AppShellNimbusLayoutComponent {
// Show all pages: clear filters and focus list
this.folderFilter = null;
this.tagFilter = null;
this.quickLinkFilter = null;
this.listQuery = '';
if (!this.responsive.isDesktop()) {
this.mobileNav.setActiveTab('list');
}
this.scheduleCloseFlyout(150);
} else if (_id === 'favorites') {
// Filter by favoris: true
this.folderFilter = null;
this.tagFilter = null;
this.quickLinkFilter = 'favoris';
this.listQuery = '';
if (!this.responsive.isDesktop()) {
this.mobileNav.setActiveTab('list');
}
this.scheduleCloseFlyout(150);
} else if (_id === 'templates') {
// Filter by template: true
this.folderFilter = null;
this.tagFilter = null;
this.quickLinkFilter = 'template';
this.listQuery = '';
if (!this.responsive.isDesktop()) {
this.mobileNav.setActiveTab('list');
}
this.scheduleCloseFlyout(150);
} else if (_id === 'tasks') {
// Filter by task: true
this.folderFilter = null;
this.tagFilter = null;
this.quickLinkFilter = 'task';
this.listQuery = '';
if (!this.responsive.isDesktop()) {
this.mobileNav.setActiveTab('list');
}
this.scheduleCloseFlyout(150);
} else if (_id === 'drafts') {
// Filter by draft: true
this.folderFilter = null;
this.tagFilter = null;
this.quickLinkFilter = 'draft';
this.listQuery = '';
if (!this.responsive.isDesktop()) {
this.mobileNav.setActiveTab('list');
}
this.scheduleCloseFlyout(150);
} else if (_id === 'archive') {
// Filter by archive: true
this.folderFilter = null;
this.tagFilter = null;
this.quickLinkFilter = 'archive';
this.listQuery = '';
if (!this.responsive.isDesktop()) {
this.mobileNav.setActiveTab('list');
}
// If flyout is open, keep it or close? Close gracefully
this.scheduleCloseFlyout(150);
}
}
@@ -0,0 +1,157 @@
import { Component, ChangeDetectionStrategy, input, output, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { VaultNode, VaultFile, VaultFolder } from '../../../../types';
import { VaultService } from '../../../../services/vault.service';
import { BadgeCountComponent } from '../../../../app/shared/ui/badge-count.component';
@Component({
selector: 'app-trash-explorer',
template: `
<ul class="space-y-0.5">
@for(node of nodes(); track node.path) {
<li>
@if (isFolder(node)) {
@let folder = node;
<div>
<div
(click)="onFolderClick(folder)"
class="flex items-center cursor-pointer px-3 py-2 rounded-lg my-0.5 text-sm hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 transition-transform text-obs-l-text-muted dark:text-obs-d-text-muted" [class.rotate-90]="folder.isOpen" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 flex-shrink-0 text-obs-l-text-muted dark:text-obs-d-text-muted" 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"></path></svg>
<span class="font-semibold truncate">{{ folder.name }}</span>
<app-badge-count class="ml-auto" [count]="folderCount(folder.path)" color="slate"></app-badge-count>
</div>
@if (folder.isOpen) {
<div class="pl-5">
<app-trash-explorer
[nodes]="folder.children"
[selectedNoteId]="selectedNoteId()"
[foldersOnly]="foldersOnly()"
(folderSelected)="folderSelected.emit($event)"
(fileSelected)="onFileSelected($event)"
></app-trash-explorer>
</div>
}
</div>
} @else {
@if (!foldersOnly()) {
@let file = node;
<div
(click)="onFileSelected(file.id)"
class="flex items-center cursor-pointer px-3 py-2 rounded-lg my-0.5 text-sm ml-5"
[class.bg-obs-l-bg-main]="selectedNoteId() === file.id"
[class.dark:bg-obs-d-bg-main]="selectedNoteId() === file.id"
[class.hover:bg-slate-500/10]="selectedNoteId() !== file.id"
[class.dark:hover:bg-slate-200/10]="selectedNoteId() !== file.id"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 flex-shrink-0 text-obs-l-text-muted dark:text-obs-d-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<span class="truncate">{{ file.name }}</span>
</div>
}
}
</li>
}
</ul>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, BadgeCountComponent],
})
export class TrashExplorerComponent {
nodes = input.required<VaultNode[]>();
selectedNoteId = input<string | null>(null);
foldersOnly = input<boolean>(false);
fileSelected = output<string>();
folderSelected = output<string>();
private vaultService = inject(VaultService);
folderCount(path: string): number {
const counts = this.vaultService.folderCounts();
return counts[path] ?? 0;
}
onFileSelected(noteId: string) {
if(noteId) {
this.fileSelected.emit(noteId);
}
}
onFolderClick(folder: VaultFolder) {
this.toggleFolder(folder);
if (folder?.path) {
this.folderSelected.emit(folder.path);
}
}
toggleFolder(folder: VaultFolder) {
this.vaultService.toggleFolder(folder.path);
}
isFolder(node: VaultNode): node is VaultFolder {
return node.type === 'folder';
}
}
// export class TrashExplorerComponent {
// // align with FileExplorer API (even si non utilisés ici)
// selectedNoteId = input<string | null>(null);
// foldersOnly = input<boolean>(false);
// fileSelected = output<string>();
// folderSelected = output<string>();
// private vaultService = inject(VaultService);
// /** Arborescence limitée au dossier .trash */
// get nodes(): VaultNode[] {
// const tree = this.vaultService.trashTree();
// try { console.debug('[TrashExplorer] .trash tree', tree); } catch {}
// return tree ?? [];
// }
// /** Compte les éléments d’un dossier de la corbeille */
// trashFolderCount(path: string): number {
// const counts = this.vaultService.trashFolderCounts();
// const raw = (path || '').replace(/\\/g, '/');
// const norm = raw.replace(/^\/+|\/+$/g, '').toLowerCase();
// return (counts?.[norm] ?? counts?.[raw] ?? counts?.[raw.toLowerCase()] ?? 0);
// }
// onFileSelected(noteId: string) {
// if (noteId) {
// this.fileSelected.emit(noteId);
// }
// }
// onFolderClick(folder: VaultFolder) {
// this.toggleFolder(folder);
// if (folder?.path) {
// this.folderSelected.emit(folder.path);
// }
// }
// toggleFolder(folder: VaultFolder) {
// if (!folder?.path) return;
// this.vaultService.toggleFolder(folder.path);
// }
// isFolder(node: VaultNode): node is VaultFolder {
// return node?.type === 'folder';
// }
// trackPath(_: number, n: VaultNode) {
// return n.path;
// }
// getFolderLabel(folder: VaultFolder): string {
// const name = (folder?.name || '').trim();
// if (name) return name;
// const path = (folder?.path || '').replace(/\\/g, '/');
// const last = path.split('/').filter(Boolean).pop();
// return last || '(unnamed)';
// }
// }
@@ -0,0 +1,33 @@
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-badge-count',
standalone: true,
imports: [CommonModule],
template: `
<span
class="inline-flex items-center justify-center w-6 h-6 rounded-full text-xs font-semibold text-white"
[ngClass]="bgClass"
aria-label="count"
>
{{ count }}
</span>
`,
})
export class BadgeCountComponent {
@Input() count = 0;
@Input() color: 'slate'|'rose'|'amber'|'indigo'|'emerald'|'stone'|'zinc' = 'slate';
get bgClass() {
return {
'bg-slate-600': this.color === 'slate',
'bg-rose-600': this.color === 'rose',
'bg-amber-600': this.color === 'amber',
'bg-indigo-600': this.color === 'indigo',
'bg-emerald-600': this.color === 'emerald',
'bg-stone-600': this.color === 'stone',
'bg-zinc-700': this.color === 'zinc',
};
}
}
@@ -2,6 +2,7 @@ import { Component, ChangeDetectionStrategy, input, output, inject } from '@angu
import { CommonModule } from '@angular/common';
import { VaultNode, VaultFile, VaultFolder } from '../../types';
import { VaultService } from '../../services/vault.service';
import { BadgeCountComponent } from '../../app/shared/ui/badge-count.component';
@Component({
selector: 'app-file-explorer',
@@ -9,18 +10,19 @@ import { VaultService } from '../../services/vault.service';
<ul class="space-y-0.5">
@for(node of nodes(); track node.path) {
<li>
@if (isFolder(node)) {
@if (isFolder(node) && node.name !== '.trash') {
@let folder = node;
<div>
<div
(click)="onFolderClick(folder)"
class="flex items-center cursor-pointer p-2 rounded my-0.5 text-sm hover:bg-obs-l-bg-main dark:hover:bg-obs-d-bg-main"
class="flex items-center cursor-pointer px-3 py-2 rounded-lg my-0.5 text-sm hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 transition-transform text-obs-l-text-muted dark:text-obs-d-text-muted" [class.rotate-90]="folder.isOpen" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 flex-shrink-0 text-obs-l-text-muted dark:text-obs-d-text-muted" 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"></path></svg>
<span class="font-semibold truncate">{{ folder.name }}</span>
<app-badge-count class="ml-auto" [count]="folderCount(folder.path)" color="slate"></app-badge-count>
</div>
@if (folder.isOpen) {
<div class="pl-5">
@@ -39,11 +41,11 @@ import { VaultService } from '../../services/vault.service';
@let file = node;
<div
(click)="onFileSelected(file.id)"
class="flex items-center cursor-pointer p-2 rounded my-0.5 text-sm ml-5"
class="flex items-center cursor-pointer px-3 py-2 rounded-lg my-0.5 text-sm ml-5"
[class.bg-obs-l-bg-main]="selectedNoteId() === file.id"
[class.dark:bg-obs-d-bg-main]="selectedNoteId() === file.id"
[class.hover:bg-obs-l-bg-main]="selectedNoteId() !== file.id"
[class.dark:hover:bg-obs-d-bg-main]="selectedNoteId() !== file.id"
[class.hover:bg-slate-500/10]="selectedNoteId() !== file.id"
[class.dark:hover:bg-slate-200/10]="selectedNoteId() !== file.id"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 flex-shrink-0 text-obs-l-text-muted dark:text-obs-d-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
@@ -57,17 +59,30 @@ import { VaultService } from '../../services/vault.service';
</ul>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule],
imports: [CommonModule, BadgeCountComponent],
})
export class FileExplorerComponent {
nodes = input.required<VaultNode[]>();
selectedNoteId = input<string | null>(null);
foldersOnly = input<boolean>(false);
useTrashCounts = input<boolean>(false);
fileSelected = output<string>();
folderSelected = output<string>();
private vaultService = inject(VaultService);
folderCount(path: string): number {
if (this.useTrashCounts()) {
const counts = this.vaultService.trashFolderCounts();
const raw = (path || '').replace(/\\/g, '/');
const norm = raw.replace(/^\/+|\/+$/g, '').toLowerCase();
return (counts[norm] ?? counts[raw] ?? counts[raw.toLowerCase()] ?? 0);
} else {
const counts = this.vaultService.folderCounts();
return counts[path] ?? 0;
}
}
onFileSelected(noteId: string) {
if(noteId) {
this.fileSelected.emit(noteId);
File diff suppressed because it is too large Load Diff
+2
View File
@@ -10,6 +10,8 @@ export interface NoteFrontmatter {
status?: string;
publish?: boolean;
favoris?: boolean;
template?: boolean;
task?: boolean;
archive?: boolean;
draft?: boolean;
private?: boolean;