refactor: delete dead old PDF annotator screen
All checks were successful
CI / Windows build (push) Successful in 11m52s

Now that every PDF entry point (home import, home open, search jump)
routes to PenEditorScreen, the old SfPdfViewer-based annotator is
unreachable. Remove it and the two widgets it solely owned:
- screens/pdf_annotator_screen.dart (981 lines)
- widgets/page_thumbnail_sidebar.dart
- widgets/pdf_annotation_layer.dart

annotation_toolbar and ink_canvas stay (still used by the note/ppt/
split-view screens). No references remain to the deleted files.

flutter analyze: 0 issues. Full suite: 258/258.
This commit is contained in:
2026-06-23 10:06:02 +08:00
parent 96594fbe1b
commit 3507e929b1
3 changed files with 0 additions and 1349 deletions

View File

@@ -1,206 +0,0 @@
import 'package:flutter/material.dart';
import '../services/thumbnail_service.dart';
/// Vertical sidebar showing page thumbnails for quick navigation.
///
/// Thumbnails are lazily generated and cached on disk. The current page is
/// highlighted with a blue border, and bookmarked pages show a colored dot.
class PageThumbnailSidebar extends StatefulWidget {
final String documentId;
final String filePath;
final int pageCount;
final int currentPage;
final ValueChanged<int> onPageTap;
final Set<int> bookmarkedPages;
const PageThumbnailSidebar({
super.key,
required this.documentId,
required this.filePath,
required this.pageCount,
required this.currentPage,
required this.onPageTap,
this.bookmarkedPages = const {},
});
@override
State<PageThumbnailSidebar> createState() => _PageThumbnailSidebarState();
}
class _PageThumbnailSidebarState extends State<PageThumbnailSidebar> {
/// Cached thumbnail image data keyed by page index.
final Map<int, ImageProvider> _cache = {};
/// Pages currently being generated (to avoid duplicate work).
final Set<int> _loading = {};
/// Pages that permanently failed thumbnail generation (null result or throw).
/// Skipped on subsequent rebuilds to avoid a retry storm.
final Set<int> _failed = {};
@override
void didUpdateWidget(PageThumbnailSidebar oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.documentId != widget.documentId) {
_cache.clear();
_loading.clear();
_failed.clear();
}
}
Future<void> _loadThumbnail(int pageIndex) async {
if (_cache.containsKey(pageIndex) ||
_loading.contains(pageIndex) ||
_failed.contains(pageIndex)) {
return;
}
_loading.add(pageIndex);
try {
// Check disk cache first.
final cached = await ThumbnailService.getCached(
widget.documentId,
pageIndex,
);
if (cached != null && mounted) {
setState(() {
_cache[pageIndex] = FileImage(cached);
});
_loading.remove(pageIndex);
return;
}
// Generate from the PDF.
final bytes = await ThumbnailService.generate(
widget.filePath,
pageIndex,
maxWidth: 160,
);
if (bytes != null) {
await ThumbnailService.cacheThumbnail(
widget.documentId,
pageIndex,
bytes,
);
if (mounted) {
setState(() {
_cache[pageIndex] = MemoryImage(bytes);
});
}
} else {
// Null result means generation failed permanently for this page.
_failed.add(pageIndex);
}
} catch (_) {
// Any exception is treated as a permanent failure to avoid retry storms.
_failed.add(pageIndex);
} finally {
_loading.remove(pageIndex);
}
}
@override
Widget build(BuildContext context) {
return Container(
width: 120,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
border: Border(
right: BorderSide(color: Theme.of(context).dividerColor, width: 1),
),
),
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: widget.pageCount,
itemBuilder: (context, index) {
_loadThumbnail(index);
final isCurrentPage = index == widget.currentPage;
final isBookmarked = widget.bookmarkedPages.contains(index);
return GestureDetector(
onTap: () => widget.onPageTap(index),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
border: Border.all(
color: isCurrentPage
? Theme.of(context).colorScheme.primary
: Colors.grey.shade400,
width: isCurrentPage ? 2.5 : 1.0,
),
borderRadius: BorderRadius.circular(4),
),
child: Stack(
children: [
// Thumbnail image or placeholder.
AspectRatio(
aspectRatio: 8.5 / 11, // US Letter-ish ratio
child: ClipRRect(
borderRadius: BorderRadius.circular(3),
child: _cache.containsKey(index)
? Image(image: _cache[index]!, fit: BoxFit.cover)
: Container(
color: Theme.of(
context,
).colorScheme.surfaceContainerLow,
child: Center(
child: Text(
'${index + 1}',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
),
),
),
),
// Page number overlay.
Positioned(
bottom: 2,
right: 2,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 1,
),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(3),
),
child: Text(
'${index + 1}',
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
),
),
),
// Bookmark indicator.
if (isBookmarked)
Positioned(
top: 2,
left: 2,
child: Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
),
),
),
],
),
),
);
},
),
);
}
}

View File

@@ -1,162 +0,0 @@
import 'package:flutter/material.dart';
import '../models/ink_point.dart';
import '../models/ink_stroke.dart';
import '../models/pen_tool.dart';
import '../widgets/ink_canvas.dart';
/// Transparent overlay widget positioned on top of the PDF viewer.
///
/// Reuses the existing [InkCanvas] widget for ink rendering.
/// Coordinates are normalized to [0, 1] relative to the overlay size,
/// enabling correct mapping to PDF page coordinates during export.
class PdfAnnotationLayer extends StatefulWidget {
final List<InkStroke> strokes;
final void Function(InkStroke stroke)? onStrokeComplete;
final void Function(String strokeId, List<InkStroke> replacements)? onErase;
final PenTool tool;
final Color color;
final double strokeWidth;
final bool filled;
final InteractionMode interactionMode;
final int rotation;
const PdfAnnotationLayer({
super.key,
required this.strokes,
this.onStrokeComplete,
this.onErase,
this.tool = PenTool.pen,
this.color = Colors.black,
this.strokeWidth = 2.0,
this.filled = false,
this.interactionMode = InteractionMode.draw,
this.rotation = 0,
});
@override
State<PdfAnnotationLayer> createState() => _PdfAnnotationLayerState();
}
class _PdfAnnotationLayerState extends State<PdfAnnotationLayer> {
Size _canvasSize = Size.zero;
/// Applies inverse rotation to normalized coordinates for rendering.
/// Converts from stored (possibly rotated) coords back to display coords.
Offset _inverseRotate(double nx, double ny, int rotation) {
switch (rotation % 360) {
case 90:
return Offset(1.0 - ny, nx);
case 180:
return Offset(1.0 - nx, 1.0 - ny);
case 270:
return Offset(ny, 1.0 - nx);
default:
return Offset(nx, ny);
}
}
/// Applies forward rotation to normalized coordinates before storage.
/// Converts from display coords to the canonical rotated representation.
Offset _forwardRotate(double nx, double ny, int rotation) {
switch (rotation % 360) {
case 90:
return Offset(ny, 1.0 - nx);
case 180:
return Offset(1.0 - nx, 1.0 - ny);
case 270:
return Offset(1.0 - ny, nx);
default:
return Offset(nx, ny);
}
}
/// Scales a stroke's points from normalized [0, 1] coordinates to
/// the current canvas pixel coordinates for rendering.
/// Applies inverse rotation before scaling so strokes render correctly
/// on a rotated page.
List<InkStroke> get _scaledStrokes {
if (_canvasSize == Size.zero) return widget.strokes;
return widget.strokes.map((stroke) {
return InkStroke(
id: stroke.id,
points: stroke.points.map((pt) {
final rotated = _inverseRotate(pt.x, pt.y, widget.rotation);
return InkPoint(
x: rotated.dx * _canvasSize.width,
y: rotated.dy * _canvasSize.height,
pressure: pt.pressure,
tilt: pt.tilt,
timestamp: pt.timestamp,
pointerDeviceKind: pt.pointerDeviceKind,
);
}).toList(),
tool: stroke.tool,
color: stroke.color,
strokeWidth: stroke.strokeWidth,
createdAt: stroke.createdAt,
filled: stroke.filled,
textContent: stroke.textContent,
fontSize: stroke.fontSize,
);
}).toList();
}
/// Normalizes a stroke's points from canvas pixel coordinates to
/// [0, 1] relative to the overlay size.
/// Applies forward rotation before storage so the canonical representation
/// accounts for the current page rotation.
InkStroke _normalizeStroke(InkStroke stroke) {
if (_canvasSize == Size.zero) return stroke;
return InkStroke(
id: stroke.id,
points: stroke.points.map((pt) {
final nx = pt.x / _canvasSize.width;
final ny = pt.y / _canvasSize.height;
final rotated = _forwardRotate(nx, ny, widget.rotation);
return InkPoint(
x: rotated.dx,
y: rotated.dy,
pressure: pt.pressure,
tilt: pt.tilt,
timestamp: pt.timestamp,
pointerDeviceKind: pt.pointerDeviceKind,
);
}).toList(),
tool: stroke.tool,
color: stroke.color,
strokeWidth: stroke.strokeWidth,
createdAt: stroke.createdAt,
filled: stroke.filled,
textContent: stroke.textContent,
fontSize: stroke.fontSize,
);
}
void _onStrokeComplete(InkStroke stroke) {
widget.onStrokeComplete?.call(_normalizeStroke(stroke));
}
void _onErase(String strokeId, List<InkStroke> replacements) {
widget.onErase?.call(strokeId, replacements.map(_normalizeStroke).toList());
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
_canvasSize = Size(constraints.maxWidth, constraints.maxHeight);
return InkCanvas(
strokes: _scaledStrokes,
onStrokeComplete: _onStrokeComplete,
onErase: _onErase,
tool: widget.tool,
color: widget.color,
strokeWidth: widget.strokeWidth,
filled: widget.filled,
interactionMode: widget.interactionMode,
);
},
);
}
}