Files
BadNote/lib/providers/search_provider.dart
Akiba So 24d13642fd
Some checks failed
CI / Windows build (push) Has been cancelled
feat(storage): app-pause flush + vault search index
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.
2026-06-24 23:19:21 +08:00

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;
});