diff --git a/src/ui/initial-scan-modal.ts b/src/ui/initial-scan-modal.ts new file mode 100644 index 0000000..65df147 --- /dev/null +++ b/src/ui/initial-scan-modal.ts @@ -0,0 +1,153 @@ +import { App, Modal, TFile } from 'obsidian'; +import { LogfireSettings, categoryForType } from '../types'; +import { ContentAnalyzer } from '../core/content-analyzer'; +import { DatabaseManager } from '../core/database'; +import { EventBus } from '../core/event-bus'; +import { SessionManager } from '../core/session-manager'; + +const BATCH_SIZE = 50; +const BATCH_DELAY_MS = 50; + +export class InitialScanModal extends Modal { + private cancelled = false; + private resolve!: (scanned: boolean) => void; + + constructor( + app: App, + private settings: LogfireSettings, + private analyzer: ContentAnalyzer, + private db: DatabaseManager, + private eventBus: EventBus, + private sessionManager: SessionManager, + private shouldTrack: (path: string) => boolean, + ) { + super(app); + } + + open(): Promise { + return new Promise((resolve) => { + this.resolve = resolve; + super.open(); + }); + } + + onOpen(): void { + const { contentEl } = this; + contentEl.empty(); + contentEl.addClass('logfire-scan-modal'); + + const files = this.app.vault.getMarkdownFiles() + .filter(f => this.shouldTrack(f.path)); + + contentEl.createEl('h2', { text: 'Logfire – Vault-Scan' }); + contentEl.createEl('p', { + text: `${files.length} Markdown-Dateien gefunden. Der Scan erstellt die Baseline für Content-Tracking.`, + }); + + const progressContainer = contentEl.createDiv({ cls: 'logfire-scan-progress' }); + const progressBar = progressContainer.createEl('progress', { + attr: { max: String(files.length), value: '0' }, + }); + progressBar.style.width = '100%'; + + const statusEl = contentEl.createDiv({ cls: 'logfire-scan-status' }); + const currentFileEl = statusEl.createDiv(); + const totalsEl = statusEl.createDiv(); + totalsEl.style.marginTop = '8px'; + totalsEl.style.opacity = '0.8'; + + const buttonContainer = contentEl.createDiv({ cls: 'logfire-scan-buttons' }); + buttonContainer.style.display = 'flex'; + buttonContainer.style.justifyContent = 'flex-end'; + buttonContainer.style.gap = '8px'; + buttonContainer.style.marginTop = '16px'; + + const cancelBtn = buttonContainer.createEl('button', { text: 'Abbrechen' }); + cancelBtn.addEventListener('click', () => { + this.cancelled = true; + }); + + const startBtn = buttonContainer.createEl('button', { text: 'Scan starten', cls: 'mod-cta' }); + startBtn.addEventListener('click', async () => { + startBtn.disabled = true; + cancelBtn.textContent = 'Stoppen'; + + let scanned = 0; + let totalWords = 0; + let totalLinks = 0; + let totalTags = 0; + + for (let i = 0; i < files.length; i += BATCH_SIZE) { + if (this.cancelled) break; + + const batch = files.slice(i, i + BATCH_SIZE); + for (const file of batch) { + if (this.cancelled) break; + + try { + const content = await this.app.vault.cachedRead(file); + const metadata = this.app.metadataCache.getFileCache(file); + const snapshot = this.analyzer.buildSnapshot(file.path, content, metadata ?? null); + + this.db.upsertBaseline( + snapshot, + file.stat.ctime, + file.stat.mtime, + file.stat.size, + ); + + totalWords += snapshot.wordCount; + totalLinks += snapshot.links.length; + totalTags += snapshot.tags.length; + scanned++; + + progressBar.value = scanned; + currentFileEl.textContent = file.path; + totalsEl.textContent = `${scanned}/${files.length} Dateien | ${totalWords.toLocaleString()} Wörter | ${totalLinks} Links | ${totalTags} Tags`; + } catch (err) { + console.error(`[Logfire] Scan-Fehler bei ${file.path}:`, err); + } + } + + await sleep(BATCH_DELAY_MS); + } + + this.eventBus.emit({ + id: crypto.randomUUID(), + timestamp: Date.now(), + type: this.cancelled ? 'system:rescan' : 'system:baseline-scan', + category: categoryForType('system:baseline-scan'), + source: this.app.vault.getName(), + payload: { + wordCount: totalWords, + linkCount: totalLinks, + tagCount: totalTags, + filesScanned: scanned, + filesTotal: files.length, + }, + session: this.sessionManager.currentSessionId, + }); + + currentFileEl.textContent = this.cancelled + ? `Scan gestoppt. ${scanned} von ${files.length} Dateien gescannt.` + : `Scan abgeschlossen! ${scanned} Dateien gescannt.`; + cancelBtn.style.display = 'none'; + + const closeBtn = buttonContainer.createEl('button', { text: 'Schließen', cls: 'mod-cta' }); + closeBtn.addEventListener('click', () => { + this.close(); + this.resolve(true); + }); + }); + } + + onClose(): void { + this.cancelled = true; + this.contentEl.empty(); + this.resolve(false); + } +} + +function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +}