Some checks failed
CI / Windows build (push) Has been cancelled
Phase 6 (final storage phase). - SidecarRepositoryRegistry tracks every open repo; SidecarFlushObserver (a WidgetsBindingObserver in main) flushes them all on inactive/hidden/paused/detached, awaiting each flush — the last strokes can't be lost on app close, not just on the 800ms timer. - VaultSearchIndex rebuilds by scanning vault sidecars (the source of truth) — note titles, OCR text and document names — and search_provider queries it, so search spans notes + PDFs. Rebuilt on launch / after import. The vault file-based storage migration (Phases 0-6) is complete: annotations travel with the file, picked vault folder, atomic autosave, one Import-file entry, SQLite migrated to sidecars. analyze clean, tests green.
95 lines
2.9 KiB
Dart
95 lines
2.9 KiB
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../models/note.dart';
|
|
import '../services/vault_search_index.dart';
|
|
import 'document_provider.dart' show vaultServiceProvider;
|
|
|
|
/// The search index, rebuilt by SCANNING the vault sidecars (the source of
|
|
/// truth) — NOT the demoted SQLite cache (Phase 6, §B/§F). Bumping
|
|
/// [searchIndexEpochProvider] (e.g. after an import or note edit) invalidates
|
|
/// this provider so the next read re-scans the vault from disk.
|
|
final vaultSearchIndexProvider = FutureProvider<VaultSearchIndex>((ref) async {
|
|
ref.watch(searchIndexEpochProvider);
|
|
final vault = await ref.watch(vaultServiceProvider.future);
|
|
final index = VaultSearchIndex(vault);
|
|
await index.rebuild();
|
|
return index;
|
|
});
|
|
|
|
/// Bump to force the search index to rebuild from disk (e.g. after an import).
|
|
final searchIndexEpochProvider = StateProvider<int>((ref) => 0);
|
|
|
|
final searchQueryProvider = StateProvider<String>((ref) => '');
|
|
|
|
/// A search result that can be either a note hit or a document hit.
|
|
sealed class SearchResult {
|
|
const SearchResult();
|
|
}
|
|
|
|
class NoteSearchHit extends SearchResult {
|
|
final Note note;
|
|
final String snippet;
|
|
const NoteSearchHit({required this.note, this.snippet = ''});
|
|
}
|
|
|
|
class DocumentSearchHit extends SearchResult {
|
|
final String documentId;
|
|
final String filename;
|
|
final String filePath;
|
|
final int pageNumber;
|
|
final String snippet;
|
|
const DocumentSearchHit({
|
|
required this.documentId,
|
|
required this.filename,
|
|
required this.filePath,
|
|
required this.pageNumber,
|
|
this.snippet = '',
|
|
});
|
|
}
|
|
|
|
final searchResultsProvider = FutureProvider<List<SearchResult>>((ref) async {
|
|
final query = ref.watch(searchQueryProvider);
|
|
if (query.trim().isEmpty) return [];
|
|
|
|
final index = await ref.watch(vaultSearchIndexProvider.future);
|
|
|
|
final hits = await index.search(query);
|
|
|
|
final results = <SearchResult>[];
|
|
for (final hit in hits) {
|
|
final entry = hit.entry;
|
|
final snippet = hit.snippet.text;
|
|
if (entry.isNote) {
|
|
// Construct a lightweight Note whose id is the synthetic note path so
|
|
// PenNoteScreen re-keys the right sidecar on open. Strokes are hydrated
|
|
// lazily by the editor; the search list only needs id/title.
|
|
final now = DateTime.now();
|
|
results.add(
|
|
NoteSearchHit(
|
|
note: Note(
|
|
id: entry.openPath,
|
|
title: entry.title,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
),
|
|
snippet: snippet,
|
|
),
|
|
);
|
|
} else {
|
|
results.add(
|
|
DocumentSearchHit(
|
|
documentId: entry.id,
|
|
filename: entry.title,
|
|
filePath: entry.openPath,
|
|
// The scan-based index matches whole-notebook text, not per-page, so
|
|
// the document opens at its first page.
|
|
pageNumber: 0,
|
|
snippet: snippet,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
});
|