refactor: delete dead old PDF annotator screen
All checks were successful
CI / Windows build (push) Successful in 11m52s
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:
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user