All checks were successful
CI / Windows build (push) Successful in 12m55s
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.
82 lines
3.0 KiB
Dart
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());
|
|
}
|
|
}
|