// lib/editor/notebook/page_map.dart // // The one-notebook-per-PDF logical page model (F6). A notebook is an ordered // list of LOGICAL pages; each is either a SOURCE page (shows PDF page N, vector // preserved) or a BLANK page inserted between/after PDF pages (SpeedyNote-style // binding the user asked for). Ink hosts attach to logical pages, so inserting // or reordering pages must NOT renumber the PDF underlay — the source index is // carried on each page. // // Pure, immutable value type (every edit returns a NEW PageMap) so the model is // fully unit-tested; the `notebook_pages` table + the viewport wire it later. import 'package:flutter/foundation.dart'; enum NotebookPageKind { source, blank } /// One logical page. [sourcePageIndex] is the 0-based PDF page it renders, or /// null for a [NotebookPageKind.blank] inserted page. @immutable class NotebookPage { const NotebookPage.source(this.sourcePageIndex) : kind = NotebookPageKind.source; const NotebookPage.blank() : kind = NotebookPageKind.blank, sourcePageIndex = null; final NotebookPageKind kind; final int? sourcePageIndex; bool get isBlank => kind == NotebookPageKind.blank; bool get isSource => kind == NotebookPageKind.source; @override bool operator ==(Object other) => other is NotebookPage && other.kind == kind && other.sourcePageIndex == sourcePageIndex; @override int get hashCode => Object.hash(kind, sourcePageIndex); @override String toString() => isBlank ? 'NotebookPage.blank' : 'NotebookPage.source($sourcePageIndex)'; } /// Ordered logical page list with copy-on-write edits. @immutable class PageMap { const PageMap(this.pages); /// Identity map: one logical SOURCE page per PDF page, in order. factory PageMap.fromSource(int sourcePageCount) { assert(sourcePageCount >= 0); return PageMap( List.unmodifiable( List.generate( sourcePageCount, (i) => NotebookPage.source(i), ), ), ); } final List pages; int get length => pages.length; bool get isEmpty => pages.isEmpty; NotebookPage operator [](int index) => pages[index]; /// Source PDF page rendered at logical [index], or null when it's a blank. int? sourceIndexAt(int index) => pages[index].sourcePageIndex; int get blankCount => pages.where((p) => p.isBlank).length; int get sourceCount => pages.where((p) => p.isSource).length; /// Insert a blank page at logical [index] (0..length). Throws [RangeError] /// for an out-of-range index. PageMap insertBlankAt(int index) { RangeError.checkValueInInterval(index, 0, length, 'index'); final next = List.of(pages) ..insert(index, const NotebookPage.blank()); return PageMap(List.unmodifiable(next)); } /// Insert a blank page immediately after logical [index] (-1 prepends). PageMap insertBlankAfter(int index) { RangeError.checkValueInInterval(index, -1, length - 1, 'index'); return insertBlankAt(index + 1); } /// Remove the logical page at [index]. PageMap removeAt(int index) { RangeError.checkValidIndex(index, pages, 'index'); final next = List.of(pages)..removeAt(index); return PageMap(List.unmodifiable(next)); } /// Move the page at [from] to position [to] (drag-reorder). PageMap move(int from, int to) { RangeError.checkValidIndex(from, pages, 'from'); RangeError.checkValueInInterval(to, 0, length - 1, 'to'); if (from == to) return this; final next = List.of(pages); final page = next.removeAt(from); next.insert(to, page); return PageMap(List.unmodifiable(next)); } @override bool operator ==(Object other) => other is PageMap && listEquals(other.pages, pages); @override int get hashCode => Object.hashAll(pages); @override String toString() => 'PageMap($pages)'; }