- 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
1091 lines
50 KiB
TypeScript
1091 lines
50 KiB
TypeScript
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();
|
||
}
|
||
}
|