feat(storage): PDF editor persists to per-file sidecar
Some checks failed
CI / Windows build (push) Has been cancelled
Some checks failed
CI / Windows build (push) Has been cancelled
Phase 2 (core swap). The PDF editor and split-view scratchpad stop writing SQLite and persist to a per-file sidecar `<pdfPath>.badnote.json` (debounced, atomic temp+rename+.bak) — so annotations travel with the file. The source path is the identity (no more djb2 doc-id). - SidecarRepository wraps the Phase-1 store with debounced autosave. - pen_editor: per-page ink, scratch-links AND highlights now persist to the sidecar and restore on reopen (closes persist-highlights). - New "un-highlight" tool: tap a stored highlight to remove it — the highlight could not be removed before. - split_view: each anchor's scratchpad lives in the sidecar's scratchLinks[id].scratchpad, keyed by anchor id. Note: pre-existing SQLite annotations are migrated later (Phase 5); note/slide editors swap in Phase 4. analyze clean, tests green.
This commit is contained in:
@@ -13,7 +13,6 @@
|
||||
// ToolButton / color dots), replacing the old AnnotationToolbar.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pdfrx/pdfrx.dart';
|
||||
@@ -25,21 +24,22 @@ import '../editor/canvas/pen_stroke.dart';
|
||||
import '../editor/engine/brush.dart';
|
||||
import '../editor/input/pen_config.dart' show kDefaultEraserRadius;
|
||||
import '../editor/notebook/ink_stroke_adapter.dart';
|
||||
import '../editor/persistence/sidecar_repository.dart';
|
||||
import '../models/ink_stroke.dart';
|
||||
import '../services/database_service.dart';
|
||||
import '../services/undo_manager.dart';
|
||||
import '../storage/badnote_sidecar.dart';
|
||||
|
||||
/// Split-view derivation surface for a single scratch-link anchor: left pane =
|
||||
/// the reference PDF (at [initialPage]), right pane = the anchor's private
|
||||
/// infinite scratchpad. Scratchpad strokes persist per ANCHOR via
|
||||
/// [DatabaseService.saveScratchpad] / [DatabaseService.loadScratchpad], keyed by
|
||||
/// [scratchLinkId].
|
||||
/// infinite scratchpad. Scratchpad strokes persist inside the source file's
|
||||
/// SIDECAR (`<filePath>.badnote.json`), embedded in the anchor's
|
||||
/// `scratchLinks[id].scratchpad` (keyed by [scratchLinkId]). Strokes keep the
|
||||
/// absolute world-pixel [InkStroke] format unchanged.
|
||||
class SplitViewScreen extends StatefulWidget {
|
||||
final String filePath;
|
||||
final String documentId;
|
||||
|
||||
/// The owning anchor id. Doubles as the scratchpad storage key so this is the
|
||||
/// anchor's private scratch space.
|
||||
/// The owning anchor id. Selects which `scratchLinks[].scratchpad` in the
|
||||
/// sidecar this is the private scratch space for.
|
||||
final String scratchLinkId;
|
||||
|
||||
/// 0-based page the anchor sits on; the left PDF opens here.
|
||||
@@ -48,7 +48,6 @@ class SplitViewScreen extends StatefulWidget {
|
||||
const SplitViewScreen({
|
||||
super.key,
|
||||
required this.filePath,
|
||||
required this.documentId,
|
||||
required this.scratchLinkId,
|
||||
this.initialPage = 0,
|
||||
});
|
||||
@@ -102,6 +101,11 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
Timer? _saveTimer;
|
||||
bool _dirty = false;
|
||||
|
||||
/// Sidecar persistence for the source file (the scratchpad is embedded in the
|
||||
/// anchor's `scratchLinks[id].scratchpad`). Null until [_loadScratchpad]
|
||||
/// resolves.
|
||||
SidecarRepository? _repo;
|
||||
|
||||
static const double _edgeThreshold = 200.0;
|
||||
static const double _expandAmount = 1000.0;
|
||||
|
||||
@@ -116,18 +120,34 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
void dispose() {
|
||||
_saveTimer?.cancel();
|
||||
_saveImmediate();
|
||||
final repo = _repo;
|
||||
if (repo != null) {
|
||||
repo.flush(); // fire-and-forget; atomic write finishes off the tree
|
||||
repo.dispose();
|
||||
}
|
||||
// PdfViewerController (pdfrx) has no dispose(); it detaches with the viewer.
|
||||
_scratchTransform.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// -- Persistence (keyed by the ANCHOR id, not the documentId) --
|
||||
// -- Persistence (sidecar's scratchLinks[id].scratchpad, keyed by anchor id) --
|
||||
|
||||
Future<void> _loadScratchpad() async {
|
||||
final db = await DatabaseService.getInstance();
|
||||
final strokes = await db.loadScratchpad(widget.scratchLinkId);
|
||||
if (!mounted) return;
|
||||
final repo = await SidecarRepository.open(widget.filePath, docType: 'pdf');
|
||||
if (!mounted) {
|
||||
repo.dispose();
|
||||
return;
|
||||
}
|
||||
_repo = repo;
|
||||
final pad = repo.scratchpadFor(widget.scratchLinkId);
|
||||
setState(() {
|
||||
if (pad != null) {
|
||||
// Restore the world size so the infinite canvas reopens at its grown
|
||||
// extent (previously always reset to 4000×4000).
|
||||
_canvasWidth = pad.canvasWidth;
|
||||
_canvasHeight = pad.canvasHeight;
|
||||
}
|
||||
final strokes = pad?.strokes ?? const <InkStroke>[];
|
||||
// Keep only freehand strokes so the canvas list stays 1:1 with the undo
|
||||
// manager (shapes/text have no pen-canvas representation).
|
||||
final freehand = strokes.where((s) => isFreehandTool(s.tool)).toList();
|
||||
@@ -147,9 +167,17 @@ class _SplitViewState extends State<SplitViewScreen> {
|
||||
Future<void> _saveImmediate() async {
|
||||
if (!_dirty) return;
|
||||
_dirty = false;
|
||||
final db = await DatabaseService.getInstance();
|
||||
final json = jsonEncode(_strokes.map((s) => s.toJson()).toList());
|
||||
await db.saveScratchpad(widget.scratchLinkId, json);
|
||||
final repo = _repo;
|
||||
if (repo == null) return;
|
||||
repo.scheduleScratchpadSave(
|
||||
widget.scratchLinkId,
|
||||
SidecarScratchpad(
|
||||
canvasWidth: _canvasWidth,
|
||||
canvasHeight: _canvasHeight,
|
||||
strokes: List<InkStroke>.of(_strokes),
|
||||
),
|
||||
);
|
||||
await repo.flush();
|
||||
}
|
||||
|
||||
// -- Scratchpad stroke callbacks --
|
||||
|
||||
Reference in New Issue
Block a user