diff --git a/index.tsx b/index.tsx index fb6519d..648e912 100644 --- a/index.tsx +++ b/index.tsx @@ -5,6 +5,7 @@ import { LOCALE_ID, provideZonelessChangeDetection, APP_INITIALIZER } from '@ang import localeFr from '@angular/common/locales/fr'; import { AppComponent } from './src/app.component'; +import { provideViewers } from './src/app/services/file-viewer-registry.service'; import { initializeRouterLogging } from './src/core/logging/log.router-listener'; import { initializeVisibilityLogging } from './src/core/logging/log.visibility-listener'; import '@excalidraw/excalidraw'; @@ -16,6 +17,7 @@ bootstrapApplication(AppComponent, { provideZonelessChangeDetection(), provideHttpClient(), { provide: LOCALE_ID, useValue: 'fr' }, + ...provideViewers(), { provide: APP_INITIALIZER, useFactory: initializeRouterLogging, diff --git a/package-lock.json b/package-lock.json index 0cbc835..002f331 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,7 +68,9 @@ "markdown-it-task-lists": "^2.1.1", "meilisearch": "^0.44.1", "mermaid": "^11.12.0", + "ngx-extended-pdf-viewer": "^25.6.0-alpha.3", "pathe": "^1.1.2", + "pdfjs-dist": "^5.4.296", "react": "^18.3.1", "react-dom": "^18.3.1", "react-to-webcomponent": "^2.0.0", @@ -4590,6 +4592,191 @@ "win32" ] }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.81.tgz", + "integrity": "sha512-ReCjd5SYI/UKx/olaQLC4GtN6wUQGjlgHXs1lvUvWGXfBMR3Fxnik3cL+OxKN5ithNdoU0/GlCrdKcQDFh2XKQ==", + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.81", + "@napi-rs/canvas-darwin-arm64": "0.1.81", + "@napi-rs/canvas-darwin-x64": "0.1.81", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.81", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.81", + "@napi-rs/canvas-linux-arm64-musl": "0.1.81", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.81", + "@napi-rs/canvas-linux-x64-gnu": "0.1.81", + "@napi-rs/canvas-linux-x64-musl": "0.1.81", + "@napi-rs/canvas-win32-x64-msvc": "0.1.81" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.81.tgz", + "integrity": "sha512-78Lz+AUi+MsWupyZjXwpwQrp1QCwncPvRZrdvrROcZ9Gq9grP7LfQZiGdR8LKyHIq3OR18mDP+JESGT15V1nXw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.81.tgz", + "integrity": "sha512-omejuKgHWKDGoh8rsgsyhm/whwxMaryTQjJTd9zD7hiB9/rzcEEJLHnzXWR5ysy4/tTjHaQotE6k2t8eodTLnA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.81.tgz", + "integrity": "sha512-EYfk+co6BElq5DXNH9PBLYDYwc4QsvIVbyrsVHsxVpn4p6Y3/s8MChgC69AGqj3vzZBQ1qx2CRCMtg5cub+XuQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.81.tgz", + "integrity": "sha512-teh6Q74CyAcH31yLNQGR9MtXSFxlZa5CI6vvNUISI14gWIJWrhOwUAOly+KRe1aztWR0FWTVSPxM4p5y+06aow==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.81.tgz", + "integrity": "sha512-AGEopHFYRzJOjxY+2G1RmHPRnuWvO3Qdhq7sIazlSjxb3Z6dZHg7OB/4ZimXaimPjDACm9qWa6t5bn9bhXvkcw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.81.tgz", + "integrity": "sha512-Bj3m1cl4GIhsigkdwOxii4g4Ump3/QhNpx85IgAlCCYXpaly6mcsWpuDYEabfIGWOWhDUNBOndaQUPfWK1czOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.81.tgz", + "integrity": "sha512-yg/5NkHykVdwPlD3XObwCa/EswkOwLHswJcI9rHrac+znHsmCSj5AMX/RTU9Z9F6lZTwL60JM2Esit33XhAMiw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.81.tgz", + "integrity": "sha512-tPfMpSEBuV5dJSKexO/UZxpOqnYTaNbG8aKa1ek8QsWu+4SJ/foWkaxscra/RUv85vepx6WWDjzBNbNJsTnO0w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.81.tgz", + "integrity": "sha512-1L0xnYgzqn8Baef+inPvY4dKqdmw3KCBoe0NEDgezuBZN7MA5xElwifoG8609uNdrMtJ9J6QZarsslLRVqri7g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.81.tgz", + "integrity": "sha512-57ryVbhm/z7RE9/UVcS7mrLPdlayLesy+9U0Uf6epCoeSGrs99tfieCcgZWFbIgmByQ1AZnNtFI2N6huqDLlWQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@napi-rs/nice": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", @@ -13999,6 +14186,19 @@ "dev": true, "license": "MIT" }, + "node_modules/ngx-extended-pdf-viewer": { + "version": "25.6.0-alpha.3", + "resolved": "https://registry.npmjs.org/ngx-extended-pdf-viewer/-/ngx-extended-pdf-viewer-25.6.0-alpha.3.tgz", + "integrity": "sha512-9l1JJ0FQkdOgVpiQvGvWRXJ7XaLydsx/pgNgWO7CA5+Vn/kd0tsiBbMpmGA92Q3HSmjvQvPMRBlSQmoH6e4PAA==", + "license": "Apache-2.0 with Commons-Clause", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=17.0.0 <21.0.0", + "@angular/core": ">=17.0.0 <21.0.0" + } + }, "node_modules/node-addon-api": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", @@ -14822,6 +15022,18 @@ "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", "license": "MIT" }, + "node_modules/pdfjs-dist": { + "version": "5.4.296", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", + "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.16.0 || >=22.3.0" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.80" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", diff --git a/package.json b/package.json index 8eae4b3..c020ca6 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,9 @@ "markdown-it-task-lists": "^2.1.1", "meilisearch": "^0.44.1", "mermaid": "^11.12.0", + "ngx-extended-pdf-viewer": "^25.6.0-alpha.3", "pathe": "^1.1.2", + "pdfjs-dist": "^5.4.296", "react": "^18.3.1", "react-dom": "^18.3.1", "react-to-webcomponent": "^2.0.0", diff --git a/proxy.conf.json b/proxy.conf.json index 4774dd8..312ccb1 100644 --- a/proxy.conf.json +++ b/proxy.conf.json @@ -4,5 +4,11 @@ "secure": false, "changeOrigin": true, "logLevel": "warn" + }, + "/vault": { + "target": "http://localhost:4000", + "secure": false, + "changeOrigin": true, + "logLevel": "warn" } } diff --git a/server/index.mjs b/server/index.mjs index bb25442..7672772 100644 --- a/server/index.mjs +++ b/server/index.mjs @@ -344,6 +344,61 @@ const buildFileMetadata = (notes) => updatedAt: note.updatedAt, })); +// Scan vault for NON-markdown files to include in metadata (images/pdf/video/code/others) +const scanVaultNonNotes = (vaultPath) => { + const items = []; + const includeExt = new Set([ + // images + '.png','.jpg','.jpeg','.gif','.svg','.webp','.bmp','.ico', + // pdf + '.pdf', + // video + '.mp4','.mov','.avi','.mkv','.webm', + // code/text configs + '.json','.js','.ts','.jsx','.tsx','.html','.css','.yml','.yaml','.toml','.ini','.cfg','.conf','.sh','.bash','.ps1','.bat','.csv','.txt' + ]); + + const walk = (dir) => { + let entries = []; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + for (const entry of entries) { + const entryPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + walk(entryPath); + continue; + } + if (!entry.isFile()) continue; + + const lower = entry.name.toLowerCase(); + if (lower.endsWith('.md') || lower.endsWith('.excalidraw.md')) continue; // handled elsewhere + + const ext = path.extname(lower); + if (!includeExt.has(ext)) continue; + + try { + const stats = fs.statSync(entryPath); + const relPath = path.relative(vaultPath, entryPath).replace(/\\/g, '/'); + const id = slugifyPath(relPath.replace(/\.[^.]+$/i, '')); + const title = path.basename(relPath); + items.push({ + id, + title, + path: relPath, + createdAt: new Date(stats.birthtimeMs ? stats.birthtimeMs : stats.ctimeMs).toISOString(), + updatedAt: new Date(stats.mtimeMs).toISOString(), + }); + } catch {} + } + }; + + walk(vaultPath); + return items; +}; + const normalizeDateInput = (value) => { if (!value) { return null; @@ -656,6 +711,7 @@ app.get('/api/files/metadata', async (req, res) => { // Merge .excalidraw files discovered via FS const drawings = scanVaultDrawings(vaultDir); + const nonNotes = scanVaultNonNotes(vaultDir); const byPath = new Map(items.map(it => [String(it.path).toLowerCase(), it])); for (const d of drawings) { const key = String(d.path).toLowerCase(); @@ -663,6 +719,10 @@ app.get('/api/files/metadata', async (req, res) => { byPath.set(key, d); } } + for (const f of nonNotes) { + const key = String(f.path).toLowerCase(); + if (!byPath.has(key)) byPath.set(key, f); + } return res.json(Array.from(byPath.values())); } @@ -671,11 +731,16 @@ app.get('/api/files/metadata', async (req, res) => { const notes = await loadVaultNotes(vaultDir); const base = buildFileMetadata(notes); const drawings = scanVaultDrawings(vaultDir); + const nonNotes = scanVaultNonNotes(vaultDir); const byPath = new Map(base.map(it => [String(it.path).toLowerCase(), it])); for (const d of drawings) { const key = String(d.path).toLowerCase(); if (!byPath.has(key)) byPath.set(key, d); } + for (const f of nonNotes) { + const key = String(f.path).toLowerCase(); + if (!byPath.has(key)) byPath.set(key, f); + } return res.json(Array.from(byPath.values())); } catch (error) { console.error('Failed to load file metadata:', error); diff --git a/src/app/features/code-viewer/code-viewer.component.ts b/src/app/features/code-viewer/code-viewer.component.ts new file mode 100644 index 0000000..65fe24a --- /dev/null +++ b/src/app/features/code-viewer/code-viewer.component.ts @@ -0,0 +1,81 @@ +import { Component, Input, signal, computed } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-code-viewer', + standalone: true, + imports: [CommonModule], + styles: [` + :host { display: block; height: 100%; } + .root { height: 100%; display: flex; flex-direction: column; } + .toolbar { display: flex; align-items: center; gap: .5rem; padding: .5rem .75rem; border-bottom: 1px solid var(--border); background: var(--card); } + .btn { display: inline-flex; align-items: center; gap: .375rem; padding: .25rem .5rem; border-radius: .375rem; } + .btn:hover { background: color-mix(in oklab, var(--card) 90%, black 10%); } + .content { flex: 1; min-height: 0; overflow: auto; background: var(--card); } + pre { margin: 0; padding: .75rem 1rem; font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); font-size: .875rem; line-height: 1.5; } + .line { display: block; white-space: pre; } + .nowrap .line { white-space: pre; } + .wrap .line { white-space: pre-wrap; word-break: break-word; } + .ln { color: var(--text-muted); user-select: none; width: 3ch; display: inline-block; text-align: right; padding-right: .75ch; } + `], + template: ` +
+
+ + + + ⬇️ + {{ fileName() }} +
+
+
+          
+            {{ i + 1 }}{{ line }}
+          
+        
+
+
+ ` +}) +export class CodeViewerComponent { + @Input() path: string = ''; + @Input() content: string = ''; + @Input() editable: boolean = false; // reserved for future + + wrap = signal(false); + + toggleWrap() { this.wrap.update(v => !v); } + + fileName = computed(() => { + const p = this.path || ''; + return p.split('/').pop() || p.split('\\').pop() || 'file'; + }); + + lines = computed(() => { + const raw = this.content || ''; + // Light safeguard for huge files: cap at 5000 lines + const parts = raw.split(/\r?\n/); + return parts.slice(0, 5000); + }); + + downloadHref() { + if (!this.path) return '#'; + return `/api/files/${encodeURIComponent(this.path)}`; + } + + onFind() { + try { + // Let browser open find dialog + (document.activeElement as HTMLElement | null)?.blur(); + // No-op; Ctrl/Cmd+F handled by browser + } catch {} + } + + async copyAll() { + try { + await navigator.clipboard.writeText(this.content || ''); + } catch (e) { + console.warn('Copy failed', e); + } + } +} diff --git a/src/app/features/drawings/drawings-editor.component.html b/src/app/features/drawings/drawings-editor.component.html index a63b574..2b7fb29 100644 --- a/src/app/features/drawings/drawings-editor.component.html +++ b/src/app/features/drawings/drawings-editor.component.html @@ -1,104 +1,92 @@ -
- -
-
- - +
+ +
+
+
+ + {{ (path || '').split('/').pop() }} + + + Aucun fichier + +
+ + + + + + + {{ dirty() ? 'Non sauvegardé' : 'Sauvegardé' }} + +
+
+
- - - +
+
Sélectionner un fichier *.excalidraw.md
+
    +
  • + +
  • +
+
+
+ - - +
+ +
+ + +
+ + +
+ +
+
+
+
@@ -120,9 +108,6 @@
- -
-
@@ -134,7 +119,7 @@
('info'); hasConflict = signal(false); isFullscreen = signal(false); + // Header menus & helpers (ported from Test Excalidraw) + openPicker = signal(false); + openExport = signal(false); + excalidrawFiles = signal>([]); + private saveTimer: any = null; private saveSub: Subscription | null = null; private dirtyCheckSub: Subscription | null = null; @@ -79,6 +88,77 @@ export class DrawingsEditorComponent implements OnInit, AfterViewInit, OnDestroy } } + ngOnChanges(changes: SimpleChanges): void { + if (changes['path'] && !changes['path'].firstChange) { + // Reset state and reload when the selected file changes + this.error.set(null); + this.hasConflict.set(false); + this.isLoading.set(true); + this.dirty.set(false); + this.reloadFromDisk(); + } + } + + // ===== Test-page parity helpers ===== + refreshFileList(): void { + try { + const notes = this.vault.allNotes(); + const list = notes + .filter(n => /\.excalidraw\.md$/i.test(n.filePath || '')) + .map(n => ({ filePath: n.filePath })); + this.excalidrawFiles.set(list); + } catch {} + } + + toggleOpenPicker(): void { + this.openPicker.update(v => !v); + if (this.openPicker()) this.refreshFileList(); + } + + toggleExport(): void { + this.openExport.update(v => !v); + } + + async createNew(): Promise { + const now = new Date(); + const y = now.getFullYear(); + const m = String(now.getMonth() + 1).padStart(2, '0'); + const d = String(now.getDate()).padStart(2, '0'); + const hh = String(now.getHours()).padStart(2, '0'); + const mm = String(now.getMinutes()).padStart(2, '0'); + const base = `Drawing-${y}${m}${d}-${hh}${mm}`; + const name = prompt('Nom du fichier', `${base}.excalidraw.md`); + if (!name) return; + const file = name.endsWith('.excalidraw.md') ? name : `${name}.excalidraw.md`; + const path = file.replace(/^\/+/, ''); + const scene: ExcalidrawScene = { elements: [], appState: {}, files: {} }; + const fm = `---\nexcalidraw-plugin: parsed\ncreated: "${now.toISOString()}"\nupdated: "${now.toISOString()}"\ntitle: "${(file || '').replace(/\.excalidraw\.md$/i,'')}"\n---`; + const md = this.excalIo.toObsidianMd(scene as any, fm); + await firstValueFrom(this.files.putText(path, md)); + this.path = path; + this.dirty.set(false); + this.openPicker.set(false); + this.reloadFromDisk(); + } + + openFile(path: string): void { + this.path = path; + this.openPicker.set(false); + this.dirty.set(false); + this.reloadFromDisk(); + } + + closeFile(): void { + this.path = ''; + this.scene.set({ elements: [], appState: { viewBackgroundColor: '#1e1e1e', theme: this.themeName() }, files: {} } as any); + this.dirty.set(false); + } + + async saveDebounced(): Promise { + if (this.saveTimer) clearTimeout(this.saveTimer); + this.saveTimer = setTimeout(() => this.saveNow(), 800); + } + private waitForNextSceneChange(host: any, timeoutMs = 300): Promise { return new Promise((resolve) => { let done = false; @@ -719,6 +799,7 @@ export class DrawingsEditorComponent implements OnInit, AfterViewInit, OnDestroy ngOnDestroy(): void { this.saveSub?.unsubscribe(); + if (this.saveTimer) clearTimeout(this.saveTimer); // Clean up fullscreen listener document.removeEventListener('fullscreenchange', () => { this.isFullscreen.set(!!document.fullscreenElement); diff --git a/src/app/features/list/notes-list.component.ts b/src/app/features/list/notes-list.component.ts index d7c4dd1..f39924e 100644 --- a/src/app/features/list/notes-list.component.ts +++ b/src/app/features/list/notes-list.component.ts @@ -12,6 +12,7 @@ import { NoteContextMenuService } from '../../services/note-context-menu.service import { UrlStateService } from '../../services/url-state.service'; import { EditorStateService } from '../../../services/editor-state.service'; import { VaultService } from '../../../services/vault.service'; +import { FileTypeDetectorService } from '../../../services/file-type-detector.service'; @Component({ selector: 'app-notes-list', @@ -79,6 +80,11 @@ import { VaultService } from '../../../services/vault.service'; title="Mode d'affichage"> + + + + {{ kindIcon(kindFilter()!) }} +
@@ -103,17 +109,23 @@ import { VaultService } from '../../../services/vault.service';
- -
-
- - - {{ stats.duration }}ms - - - - {{ stats.duration }}ms - + +
+
+ + {{ filtered().length }} +
+
+
+ + + {{ stats.duration }}ms + + + + {{ stats.duration }}ms + +
@@ -166,12 +178,14 @@ import { VaultService } from '../../../services/vault.service';
+ {{ typeIcon(n) }}
{{ n.title }}
+ {{ typeIcon(n) }}
{{ n.title }}
{{ n.filePath }}
@@ -181,6 +195,7 @@ import { VaultService } from '../../../services/vault.service';
+ {{ typeIcon(n) }}
{{ n.title }}
{{ n.filePath }}
@@ -466,6 +481,7 @@ export class NotesListComponent { tagFilter = input(null); quickLinkFilter = input<'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | null>(null); selectedId = input(null); + kindFilter = input<'image' | 'video' | 'pdf' | 'markdown' | 'excalidraw' | 'code' | 'all' | null>(null); @Output() openNote = new EventEmitter(); @Output() queryChange = new EventEmitter(); @@ -482,6 +498,7 @@ export class NotesListComponent { private pendingSelectId = signal(null); private editorState = inject(EditorStateService); private vault = inject(VaultService); + private fileTypes = inject(FileTypeDetectorService); // Delete warning modal state deleteWarningOpen = signal(false); @@ -539,6 +556,80 @@ export class NotesListComponent { }, 10); }); + private buildUnifiedList(): Note[] { + const notes = this.notes(); + const notePaths = new Set(notes.map(n => (n.filePath || '').toLowerCase().replace(/\\/g, '/'))); + const otherFiles = this.collectFilesFromFastTree() + .filter(f => !notePaths.has(f.filePath.toLowerCase().replace(/\\/g, '/'))) + .map(f => this.asNoteLike(f)); + return [...notes, ...otherFiles]; + } + + private collectFilesFromFastTree(): Array<{ id: string; filePath: string; fileName: string; originalPath: string }> { + const out: Array<{ id: string; filePath: string; fileName: string; originalPath: string }> = []; + const visit = (nodes: any[], parentPath: string = '') => { + for (const n of nodes || []) { + if (n.type === 'folder') { + visit(n.children, n.path || parentPath); + } else if (n.type === 'file') { + const filePath = (n.path || '').replace(/^\/+/, ''); + const fileName = n.name || filePath.split('/').pop() || ''; + const originalPath = (n.path || '').split('/').slice(0, -1).join('/'); + out.push({ id: n.id || filePath, filePath, fileName, originalPath }); + } + } + }; + try { visit(this.vault.fastFileTree()); } catch {} + return out; + } + + private asNoteLike(f: { id: string; filePath: string; fileName: string; originalPath: string }): Note { + return { + id: f.id, + title: f.fileName, + content: '', + rawContent: '', + tags: [], + frontmatter: {} as any, + backlinks: [], + mtime: 0, + fileName: f.fileName, + filePath: f.filePath, + originalPath: f.originalPath, + } as Note; + } + + kindIcon(k: 'image'|'video'|'pdf'|'markdown'|'excalidraw'|'code'|'all'): string { + switch (k) { + case 'markdown': return '📝'; + case 'excalidraw': return '✏️'; + case 'pdf': return '📄'; + case 'image': return '🖼️'; + case 'video': return '🎬'; + case 'code': return ''; + default: return '✨'; + } + } + + private matchesKind(n: Note, kind: 'image' | 'video' | 'pdf' | 'markdown' | 'excalidraw' | 'code' | 'all'): boolean { + if (!kind || kind === 'all') return true; + const t = this.fileTypes.getViewerType(n.filePath, n.rawContent ?? n.content); + return t === kind; + } + + typeIcon(n: Note): string { + const t = this.fileTypes.getViewerType(n.filePath, n.rawContent ?? n.content); + switch (t) { + case 'markdown': return '📝'; + case 'excalidraw': return '✏️'; + case 'pdf': return '📄'; + case 'image': return '🖼️'; + case 'video': return '🎬'; + case 'code': return ''; + default: return '📎'; + } + } + private scrollToSelectedEffect = effect(() => { const id = this.selectedId() || this.pendingSelectId(); if (!id) return; @@ -592,8 +683,10 @@ export class NotesListComponent { const folder = (this.folderFilter() || '').toLowerCase().replace(/^\/+|\/+$/g, ''); const tag = (this.activeTag() || '').toLowerCase(); const quickLink = this.quickLinkFilter(); + const kind = this.kindFilter(); const sortBy = this.state.sortBy(); - let list = this.notes(); + // Build source list: notes + other files (images, pdf, video, code, etc.) + let list = this.buildUnifiedList(); if (folder !== '.trash') { list = list.filter(n => { @@ -635,6 +728,11 @@ export class NotesListComponent { }); } + // Kind filter (file type) + if (kind && kind !== 'all') { + list = list.filter(n => this.matchesKind(n, kind)); + } + const parseDate = (s?: string) => (s ? Date.parse(s) : 0) || 0; return [...list].sort((a, b) => { switch (sortBy) { diff --git a/src/app/features/note-view/components/code-renderer/code-renderer.component.ts b/src/app/features/note-view/components/code-renderer/code-renderer.component.ts new file mode 100644 index 0000000..646291d --- /dev/null +++ b/src/app/features/note-view/components/code-renderer/code-renderer.component.ts @@ -0,0 +1,89 @@ +import { Component, Input, computed } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import hljs from 'highlight.js/lib/core'; +import javascript from 'highlight.js/lib/languages/javascript'; +import typescript from 'highlight.js/lib/languages/typescript'; +import json from 'highlight.js/lib/languages/json'; +import python from 'highlight.js/lib/languages/python'; +import java from 'highlight.js/lib/languages/java'; +import xml from 'highlight.js/lib/languages/xml'; +import css from 'highlight.js/lib/languages/css'; +import bash from 'highlight.js/lib/languages/bash'; +import yaml from 'highlight.js/lib/languages/yaml'; +import sql from 'highlight.js/lib/languages/sql'; + +hljs.registerLanguage('javascript', javascript); +hljs.registerLanguage('typescript', typescript); +hljs.registerLanguage('json', json); +hljs.registerLanguage('python', python); +hljs.registerLanguage('java', java); +hljs.registerLanguage('xml', xml); +hljs.registerLanguage('html', xml); +hljs.registerLanguage('css', css); +hljs.registerLanguage('bash', bash); +hljs.registerLanguage('yaml', yaml); +hljs.registerLanguage('yml', yaml); +hljs.registerLanguage('sql', sql); + +@Component({ + selector: 'app-code-renderer', + standalone: true, + imports: [CommonModule], + styles: [` + :host { display:block; } + .root { border: 1px solid var(--border); background: var(--card); border-radius: 1rem; overflow: hidden; } + .header { display:flex; align-items:center; gap:.5rem; padding:.5rem .75rem; font-size:.75rem; color: var(--text-muted); border-bottom: 1px solid var(--border); background: color-mix(in oklab, var(--card) 92%, black 8%); } + .title { display:inline-flex; align-items:center; gap:.5rem; font-weight:600; color: var(--text-main); } + .title svg { width: 16px; height: 16px; color: var(--accent, #9b87f5); } + .lang { margin-left:auto; padding:.125rem .5rem; border:1px solid var(--border); border-radius:.5rem; font-weight:600; color: var(--text-main); } + pre { margin:0; padding: .75rem 1rem; font-family: var(--font-mono, ui-monospace, Menlo, Monaco, Consolas, 'Courier New', monospace); font-size:.875rem; line-height:1.5; overflow:auto; } + code { display:block; } + .hljs { color: var(--text-main); } + `], + template: ` +
+
+ + + code + + {{ languageLabel() }} +
+
+
+ ` +}) +export class CodeRendererComponent { + @Input() path: string = ''; + @Input() content: string = ''; + + fileName = computed(() => { + const p = this.path || ''; + return p.split('/').pop() || p.split('\\').pop() || 'code'; + }); + + languageLabel = computed(() => { + const src = this.content || ''; + if (!src.trim()) return 'TEXT'; + try { + const res = hljs.highlightAuto(src); + return (res.language || 'text').toUpperCase(); + } catch { + return 'TEXT'; + } + }); + + highlighted = computed(() => { + const src = this.content || ''; + try { + if (!src.trim()) { return hljs.highlightAuto('').value; } + const res = hljs.highlightAuto(src); + return res.value; + } catch { + return hljs.highlightAuto(src).value; + } + }); +} diff --git a/src/app/features/note-view/components/file-info-panel/file-info-panel.component.ts b/src/app/features/note-view/components/file-info-panel/file-info-panel.component.ts new file mode 100644 index 0000000..c9c3c13 --- /dev/null +++ b/src/app/features/note-view/components/file-info-panel/file-info-panel.component.ts @@ -0,0 +1,59 @@ +import { Component, EventEmitter, Input, Output, computed, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +export interface FileMetadataView { + type: string; + size?: number; + sizeHuman?: string; + path: string; + createdAt?: string; + modifiedAt?: string; + dimensions?: { width: number; height: number }; + duration?: string; + codec?: string; + frameRate?: number; + dataRate?: number; +} + +@Component({ + selector: 'app-file-info-panel', + standalone: true, + imports: [CommonModule], + styles: [` + :host { display:block; } + .panel { position: absolute; top: 0; right: 0; height: 100%; width: min(88vw, 320px); background: var(--card); border-left: 1px solid var(--border); box-shadow: 0 8px 30px rgba(0,0,0,.18); transform: translateX(100%); transition: transform .25s ease, opacity .25s ease; opacity: .98; border-top-left-radius: 12px; border-bottom-left-radius: 12px; } + .panel.open { transform: translateX(0); } + .header { height: 44px; display:flex; align-items:center; gap:.5rem; padding: 0 .75rem; border-bottom:1px solid var(--border); } + .content { padding: .75rem .75rem 1rem; font-size: .875rem; } + .row { display:flex; align-items:flex-start; gap:.5rem; padding:.4rem 0; border-bottom:1px dashed var(--border); } + .row:last-child { border-bottom: 0; } + .k { color: var(--text-muted); width: 120px; flex: 0 0 auto; } + .v { color: var(--text-main); word-break: break-word; } + .close { margin-left:auto; border-radius:8px; width:28px; height:28px; display:inline-flex; align-items:center; justify-content:center; } + .close:hover { background: color-mix(in oklab, var(--card) 90%, black 10%); } + `], + template: ` +
+
+ Détails + +
+
+
Type
{{ data?.type || '—' }}
+
Taille
{{ data!.size | number }} o ({{ data!.sizeHuman }})
+
Chemin
{{ data?.path }}
+
Créé
{{ data!.createdAt | date:'medium' }}
+
Modifié
{{ data!.modifiedAt | date:'medium' }}
+
Dimensions
{{ data!.dimensions!.width }} × {{ data!.dimensions!.height }} px
+
Durée
{{ data!.duration }}
+
Frame rate
{{ data!.frameRate }} fps
+
Codec
{{ data!.codec }}
+
+
+ ` +}) +export class FileInfoPanelComponent { + @Input() visible: boolean = false; + @Input() data: FileMetadataView | null = null; + @Output() close = new EventEmitter(); +} diff --git a/src/app/features/note-view/components/image-viewer/image-viewer.component.ts b/src/app/features/note-view/components/image-viewer/image-viewer.component.ts new file mode 100644 index 0000000..51b44a1 --- /dev/null +++ b/src/app/features/note-view/components/image-viewer/image-viewer.component.ts @@ -0,0 +1,29 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-image-viewer', + standalone: true, + imports: [CommonModule], + template: ` +
+ +
+ `, + styles: [` + :host { display:block; height:100%; } + .img-root { height:100%; display:flex; align-items:center; justify-content:center; padding: 1rem; } + .media { max-width: 100%; max-height: 90vh; object-fit: contain; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,.16); } + `] +}) +export class ImageViewerComponent { + @Input() src: string = ''; + @Input() alt: string = ''; + @Output() dimensions = new EventEmitter<{ width: number; height: number }>(); + + onLoad(ev: Event) { + const el = ev.target as HTMLImageElement | null; + if (!el) return; + this.dimensions.emit({ width: el.naturalWidth || el.width || 0, height: el.naturalHeight || el.height || 0 }); + } +} diff --git a/src/app/features/note-view/components/pdf-viewer/pdf-viewer.component.html b/src/app/features/note-view/components/pdf-viewer/pdf-viewer.component.html new file mode 100644 index 0000000..8fadc3d --- /dev/null +++ b/src/app/features/note-view/components/pdf-viewer/pdf-viewer.component.html @@ -0,0 +1,8 @@ +
+ + + +
diff --git a/src/app/features/note-view/components/pdf-viewer/pdf-viewer.component.scss b/src/app/features/note-view/components/pdf-viewer/pdf-viewer.component.scss new file mode 100644 index 0000000..5f8fb79 --- /dev/null +++ b/src/app/features/note-view/components/pdf-viewer/pdf-viewer.component.scss @@ -0,0 +1,6 @@ +:host { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; +} \ No newline at end of file diff --git a/src/app/features/note-view/components/pdf-viewer/pdf-viewer.component.ts b/src/app/features/note-view/components/pdf-viewer/pdf-viewer.component.ts new file mode 100644 index 0000000..6319f4a --- /dev/null +++ b/src/app/features/note-view/components/pdf-viewer/pdf-viewer.component.ts @@ -0,0 +1,76 @@ +import { Component, Input, computed, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; + +@Component({ + selector: 'app-pdf-viewer', + standalone: true, + imports: [CommonModule], + template: ` +
+
+ 📄 + PDF + {{ fileName() }} +
+ +
+ `, + styles: [` + :host { display:flex; flex:1 1 auto; width:100%; min-width:0; min-height:0; padding: 0; } + .pdf-container { + flex:1 1 auto; + display:flex; + flex-direction:column; + min-height:0; + border-radius: 0.5rem; + border: 1px solid var(--border); + background: var(--card); + overflow: hidden; + } + .pdf-header { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border); + background: color-mix(in srgb, var(--card) 95%, var(--text-main) 5%); + font-size: 0.875rem; + } + .pdf-icon { font-size: 1.25rem; } + .pdf-title { + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); + } + .pdf-filename { + margin-left: auto; + font-weight: 500; + color: var(--text-main); + max-width: 50%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .viewer { flex:1 1 auto; display:block; width:100%; height:100%; border:0; min-height:0; } + `] +}) +export class PdfViewerComponent { + @Input() src: string = ''; + @Input() path: string = ''; + private sanitizer = inject(DomSanitizer); + + safeSrc = computed(() => this.sanitizer.bypassSecurityTrustResourceUrl(this.src || '')); + + fileName = computed(() => { + const p = this.path || this.src || ''; + try { + const u = decodeURI(p); + return u.split('/').pop() || u.split('\\').pop() || 'document.pdf'; + } catch { + return p.split('/').pop() || p.split('\\').pop() || 'document.pdf'; + } + }); + +} diff --git a/src/app/features/note-view/components/video-player/video-player.component.ts b/src/app/features/note-view/components/video-player/video-player.component.ts new file mode 100644 index 0000000..2f42941 --- /dev/null +++ b/src/app/features/note-view/components/video-player/video-player.component.ts @@ -0,0 +1,28 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-video-player', + standalone: true, + imports: [CommonModule], + styles: [` + :host { display:block; height:100%; } + .root { height:100%; display:flex; align-items:center; justify-content:center; background: var(--card); border-radius: 12px; overflow:hidden; border:1px solid var(--border); } + video { width: 100%; height: 100%; max-height: 90vh; background: black; } + `], + template: ` +
+ +
+ ` +}) +export class VideoPlayerComponent { + @Input() src: string = ''; + @Output() metadata = new EventEmitter<{ width: number; height: number; duration: number }>(); + + onMeta(ev: Event) { + const v = ev.target as HTMLVideoElement | null; + if (!v) return; + this.metadata.emit({ width: v.videoWidth || 0, height: v.videoHeight || 0, duration: isFinite(v.duration) ? v.duration : 0 }); + } +} diff --git a/src/app/features/note/components/properties-popover/properties-popover.component.html b/src/app/features/note/components/properties-popover/properties-popover.component.html index 5577e60..6367771 100644 --- a/src/app/features/note/components/properties-popover/properties-popover.component.html +++ b/src/app/features/note/components/properties-popover/properties-popover.component.html @@ -10,6 +10,24 @@
+ +
+

Infos fichier

+
+
Type
+
{{ fi.type }}
+
Taille
+
{{ fi.size || 0 | number }} o ({{ fi.sizeHuman }})
+
Chemin
+
{{ fi.path }}
+
Créé le
+
{{ formatDate(fi.createdAt) }}
+
Modifié le
+
{{ formatDate(fi.modifiedAt) }}
+
+
+
+
diff --git a/src/app/features/note/components/properties-popover/properties-popover.component.ts b/src/app/features/note/components/properties-popover/properties-popover.component.ts index 25f06ca..6d9d010 100644 --- a/src/app/features/note/components/properties-popover/properties-popover.component.ts +++ b/src/app/features/note/components/properties-popover/properties-popover.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, Output, inject } from '@angular/core'; +import { Component, EventEmitter, Input, Output, inject, OnChanges, SimpleChanges, signal, computed } from '@angular/core'; import { CommonModule } from '@angular/common'; import { StateChipComponent } from '../state-chip/state-chip.component'; import { @@ -9,6 +9,7 @@ import { } from '../../shared/note-properties.model'; import { VaultService } from '../../../../../services/vault.service'; import { FrontmatterPropertiesService } from '../../shared/frontmatter-properties.service'; +import { FileTypeDetectorService } from '../../../../../services/file-type-detector.service'; @Component({ selector: 'app-properties-popover', @@ -16,14 +17,63 @@ import { FrontmatterPropertiesService } from '../../shared/frontmatter-propertie imports: [CommonModule, StateChipComponent], templateUrl: './properties-popover.component.html' }) -export class PropertiesPopoverComponent { +export class PropertiesPopoverComponent implements OnChanges { @Input() props: NoteProperties | null = null; - @Input() noteId: string | null = null; + private _noteId: string | null = null; + @Input() set noteId(value: string | null) { + this._noteId = value; + // Trigger refresh when input is assigned programmatically by overlay + this.refreshFileInfo(); + } + get noteId(): string | null { return this._noteId; } @Output() requestClose = new EventEmitter(); @Output() cancelClose = new EventEmitter(); private vault = inject(VaultService); private frontmatter = inject(FrontmatterPropertiesService); + private fileType = inject(FileTypeDetectorService); + + // Lightweight file info for non-markdown + readonly fileInfo = signal<{ + type: string; + path: string; + size?: number; + sizeHuman?: string; + createdAt?: string; + modifiedAt?: string; + } | null>(null); + + ngOnChanges(changes: SimpleChanges): void { + if (changes['noteId']) { + this.refreshFileInfo(); + } + } + + private async refreshFileInfo(): Promise { + const id = this.noteId; + if (!id) { this.fileInfo.set(null); return; } + const note = this.vault.getNoteById(id); + if (!note?.filePath) { this.fileInfo.set(null); return; } + const type = this.fileType.getViewerType(note.filePath, note.rawContent ?? note.content); + if (type === 'markdown' || type === 'excalidraw') { this.fileInfo.set(null); return; } + + const meta = this.vault.getFastMetaByPath(note.filePath); + let size: number | undefined; + try { + const res = await fetch(`/vault/${encodeURI(note.filePath)}`, { method: 'HEAD' }); + const len = res.headers.get('content-length'); + size = len ? Number(len) : undefined; + } catch {} + + this.fileInfo.set({ + type, + path: note.filePath, + size, + sizeHuman: this.formatSize(size), + createdAt: meta?.createdAt, + modifiedAt: meta?.updatedAt, + }); + } private readonly summaryConfig: Array<{ key: keyof NotePropertySummary; @@ -95,6 +145,15 @@ export class PropertiesPopoverComponent { }).format(date); } + private formatSize(bytes?: number): string | undefined { + if (bytes == null || !Number.isFinite(bytes)) return undefined; + const units = ['o','Ko','Mo','Go','To']; + let b = Math.max(0, Number(bytes)); + let i = 0; + while (b >= 1024 && i < units.length - 1) { b /= 1024; i++; } + return `${b.toFixed(b < 10 && i > 0 ? 1 : 0)} ${units[i]}`; + } + hasStates(): boolean { return this.stateEntries.length > 0; } diff --git a/src/app/features/sidebar/nimbus-sidebar.component.ts b/src/app/features/sidebar/nimbus-sidebar.component.ts index 4c58727..5771859 100644 --- a/src/app/features/sidebar/nimbus-sidebar.component.ts +++ b/src/app/features/sidebar/nimbus-sidebar.component.ts @@ -7,6 +7,7 @@ import { ScrollableOverlayDirective } from '../../shared/overlay-scrollbar/scrol import type { VaultNode, TagInfo } from '../../../types'; import { environment } from '../../../environments/environment'; import { VaultService } from '../../../services/vault.service'; +import { UrlStateService } from '../../services/url-state.service'; @Component({ selector: 'app-nimbus-sidebar', @@ -75,13 +76,39 @@ import { VaultService } from '../../../services/vault.service'; 📁 Folders +
+
+ + + + + + + +
(); env = environment; - open = { quick: true, folders: false, tags: false, trash: false, tests: false }; + open = { quick: true, folders: true, tags: false, trash: false, tests: false }; private vault = inject(VaultService); + urlState = inject(UrlStateService); @ViewChild('foldersExplorer') private foldersExplorer?: FileExplorerComponent; ngOnChanges(changes: SimpleChanges): void { @@ -245,4 +273,14 @@ export class NimbusSidebarComponent implements OnChanges { this.folderSelected.emit('.trash'); } } + + setKind(kind: 'image'|'video'|'pdf'|'markdown'|'excalidraw'|'code'|'all') { + this.urlState.filterByKind(kind); + } + + chipClass(active: boolean): string { + return active + ? 'bg-primary/15 text-primary ring-1 ring-primary/40' + : 'bg-surface1/50 text-muted hover:bg-surface1 dark:hover:bg-card'; + } } diff --git a/src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts b/src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts index b04c0b4..a772b15 100644 --- a/src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts +++ b/src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts @@ -35,7 +35,7 @@ import { InPageSearchOverlayComponent } from '../../shared/search/in-page-search
-
+
-
+
@@ -253,9 +254,9 @@ import { InPageSearchOverlayComponent } from '../../shared/search/in-page-search
- +
-
+
@@ -286,29 +287,29 @@ import { InPageSearchOverlayComponent } from '../../shared/search/in-page-search @if (mobileNav.activeTab() === 'list') {
- +
} @if (mobileNav.activeTab() === 'page') { @if (activeView === 'parameters') { -
+
} @else if (activeView === 'markdown-playground') { -
+
} @else if (activeView === 'tests-panel') { -
+
} @else if (activeView === 'tests-excalidraw') { -
+
} @else { -
+
@if (selectedNote) { } @else { diff --git a/src/app/services/file-viewer-registry.service.ts b/src/app/services/file-viewer-registry.service.ts new file mode 100644 index 0000000..2e56e7c --- /dev/null +++ b/src/app/services/file-viewer-registry.service.ts @@ -0,0 +1,43 @@ +import { Injectable, Provider } from '@angular/core'; +import { FileTypeDetectorService } from '../../services/file-type-detector.service'; + +export type ViewerKind = 'markdown' | 'excalidraw' | 'image' | 'video' | 'pdf' | 'code' | 'text' | 'unknown'; + +export interface FileViewerEntry { + kind: ViewerKind; + /** Optional dynamic component loader; not required by current SmartFileViewer strategy */ + load?: () => Promise; +} + +@Injectable({ providedIn: 'root' }) +export class FileViewerRegistry { + constructor(private readonly detector: FileTypeDetectorService) {} + + getViewerKind(path: string, content: string): ViewerKind { + return this.detector.getViewerType(path, content) as ViewerKind; + } + + /** + * Optional registry map for future plugin-style viewers. Not used by current UI, but available. + */ + getDefaultEntries(): Record { + return { + markdown: { kind: 'markdown', load: async () => (await import('../../components/markdown-viewer/markdown-viewer.component')).MarkdownViewerComponent }, + excalidraw: { kind: 'excalidraw', load: async () => (await import('../features/drawings/drawings-editor.component')).DrawingsEditorComponent }, + image: { kind: 'image' }, + video: { kind: 'video' }, + pdf: { kind: 'pdf' }, + code: { kind: 'code', load: async () => (await import('../features/code-viewer/code-viewer.component')).CodeViewerComponent }, + text: { kind: 'text' }, + unknown: { kind: 'unknown' } + }; + } +} + +/** + * Hook to register viewer-related providers. Current services are providedIn: 'root', + * so this returns an empty list to keep bootstrap code explicit and future-proof. + */ +export function provideViewers(): Provider[] { + return []; +} diff --git a/src/app/services/url-state.service.ts b/src/app/services/url-state.service.ts index cf59bca..6210654 100644 --- a/src/app/services/url-state.service.ts +++ b/src/app/services/url-state.service.ts @@ -15,6 +15,7 @@ export interface UrlState { folder?: string; // Dossier de filtrage quick?: string; // Quick link de filtrage search?: string; // Terme de recherche + kind?: 'image' | 'video' | 'pdf' | 'markdown' | 'excalidraw' | 'code' | 'all'; } export interface UrlStateChangeEvent { @@ -120,6 +121,15 @@ export class UrlStateService implements OnDestroy { return search; }); + /** + * Filtre de type de fichier actif + */ + readonly activeKind = computed(() => { + const k = this.currentStateSignal().kind || null; + console.log('🧩 activeKind():', k); + return k; + }); + // ======================================== // CONSTRUCTOR & LIFECYCLE // ======================================== @@ -138,6 +148,7 @@ export class UrlStateService implements OnDestroy { const v = qpm.get(k); if (v !== null) params[k] = v; } + // Fallback: at first load with certain servers, Router may expose empty params if (Object.keys(params).length === 0 && typeof window !== 'undefined') { const sp = new URLSearchParams(window.location.search || ''); @@ -164,6 +175,26 @@ export class UrlStateService implements OnDestroy { this.previousStateSignal.set(previousState); this.currentStateSignal.set(newState); this.stateChangeSubject.next({ previous: previousState, current: newState, changed }); + + // If the active list section changed (tag/folder/quick), normalize URL: + // - Keep only the new section key + // - Clear others (including search), reset kind to 'all', and clear opened note + const prevSection = this.getActiveSection(previousState); + const nextSection = this.getActiveSection(newState); + if (prevSection !== nextSection && nextSection !== null) { + // Schedule to avoid interfering within the same router tick + setTimeout(() => { + const partial: Partial = { + tag: nextSection === 'tag' ? newState.tag! : null, + folder: nextSection === 'folder' ? newState.folder! : null, + quick: nextSection === 'quick' ? newState.quick! : null, + search: null, + kind: 'all', + note: null, + }; + this.updateUrl(partial); + }, 0); + } } else if (!previousState || Object.keys(previousState).length === 0) { // Première initialisation si nécessaire console.log('🌐 UrlStateService - first initialization'); @@ -233,6 +264,16 @@ export class UrlStateService implements OnDestroy { console.log('✅ parseUrlParams - search set:', state.search); } + // Kind (toujours en plus) + if (params['kind']) { + const raw = decodeURIComponent(params['kind']).toLowerCase(); + const allowed = ['image','video','pdf','markdown','excalidraw','code','all']; + if (allowed.includes(raw)) { + state.kind = raw as UrlState['kind']; + console.log('✅ parseUrlParams - kind set:', state.kind); + } + } + console.log('🎯 parseUrlParams final state:', state); return state; } @@ -243,7 +284,7 @@ export class UrlStateService implements OnDestroy { private detectChanges(previous: UrlState, current: UrlState): (keyof UrlState)[] { const changed: (keyof UrlState)[] = []; - const keys: (keyof UrlState)[] = ['note', 'tag', 'folder', 'quick', 'search']; + const keys: (keyof UrlState)[] = ['note', 'tag', 'folder', 'quick', 'search', 'kind']; for (const key of keys) { if (previous[key] !== current[key]) { @@ -254,6 +295,16 @@ export class UrlStateService implements OnDestroy { return changed; } + /** + * Retourne la section active parmi tag/folder/quick, sinon null + */ + private getActiveSection(state: UrlState): 'tag' | 'folder' | 'quick' | null { + if (state.tag) return 'tag'; + if (state.folder) return 'folder'; + if (state.quick) return 'quick'; + return null; + } + // ======================================== // STATE UPDATES // ======================================== @@ -348,6 +399,14 @@ export class UrlStateService implements OnDestroy { await this.updateUrl({ search: searchTerm || undefined }); } + /** + * Filtrer par type de fichier (image, video, pdf, markdown, excalidraw, code, all) + */ + async filterByKind(kind: UrlState['kind']): Promise { + const normalized = (kind || 'all'); + await this.updateUrl({ kind: normalized }); + } + /** * Définir la note et optionnellement le dossier (pour création de note) * Utilise merge pour conserver les autres paramètres (search, etc.) @@ -418,6 +477,7 @@ export class UrlStateService implements OnDestroy { if (stateToShare.folder) params.set('folder', stateToShare.folder); if (stateToShare.quick) params.set('quick', stateToShare.quick); if (stateToShare.search) params.set('search', stateToShare.search); + if (stateToShare.kind) params.set('kind', stateToShare.kind); const baseUrl = window.location.origin + window.location.pathname; return `${baseUrl}?${params.toString()}`; @@ -436,6 +496,13 @@ export class UrlStateService implements OnDestroy { } } + /** + * Show all files and clear contextual filters/search so the next search applies globally. + */ + async showAllAndReset(): Promise { + await this.updateUrl({ tag: null, folder: null, quick: null, search: null, kind: 'all', note: null }); + } + // ======================================== // OBSERVABLES & EVENTS // ======================================== @@ -488,6 +555,13 @@ export class UrlStateService implements OnDestroy { return this.currentStateSignal().quick === quickLink; } + /** + * Vérifier si un type est actif + */ + isKindActive(kind: UrlState['kind']): boolean { + return (this.currentStateSignal().kind || 'all') === (kind || 'all'); + } + /** * Obtenir l'état actuel (snapshot) */ diff --git a/src/components/smart-file-viewer/smart-file-viewer.component.ts b/src/components/smart-file-viewer/smart-file-viewer.component.ts index 5755aea..4cc6651 100644 --- a/src/components/smart-file-viewer/smart-file-viewer.component.ts +++ b/src/components/smart-file-viewer/smart-file-viewer.component.ts @@ -1,9 +1,14 @@ -import { Component, Input, OnChanges, SimpleChanges, signal, computed, inject, CUSTOM_ELEMENTS_SCHEMA, ViewContainerRef, ComponentRef, ViewChild, effect } from '@angular/core'; +import { Component, Input, OnChanges, SimpleChanges, signal, computed, inject, CUSTOM_ELEMENTS_SCHEMA, ViewContainerRef, ComponentRef, ViewChild, effect, Output, EventEmitter } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MarkdownViewerComponent } from '../markdown-viewer/markdown-viewer.component'; import { FileTypeDetectorService } from '../../services/file-type-detector.service'; import { Note } from '../../types'; import { EditorStateService } from '../../services/editor-state.service'; +import { DrawingsEditorComponent } from '../../app/features/drawings/drawings-editor.component'; +import { PdfViewerComponent } from '../../app/features/note-view/components/pdf-viewer/pdf-viewer.component'; +import { CodeRendererComponent } from '../../app/features/note-view/components/code-renderer/code-renderer.component'; +import { ImageViewerComponent } from '../../app/features/note-view/components/image-viewer/image-viewer.component'; +import { VideoPlayerComponent } from '../../app/features/note-view/components/video-player/video-player.component'; /** * Composant intelligent qui détecte automatiquement le type de fichier @@ -22,7 +27,7 @@ import { EditorStateService } from '../../services/editor-state.service'; @Component({ selector: 'app-smart-file-viewer', standalone: true, - imports: [CommonModule, MarkdownViewerComponent], + imports: [CommonModule, MarkdownViewerComponent, DrawingsEditorComponent, PdfViewerComponent, CodeRendererComponent, ImageViewerComponent, VideoPlayerComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], template: `
@@ -31,41 +36,53 @@ import { EditorStateService } from '../../services/editor-state.service'; + (editModeRequested)="onEditModeRequested($event)" + class="animate-fadeIn"> + +
+ +
+ -
- +
+
-
- +
+ +
+ + +
+ +
+ + +
+
-
-
{{ content }}
+
+
{{ resolvedContent() }}
-
+
@@ -80,12 +97,17 @@ import { EditorStateService } from '../../services/editor-state.service'; :host { display: block; height: 100%; + width: 100%; + min-width: 0; } .smart-file-viewer { height: 100%; + width: 100%; display: flex; flex-direction: column; + min-height: 0; + min-width: 0; } .smart-file-viewer__editor { @@ -105,14 +127,42 @@ import { EditorStateService } from '../../services/editor-state.service'; overflow: auto; } + /* Excalidraw should stretch exactly like PDF */ + .smart-file-viewer__excalidraw { + flex: 1; + display: flex; + padding: 0; + min-height: 0; + } + + .smart-file-viewer__excalidraw app-drawings-editor, + .smart-file-viewer__pdf app-pdf-viewer { + flex: 1 1 auto; + display: flex; + min-height: 0; + width: 100%; + min-width: 0; + } + + .smart-file-viewer__pdf { + padding: 0; + align-items: stretch; + justify-content: stretch; + min-height: 0; + } + + .smart-file-viewer__video { + flex: 1; + display: flex; + padding: 0; + } + .smart-file-viewer__image img { max-height: 90vh; object-fit: contain; } - .smart-file-viewer__pdf iframe { - min-height: 600px; - } + /* Removed min-height on PDF iframe to let it stretch with flex */ .smart-file-viewer__text pre { width: 100%; @@ -125,11 +175,14 @@ import { EditorStateService } from '../../services/editor-state.service'; @media (max-width: 768px) { .smart-file-viewer__image, - .smart-file-viewer__pdf, .smart-file-viewer__text, .smart-file-viewer__unknown { padding: 1rem; } + + .smart-file-viewer__pdf { + padding: 0; + } } `] }) @@ -139,42 +192,62 @@ export class SmartFileViewerComponent implements OnChanges { private fileTypeDetector = inject(FileTypeDetectorService); private editorStateService = inject(EditorStateService); private editorComponentRef?: ComponentRef; + private codeComponentRef?: ComponentRef; @Input() filePath: string = ''; @Input() content: string = ''; + private filePathSig = signal(''); + private contentSig = signal(''); @Input() allNotes: Note[] = []; @Input() currentNote?: Note; @Input() showToolbar: boolean = true; @Input() fullscreenMode: boolean = true; + @Output() imageDimensions = new EventEmitter<{ width: number; height: number }>(); + @Output() videoMetadata = new EventEmitter<{ width: number; height: number; duration: number }>(); - viewerType = signal<'markdown' | 'excalidraw' | 'image' | 'pdf' | 'text' | 'unknown'>('unknown'); + viewerType = signal<'markdown' | 'excalidraw' | 'image' | 'pdf' | 'video' | 'code' | 'text' | 'unknown'>('unknown'); isEditMode = computed(() => this.editorStateService.isEditMode()); fileName = computed(() => { - return this.filePath.split('/').pop() || this.filePath.split('\\').pop() || 'Unknown file'; + const p = this.filePathSig(); + return p.split('/').pop() || p.split('\\').pop() || 'Unknown file'; }); imageSrc = computed(() => { if (this.viewerType() !== 'image') return ''; // If content is base64, use it directly - if (this.content.startsWith('data:image')) { - return this.content; + const c = this.contentSig(); + if (c.startsWith('data:image')) { + return c; } - // Otherwise, construct API path - return `/api/files/${encodeURIComponent(this.filePath)}`; + // Otherwise, serve directly from vault static path + return `/vault/${encodeURI(this.filePathSig())}`; }); pdfSrc = computed(() => { if (this.viewerType() !== 'pdf') return ''; - return `/api/files/${encodeURIComponent(this.filePath)}`; + const path = encodeURI(this.filePathSig()); + return `/vault/${path}#view=FitH&zoom=page-width`; + }); + + videoSrc = computed(() => { + if (this.viewerType() !== 'video') return ''; + return `/vault/${encodeURI(this.filePathSig())}`; + }); + + // Internal fetched content for non-markdown text/code files + private fetchedContent = signal(''); + resolvedContent = computed(() => { + const c = this.contentSig(); + return (c && c.length > 0) ? c : this.fetchedContent(); }); ngOnChanges(changes: SimpleChanges): void { - if (changes['filePath'] || changes['content']) { - this.detectViewerType(); - } + if (changes['filePath']) this.filePathSig.set(this.filePath || ''); + if (changes['content']) this.contentSig.set(this.content || ''); + if (changes['filePath'] || changes['content']) this.detectViewerType(); } private detectViewerType(): void { @@ -192,6 +265,29 @@ export class SmartFileViewerComponent implements OnChanges { this.unloadEditor(); } }); + + // No-op for code lazy loading: now using inline CodeRendererComponent + + // Fetch textual content for code/text viewers when filePath or viewer changes + effect(() => { + const vt = this.viewerType(); + const path = this.filePathSig(); + if (!path) { this.fetchedContent.set(''); return; } + if (vt === 'code' || vt === 'text') { + // Do not attempt to fetch files inside the .obsidian folder (restricted) + if (path.startsWith('.obsidian/')) { + this.fetchedContent.set(''); + } else { + const url = `/vault/${encodeURI(path)}`; + fetch(url) + .then(r => r.text().catch(() => '')) + .then(txt => this.fetchedContent.set(txt)) + .catch(() => this.fetchedContent.set('')); + } + } else { + this.fetchedContent.set(''); + } + }); } async onEditModeRequested(event: { path: string; content: string }): Promise { @@ -241,4 +337,8 @@ export class SmartFileViewerComponent implements OnChanges { console.log('[SmartFileViewer] Editor unloaded'); } + + // Bridge child viewer events to parent consumers + onImgDims(ev: { width: number; height: number }) { this.imageDimensions.emit(ev); } + onVidMeta(ev: { width: number; height: number; duration: number }) { this.videoMetadata.emit(ev); } } diff --git a/src/components/tags-view/note-viewer/note-viewer.component.html b/src/components/tags-view/note-viewer/note-viewer.component.html index c74596b..46b3770 100644 --- a/src/components/tags-view/note-viewer/note-viewer.component.html +++ b/src/components/tags-view/note-viewer/note-viewer.component.html @@ -1,56 +1,24 @@ @if(note(); as note) { -
-
-
-

{{ note.title }}

-
- Mise à jour : {{ note.updatedAt | date:'medium' }} -
-
- @for(tag of note.tags; track tag) { - {{ tag }} - } -
-
+
+ @if(note.type === 'pdf') { + + + } @else { +
+
+
+

{{ note.title }}

+
+ Mise à jour : {{ note.updatedAt | date:'medium' }} +
+
-
-
- - @if (note.backlinks.length > 0) { -
-

Backlinks

-
    - @for (backlinkId of note.backlinks; track backlinkId) { -
  • - -
  • - } -
-
- } -
- - @if (tableOfContents().length > 0) { - +
}
} diff --git a/src/components/tags-view/note-viewer/note-viewer.component.ts b/src/components/tags-view/note-viewer/note-viewer.component.ts index 99ed134..9a664d1 100644 --- a/src/components/tags-view/note-viewer/note-viewer.component.ts +++ b/src/components/tags-view/note-viewer/note-viewer.component.ts @@ -16,6 +16,8 @@ import { Note } from '../../../types'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { NotePreviewService, PreviewData } from '../../../services/note-preview.service'; import { NoteHeaderComponent } from '../../../app/features/note/components/note-header/note-header.component'; +import { SmartFileViewerComponent } from '../../smart-file-viewer/smart-file-viewer.component'; +import { FileTypeDetectorService } from '../../../services/file-type-detector.service'; import { MarkdownEditorComponent } from '../../../app/features/editor/markdown-editor.component'; import { EditorStateService } from '../../../services/editor-state.service'; import { ClipboardService } from '../../../app/shared/services/clipboard.service'; @@ -45,10 +47,18 @@ export interface WikiLinkActivation { @Component({ selector: 'app-note-viewer', standalone: true, - imports: [CommonModule, NoteHeaderComponent, MarkdownEditorComponent], + imports: [CommonModule, NoteHeaderComponent, MarkdownEditorComponent, SmartFileViewerComponent], changeDetection: ChangeDetectionStrategy.OnPush, styles: [` - .note-viewer-root { position: relative; border-radius: 0.5rem; overflow: visible; } + .note-viewer-root { + position: relative; + border-radius: 0.5rem; + overflow: hidden; + display:flex; + flex-direction:column; + height: 100%; /* inherit height from parent section (app shell controls layout) */ + min-height: 0; + } .note-viewer-root::before { content: ''; position: absolute; @@ -66,275 +76,297 @@ export interface WikiLinkActivation { z-index: 0; } .note-viewer-root > * { position: relative; z-index: 1; } + .note-viewer-root app-smart-file-viewer { flex: 1 1 auto; min-height: 0; display: flex; } + + .note-viewer-scroll { + flex: 1 1 auto; + width: 100%; + display: flex; + flex-direction: column; + min-height: 0; + overflow-y: auto; + } `], template: ` -
+
{{ copyStatus() }}
- -
- +
+ +
+ -
- - - - - - - - -
- - @if (menuOpen()) { -
- - + } +
+ } + +
+ +
+ + + + + {{ note.updatedAt | date:'medium' }} + + + + + + {{ getAuthorFromFrontmatter() ?? note.author ?? 'Auteur inconnu' }} + +
+ + +
+ @if (hasState('favoris')) { + + } + @if (hasState('publish')) { + + } + @if (hasState('draft')) { + + } + @if (hasState('template')) { + + } + @if (hasState('task')) { + + } + @if (hasState('private')) { + + } + @if (hasState('archive')) { + + }
- }
-
-
- @if (isEditMode()) { - - } @else { - - @if (frontmatterTags().length > 0) { -
- @for (tag of frontmatterTags(); track tag) { - - } -
- } - -
- -
- - - - - {{ note.updatedAt | date:'medium' }} - - - - - - {{ getAuthorFromFrontmatter() ?? note.author ?? 'Auteur inconnu' }} - -
- - -
- @if (hasState('favoris')) { - - } - @if (hasState('publish')) { - - } - @if (hasState('draft')) { - - } - @if (hasState('template')) { - - } - @if (hasState('task')) { - - } - @if (hasState('private')) { - - } - @if (hasState('archive')) { - } -
- -
- } - - - - @if (backlinksNote.backlinks.length > 0) { -
-

Backlinks

- -
- }
`, @@ -358,6 +390,7 @@ export class NoteViewerComponent implements OnDestroy { private readonly elementRef = inject(ElementRef); private readonly sanitizer = inject(DomSanitizer); + private readonly fileTypeDetector = inject(FileTypeDetectorService); private readonly previewService = inject(NotePreviewService); private readonly clipboard = inject(ClipboardService); private readonly toast = inject(ToastService); @@ -380,6 +413,7 @@ export class NoteViewerComponent implements OnDestroy { readonly menuOpen = signal(false); readonly copyStatus = signal(''); + // Edition state readonly isEditMode = this.editorState.isEditMode; @@ -387,6 +421,16 @@ export class NoteViewerComponent implements OnDestroy { this.sanitizer.bypassSecurityTrustHtml(this.noteHtmlContent()) ); + readonly viewerType = computed(() => { + try { + const n = this.note(); + return this.fileTypeDetector.getViewerType(n.filePath, n.rawContent ?? n.content); + } catch { + return 'markdown' as const; + } + }); + + frontmatterTags = computed(() => { const tags = this.note().frontmatter?.tags; const headerTags = new Set( diff --git a/src/services/file-type-detector.service.ts b/src/services/file-type-detector.service.ts index fe8d49f..8d569d5 100644 --- a/src/services/file-type-detector.service.ts +++ b/src/services/file-type-detector.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; export interface FileTypeInfo { - type: 'markdown' | 'excalidraw' | 'image' | 'pdf' | 'unknown'; + type: 'markdown' | 'excalidraw' | 'image' | 'pdf' | 'video' | 'code' | 'unknown'; isEditable: boolean; requiresSpecialViewer: boolean; icon: string; @@ -52,6 +52,29 @@ export class FileTypeDetectorService { return path.toLowerCase().endsWith('.pdf'); } + /** + * Détecte si un fichier est une vidéo + */ + isVideoFile(path: string): boolean { + if (!path) return false; + const normalized = path.toLowerCase(); + const videoExtensions = ['.mp4', '.mov', '.avi', '.mkv', '.webm']; + return videoExtensions.some(ext => normalized.endsWith(ext)); + } + + /** + * Détecte si un fichier est un fichier de code/texte structuré + */ + isCodeFile(path: string): boolean { + if (!path) return false; + const normalized = path.toLowerCase(); + const codeExt = [ + '.json','.py','.java','.ts','.js','.mjs','.cjs','.cs','.cpp','.c','.hpp','.h','.go','.rs','.rb','.php','.kt','.kts','.scala','.swift','.sh','.bash','.ps1','.zsh','.yml','.yaml','.toml','.ini','.conf','.cfg','.env','.xml','.xsd','.xslt','.sql','.dockerfile','.makefile','.gradle','.cmake','.tex','.r','.jl','.lua', + '.html','.css','.jsx','.tsx' + ]; + return codeExt.some(ext => normalized.endsWith(ext)); + } + /** * Retourne les informations complètes sur le type de fichier */ @@ -97,6 +120,25 @@ export class FileTypeDetectorService { }; } + if (this.isVideoFile(path)) { + return { + type: 'video', + isEditable: false, + requiresSpecialViewer: false, + icon: '🎬', + mimeType: 'video/*' + }; + } + + if (this.isCodeFile(path)) { + return { + type: 'code', + isEditable: false, + requiresSpecialViewer: true, + icon: '' + } as any; + } + return { type: 'unknown', isEditable: false, @@ -154,7 +196,7 @@ export class FileTypeDetectorService { /** * Détermine le viewer approprié pour un fichier */ - getViewerType(path: string, content?: string): 'markdown' | 'excalidraw' | 'image' | 'pdf' | 'text' | 'unknown' { + getViewerType(path: string, content?: string): 'markdown' | 'excalidraw' | 'image' | 'pdf' | 'video' | 'code' | 'text' | 'unknown' { // Check file extension first if (this.isExcalidrawFile(path)) { return 'excalidraw'; @@ -180,8 +222,16 @@ export class FileTypeDetectorService { return 'pdf'; } + if (this.isVideoFile(path)) { + return 'video'; + } + + if (this.isCodeFile(path)) { + return 'code'; + } + // Check if it's a text file - const textExtensions = ['.txt', '.json', '.xml', '.csv', '.log', '.yaml', '.yml', '.toml']; + const textExtensions = ['.txt', '.csv', '.log']; if (textExtensions.some(ext => path.toLowerCase().endsWith(ext))) { return 'text'; } diff --git a/src/services/vault.service.ts b/src/services/vault.service.ts index 5b8f8aa..b926c9c 100644 --- a/src/services/vault.service.ts +++ b/src/services/vault.service.ts @@ -267,7 +267,40 @@ export class VaultService implements OnDestroy { } getNoteById(id: string): Note | undefined { - return this.notesMap().get(id); + const existing = this.notesMap().get(id); + if (existing) return existing; + // Fallback: synthesize a virtual note from fast metadata (for non-markdown files) + const path = this.idToPathIndex.get(id) || this.slugIdToPathIndex.get(id); + const meta = path ? this.metaByPathIndex.get(path) : undefined; + if (!meta || !path) return undefined; + + const filePath = this.normalizePath(path); + const fileName = filePath.split('/').pop() || filePath; + const originalPath = filePath.replace(/\.md$/i, '').replace(/\\/g, '/'); + const title = meta.title || fileName; + const createdAt = meta.createdAt || undefined; + const updatedAt = meta.updatedAt || undefined; + + const virtualNote: Note = { + id, + title, + content: '', + rawContent: '', + tags: [], + frontmatter: {} as any, + backlinks: [], + mtime: Date.parse(updatedAt || createdAt || '') || 0, + fileName, + filePath, + originalPath, + createdAt, + updatedAt, + }; + // Cache synthesized note to make it selectable + const next = new Map(this.notesMap()); + next.set(id, virtualNote); + this.notesMap.set(next); + return virtualNote; } /** @@ -327,7 +360,36 @@ export class VaultService implements OnDestroy { if (!id || this.getNoteById(id)) return !!id; const path = this.idToPathIndex.get(id) || this.slugIdToPathIndex.get(id); - return path ? this.ensureNoteLoadedByPath(path) : false; + if (!path) return false; + // Try to load real markdown content; if not possible, synthesize virtual note + const ok = await this.ensureNoteLoadedByPath(path); + if (ok) return true; + const meta = this.metaByPathIndex.get(path); + if (!meta) return false; + // Create a virtual note for non-markdown types + const filePath = this.normalizePath(path); + const fileName = filePath.split('/').pop() || filePath; + const originalPath = filePath.replace(/\.md$/i, '').replace(/\\/g, '/'); + const title = meta.title || fileName; + const createdAt = meta.createdAt || undefined; + const updatedAt = meta.updatedAt || undefined; + const virtualNote: Note = { + id, + title, + content: '', + rawContent: '', + tags: [], + frontmatter: {} as any, + backlinks: [], + mtime: Date.parse(updatedAt || createdAt || '') || 0, + fileName, + filePath, + originalPath, + createdAt, + updatedAt, + }; + this.addNoteToMap(virtualNote); + return true; } async ensureNoteLoadedByPath(path: string): Promise { @@ -343,6 +405,7 @@ export class VaultService implements OnDestroy { this.addNoteToMap(note); return true; } catch { + // Non-markdown or unsupported types will fail here; signal caller to synthesize return false; } } @@ -395,6 +458,12 @@ export class VaultService implements OnDestroy { return path ? this.metaByPathIndex.get(path) : undefined; } + getFastMetaByPath(filePath: string): FileMetadata | undefined { + if (!filePath) return undefined; + const p = this.normalizePath(filePath); + return this.metaByPathIndex.get(p); + } + async updateNoteTags(noteId: string, tags: string[]): Promise { const note = this.getNoteById(noteId); if (!note?.filePath) return false; @@ -589,12 +658,16 @@ export class VaultService implements OnDestroy { } private buildTrashTree(): VaultNode[] { + // Touch fast tree to make this computed reactive to file metadata updates + const _fast = this.fastFileTree(); + const root = this.createFolder(TRASH_FOLDER, TRASH_FOLDER, true); const folders = new Map([[TRASH_FOLDER, root]]); const openFolders = this.openFolderPaths(); - for (const note of this.allNotes()) { - const filePath = this.normalizePath(note.filePath || note.originalPath || ''); + // Include ALL files (not only markdown notes) from metadata index + for (const [pathKey] of this.metaByPathIndex.entries()) { + const filePath = this.normalizePath(pathKey); if (!this.isInTrash(filePath)) continue; const segments = this.parseTrashFolderSegments(filePath); @@ -602,7 +675,9 @@ export class VaultService implements OnDestroy { ? this.ensureTrashFolderPath(segments, folders, root, openFolders) : root; - this.addFileNode(parentFolder, note.filePath, note.id, note.fileName); + const id = this.buildSlugIdFromPath(filePath); + const fileName = filePath.split('/').pop() || filePath; + this.addFileNode(parentFolder, filePath, id, fileName); } this.sortAndCleanFolderChildren(root); @@ -706,14 +781,18 @@ export class VaultService implements OnDestroy { } private calculateFolderCounts(): Record { + // Touch fast tree to make this computed reactive to file metadata updates + const _fast = this.fastFileTree(); + const counts: Record = {}; - for (const note of this.allNotes()) { - const path = this.normalizePath(note.originalPath || note.filePath || ''); + // Count ALL files from metadata index (fast, includes non-markdown) + for (const [pathKey] of this.metaByPathIndex.entries()) { + const path = this.normalizePath(pathKey); if (!path || this.isInTrash(path) || this.isBuiltinPath(path)) continue; const parts = path.split('/'); - parts.pop(); // Remove filename + parts.pop(); // remove filename let acc = ''; for (const segment of parts) { @@ -727,6 +806,9 @@ export class VaultService implements OnDestroy { } private calculateTrashFolderCounts(): Record { + // Touch fast tree to make this computed reactive to file metadata updates + const _fast = this.fastFileTree(); + const counts: Record = {}; const increment = (path: string) => { @@ -735,8 +817,9 @@ export class VaultService implements OnDestroy { counts[norm] = (counts[norm] ?? 0) + 1; }; - for (const note of this.allNotes()) { - const filePath = this.normalizePath(note.filePath || note.originalPath || ''); + // Count ALL files from metadata index that are in trash + for (const [pathKey] of this.metaByPathIndex.entries()) { + const filePath = this.normalizePath(pathKey); if (!this.isInTrash(filePath)) continue; const segments = this.parseTrashFolderSegments(filePath); @@ -750,6 +833,7 @@ export class VaultService implements OnDestroy { let current = TRASH_FOLDER; for (const segment of segments) { + if (!segment) continue; current = `${current}/${segment}`; increment(current); } diff --git a/src/styles.css b/src/styles.css index b2e35fc..0701cf4 100644 --- a/src/styles.css +++ b/src/styles.css @@ -636,6 +636,20 @@ html, body { } @layer utilities { + /* Full-bleed container for non-markdown viewers */ + .ov-fullbleed { + width: 100% !important; + max-width: none !important; + } + + /* Ensure viewer wrappers are never constrained by inherited max-width */ + .ov-viewer, + .ov-pdf, + .ov-excalidraw { + max-width: none !important; + width: 100% !important; + } + /* Mobile animations */ @keyframes fadeIn { from { diff --git a/vault/.obsidian/workspace.json b/vault/.obsidian/workspace.json index 1522e60..9f0a81d 100644 --- a/vault/.obsidian/workspace.json +++ b/vault/.obsidian/workspace.json @@ -13,10 +13,10 @@ "state": { "type": "excalidraw", "state": { - "file": "Drawing-20251028-1452.excalidraw.md" + "file": "dessin.excalidraw.md" }, "icon": "excalidraw-icon", - "title": "Drawing-20251028-1452.excalidraw" + "title": "dessin.excalidraw" } } ] @@ -178,11 +178,22 @@ }, "active": "8309e5042a8fb85c", "lastOpenFiles": [ + "Dessin-02.png", + "mixe/Relaxing Music relax music music _hls-480_.mp4", + "Dessin-02.excalidraw.md", + "dessin.excalidraw.md", + "Drawing-20251028-1452.excalidraw.md", + "Dessin-02.excalidraw.md.tmp", + "Dessin-02.excalidraw.md.bak", + "mixe/ThinkBook_16_G7_ARP_Spec.pdf", + "mixe/Claude_ObsiViewer_V1.png", + "mixe/image_no_bg_clean.svg", + "mixe/dessin.json", + "mixe", + "New folder", "Drawing-20251028-1452.png", "Drawing-20251028-1452.excalidraw.md.bak", - "Drawing-20251028-1452.excalidraw.md", "dessin.svg", - "dessin.excalidraw.md", "dessin.png", "dessin.excalidraw.md.bak", "dessin_05.svg", @@ -190,16 +201,9 @@ "dessin_05.excalidraw.md", "dessin_03.excalidraw.md", "dessin_05.excalidraw.md.bak", - "dessin_04.excalidraw.md.tmp", - "dessin_04.excalidraw.md.bak", "dessin_04.excalidraw.md", "dessin-002.excalidraw.md", - "dessin_03.excalidraw.md.tmp", - "dessin_03.excalidraw.md.bak", - "dessin-002.excalidraw.md.tmp", - "dessin-002.excalidraw.md.bak", "Dessin_001.excalidraw.md", - "Dessin_001.excalidraw.md.tmp", "Drawing 2025-10-28 11.11.59.excalidraw.md", "dessin-06.excalidraw.md", "Dessin-5.excalidraw.md", @@ -217,7 +221,6 @@ "HOME.md", "Drawing-20251027-1914.excalidraw.md", "Drawing-20251027-1705.excalidraw.md", - "Drawing 2025-10-27 16.43.48.excalidraw.md", "Untitled.canvas" ] } \ No newline at end of file diff --git a/vault/Dessin-02.excalidraw.md b/vault/Dessin-02.excalidraw.md new file mode 100644 index 0000000..e5964ab --- /dev/null +++ b/vault/Dessin-02.excalidraw.md @@ -0,0 +1,15 @@ +--- +excalidraw-plugin: parsed +updated: "2025-10-30T01:40:26.435Z" +--- +==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠== + +# Excalidraw Data + +## Text Elements +%% +## Drawing +```compressed-json +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= +``` +%% \ No newline at end of file diff --git a/vault/Dessin-02.png b/vault/Dessin-02.png new file mode 100644 index 0000000..337d486 Binary files /dev/null and b/vault/Dessin-02.png differ diff --git a/vault/Drawing-20251028-1452.excalidraw.md b/vault/Drawing-20251028-1452.excalidraw.md deleted file mode 100644 index 5345e3c..0000000 --- a/vault/Drawing-20251028-1452.excalidraw.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -excalidraw-plugin: parsed -updated: "2025-10-29T00:30:06.609Z" ---- -==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠== - -# Excalidraw Data - -## Text Elements -%% -## Drawing -```compressed-json -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 -``` -%% \ No newline at end of file diff --git a/vault/dessin.excalidraw.md b/vault/dessin.excalidraw.md index 3582966..e2e2287 100644 --- a/vault/dessin.excalidraw.md +++ b/vault/dessin.excalidraw.md @@ -1,6 +1,6 @@ --- excalidraw-plugin: parsed -updated: "2025-10-28T18:48:43.751Z" +updated: "2025-10-30T11:48:55.327Z" --- ==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠== @@ -10,6 +10,6 @@ updated: "2025-10-28T18:48:43.751Z" %% ## Drawing ```compressed-json -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 ``` %% \ No newline at end of file diff --git a/vault/dessin.png b/vault/dessin.png index ab41a3b..c33a564 100644 Binary files a/vault/dessin.png and b/vault/dessin.png differ diff --git a/vault/mixe/Claude_ObsiViewer_V1.png b/vault/mixe/Claude_ObsiViewer_V1.png new file mode 100644 index 0000000..6a8da5b Binary files /dev/null and b/vault/mixe/Claude_ObsiViewer_V1.png differ diff --git a/vault/mixe/ExampleScript.ps1 b/vault/mixe/ExampleScript.ps1 new file mode 100644 index 0000000..f644bdb --- /dev/null +++ b/vault/mixe/ExampleScript.ps1 @@ -0,0 +1,72 @@ +<# +.SYNOPSIS + Exemple de script PowerShell de démonstration. + +.DESCRIPTION + Ce script montre comment : + - Définir des variables + - Lire des entrées utilisateur + - Utiliser des conditions et des boucles + - Écrire dans un fichier de log + - Gérer des erreurs + +.AUTHOR + Bruno Charest +#> + +# ======================= +# ⚙️ Variables de base +# ======================= +$UserName = $env:USERNAME +$DateNow = Get-Date -Format "yyyy-MM-dd HH:mm:ss" +$LogFile = "C:\Logs\ExampleScript.log" + +# Crée le répertoire de logs si nécessaire +if (!(Test-Path (Split-Path $LogFile))) { + New-Item -Path (Split-Path $LogFile) -ItemType Directory -Force | Out-Null +} + +# ======================= +# 📝 Fonction pour journaliser les événements +# ======================= +function Write-Log { + param ( + [string]$Message, + [string]$Level = "INFO" + ) + $LogEntry = "$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')) [$Level] $Message" + Add-Content -Path $LogFile -Value $LogEntry + Write-Host $LogEntry +} + +Write-Log "Script démarré par $UserName" + +# ======================= +# 👋 Interaction utilisateur +# ======================= +$name = Read-Host "Quel est ton prénom" +Write-Log "Nom entré : $name" + +# ======================= +# 🔁 Exemple de boucle +# ======================= +for ($i = 1; $i -le 5; $i++) { + Write-Host "Itération $i" + Start-Sleep -Milliseconds 500 +} + +# ======================= +# ⚠️ Exemple de gestion d'erreur +# ======================= +try { + # Exemple : tentative de suppression d’un fichier inexistant + Remove-Item "C:\FichierInexistant.txt" -ErrorAction Stop +} catch { + Write-Log "Erreur détectée : $($_.Exception.Message)" "ERROR" +} + +# ======================= +# ✅ Fin du script +# ======================= +Write-Log "Script terminé avec succès." +Write-Host "`n✅ Exécution terminée avec succès." diff --git a/vault/mixe/Relaxing Music relax music music _hls-480_.mp4 b/vault/mixe/Relaxing Music relax music music _hls-480_.mp4 new file mode 100644 index 0000000..4407ecf Binary files /dev/null and b/vault/mixe/Relaxing Music relax music music _hls-480_.mp4 differ diff --git a/vault/mixe/ThinkBook_16_G7_ARP_Spec.pdf b/vault/mixe/ThinkBook_16_G7_ARP_Spec.pdf new file mode 100644 index 0000000..0832ae1 Binary files /dev/null and b/vault/mixe/ThinkBook_16_G7_ARP_Spec.pdf differ diff --git a/vault/mixe/dessin.json b/vault/mixe/dessin.json new file mode 100644 index 0000000..d19535c --- /dev/null +++ b/vault/mixe/dessin.json @@ -0,0 +1,217 @@ +{ + "elements": [ + { + "type": "ellipse", + "version": 132, + "versionNonce": 283426111, + "isDeleted": false, + "id": "75xXCsrYeEIpx-bVFdD26", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 152, + "y": 143.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 245.50000000000003, + "height": 174.49999999999997, + "seed": 1925880401, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1761676663352, + "link": null, + "locked": false + }, + { + "type": "line", + "version": 281, + "versionNonce": 1637513215, + "isDeleted": false, + "id": "Ee7Y120RtAdQsiCUEU0r7", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 209.5, + "y": 158.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 5.5, + "height": 147.5, + "seed": 1375870513, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1761676682202, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 5.5, + 147.5 + ] + ] + }, + { + "type": "line", + "version": 200, + "versionNonce": 1266585960, + "isDeleted": false, + "id": "TwSQiyuxGNT_cWR9zLRTk", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 213.57179654033308, + "y": 230.71709399425663, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 187, + "height": 3.5, + "seed": 1447624159, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1761677094707, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 187, + -3.5 + ] + ] + } + ], + "appState": { + "showWelcomeScreen": true, + "theme": "dark", + "currentChartType": "bar", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemRoundness": "round", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "activeEmbeddable": null, + "draggingElement": null, + "editingElement": null, + "editingGroupId": null, + "editingLinearElement": null, + "activeTool": { + "type": "hand", + "customType": null, + "locked": false, + "lastActiveTool": null + }, + "penMode": false, + "penDetected": false, + "errorMessage": null, + "exportBackground": true, + "exportScale": 2, + "exportEmbedScene": false, + "exportWithDarkMode": false, + "fileHandle": null, + "gridSize": null, + "isBindingEnabled": true, + "defaultSidebarDockedPreference": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "touch", + "multiElement": null, + "name": "Untitled-2025-10-28-1436", + "contextMenu": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "openDialog": null, + "pasteDialog": { + "shown": false, + "data": null + }, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrolledOutside": false, + "scrollX": 141.7025899014401, + "scrollY": 113.16506876319784, + "selectedElementIds": {}, + "selectedGroupIds": {}, + "selectedElementsAreBeingDragged": false, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showStats": false, + "startBoundElement": null, + "suggestedBindings": [], + "frameRendering": { + "enabled": true, + "clip": true, + "name": true, + "outline": true + }, + "frameToHighlight": null, + "editingFrame": null, + "elementsToHighlight": null, + "toast": null, + "viewBackgroundColor": "#f8f9fa", + "zenModeEnabled": false, + "zoom": { + "value": 0.7938006350863728 + }, + "viewModeEnabled": false, + "pendingImageElementId": null, + "showHyperlinkPopup": false, + "selectedLinearElement": null, + "snapLines": [], + "originSnapOffset": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 723, + "offsetTop": 228, + "width": 665, + "height": 546 + }, + "files": {} +} \ No newline at end of file diff --git a/vault/mixe/image_no_bg_clean.svg b/vault/mixe/image_no_bg_clean.svg new file mode 100644 index 0000000..6514451 --- /dev/null +++ b/vault/mixe/image_no_bg_clean.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/vault/mixe/test.py b/vault/mixe/test.py new file mode 100644 index 0000000..c9f9139 --- /dev/null +++ b/vault/mixe/test.py @@ -0,0 +1,61 @@ +"""Exemple de script Python illustrant l'usage des dataclasses.""" + +from dataclasses import dataclass, field +from datetime import datetime, timedelta +from typing import Iterable + + +@dataclass +class Task: + """Représente une tâche planifiée avec une durée estimée.""" + + name: str + estimated_minutes: int + created_at: datetime = field(default_factory=datetime.now) + completed: bool = False + + def remaining_minutes(self) -> int: + """Calcule le temps restant estimé.""" + return 0 if self.completed else self.estimated_minutes + + def mark_done(self) -> None: + """Marque la tâche comme terminée.""" + self.completed = True + + +def summarize(tasks: Iterable[Task]) -> str: + """Retourne un résumé textuel de la charge de travail.""" + task_list = list(tasks) + total = sum(task.estimated_minutes for task in task_list) + remaining = sum(task.remaining_minutes() for task in task_list) + done = total - remaining + return ( + f"Tâches: {len(task_list)}\n" + f"Terminé: {done} min\n" + f"Restant: {remaining} min\n" + f"Charge totale: {total} min" + ) + + +def main() -> None: + """Point d'entrée du script avec quelques exemples.""" + tasks = [ + Task("Préparer la réunion", estimated_minutes=45), + Task("Répondre aux emails", estimated_minutes=20), + Task("Prototyper l'interface", estimated_minutes=90), + ] + + # Marquer une tâche comme terminée pour illustrer la logique. + tasks[1].mark_done() + + print("Résumé du jour:") + print(summarize(tasks)) + + # Afficher l'échéance approximative si toutes les tâches sont démarrées maintenant. + deadline = datetime.now() + timedelta(minutes=sum(task.remaining_minutes() for task in tasks)) + print(f"Heure de fin estimée: {deadline:%H:%M}") + + +if __name__ == "__main__": + main() + diff --git a/web-components/excalidraw/ExcalidrawElement.tsx b/web-components/excalidraw/ExcalidrawElement.tsx index c50e22a..0817ae8 100644 --- a/web-components/excalidraw/ExcalidrawElement.tsx +++ b/web-components/excalidraw/ExcalidrawElement.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useMemo, useRef } from 'react'; +// import '@excalidraw/excalidraw/dist/excalidraw.min.css'; import { Excalidraw, exportToBlob, exportToSvg } from '@excalidraw/excalidraw'; import type { AppState, ExcalidrawImperativeAPI } from '@excalidraw/excalidraw/types/types'; import type { ExcalidrawWrapperProps, Scene, SceneChangeDetail, ReadyDetail } from './types';