feat(storage): notes are vault sidecar notebooks
All checks were successful
CI / Windows build (push) Successful in 12m55s
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.
This commit is contained in:
@@ -1,68 +1,81 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../models/note.dart';
|
||||
import '../services/database_service.dart';
|
||||
|
||||
const _uuid = Uuid();
|
||||
|
||||
final databaseServiceProvider = FutureProvider<DatabaseService>((ref) async {
|
||||
return DatabaseService.getInstance();
|
||||
});
|
||||
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<DatabaseService> get _db => ref.read(databaseServiceProvider.future);
|
||||
Future<VaultService> get _vault => ref.read(vaultServiceProvider.future);
|
||||
|
||||
@override
|
||||
Future<List<Note>> build() async {
|
||||
final db = await _db;
|
||||
return db.getAllNotes();
|
||||
return _scan();
|
||||
}
|
||||
|
||||
/// Reloads notes from the database and publishes the result to [state] so
|
||||
/// the UI rebuilds. Used by pull-to-refresh.
|
||||
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(() async {
|
||||
final db = await _db;
|
||||
return db.getAllNotes();
|
||||
});
|
||||
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 db = await _db;
|
||||
final vault = await _vault;
|
||||
final notePath = await vault.createEmptyNotebook(title);
|
||||
final now = DateTime.now();
|
||||
final note = Note(
|
||||
id: _uuid.v4(),
|
||||
id: notePath,
|
||||
title: title,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
);
|
||||
await db.insertNote(note);
|
||||
state = AsyncData([note, ...state.value ?? []]);
|
||||
return note;
|
||||
}
|
||||
|
||||
Future<void> updateNote(Note note) async {
|
||||
final db = await _db;
|
||||
await db.updateNote(note);
|
||||
final current = state.value ?? [];
|
||||
state = AsyncData(current.map((n) => n.id == note.id ? note : n).toList());
|
||||
}
|
||||
|
||||
/// 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 db = await _db;
|
||||
await db.deleteNote(id);
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
final noteProvider = FutureProvider.family<Note?, String>((ref, id) async {
|
||||
final db = await ref.watch(databaseServiceProvider.future);
|
||||
return db.getNoteById(id);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user