Files
BadNote/lib/providers/note_provider.dart
Akiba So f4f0853eae
All checks were successful
CI / Windows build (push) Successful in 12m55s
feat(storage): notes are vault sidecar notebooks
Phase 4. Standalone notes move off SQLite into the vault, like the
PDF annotations.

- "Create notebook" makes a vault folder with a notebook.badnote.json
  (BadnoteSidecar docType 'notebook' + a title field), opened via
  SidecarRepository.
- PenNoteScreen loads/saves its strokes (page 0) + title to that
  sidecar instead of the SQLite Note model.
- note_provider lists notes from a vault scan (VaultService.scanNotes
  = folders with notebook.badnote.json and no source file); the doc
  scan still excludes them. Delete removes the folder.

PDF/slide editors unchanged; pre-existing SQLite notes migrate in
Phase 5. analyze clean, tests green.
2026-06-24 22:48:18 +08:00

82 lines
3.0 KiB
Dart

import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/note.dart';
import '../services/vault_service.dart';
import 'document_provider.dart' show vaultServiceProvider;
/// The home-screen note list is now sourced from a VAULT SCAN of standalone
/// (free-ink) notebook folders — each a folder holding a `notebook.badnote.json`
/// and NO importable source file — NOT the SQLite `notes` table. The sidecar
/// that lives in the folder is the source of truth ("跟着文件走").
///
/// Each scanned note is adapted into the existing [Note] model the home screen
/// already renders: `id` = the synthetic note path (`<folder>/notebook`, also a
/// stable id), `title`, `updatedAt` = the sidecar mtime. Strokes are NOT loaded
/// here — they are hydrated lazily by the editor from the sidecar, so the list
/// stays cheap (one directory listing). Home tiles that show a stroke count will
/// therefore read 0 until the note is opened; the count is no longer cached.
final noteListProvider = AsyncNotifierProvider<NoteListNotifier, List<Note>>(
NoteListNotifier.new,
);
class NoteListNotifier extends AsyncNotifier<List<Note>> {
Future<VaultService> get _vault => ref.read(vaultServiceProvider.future);
@override
Future<List<Note>> build() async {
return _scan();
}
Future<List<Note>> _scan() async {
final vault = await _vault;
final notes = await vault.scanNotes();
return notes.map(_toNote).toList();
}
/// Adapt a scanned [VaultNote] into the [Note] shape the home tiles render.
/// `id` is the synthetic note path so opening it re-keys the right sidecar.
Note _toNote(VaultNote n) => Note(
id: n.notePath,
title: n.title,
createdAt: n.modified,
updatedAt: n.modified,
);
/// Re-scan the vault and publish the result. Used by pull-to-refresh and
/// after a note is created or edited.
Future<void> loadNotes() async {
state = const AsyncLoading();
state = await AsyncValue.guard(_scan);
}
/// Create an empty standalone notebook folder with [title] and return the
/// adapted [Note] (whose `id` is the synthetic note path). The home screen
/// opens the editor on it; persistence flows through the sidecar.
Future<Note> createNote({String title = 'Untitled'}) async {
final vault = await _vault;
final notePath = await vault.createEmptyNotebook(title);
final now = DateTime.now();
final note = Note(
id: notePath,
title: title,
createdAt: now,
updatedAt: now,
);
state = AsyncData([note, ...state.value ?? []]);
return note;
}
/// Delete a note by removing its notebook folder (the sidecar travels with
/// it). [id] is the synthetic note path `<folder>/notebook`.
Future<void> deleteNote(String id) async {
final folder = Directory(File(id).parent.path);
if (await folder.exists()) {
await folder.delete(recursive: true);
}
final current = state.value ?? [];
state = AsyncData(current.where((n) => n.id != id).toList());
}
}