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