Initial-Scan-Modal mit Fortschrittsanzeige
Batch-Processing (50 Dateien pro Batch) mit UI-Yield, Fortschrittsbalken, Datei-/Wort-/Link-/Tag-Zähler, Abbruch-Möglichkeit, Baseline-Event nach Scan. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e0d9f301d6
commit
91cc22c3e5
1 changed files with 153 additions and 0 deletions
153
src/ui/initial-scan-modal.ts
Normal file
153
src/ui/initial-scan-modal.ts
Normal file
|
|
@ -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<boolean> {
|
||||||
|
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<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue