ObsiViewer/src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts
Bruno Charest 59d8a9f83a feat: add multi-select notes and Gemini AI integration
- Implemented multi-selection for notes with Ctrl+click, long-press, and keyboard shortcuts (Ctrl+A, Escape)
- Added Gemini API integration with environment configuration and routes
- Enhanced code block UI with improved copy feedback animation and visual polish
- Added sort order toggle (asc/desc) for note lists with persistent state
2025-11-04 09:54:03 -05:00

1091 lines
50 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Component, EventEmitter, HostListener, Input, Output, inject, effect, 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';
import { MobileNavService } from '../../shared/services/mobile-nav.service';
import { FileExplorerComponent } from '../../../components/file-explorer/file-explorer.component';
import { NoteViewerComponent } from '../../../components/tags-view/note-viewer/note-viewer.component';
import { VaultService } from '../../../services/vault.service';
import type { VaultNode, Note, TagInfo } from '../../../types';
import { AppBottomNavigationComponent } from '../../features/bottom-nav/app-bottom-navigation.component';
import { AppSidebarDrawerComponent } from '../../features/sidebar/app-sidebar-drawer.component';
import { AppTocOverlayComponent } from '../../features/note-view/app-toc-overlay.component';
import { SwipeNavDirective } from '../../shared/directives/swipe-nav.directive';
import { PaginatedNotesListComponent } from '../../features/list/paginated-notes-list.component';
import { NimbusSidebarComponent } from '../../features/sidebar/nimbus-sidebar.component';
import { QuickLinksComponent } from '../../features/quick-links/quick-links.component';
import { ScrollableOverlayDirective } from '../../shared/overlay-scrollbar/scrollable-overlay.directive';
import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playground/markdown-playground.component';
import { TestsPanelComponent } from '../../features/tests/tests-panel.component';
import { TestExcalidrawPageComponent } from '../../features/tests/test-excalidraw-page.component';
import { ParametersPage } from '../../features/parameters/parameters.page';
import { AboutPanelComponent } from '../../features/about/about-panel.component';
import { UrlStateService } from '../../services/url-state.service';
import { FilterService } from '../../services/filter.service';
import { NoteInfoModalComponent } from '../../features/note-info/note-info-modal.component';
import { NoteInfoModalService } from '../../services/note-info-modal.service';
import { InPageSearchService } from '../../shared/search/in-page-search.service';
import { InPageSearchOverlayComponent } from '../../shared/search/in-page-search-overlay.component';
import { GeminiPanelComponent } from '../../features/gemini/gemini-panel.component';
import { AIToolsService } from '../../services/ai-tools.service';
import { KeyboardShortcutsService } from '../../services/keyboard-shortcuts.service';
@Component({
selector: 'app-shell-nimbus-layout',
standalone: true,
imports: [CommonModule, FileExplorerComponent, NoteViewerComponent, AppBottomNavigationComponent, AppSidebarDrawerComponent, AppTocOverlayComponent, SwipeNavDirective, PaginatedNotesListComponent, NimbusSidebarComponent, QuickLinksComponent, ScrollableOverlayDirective, MarkdownPlaygroundComponent, TestsPanelComponent, TestExcalidrawPageComponent, ParametersPage, AboutPanelComponent, NoteInfoModalComponent, InPageSearchOverlayComponent, GeminiPanelComponent],
template: `
<div class="relative h-screen flex flex-col bg-card dark:bg-main text-main dark:text-gray-100" [style.--sidebar-width.px]="isSidebarOpen ? leftSidebarWidth : 64">
<!-- Fullscreen overlay for note -->
<div *ngIf="noteFullScreen && selectedNote && activeView !== 'markdown-playground' && activeView !== 'tests-excalidraw'" class="absolute inset-0 z-50 flex flex-col bg-card dark:bg-main">
<div class="note-content-area flex-1 overflow-y-auto" appScrollableOverlay>
<app-note-viewer
[note]="selectedNote || null"
[noteHtmlContent]="renderedNoteContent"
[allNotes]="vault.allNotes()"
(noteLinkClicked)="noteSelected.emit($event)"
(tagClicked)="onTagSelected($event)"
(wikiLinkActivated)="wikiLinkActivated.emit($event)"
[fullScreenActive]="noteFullScreen"
(fullScreenRequested)="toggleNoteFullScreen()"
(legacyRequested)="ui.toggleUIMode()"
(parametersRequested)="onParametersOpen()"
(searchRequested)="openInPageSearch()"
(showToc)="toggleOutlineRequest.emit()"
(directoryClicked)="onFolderSelected($event)"
[tocOpen]="isOutlineOpen"
></app-note-viewer>
<app-in-page-search-overlay></app-in-page-search-overlay>
</div>
</div>
<!-- Desktop 3-column layout -->
<div *ngIf="responsive.isDesktop() && !noteFullScreen" class="flex-1 flex overflow-hidden relative">
<!-- Left: full sidebar or collapsed rail -->
<ng-container *ngIf="isSidebarOpen; else collapsedRail">
<aside class="flex flex-col border-r border-border dark:border-gray-800 min-h-0" [style.width.px]="leftSidebarWidth">
<app-nimbus-sidebar
[vaultName]="vaultName"
[effectiveFileTree]="effectiveFileTree"
[selectedNoteId]="selectedNoteId"
[tags]="tags"
[quickLinkFilter]="quickLinkFilter"
[forceOpenSection]="
(!tagFilter && !folderFilter && !quickLinkFilter && urlState.activeKind() === 'markdown')
? 'quick'
: (tagFilter ? 'tags' : (folderFilter ? 'folders' : (quickLinkFilter ? 'quick' : null)))
"
(toggleSidebarRequest)="toggleSidebarRequest.emit()"
(folderSelected)="onFolderSelected($event)"
(fileSelected)="noteSelected.emit($event)"
(tagSelected)="onTagSelected($event)"
(quickLinkSelected)="onQuickLink($event)"
(markdownPlaygroundSelected)="onMarkdownPlaygroundSelected()"
(testsPanelSelected)="onTestsPanelSelected()"
(testsExcalidrawSelected)="onTestsExcalidrawSelected()"
(helpPageSelected)="onHelpPageSelected()"
(aboutSelected)="onAboutSelected()"
(noteCreated)="onNoteCreated($event)"
(aiToolSelected)="onAIToolSelected($event)"
/>
</aside>
</ng-container>
<ng-template #collapsedRail>
<aside class="border-r border-border dark:border-gray-800 h-full w-14 flex flex-col items-center py-3 gap-3">
<img src="assets/favicon.svg" alt="ObsiViewer" class="h-6 w-6 mb-2" />
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (click)="toggleSidebarRequest.emit()" title="Show Sidebar">☰</button>
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('quick')" (mouseleave)="scheduleCloseFlyout()" title="Quick Links">▦</button>
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('folders')" (mouseleave)="scheduleCloseFlyout()" title="Folders">📁</button>
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('tags')" (mouseleave)="scheduleCloseFlyout()" title="Tags">🏷️</button>
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('ai')" (mouseleave)="scheduleCloseFlyout()" title="AI Tools">🤖</button>
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('trash')" (mouseleave)="scheduleCloseFlyout()" title="Trash">🗑️</button>
<div class="h-px w-8 bg-border dark:bg-gray-800 my-1"></div>
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('tests')" (mouseleave)="scheduleCloseFlyout()" title="Tests">🧪</button>
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('help')" (mouseleave)="scheduleCloseFlyout()" title="Help">🆘</button>
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('about')" (mouseleave)="scheduleCloseFlyout()" title="About"></button>
</aside>
<!-- Flyouts -->
<div class="absolute left-14 top-0 bottom-0 w-80 max-w-[70vw] bg-card dark:bg-main border-r border-border dark:border-gray-800 shadow-xl z-50" *ngIf="hoveredFlyout as f" (mouseenter)="cancelCloseFlyout()" (mouseleave)="scheduleCloseFlyout()">
<div class="h-12 flex items-center justify-between px-3 border-b border-border dark:border-gray-800">
<div class="text-sm font-semibold">{{ flyoutTitle(f) }}</div>
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (click)="hoveredFlyout=null">✕</button>
</div>
<div class="h-[calc(100%-3rem)] overflow-y-auto" appScrollableOverlay>
<ng-container [ngSwitch]="f">
<app-quick-links *ngSwitchCase="'quick'" (quickLinkSelected)="onQuickLink($event)"></app-quick-links>
<div *ngSwitchCase="'folders'" class="p-2">
<app-file-explorer [nodes]="effectiveFileTree" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" [quickLinkFilter]="quickLinkFilter" (folderSelected)="onFolderSelected($event)" (fileSelected)="noteSelected.emit($event)"></app-file-explorer>
</div>
<div *ngSwitchCase="'tags'" class="p-2">
<ul class="space-y-0.5 text-sm">
<li *ngFor="let t of tags">
<button (click)="onTagSelected(t.name)" class="w-full text-left px-2 py-1 rounded hover:bg-surface1 dark:hover:bg-card truncate">🏷️ {{ t.name }} <span class="text-xs text-muted">{{ t.count }}</span></button>
</li>
</ul>
</div>
<div *ngSwitchCase="'ai'" class="p-2">
<ul class="space-y-0.5 text-sm">
<li *ngFor="let tool of aiTools.tools">
<button type="button" class="w-full text-left px-2 py-1 rounded hover:bg-surface1 dark:hover:bg-card truncate"
[disabled]="!tool.enabled" [class.opacity-50]="!tool.enabled" (click)="onAIToolSelected(tool.id)">
<span>{{ tool.icon }}</span>
<span class="ml-2">{{ tool.label }}</span>
</button>
</li>
</ul>
</div>
<div *ngSwitchCase="'trash'" class="p-2">
<ng-container *ngIf="(vault.trashTree() || []).length > 0; else emptyTrash">
<app-file-explorer
[nodes]="vault.trashTree()"
[selectedNoteId]="selectedNoteId"
[foldersOnly]="true"
[useTrashCounts]="true"
(folderSelected)="onFolderSelected($event)"
(fileSelected)="noteSelected.emit($event)">
</app-file-explorer>
</ng-container>
<ng-template #emptyTrash>
<div class="px-3 py-2 text-muted text-sm">La corbeille est vide</div>
</ng-template>
</div>
<div *ngSwitchCase="'help'" class="p-3">
<button type="button" class="w-full text-left flex items-center gap-2 px-3 py-2 text-sm rounded-lg hover:bg-surface1 dark:hover:bg-card" (click)="onHelpPageSelected(); $event.stopPropagation(); hoveredFlyout = null">🆘 <span>Help Page</span></button>
</div>
<div *ngSwitchCase="'about'" class="p-3">
<button type="button" class="w-full text-left flex items-center gap-2 px-3 py-2 text-sm rounded-lg hover:bg-surface1 dark:hover:bg-card" (click)="onAboutSelected(); $event.stopPropagation(); hoveredFlyout = null"> <span>About</span></button>
</div>
<div *ngSwitchCase="'tests'" class="p-3">
<button type="button" class="w-full text-left flex items-center gap-2 px-3 py-2 text-sm rounded-lg hover:bg-surface1 dark:hover:bg-card" (click)="onTestsPanelSelected(); $event.stopPropagation(); hoveredFlyout = null">🔬 <span>API Tests Panel</span></button>
<button type="button" class="w-full text-left flex items-center gap-2 px-3 py-2 text-sm rounded-lg hover:bg-surface1 dark:hover:bg-card" (click)="onMarkdownPlaygroundSelected(); $event.stopPropagation(); hoveredFlyout = null">📝 <span>Markdown Playground</span></button>
<button type="button" class="w-full text-left flex items-center gap-2 px-3 py-2 text-sm rounded-lg hover:bg-surface1 dark:hover:bg-card" (click)="onTestsExcalidrawSelected(); $event.stopPropagation(); hoveredFlyout = null">🎨 <span>Test Excalidraw</span></button>
</div>
<div *ngSwitchCase="'playground'" class="p-3">
<button type="button" class="w-full text-left flex items-center gap-2 px-3 py-2 text-sm rounded-lg hover:bg-surface1 dark:hover:bg-card" (click)="onMarkdownPlaygroundSelected(); $event.stopPropagation(); hoveredFlyout = null">🧪 <span>Markdown Playground</span></button>
</div>
<div *ngSwitchDefault class="p-3 text-sm text-muted">Empty</div>
</ng-container>
</div>
</div>
</ng-template>
<!-- Left Resizer -->
<div class="h-full w-1 cursor-col-resize hover:bg-surface2/50 dark:hover:bg-surface2/50" (pointerdown)="leftResizeStart.emit($event)" role="separator" aria-orientation="vertical" aria-label="Redimensionner la barre latérale gauche"></div>
<!-- Center List -->
<section class="border-r border-border dark:border-gray-800 overflow-hidden" [style.width.px]="centerPanelWidth">
<div class="h-full flex flex-col">
<app-paginated-notes-list class="flex-1"
[folderFilter]="folderFilter"
[tagFilter]="tagFilter"
[quickLinkFilter]="quickLinkFilter"
[kindFilter]="urlState.activeKind()"
[query]="listQuery"
[selectedId]="selectedNoteId"
(openNote)="onOpenNote($event)"
(queryChange)="onQueryChange($event)"
(clearQuickLinkFilter)="onClearQuickLinkFilter()"
(clearFolderFilter)="onClearFolderFromList()"
/>
</div>
</section>
<!-- Center Resizer (between list and note) -->
<div class="h-full w-1 cursor-col-resize hover:bg-surface2/50 dark:hover:bg-surface2/50" (pointerdown)="centerResizeStart.emit($event)" role="separator" aria-orientation="vertical" aria-label="Redimensionner la zone de liste"></div>
<!-- Note View + ToC -->
<section class="flex-1 relative min-w-0 flex">
<div #pageRoot class="note-content-area flex-1 overflow-y-auto" appScrollableOverlay>
<app-markdown-playground *ngIf="activeView === 'markdown-playground'"></app-markdown-playground>
<app-parameters *ngIf="activeView === 'parameters'"></app-parameters>
<app-tests-panel *ngIf="activeView === 'tests-panel'"></app-tests-panel>
<app-test-excalidraw-page *ngIf="activeView === 'tests-excalidraw'"></app-test-excalidraw-page>
<app-note-viewer *ngIf="activeView !== 'markdown-playground' && activeView !== 'parameters' && activeView !== 'tests-panel' && activeView !== 'tests-excalidraw'"
[note]="selectedNote || null"
[noteHtmlContent]="renderedNoteContent"
[allNotes]="vault.allNotes()"
(noteLinkClicked)="noteSelected.emit($event)"
(tagClicked)="onTagSelected($event)"
(wikiLinkActivated)="wikiLinkActivated.emit($event)"
(searchRequested)="openInPageSearch()"
(fullScreenRequested)="toggleNoteFullScreen()"
(parametersRequested)="onParametersOpen()"
(showToc)="toggleOutlineRequest.emit()"
(directoryClicked)="onFolderSelected($event)"
[tocOpen]="isOutlineOpen"
></app-note-viewer>
<app-in-page-search-overlay></app-in-page-search-overlay>
</div>
<aside class="hidden xl:block border-l border-border dark:border-gray-800 overflow-y-auto transition-all duration-300 ease-in-out" appScrollableOverlay [style.width.px]="isOutlineOpen ? rightSidebarWidth : 0" [class.opacity-0]="!isOutlineOpen" [class.pointer-events-none]="!isOutlineOpen">
<div class="p-3 space-y-3">
<div class="flex items-center justify-between">
<h2 class="text-sm font-semibold">Sommaire</h2>
<button
type="button"
class="inline-flex items-center justify-center w-8 h-8 rounded-full hover:bg-surface1 dark:hover:bg-card transition"
aria-label="Fermer le sommaire"
(click)="toggleOutlineRequest.emit()">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
<ul class="space-y-1 text-sm text-muted dark:text-main">
<li *ngFor="let h of tableOfContents" class="leading-tight">
<button
type="button"
class="w-full text-left block px-3 py-2 rounded-lg transition-colors hover:bg-slate-500/10 dark:hover:bg-surface2/15 focus:outline-none"
(click)="navigateHeading.emit(h.id)"
[style.paddingLeft.rem]="(h.level - 1) * 0.75"
>
<span class="block truncate text-muted dark:text-main hover:text-main dark:hover:text-white">
{{ h.text }}
</span>
</button>
</li>
</ul>
</div>
</aside>
</section>
</div>
<!-- Tablet: simple tabbed areas -->
<div *ngIf="responsive.isTablet() && !noteFullScreen" class="flex-1 flex flex-col overflow-hidden">
<div class="h-12 border-b border-border dark:border-gray-800 flex items-center">
<button class="flex-1 h-full text-sm" [class.text-nimbus-500]="mobileNav.activeTab() === 'sidebar'" (click)="mobileNav.setActiveTab('sidebar')">Sidebar</button>
<button class="flex-1 h-full text-sm" [class.text-nimbus-500]="mobileNav.activeTab() === 'list'" (click)="mobileNav.setActiveTab('list')">Liste</button>
<button class="flex-1 h-full text-sm" [class.text-nimbus-500]="mobileNav.activeTab() === 'page'" (click)="mobileNav.setActiveTab('page')">Page</button>
</div>
<div class="flex-1 overflow-hidden">
<div [hidden]="mobileNav.activeTab() !== 'sidebar'" class="h-full overflow-y-auto p-2" appScrollableOverlay>
<app-file-explorer [nodes]="effectiveFileTree" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" [quickLinkFilter]="quickLinkFilter" (folderSelected)="onFolderSelected($event)" (fileSelected)="onOpenNote($event)"></app-file-explorer>
</div>
<div [hidden]="mobileNav.activeTab() !== 'list'" class="h-full overflow-y-auto" appScrollableOverlay>
<app-paginated-notes-list
[folderFilter]="folderFilter"
[tagFilter]="tagFilter"
[quickLinkFilter]="quickLinkFilter"
[kindFilter]="urlState.activeKind()"
[query]="listQuery"
[selectedId]="selectedNoteId"
(queryChange)="onQueryChange($event)"
(openNote)="onOpenNote($event)"
(clearQuickLinkFilter)="onClearQuickLinkFilter()"
></app-paginated-notes-list>
</div>
<div [hidden]="mobileNav.activeTab() !== 'page'" class="note-content-area h-full overflow-y-auto" appScrollableOverlay>
<app-markdown-playground *ngIf="activeView === 'markdown-playground'"></app-markdown-playground>
<app-parameters *ngIf="activeView === 'parameters'"></app-parameters>
<app-tests-panel *ngIf="activeView === 'tests-panel'"></app-tests-panel>
<app-test-excalidraw-page *ngIf="activeView === 'tests-excalidraw'"></app-test-excalidraw-page>
<app-note-viewer *ngIf="activeView !== 'markdown-playground' && activeView !== 'parameters' && activeView !== 'tests-panel' && activeView !== 'tests-excalidraw'" [note]="selectedNote || null" [noteHtmlContent]="renderedNoteContent" [allNotes]="vault.allNotes()" (noteLinkClicked)="noteSelected.emit($event)" (tagClicked)="onTagSelected($event)" (wikiLinkActivated)="wikiLinkActivated.emit($event)" [fullScreenActive]="noteFullScreen" (fullScreenRequested)="toggleNoteFullScreen()" (legacyRequested)="ui.toggleUIMode()" (parametersRequested)="onParametersOpen()" (showToc)="mobileNav.toggleToc()" (directoryClicked)="onFolderSelected($event)" [tocOpen]="mobileNav.tocOpen()"></app-note-viewer>
</div>
</div>
</div>
<!-- Mobile: bottom nav + drawer + swipe -->
<div *ngIf="responsive.isMobile() && !noteFullScreen" class="flex-1 relative overflow-hidden" appSwipeNav (swipeLeft)="nextTab()" (swipeRight)="prevTab()">
<app-sidebar-drawer
[nodes]="effectiveFileTree"
[selectedNoteId]="selectedNoteId"
[vaultName]="vaultName"
[tags]="tags"
[quickLinkFilter]="quickLinkFilter"
(noteSelected)="onNoteSelectedMobile($event)"
(folderSelected)="onFolderSelectedFromDrawer($event)"
(tagSelected)="onTagSelected($event)"
(quickLinkSelected)="onQuickLink($event)"
(markdownPlaygroundSelected)="onMarkdownPlaygroundSelected()"
(testsPanelSelected)="onTestsPanelSelected()"
(testsExcalidrawSelected)="onTestsExcalidrawSelected()"
(helpPageSelected)="onHelpPageSelected()"
(aboutSelected)="onAboutSelected()"
(aiToolSelected)="onAIToolSelected($event)"
></app-sidebar-drawer>
@if (mobileNav.activeTab() === 'list') {
<div class="h-full flex flex-col overflow-hidden animate-fadeIn">
<app-paginated-notes-list class="flex-1"
[folderFilter]="folderFilter"
[tagFilter]="tagFilter"
[quickLinkFilter]="quickLinkFilter"
[kindFilter]="urlState.activeKind()"
[query]="listQuery"
[selectedId]="selectedNoteId"
(queryChange)="onQueryChange($event)"
(openNote)="onNoteSelectedMobile($event)"
(clearQuickLinkFilter)="onClearQuickLinkFilter()"
></app-paginated-notes-list>
</div>
}
@if (mobileNav.activeTab() === 'page') {
@if (activeView === 'parameters') {
<div class="note-content-area h-full animate-fadeIn" style="overflow-y: auto !important;" appScrollableOverlay>
<app-parameters></app-parameters>
</div>
} @else if (activeView === 'markdown-playground') {
<div class="note-content-area h-full animate-fadeIn" style="overflow-y: auto !important;" appScrollableOverlay>
<app-markdown-playground></app-markdown-playground>
</div>
} @else if (activeView === 'tests-panel') {
<div class="note-content-area h-full animate-fadeIn" style="overflow-y: auto !important;" appScrollableOverlay>
<app-tests-panel></app-tests-panel>
</div>
} @else if (activeView === 'tests-excalidraw') {
<div class="note-content-area h-full animate-fadeIn" style="overflow-y: auto !important;" appScrollableOverlay>
<app-test-excalidraw-page></app-test-excalidraw-page>
</div>
} @else {
<div class="note-content-area h-full animate-fadeIn" style="overflow-y: auto !important;" appScrollableOverlay>
@if (selectedNote) {
<app-note-viewer [note]="selectedNote || null" [noteHtmlContent]="renderedNoteContent" [allNotes]="vault.allNotes()" (noteLinkClicked)="noteSelected.emit($event)" (tagClicked)="onTagSelected($event)" (wikiLinkActivated)="wikiLinkActivated.emit($event)" (searchRequested)="openInPageSearch()" (fullScreenRequested)="toggleNoteFullScreen()" (parametersRequested)="onParametersOpen()"></app-note-viewer>
} @else {
<div class="mt-10 text-center text-sm text-muted dark:text-muted">
<div class="text-4xl mb-3">📄</div>
<p>Aucune page sélectionnée pour le moment.</p>
</div>
}
</div>
}
}
<app-toc-overlay *ngIf="mobileNav.tocOpen()" [headings]="tableOfContents" (go)="onTocNavigate($event)" (close)="mobileNav.toggleToc()"></app-toc-overlay>
<app-bottom-navigation></app-bottom-navigation>
</div>
<!-- About Panel Overlay -->
<app-about-panel *ngIf="showAboutPanel" (close)="showAboutPanel = false"></app-about-panel>
<!-- Note Info Modal -->
<app-note-info-modal *ngIf="noteInfo.visible()" [note]="noteInfo.note()!" (close)="noteInfo.close()"></app-note-info-modal>
<!-- Gemini Panel -->
<app-gemini-panel *ngIf="showGeminiPanel" [selectedNote]="selectedNote" (close)="showGeminiPanel = false"></app-gemini-panel>
</div>
`
})
export class AppShellNimbusLayoutComponent implements AfterViewInit {
ui = inject(UiModeService);
vault = inject(VaultService);
responsive = inject(ResponsiveService);
mobileNav = inject(MobileNavService);
urlState = inject(UrlStateService);
filters = inject(FilterService);
noteInfo = inject(NoteInfoModalService);
inPageSearch = inject(InPageSearchService);
aiTools = inject(AIToolsService);
keyboard = inject(KeyboardShortcutsService);
noteFullScreen = false;
showAboutPanel = false;
showGeminiPanel = false;
@Input() vaultName = '';
@Input() effectiveFileTree: VaultNode[] = [];
@Input() selectedNoteId: string | null = '';
@Input() selectedNote: Note | undefined;
@Input() renderedNoteContent = '';
@Input() tableOfContents: Array<{ level: number; text: string; id: string }> = [];
@Input() isSidebarOpen = true;
@Input() isOutlineOpen = false;
@Input() leftSidebarWidth = 288;
@Input() rightSidebarWidth = 288;
@Input() searchTerm = '';
@Input() centerPanelWidth = 384;
@Input() tags: TagInfo[] = [];
@Input() activeView: string = 'files';
@ViewChild('pageRoot', { static: false }) pageRoot?: ElementRef<HTMLElement>;
@Output() noteSelected = new EventEmitter<string>();
@Output() tagClicked = new EventEmitter<string>();
@Output() wikiLinkActivated = new EventEmitter<any>();
@Output() toggleSidebarRequest = new EventEmitter<void>();
@Output() toggleOutlineRequest = new EventEmitter<void>();
@Output() leftResizeStart = new EventEmitter<PointerEvent>();
@Output() rightResizeStart = new EventEmitter<PointerEvent>();
@Output() centerResizeStart = new EventEmitter<PointerEvent>();
@Output() navigateHeading = new EventEmitter<string>();
@Output() searchTermChange = new EventEmitter<string>();
@Output() searchOptionsChange = new EventEmitter<any>();
@Output() noteCreated = new EventEmitter<string>();
@Output() noteCreatedAndSelected = new EventEmitter<{ id: string; filePath: string }>();
@Output() markdownPlaygroundSelected = new EventEmitter<void>();
@Output() parametersOpened = new EventEmitter<void>();
@Output() helpPageRequested = new EventEmitter<void>();
@Output() testsPanelRequested = new EventEmitter<void>();
@Output() testsExcalidrawRequested = new EventEmitter<void>();
folderFilter: string | null = null;
listQuery: string = '';
hoveredFlyout: 'quick' | 'folders' | 'tags' | 'ai' | 'trash' | 'help' | 'about' | 'tests' | 'playground' | null = null;
private flyoutCloseTimer: any = null;
tagFilter: string | null = null;
quickLinkFilter: 'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | null = null;
private suppressNextNoteSelection = false;
// --- URL State <-> Layout sync ---
private mapUrlQuickToInternal(q: string | null): AppShellNimbusLayoutComponent['quickLinkFilter'] {
switch ((q || '').toLowerCase()) {
case 'favoris':
case 'favorites':
return 'favoris';
case 'publié':
case 'publie':
case 'publish':
return 'publish';
case 'brouillons':
case 'drafts':
case 'draft':
return 'draft';
case 'modèles':
case 'modeles':
case 'templates':
case 'template':
return 'template';
case 'tâches':
case 'taches':
case 'tasks':
case 'task':
return 'task';
case 'privé':
case 'prive':
case 'private':
return 'private';
case 'archive':
return 'archive';
default:
return null;
}
}
private mapInternalQuickToUrl(id: AppShellNimbusLayoutComponent['quickLinkFilter']): string | null {
switch (id) {
case 'favoris': return 'Favoris';
case 'publish': return 'Publié';
case 'draft': return 'Brouillons';
case 'template': return 'Modèles';
case 'task': return 'Tâches';
case 'private': return 'Privé';
case 'archive': return 'Archive';
default: return null;
}
}
// React to URL state changes and align layout
_urlEffect = effect(() => {
const note = this.urlState.currentNote();
const tag = this.urlState.activeTag();
const folder = this.urlState.activeFolder();
const quick = this.urlState.activeQuickLink();
const search = this.urlState.activeSearch();
console.log('🎨 Layout _urlEffect:', {note, tag, folder, quick, search});
// Apply search query
if (search !== null && this.listQuery !== (search || '')) {
this.listQuery = search || '';
}
// If a note is specified, select it and focus page, but DO NOT early-return:
// we still want to apply list filters (tag/folder/quick) from the URL so the list matches.
const hasNote = !!note;
if (hasNote) {
if (this.selectedNoteId !== note!.id) {
this.noteSelected.emit(note!.id);
}
// Ensure page view visible on small screens
if (!this.responsive.isDesktop()) {
this.mobileNav.setActiveTab('page');
}
// Exit fullscreen if needed
if (this.noteFullScreen) {
this.noteFullScreen = false;
document.body.classList.remove('note-fullscreen-active');
}
}
// Otherwise, synchronize filters from URL
if (tag !== null) {
const norm = (tag || '').replace(/^#/, '').trim().toLowerCase();
if (this.tagFilter !== norm) {
this.tagFilter = norm || null;
this.folderFilter = null;
this.quickLinkFilter = null;
if (!hasNote) this.autoSelectFirstNote();
if (!this.responsive.isDesktop()) this.mobileNav.setActiveTab('list');
}
// Auto-open tags flyout when tag filter is active
if (this.hoveredFlyout !== 'tags') {
console.log('🎨 Layout - opening tags flyout for tag filter');
this.openFlyout('tags');
}
} else if (folder !== null) {
if (this.folderFilter !== (folder || null)) {
this.folderFilter = folder || null;
this.tagFilter = null;
this.quickLinkFilter = null;
if (!hasNote) this.autoSelectFirstNote();
if (!this.responsive.isDesktop()) this.mobileNav.setActiveTab('list');
}
// Auto-open folders flyout when folder filter is active
if (this.hoveredFlyout !== 'folders') {
console.log('🎨 Layout - opening folders flyout for folder filter');
this.openFlyout('folders');
}
} else if (quick !== null) {
const internal = this.mapUrlQuickToInternal(quick);
if (this.quickLinkFilter !== internal) {
this.quickLinkFilter = internal;
this.folderFilter = null;
this.tagFilter = null;
if (internal === 'favoris') {
this.suppressNextNoteSelection = true;
}
if (!hasNote && !this.suppressNextNoteSelection) {
this.autoSelectFirstNote();
}
if (!this.responsive.isDesktop()) {
this.mobileNav.setActiveTab('list');
}
} else if (!hasNote && !this.suppressNextNoteSelection) {
this.autoSelectFirstNote();
}
// Auto-open quick flyout when quick filter is active
if (this.hoveredFlyout !== 'quick') {
console.log('🎨 Layout - opening quick flyout for quick filter');
this.openFlyout('quick');
}
this.suppressNextNoteSelection = false;
} else {
// No filters -> show all
if (this.folderFilter || this.tagFilter || this.quickLinkFilter) {
this.folderFilter = null;
this.tagFilter = null;
this.quickLinkFilter = null;
if (!hasNote) this.autoSelectFirstNote();
}
this.suppressNextNoteSelection = false;
// Close any open flyout when no filters
if (this.hoveredFlyout) {
console.log('🎨 Layout - closing flyout (no active filters)');
this.scheduleCloseFlyout(0);
}
}
console.log('🎨 Layout filters after:', {
tagFilter: this.tagFilter,
folderFilter: this.folderFilter,
quickLinkFilter: this.quickLinkFilter,
hoveredFlyout: this.hoveredFlyout
});
});
// Auto-select first note when filters change
private autoSelectFirstNote() {
const filteredNotes = this.getFilteredNotes();
if (filteredNotes.length > 0 && filteredNotes[0].id !== this.selectedNoteId) {
this.noteSelected.emit(filteredNotes[0].id);
}
}
private getFilteredNotes(): Note[] {
const q = (this.listQuery || '').toLowerCase().trim();
const folder = (this.folderFilter || '').toLowerCase().replace(/^\/+|\/+$/g, '');
const tag = (this.tagFilter || '').toLowerCase();
const quickLink = this.quickLinkFilter;
let list = this.vault.allNotes();
// Exclude trash notes by default unless specifically viewing trash
if (folder !== '.trash') {
list = list.filter(n => {
const filePath = (n.filePath || n.originalPath || '').toLowerCase().replace(/\\/g, '/');
return !filePath.startsWith('.trash/') && !filePath.includes('/.trash/');
});
}
if (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.startsWith('.trash/') || 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
if (quickLink) {
list = list.filter(n => {
const frontmatter = n.frontmatter || {};
return frontmatter[quickLink] === true;
});
}
// Apply query if present
if (q) {
list = list.filter(n => {
const title = (n.title || '').toLowerCase();
const filePath = (n.filePath || '').toLowerCase();
return title.includes(q) || filePath.includes(q);
});
}
// Sort by most recent first (same as notes-list component)
const parseDate = (s?: string) => (s ? Date.parse(s) : 0) || 0;
const score = (n: Note) => n.mtime || parseDate(n.updatedAt) || parseDate(n.createdAt) || 0;
return [...list].sort((a, b) => (score(b) - score(a)));
}
ngAfterViewInit(): void {
queueMicrotask(() => this.inPageSearch.setRoot(this.pageRoot?.nativeElement || null));
}
@HostListener('document:keydown', ['$event'])
onKeydown(e: KeyboardEvent) {
const isFind = (e.ctrlKey || e.metaKey) && (e.key === 'f' || e.key === 'F');
if (isFind) {
e.preventDefault();
this.openInPageSearch();
} else if (e.key === 'Escape' && this.inPageSearch.openState()) {
this.inPageSearch.close();
} else if (e.key === 'Enter' && this.inPageSearch.openState()) {
if (e.shiftKey) this.inPageSearch.prev(); else this.inPageSearch.next();
e.preventDefault();
}
}
openInPageSearch(): void {
this.inPageSearch.open();
document.dispatchEvent(new CustomEvent('ov-search-focus'));
this.inPageSearch.setRoot(this.pageRoot?.nativeElement || null);
}
onQueryChange(query: string) {
this.listQuery = query;
// Only auto-select when query is cleared; while typing keep focus in search (handled by notes-list)
if (!query) {
this.autoSelectFirstNote();
}
// Sync URL search term
this.urlState.updateSearch(query);
}
toggleNoteFullScreen(): void {
this.noteFullScreen = !this.noteFullScreen;
document.body.classList.toggle('note-fullscreen-active', this.noteFullScreen);
}
/** Reuse the same behavior when a global fullscreen request event is dispatched */
@HostListener('window:noteFullScreenRequested', ['$event'])
onNoteFullScreenRequested(_evt: CustomEvent) {
this.toggleNoteFullScreen();
}
nextTab() {
const order: Array<'sidebar' | 'list' | 'page' | 'toc'> = ['sidebar', 'list', 'page', 'toc'];
const idx = order.indexOf(this.mobileNav.activeTab());
const next = order[Math.min(order.length - 1, idx + 1)] as any;
this.mobileNav.setActiveTab(next);
}
prevTab() {
const order: Array<'sidebar' | 'list' | 'page' | 'toc'> = ['sidebar', 'list', 'page', 'toc'];
const idx = order.indexOf(this.mobileNav.activeTab());
const prev = order[Math.max(0, idx - 1)] as any;
this.mobileNav.setActiveTab(prev);
}
async onOpenNote(target: string) {
try { console.debug('[Nimbus] onOpenNote', target); } catch {}
let filePath: string | null = null;
let noteId: string = target;
try {
const looksLikePath = /[/\\]/.test(target) || /\.[a-z0-9]+$/i.test(target);
if (looksLikePath) {
filePath = target.replace(/\\/g, '/');
try { await this.vault.ensureNoteLoadedByPath(filePath); } catch {}
try { noteId = this.vault.buildSlugIdFromPath(filePath); } catch {}
} else {
await this.vault.ensureNoteLoadedById(target);
const n = (this.vault.allNotes() || []).find(x => x.id === target);
filePath = n?.filePath || this.vault.getFastMetaById(target)?.path || null;
}
if (filePath) {
try { console.debug('[Nimbus] opening note path', filePath); } catch {}
this.urlState.openNote(filePath);
} else {
try { console.warn('[Nimbus] onOpenNote: no filePath resolved for', target); } catch {}
}
} finally {
try { console.debug('[Nimbus] emitting noteSelected', noteId); } catch {}
this.noteSelected.emit(noteId);
}
}
onClearFolderFromList() {
this.folderFilter = null;
this.urlState.clearFolderFilter();
}
onAboutSelected(): void {
this.showAboutPanel = true;
}
onGeminiPanelOpen(): void {
this.showGeminiPanel = true;
this.scheduleCloseFlyout(0); // Fermer les flyouts
}
/**
* Handler when an AI tool is selected from sidebars/flyouts.
* Do not change focus of the notes list; just execute and log.
*/
async onAIToolSelected(actionId: string): Promise<void> {
try {
// Get selected notes from keyboard service; fallback to current selected note
let notes = this.keyboard.selectedNotes();
if (!notes || notes.length === 0) {
if (this.selectedNote) notes = [this.selectedNote]; else notes = [];
}
console.log('[AI Tools] Action:', actionId, 'Notes:', notes.map(n => n.title));
if (notes.length === 0) {
console.warn('[AI Tools] No notes selected');
return;
}
const result = await this.aiTools.executeAction(actionId as any, notes as any);
console.log('[AI Tools] Result:', result);
} catch (e) {
console.error('[AI Tools] Execution error:', e);
} finally {
// Keep list focus; do not switch tabs or change URL
this.scheduleCloseFlyout(150);
}
}
onNoteCreated(noteId: string) {
this.noteCreated.emit(noteId);
}
onNoteCreatedAndSelected(event: { id: string; filePath: string }) {
this.noteCreatedAndSelected.emit(event);
}
onNoteSelectedMobile(noteId: string) {
if (!noteId) return;
this.noteSelected.emit(noteId);
this.mobileNav.setActiveTab('page');
}
onFolderSelected(path: string) {
this.folderFilter = path || null;
// Reset other filters and search when focusing a folder to prevent residual constraints
this.tagFilter = null;
this.quickLinkFilter = null;
this.listQuery = '';
try { this.filters.clearKinds(); } catch {}
try { this.filters.clearTags(); } catch {}
this.autoSelectFirstNote();
if (this.responsive.isMobile() || this.responsive.isTablet()) {
this.mobileNav.setActiveTab('list');
}
// Reflect folder in URL
if (path) {
// Clear search in URL to avoid residual query re-applying via URL effect
try { this.urlState.updateSearch(''); } catch {}
this.urlState.filterByFolder(path);
} else {
this.urlState.resetState();
}
}
onFolderSelectedFromDrawer(path: string) {
this.folderFilter = path || null;
// Reset other filters and search when focusing a folder to prevent residual constraints
this.tagFilter = null;
this.quickLinkFilter = null;
this.listQuery = '';
try { this.filters.clearKinds(); } catch {}
try { this.filters.clearTags(); } catch {}
this.autoSelectFirstNote();
this.mobileNav.setActiveTab('list');
this.mobileNav.sidebarOpen.set(false);
if (path) {
// Clear search in URL to avoid residual query re-applying via URL effect
try { this.urlState.updateSearch(''); } catch {}
this.urlState.filterByFolder(path);
} else {
this.urlState.resetState();
}
}
onQuickLink(_id: string) {
const suppressAutoSelect = _id === 'all' || _id === 'favorites';
this.suppressNextNoteSelection = suppressAutoSelect;
if (_id === 'all') {
// Show all pages: clear filters and focus list
this.folderFilter = null;
this.tagFilter = null;
this.quickLinkFilter = null;
this.listQuery = '';
// Clear local filters (kinds, cumulative tags) to avoid conflicting filters
try { this.filters.clearKinds(); } catch {}
try { this.filters.clearTags(); } catch {}
if (!this.responsive.isDesktop()) {
this.mobileNav.setActiveTab('list');
}
this.scheduleCloseFlyout(150);
this.urlState.setQuickWithMarkdown('all');
} else if (_id === 'publish') {
// Filter by publish: true
this.folderFilter = null;
this.tagFilter = null;
this.quickLinkFilter = 'publish';
this.listQuery = '';
try { this.filters.clearKinds(); } catch {}
try { this.filters.clearTags(); } catch {}
if (!this.responsive.isDesktop()) {
this.mobileNav.setActiveTab('list');
}
this.scheduleCloseFlyout(150);
const label = this.mapInternalQuickToUrl('publish');
if (label) {
this.urlState.setQuickWithMarkdown(label);
}
} else if (_id === 'favorites') {
// Filter by favoris: true
this.folderFilter = null;
this.tagFilter = null;
this.quickLinkFilter = 'favoris';
this.listQuery = '';
try { this.filters.clearKinds(); } catch {}
try { this.filters.clearTags(); } catch {}
if (!this.responsive.isDesktop()) {
this.mobileNav.setActiveTab('list');
}
this.scheduleCloseFlyout(150);
const label = this.mapInternalQuickToUrl('favoris');
if (label) {
this.urlState.setQuickWithMarkdown(label);
}
} else if (_id === 'templates') {
// Filter by template: true
this.folderFilter = null;
this.tagFilter = null;
this.quickLinkFilter = 'template';
this.listQuery = '';
try { this.filters.clearKinds(); } catch {}
try { this.filters.clearTags(); } catch {}
if (!this.responsive.isDesktop()) {
this.mobileNav.setActiveTab('list');
}
this.scheduleCloseFlyout(150);
const label = this.mapInternalQuickToUrl('template');
if (label) {
this.urlState.setQuickWithMarkdown(label);
}
} else if (_id === 'tasks') {
// Filter by task: true
this.folderFilter = null;
this.tagFilter = null;
this.quickLinkFilter = 'task';
this.listQuery = '';
try { this.filters.clearKinds(); } catch {}
try { this.filters.clearTags(); } catch {}
if (!this.responsive.isDesktop()) {
this.mobileNav.setActiveTab('list');
}
this.scheduleCloseFlyout(150);
const label = this.mapInternalQuickToUrl('task');
if (label) {
this.urlState.setQuickWithMarkdown(label);
}
} else if (_id === 'drafts') {
// Filter by draft: true
this.folderFilter = null;
this.tagFilter = null;
this.quickLinkFilter = 'draft';
this.listQuery = '';
try { this.filters.clearKinds(); } catch {}
try { this.filters.clearTags(); } catch {}
if (!this.responsive.isDesktop()) {
this.mobileNav.setActiveTab('list');
}
this.scheduleCloseFlyout(150);
const label = this.mapInternalQuickToUrl('draft');
if (label) {
this.urlState.setQuickWithMarkdown(label);
}
} else if (_id === 'private') {
// Filter by private: true
this.folderFilter = null;
this.tagFilter = null;
this.quickLinkFilter = 'private';
this.listQuery = '';
try { this.filters.clearKinds(); } catch {}
try { this.filters.clearTags(); } catch {}
if (!this.responsive.isDesktop()) {
this.mobileNav.setActiveTab('list');
}
this.scheduleCloseFlyout(150);
const label = this.mapInternalQuickToUrl('private');
if (label) {
this.urlState.setQuickWithMarkdown(label);
}
} else if (_id === 'archive') {
// Filter by archive: true
this.folderFilter = null;
this.tagFilter = null;
this.quickLinkFilter = 'archive';
this.listQuery = '';
try { this.filters.clearKinds(); } catch {}
try { this.filters.clearTags(); } catch {}
if (!this.responsive.isDesktop()) {
this.mobileNav.setActiveTab('list');
}
this.scheduleCloseFlyout(150);
const label = this.mapInternalQuickToUrl('archive');
if (label) {
this.urlState.setQuickWithMarkdown(label);
}
}
// Auto-select first note after filter changes
if (!this.suppressNextNoteSelection) {
this.autoSelectFirstNote();
}
this.suppressNextNoteSelection = false;
}
onTagSelected(tagName: string) {
const norm = (tagName || '').replace(/^#/, '').trim().toLowerCase();
if (!norm) return;
this.tagFilter = norm;
this.folderFilter = null; // clear folder when focusing tag
// Clear other filters and search to focus on tag results
this.quickLinkFilter = null;
this.listQuery = '';
// Auto-select first note after filter changes
this.autoSelectFirstNote();
// Ensure the list is visible: exit fullscreen if active
if (this.noteFullScreen) {
this.noteFullScreen = false;
document.body.classList.remove('note-fullscreen-active');
}
// Bubble up for global handlers (keeps parity with right sidebar tags)
this.tagClicked.emit(norm);
if (!this.responsive.isDesktop()) {
this.mobileNav.setActiveTab('list');
}
// If from flyout, do not close immediately; small delay allows click feedback
this.scheduleCloseFlyout(200);
// Reflect in URL using original (non-normalized) tag label
if (tagName) {
this.urlState.filterByTag(tagName.replace(/^#/, '').trim());
}
}
openFlyout(which: 'quick' | 'folders' | 'tags' | 'ai' | 'trash') {
this.cancelCloseFlyout();
this.hoveredFlyout = which;
}
scheduleCloseFlyout(delay = 200) {
this.cancelCloseFlyout();
this.flyoutCloseTimer = setTimeout(() => {
this.hoveredFlyout = null;
this.flyoutCloseTimer = null;
}, delay);
}
cancelCloseFlyout() {
if (this.flyoutCloseTimer) {
clearTimeout(this.flyoutCloseTimer);
this.flyoutCloseTimer = null;
}
}
flyoutTitle(which: 'quick' | 'folders' | 'tags' | 'ai' | 'trash' | 'help' | 'about' | 'tests' | 'playground' | null): string {
switch (which) {
case 'quick': return 'Quick Links';
case 'folders': return 'Folders';
case 'tags': return 'Tags';
case 'ai': return 'AI Tools';
case 'trash': return 'Trash';
case 'help': return 'Help';
case 'about': return 'About';
case 'tests': return 'Tests';
case 'playground': return 'Playground';
default: return '';
}
}
onMarkdownPlaygroundSelected(): void {
if (this.responsive.isMobile()) {
this.mobileNav.setActiveTab('page');
}
this.markdownPlaygroundSelected.emit();
}
onTestsPanelSelected(): void {
if (this.responsive.isMobile()) {
this.mobileNav.setActiveTab('page');
}
this.testsPanelRequested.emit();
}
onTestsExcalidrawSelected(): void {
if (this.responsive.isMobile()) {
this.mobileNav.setActiveTab('page');
}
this.testsExcalidrawRequested.emit();
}
onParametersOpen(): void {
this.parametersOpened.emit();
}
onTocNavigate(headingId: string): void {
// Ensure the page view is visible so the scroll container exists
this.mobileNav.setActiveTab('page');
// Close the TOC overlay immediately
if (this.mobileNav.tocOpen()) {
this.mobileNav.toggleToc();
}
// Wait for DOM to update before scrolling
setTimeout(() => {
this.navigateHeading.emit(headingId);
}, 100);
}
onHelpPageSelected(): void {
this.helpPageRequested.emit();
}
onClearQuickLinkFilter(): void {
this.folderFilter = null;
this.tagFilter = null;
this.quickLinkFilter = null;
this.listQuery = '';
this.autoSelectFirstNote();
}
}