feat(pdf): paragraph-precise bookmarks
Some checks failed
CI / Windows build (push) Has been cancelled
Some checks failed
CI / Windows build (push) Has been cancelled
Add a bookmark tool to the PDF editor. A bookmark anchors to a precise location: when text is selected it captures the selection's normalized rect + the start char index in the page text (the true paragraph anchor); with no selection it falls back to the tapped page + point. - Bookmark model gains optional normalized anchor rect + charIndex + label (all absent from JSON when null, so old sidecars still load). - Bookmarks persist in the sidecar (scheduleBookmarkUpsert) and load on open; a bookmarks panel lists them and tapping one jumps to its page. Delete is persisted. Scoped to the PDF editor (note bookmarks later); scroll-to-anchor is page-level for now. analyze clean, 391 tests green.
This commit is contained in:
@@ -3,15 +3,50 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
part 'bookmark.freezed.dart';
|
||||
part 'bookmark.g.dart';
|
||||
|
||||
/// A saved location in a document.
|
||||
///
|
||||
/// "Paragraph precision" (user ask: 精确到段落加书签) is expressed by the optional
|
||||
/// in-page anchor fields below, all normalized to the page in [0,1]:
|
||||
///
|
||||
/// * [anchorLeft]/[anchorTop]/[anchorRight]/[anchorBottom] — the bounding rect
|
||||
/// of the bookmarked text fragment (the FIRST fragment of the current text
|
||||
/// selection), in NORMALIZED page coords with a top-left origin (the same
|
||||
/// convention `SidecarHighlight` and the editor's highlight rects use). This
|
||||
/// is what jump-to scrolls to (via `goToRectInsidePage`), so the bookmark
|
||||
/// lands on the exact paragraph, not just the page top.
|
||||
/// * [charIndex] — the character index of the selection start in the page's
|
||||
/// `fullText` (the true text-position anchor). Stored for fidelity / future
|
||||
/// reflow-tolerant re-anchoring; not currently used for navigation.
|
||||
///
|
||||
/// When no text was selected the anchor falls back to the tapped point: only
|
||||
/// [anchorTop]/[anchorLeft] are set (a zero-size rect) and [charIndex] is null.
|
||||
/// All anchor fields are optional and absent from JSON when null, so OLD
|
||||
/// bookmarks (page-only) still decode and re-encode unchanged (back-compat).
|
||||
@freezed
|
||||
abstract class Bookmark with _$Bookmark {
|
||||
const factory Bookmark({
|
||||
required String id,
|
||||
required String documentId,
|
||||
|
||||
/// 1-based page number this bookmark lives on.
|
||||
required int pageNumber,
|
||||
@Default('') String label,
|
||||
@Default(0xFF2196F3) int color,
|
||||
required DateTime createdAt,
|
||||
|
||||
/// Normalized in-page anchor rect (top-left origin, [0,1]). Null for legacy
|
||||
/// page-only bookmarks (and tap-fallback bookmarks set only top/left). Old
|
||||
/// page-only bookmark JSON omits these keys; they decode to null and the
|
||||
/// data round-trips (back-compat). A re-encoded legacy bookmark gains
|
||||
/// explicit null keys, which readers tolerate.
|
||||
double? anchorLeft,
|
||||
double? anchorTop,
|
||||
double? anchorRight,
|
||||
double? anchorBottom,
|
||||
|
||||
/// Character index of the selection start in the page's `fullText`, or null
|
||||
/// (tap-fallback / legacy bookmarks).
|
||||
int? charIndex,
|
||||
}) = _Bookmark;
|
||||
|
||||
factory Bookmark.fromJson(Map<String, dynamic> json) =>
|
||||
|
||||
@@ -23,11 +23,27 @@ Bookmark _$BookmarkFromJson(Map<String, dynamic> json) {
|
||||
mixin _$Bookmark {
|
||||
String get id => throw _privateConstructorUsedError;
|
||||
String get documentId => throw _privateConstructorUsedError;
|
||||
|
||||
/// 1-based page number this bookmark lives on.
|
||||
int get pageNumber => throw _privateConstructorUsedError;
|
||||
String get label => throw _privateConstructorUsedError;
|
||||
int get color => throw _privateConstructorUsedError;
|
||||
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||
|
||||
/// Normalized in-page anchor rect (top-left origin, [0,1]). Null for legacy
|
||||
/// page-only bookmarks (and tap-fallback bookmarks set only top/left). Old
|
||||
/// page-only bookmark JSON omits these keys; they decode to null and the
|
||||
/// data round-trips (back-compat). A re-encoded legacy bookmark gains
|
||||
/// explicit null keys, which readers tolerate.
|
||||
double? get anchorLeft => throw _privateConstructorUsedError;
|
||||
double? get anchorTop => throw _privateConstructorUsedError;
|
||||
double? get anchorRight => throw _privateConstructorUsedError;
|
||||
double? get anchorBottom => throw _privateConstructorUsedError;
|
||||
|
||||
/// Character index of the selection start in the page's `fullText`, or null
|
||||
/// (tap-fallback / legacy bookmarks).
|
||||
int? get charIndex => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this Bookmark to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
@@ -50,6 +66,11 @@ abstract class $BookmarkCopyWith<$Res> {
|
||||
String label,
|
||||
int color,
|
||||
DateTime createdAt,
|
||||
double? anchorLeft,
|
||||
double? anchorTop,
|
||||
double? anchorRight,
|
||||
double? anchorBottom,
|
||||
int? charIndex,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -74,6 +95,11 @@ class _$BookmarkCopyWithImpl<$Res, $Val extends Bookmark>
|
||||
Object? label = null,
|
||||
Object? color = null,
|
||||
Object? createdAt = null,
|
||||
Object? anchorLeft = freezed,
|
||||
Object? anchorTop = freezed,
|
||||
Object? anchorRight = freezed,
|
||||
Object? anchorBottom = freezed,
|
||||
Object? charIndex = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
@@ -101,6 +127,26 @@ class _$BookmarkCopyWithImpl<$Res, $Val extends Bookmark>
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
anchorLeft: freezed == anchorLeft
|
||||
? _value.anchorLeft
|
||||
: anchorLeft // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
anchorTop: freezed == anchorTop
|
||||
? _value.anchorTop
|
||||
: anchorTop // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
anchorRight: freezed == anchorRight
|
||||
? _value.anchorRight
|
||||
: anchorRight // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
anchorBottom: freezed == anchorBottom
|
||||
? _value.anchorBottom
|
||||
: anchorBottom // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
charIndex: freezed == charIndex
|
||||
? _value.charIndex
|
||||
: charIndex // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
@@ -123,6 +169,11 @@ abstract class _$$BookmarkImplCopyWith<$Res>
|
||||
String label,
|
||||
int color,
|
||||
DateTime createdAt,
|
||||
double? anchorLeft,
|
||||
double? anchorTop,
|
||||
double? anchorRight,
|
||||
double? anchorBottom,
|
||||
int? charIndex,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -146,6 +197,11 @@ class __$$BookmarkImplCopyWithImpl<$Res>
|
||||
Object? label = null,
|
||||
Object? color = null,
|
||||
Object? createdAt = null,
|
||||
Object? anchorLeft = freezed,
|
||||
Object? anchorTop = freezed,
|
||||
Object? anchorRight = freezed,
|
||||
Object? anchorBottom = freezed,
|
||||
Object? charIndex = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_$BookmarkImpl(
|
||||
@@ -173,6 +229,26 @@ class __$$BookmarkImplCopyWithImpl<$Res>
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
anchorLeft: freezed == anchorLeft
|
||||
? _value.anchorLeft
|
||||
: anchorLeft // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
anchorTop: freezed == anchorTop
|
||||
? _value.anchorTop
|
||||
: anchorTop // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
anchorRight: freezed == anchorRight
|
||||
? _value.anchorRight
|
||||
: anchorRight // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
anchorBottom: freezed == anchorBottom
|
||||
? _value.anchorBottom
|
||||
: anchorBottom // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
charIndex: freezed == charIndex
|
||||
? _value.charIndex
|
||||
: charIndex // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -188,6 +264,11 @@ class _$BookmarkImpl implements _Bookmark {
|
||||
this.label = '',
|
||||
this.color = 0xFF2196F3,
|
||||
required this.createdAt,
|
||||
this.anchorLeft,
|
||||
this.anchorTop,
|
||||
this.anchorRight,
|
||||
this.anchorBottom,
|
||||
this.charIndex,
|
||||
});
|
||||
|
||||
factory _$BookmarkImpl.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -197,6 +278,8 @@ class _$BookmarkImpl implements _Bookmark {
|
||||
final String id;
|
||||
@override
|
||||
final String documentId;
|
||||
|
||||
/// 1-based page number this bookmark lives on.
|
||||
@override
|
||||
final int pageNumber;
|
||||
@override
|
||||
@@ -208,9 +291,28 @@ class _$BookmarkImpl implements _Bookmark {
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
/// Normalized in-page anchor rect (top-left origin, [0,1]). Null for legacy
|
||||
/// page-only bookmarks (and tap-fallback bookmarks set only top/left). Old
|
||||
/// page-only bookmark JSON omits these keys; they decode to null and the
|
||||
/// data round-trips (back-compat). A re-encoded legacy bookmark gains
|
||||
/// explicit null keys, which readers tolerate.
|
||||
@override
|
||||
final double? anchorLeft;
|
||||
@override
|
||||
final double? anchorTop;
|
||||
@override
|
||||
final double? anchorRight;
|
||||
@override
|
||||
final double? anchorBottom;
|
||||
|
||||
/// Character index of the selection start in the page's `fullText`, or null
|
||||
/// (tap-fallback / legacy bookmarks).
|
||||
@override
|
||||
final int? charIndex;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Bookmark(id: $id, documentId: $documentId, pageNumber: $pageNumber, label: $label, color: $color, createdAt: $createdAt)';
|
||||
return 'Bookmark(id: $id, documentId: $documentId, pageNumber: $pageNumber, label: $label, color: $color, createdAt: $createdAt, anchorLeft: $anchorLeft, anchorTop: $anchorTop, anchorRight: $anchorRight, anchorBottom: $anchorBottom, charIndex: $charIndex)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -226,7 +328,17 @@ class _$BookmarkImpl implements _Bookmark {
|
||||
(identical(other.label, label) || other.label == label) &&
|
||||
(identical(other.color, color) || other.color == color) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt));
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.anchorLeft, anchorLeft) ||
|
||||
other.anchorLeft == anchorLeft) &&
|
||||
(identical(other.anchorTop, anchorTop) ||
|
||||
other.anchorTop == anchorTop) &&
|
||||
(identical(other.anchorRight, anchorRight) ||
|
||||
other.anchorRight == anchorRight) &&
|
||||
(identical(other.anchorBottom, anchorBottom) ||
|
||||
other.anchorBottom == anchorBottom) &&
|
||||
(identical(other.charIndex, charIndex) ||
|
||||
other.charIndex == charIndex));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -239,6 +351,11 @@ class _$BookmarkImpl implements _Bookmark {
|
||||
label,
|
||||
color,
|
||||
createdAt,
|
||||
anchorLeft,
|
||||
anchorTop,
|
||||
anchorRight,
|
||||
anchorBottom,
|
||||
charIndex,
|
||||
);
|
||||
|
||||
/// Create a copy of Bookmark
|
||||
@@ -263,6 +380,11 @@ abstract class _Bookmark implements Bookmark {
|
||||
final String label,
|
||||
final int color,
|
||||
required final DateTime createdAt,
|
||||
final double? anchorLeft,
|
||||
final double? anchorTop,
|
||||
final double? anchorRight,
|
||||
final double? anchorBottom,
|
||||
final int? charIndex,
|
||||
}) = _$BookmarkImpl;
|
||||
|
||||
factory _Bookmark.fromJson(Map<String, dynamic> json) =
|
||||
@@ -272,6 +394,8 @@ abstract class _Bookmark implements Bookmark {
|
||||
String get id;
|
||||
@override
|
||||
String get documentId;
|
||||
|
||||
/// 1-based page number this bookmark lives on.
|
||||
@override
|
||||
int get pageNumber;
|
||||
@override
|
||||
@@ -281,6 +405,25 @@ abstract class _Bookmark implements Bookmark {
|
||||
@override
|
||||
DateTime get createdAt;
|
||||
|
||||
/// Normalized in-page anchor rect (top-left origin, [0,1]). Null for legacy
|
||||
/// page-only bookmarks (and tap-fallback bookmarks set only top/left). Old
|
||||
/// page-only bookmark JSON omits these keys; they decode to null and the
|
||||
/// data round-trips (back-compat). A re-encoded legacy bookmark gains
|
||||
/// explicit null keys, which readers tolerate.
|
||||
@override
|
||||
double? get anchorLeft;
|
||||
@override
|
||||
double? get anchorTop;
|
||||
@override
|
||||
double? get anchorRight;
|
||||
@override
|
||||
double? get anchorBottom;
|
||||
|
||||
/// Character index of the selection start in the page's `fullText`, or null
|
||||
/// (tap-fallback / legacy bookmarks).
|
||||
@override
|
||||
int? get charIndex;
|
||||
|
||||
/// Create a copy of Bookmark
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
|
||||
@@ -14,6 +14,11 @@ _$BookmarkImpl _$$BookmarkImplFromJson(Map<String, dynamic> json) =>
|
||||
label: json['label'] as String? ?? '',
|
||||
color: (json['color'] as num?)?.toInt() ?? 0xFF2196F3,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
anchorLeft: (json['anchorLeft'] as num?)?.toDouble(),
|
||||
anchorTop: (json['anchorTop'] as num?)?.toDouble(),
|
||||
anchorRight: (json['anchorRight'] as num?)?.toDouble(),
|
||||
anchorBottom: (json['anchorBottom'] as num?)?.toDouble(),
|
||||
charIndex: (json['charIndex'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$BookmarkImplToJson(_$BookmarkImpl instance) =>
|
||||
@@ -24,4 +29,9 @@ Map<String, dynamic> _$$BookmarkImplToJson(_$BookmarkImpl instance) =>
|
||||
'label': instance.label,
|
||||
'color': instance.color,
|
||||
'createdAt': instance.createdAt.toIso8601String(),
|
||||
'anchorLeft': instance.anchorLeft,
|
||||
'anchorTop': instance.anchorTop,
|
||||
'anchorRight': instance.anchorRight,
|
||||
'anchorBottom': instance.anchorBottom,
|
||||
'charIndex': instance.charIndex,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user