diff --git a/lib/editor/canvas/pen_editor_screen.dart b/lib/editor/canvas/pen_editor_screen.dart index e8bf9be..86dda5f 100644 --- a/lib/editor/canvas/pen_editor_screen.dart +++ b/lib/editor/canvas/pen_editor_screen.dart @@ -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 { /// Strokes per page, keyed by 0-based page index (normalized coords). final Map> _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 { @override void initState() { super.initState(); + _documentId = _documentIdFromPath(widget.pdfPath); + _initPersistence(); _open(); } + Future _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 _loadPersistedStrokes(EditorRepository repo) async { + final hosted = await repo.loadDocument(_documentId); + if (!mounted) return; + final loaded = >{}; + 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::page:"`. + 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 _open() async { try { final doc = await PdfDocument.openFile(widget.pdfPath); @@ -87,6 +166,12 @@ class _PenEditorScreenState extends State { @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 { stroke, ]; }); + // Snapshot SYNCHRONOUSLY (before any await) then schedule persistence. + final snapshot = List.of(_strokesByPage[_pageIndex]!); + _schedulePageSave(_pageIndex, snapshot); } void _eraseStroke(int index) { @@ -115,6 +203,27 @@ class _PenEditorScreenState extends State { _strokesByPage[_pageIndex] = next; } }); + // Snapshot SYNCHRONOUSLY after the mutation, then schedule persistence. + final current = _strokesByPage[_pageIndex]; + final snapshot = + current != null ? List.of(current) : []; + _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 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) {