feat(editor): persist strokes in live editor
Some checks failed
CI / Windows build (push) Has been cancelled
Some checks failed
CI / Windows build (push) Has been cancelled
Wire the new EditorRepository + SaveScheduler into PenEditorScreen: load strokes on open (keyed by a stable djb2 doc-id from the path), save per page on commit/erase via the debounced diff-write scheduler (synchronous snapshot before await), flush on dispose. Strokes now survive close/reopen. PenStroke<->EditorStroke conversion at the boundary.
This commit is contained in:
@@ -8,9 +8,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pdfrx/pdfrx.dart';
|
||||
|
||||
import '../../services/database_service.dart';
|
||||
import '../engine/stroke_model.dart';
|
||||
import '../persistence/editor_repository.dart';
|
||||
import '../persistence/save_scheduler.dart';
|
||||
import 'pen_canvas.dart';
|
||||
import 'pen_stroke.dart';
|
||||
|
||||
/// Stable deterministic document-id for a file path (djb2 hash → hex).
|
||||
///
|
||||
/// Produces a fixed-length hex string from the path so the id is filesystem-
|
||||
/// independent (no slashes, spaces, or non-ASCII characters) and stable across
|
||||
/// restarts. Collisions are astronomically unlikely for a single-user app.
|
||||
String _documentIdFromPath(String path) {
|
||||
var hash = 5381;
|
||||
for (final c in path.codeUnits) {
|
||||
hash = ((hash << 5) + hash + c) & 0xFFFFFFFF;
|
||||
}
|
||||
return hash.toRadixString(16).padLeft(8, '0');
|
||||
}
|
||||
|
||||
class PenEditorScreen extends StatefulWidget {
|
||||
const PenEditorScreen({super.key, required this.pdfPath});
|
||||
|
||||
@@ -30,6 +47,13 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
/// Strokes per page, keyed by 0-based page index (normalized coords).
|
||||
final Map<int, List<PenStroke>> _strokesByPage = {};
|
||||
|
||||
// ── Persistence ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Stable document-id derived from the PDF file path.
|
||||
late final String _documentId;
|
||||
|
||||
SaveScheduler? _saveScheduler;
|
||||
|
||||
/// One shared transform for the current page; recentred on page change so
|
||||
/// each page opens fit-to-view and centered.
|
||||
final TransformationController _transform = TransformationController();
|
||||
@@ -69,9 +93,64 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_documentId = _documentIdFromPath(widget.pdfPath);
|
||||
_initPersistence();
|
||||
_open();
|
||||
}
|
||||
|
||||
Future<void> _initPersistence() async {
|
||||
final service = await DatabaseService.getInstance();
|
||||
if (!mounted) return;
|
||||
final repo = await EditorRepository.fromService(service);
|
||||
final scheduler = SaveScheduler(repo);
|
||||
if (!mounted) {
|
||||
scheduler.dispose();
|
||||
return;
|
||||
}
|
||||
_saveScheduler = scheduler;
|
||||
// Load any previously persisted strokes for this document.
|
||||
await _loadPersistedStrokes(repo);
|
||||
}
|
||||
|
||||
/// Load all persisted strokes for [_documentId] and populate [_strokesByPage].
|
||||
Future<void> _loadPersistedStrokes(EditorRepository repo) async {
|
||||
final hosted = await repo.loadDocument(_documentId);
|
||||
if (!mounted) return;
|
||||
final loaded = <int, List<PenStroke>>{};
|
||||
for (final entry in hosted.entries) {
|
||||
final pageIndex = _pageIndexFromHostId(entry.key);
|
||||
if (pageIndex == null) continue;
|
||||
loaded[pageIndex] = entry.value
|
||||
.map((es) => PenStroke(
|
||||
points: es.points
|
||||
.map((ep) => PenPoint(ep.x, ep.y, ep.pressure))
|
||||
.toList(),
|
||||
color: es.color,
|
||||
width: es.width,
|
||||
kind: es.tool == EditorTool.highlighter
|
||||
? PenStrokeKind.highlighter
|
||||
: PenStrokeKind.pen,
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
if (loaded.isNotEmpty) {
|
||||
setState(() {
|
||||
for (final entry in loaded.entries) {
|
||||
_strokesByPage[entry.key] = entry.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the page index from a host_id of the form
|
||||
/// `"doc:<documentId>:page:<pageIndex>"`.
|
||||
int? _pageIndexFromHostId(String hostId) {
|
||||
const marker = ':page:';
|
||||
final idx = hostId.lastIndexOf(marker);
|
||||
if (idx == -1) return null;
|
||||
return int.tryParse(hostId.substring(idx + marker.length));
|
||||
}
|
||||
|
||||
Future<void> _open() async {
|
||||
try {
|
||||
final doc = await PdfDocument.openFile(widget.pdfPath);
|
||||
@@ -87,6 +166,12 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Flush any pending scheduled saves before tearing down.
|
||||
final scheduler = _saveScheduler;
|
||||
if (scheduler != null) {
|
||||
scheduler.flush(); // fire-and-forget; DB write continues in isolate
|
||||
scheduler.dispose();
|
||||
}
|
||||
_document?.dispose();
|
||||
_transform.dispose();
|
||||
super.dispose();
|
||||
@@ -105,6 +190,9 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
stroke,
|
||||
];
|
||||
});
|
||||
// Snapshot SYNCHRONOUSLY (before any await) then schedule persistence.
|
||||
final snapshot = List<PenStroke>.of(_strokesByPage[_pageIndex]!);
|
||||
_schedulePageSave(_pageIndex, snapshot);
|
||||
}
|
||||
|
||||
void _eraseStroke(int index) {
|
||||
@@ -115,6 +203,27 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
_strokesByPage[_pageIndex] = next;
|
||||
}
|
||||
});
|
||||
// Snapshot SYNCHRONOUSLY after the mutation, then schedule persistence.
|
||||
final current = _strokesByPage[_pageIndex];
|
||||
final snapshot =
|
||||
current != null ? List<PenStroke>.of(current) : <PenStroke>[];
|
||||
_schedulePageSave(_pageIndex, snapshot);
|
||||
}
|
||||
|
||||
/// Convert [strokes] to [EditorStroke]s and hand them to the save scheduler.
|
||||
///
|
||||
/// Must be called synchronously (no await between the snapshot and this call)
|
||||
/// so the scheduler receives an immutable copy of the in-memory state.
|
||||
void _schedulePageSave(int pageIndex, List<PenStroke> strokes) {
|
||||
final scheduler = _saveScheduler;
|
||||
if (scheduler == null) return;
|
||||
final editorStrokes =
|
||||
strokes.map((s) => EditorStroke.fromPenStroke(s)).toList();
|
||||
scheduler.schedule(
|
||||
'page',
|
||||
EditorRepository.pageHostId(_documentId, pageIndex),
|
||||
editorStrokes,
|
||||
);
|
||||
}
|
||||
|
||||
void _goToPage(int index) {
|
||||
|
||||
Reference in New Issue
Block a user