feat(pdf): anchored scratch links replace board
Some checks failed
CI / Windows build (push) Has been cancelled

Replace the rejected standalone sticky-card board with the real
feature: place a link anchor anywhere on a PDF page, tap it to open
split view whose right pane is THAT anchor's own infinite scratchpad
(keyed by anchor id) — like a paper sticky-note tab.

- ScratchLink model + scratch_links table (id, doc, page, nx, ny).
- PDF editor: "place link" tool drops/loads/shows tappable markers;
  tap opens SplitViewScreen for that anchor; long-press deletes.
- SplitViewScreen rebuilt on pdfrx (was syncfusion), right scratchpad
  keyed by scratchLinkId, new brush palette (was AnnotationToolbar).
- Remove board_screen + its test + the home board entry.

analyze clean, tests green.
This commit is contained in:
2026-06-24 20:02:12 +08:00
parent f757701391
commit 9bb5c483d6
13 changed files with 759 additions and 1076 deletions

View File

@@ -16,6 +16,7 @@ import '../models/ink_stroke.dart';
import '../models/note.dart';
import '../models/pen_tool.dart';
import '../models/pointer_device_kind.dart';
import '../models/scratch_link.dart';
class DatabaseService {
static DatabaseService? _instance;
@@ -56,7 +57,7 @@ class DatabaseService {
_database = await openDatabase(
dbPath,
version: 7,
version: 8,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
@@ -202,6 +203,28 @@ class DatabaseService {
// Sticky-note board cards (v7): F7 双链 + 无限便利贴.
await _createBoardCardsTable(db);
// PDF-anchored scratch links (v8).
await _createScratchLinksTable(db);
}
/// PDF-anchored scratch links table (v8). One row per [ScratchLink] anchor.
/// The anchor [id] doubles as the storage key for its private scratchpad
/// (reused from the [scratchpads] table — see [saveScratchpad]).
Future<void> _createScratchLinksTable(DatabaseExecutor db) async {
await db.execute('''
CREATE TABLE scratch_links (
id TEXT PRIMARY KEY,
document_id TEXT NOT NULL,
page_index INTEGER NOT NULL,
nx REAL NOT NULL,
ny REAL NOT NULL,
created_at TEXT NOT NULL
)
''');
await db.execute(
'CREATE INDEX idx_scratch_links_doc ON scratch_links(document_id)',
);
}
/// Sticky-note board cards table (F7). One row per [BoardCard]; a board is the
@@ -232,6 +255,13 @@ class DatabaseService {
if (oldVersion < 5) await _migrateV4toV5(db);
if (oldVersion < 6) await _migrateV5toV6(db);
if (oldVersion < 7) await _migrateV6toV7(db);
if (oldVersion < 8) await _migrateV7toV8(db);
}
Future<void> _migrateV7toV8(Database db) async {
await db.transaction((txn) async {
await _createScratchLinksTable(txn);
});
}
Future<void> _migrateV6toV7(Database db) async {
@@ -1003,4 +1033,54 @@ class DatabaseService {
),
]);
}
// ── Scratch links CRUD (PDF-anchored scratchpad tabs) ──────────────────
/// Insert or replace a [ScratchLink] anchor. The anchor's private scratchpad
/// lives in the [scratchpads] table keyed by [ScratchLink.id] — saved/loaded
/// via [saveScratchpad] / [loadScratchpad].
Future<void> saveScratchLink(ScratchLink link) async {
await _database.insert(
'scratch_links',
{
'id': link.id,
'document_id': link.documentId,
'page_index': link.pageIndex,
'nx': link.nx,
'ny': link.ny,
'created_at': DateTime.now().toIso8601String(),
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
/// Load all anchors for [documentId], oldest first.
Future<List<ScratchLink>> loadScratchLinks(String documentId) async {
final rows = await _database.query(
'scratch_links',
where: 'document_id = ?',
whereArgs: [documentId],
orderBy: 'created_at ASC',
);
return rows
.map(
(row) => ScratchLink(
id: row['id'] as String,
documentId: row['document_id'] as String,
pageIndex: row['page_index'] as int,
nx: (row['nx'] as num).toDouble(),
ny: (row['ny'] as num).toDouble(),
),
)
.toList();
}
/// Delete an anchor and its private scratchpad (the scratchpad row keyed by
/// the anchor id), so a deleted anchor leaves no orphaned ink behind.
Future<void> deleteScratchLink(String id) async {
await _database.transaction((txn) async {
await txn.delete('scratch_links', where: 'id = ?', whereArgs: [id]);
await txn.delete('scratchpads', where: 'document_id = ?', whereArgs: [id]);
});
}
}