Files
BadNote/lib/editor/notebook/page_map.dart
Akiba So 18ebce6d73
Some checks failed
CI / Windows build (push) Has been cancelled
feat(f6): one-notebook-per-PDF logical PageMap (insert-blank/reorder) pure core
The SpeedyNote-style page binding the user asked for: a notebook is an ordered
list of logical pages, each a SOURCE page (renders PDF page N, vector preserved)
or a BLANK inserted page. Crucially, inserting/reordering logical pages does NOT
renumber the PDF underlay — each page carries its source index. Copy-on-write
edits (insertBlankAt/After, removeAt, move) return a new immutable PageMap;
out-of-range edits throw RangeError; value equality + unmodifiable page list.

Pure model (no DB/widget) so it's fully unit-tested; the notebook_pages table +
viewport wire it later.

flutter analyze lib/editor clean; 156/156 tests (+11).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 03:23:58 +08:00

122 lines
3.9 KiB
Dart

// 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<NotebookPage>.unmodifiable(
List<NotebookPage>.generate(
sourcePageCount,
(i) => NotebookPage.source(i),
),
),
);
}
final List<NotebookPage> 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<NotebookPage>.of(pages)
..insert(index, const NotebookPage.blank());
return PageMap(List<NotebookPage>.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<NotebookPage>.of(pages)..removeAt(index);
return PageMap(List<NotebookPage>.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<NotebookPage>.of(pages);
final page = next.removeAt(from);
next.insert(to, page);
return PageMap(List<NotebookPage>.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)';
}