-
-
Notifications
You must be signed in to change notification settings - Fork 128
Description
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pdfrx/pdfrx.dart';
import 'package:printing/printing.dart';
class Marker {
Marker(this.color, this.range);
final Color color;
final PdfPageTextRange range;
}
class PdfView extends StatefulWidget {
final Uint8List pdfBytes;
final String uniqueId;
final PdfViewerController? controller;
final bool? isPrintAllowed;
/// List of text strings to highlight in the PDF
final List? highlightTexts;
/// Page to navigate to (1-based)
final int? initialPage;
const PdfView({
super.key,
required this.pdfBytes,
required this.uniqueId,
this.controller,
this.isPrintAllowed,
this.highlightTexts,
this.initialPage,
});
@OverRide
State createState() => _PdfViewState();
}
class _PdfViewState extends State {
final pdfController = PdfViewerController();
int _currentPage = 1;
int _savedPageBeforeRotation = 1;
Uint8List? _currentPdfBytes;
bool _isRotating = false;
PdfViewerController? _activeController;
int _rebuiltCounter = 0;
final _markers = <int, List>{};
/// Text searcher for highlighting - uses web-compatible search
PdfTextSearcher? _textSearcher;
@OverRide
void initState() {
super.initState();
_currentPdfBytes = widget.pdfBytes;
_activeController = widget.controller ?? pdfController;
if (widget.controller != null) {
widget.controller!.addListener(_onPdfLoaded);
} else {
pdfController.addListener(_onPdfLoaded);
}
}
Future _rotateCurrentPage(String rotationType) async {
if (_isRotating || _currentPdfBytes == null) return;
final controller = _activeController;
if (controller == null) return;
final currentPageNumber = controller.pageNumber ?? _currentPage;
_savedPageBeforeRotation = currentPageNumber;
setState(() {
_isRotating = true;
});
try {
final doc = await PdfDocument.openData(_currentPdfBytes!);
final selectedIndex =
(_savedPageBeforeRotation - 1).clamp(0, doc.pages.length - 1);
final List<PdfPage> newPages = [];
for (int i = 0; i < doc.pages.length; i++) {
if (i == selectedIndex) {
switch (rotationType) {
case 'cw90':
newPages.add(doc.pages[i].rotatedCW90());
break;
case 'ccw90':
newPages.add(doc.pages[i].rotatedCCW90());
break;
case '180':
newPages.add(doc.pages[i].rotated180());
break;
default:
newPages.add(doc.pages[i]);
}
} else {
newPages.add(doc.pages[i]);
}
}
doc.pages = newPages;
final updatedBytes = await doc.encodePdf();
doc.dispose();
// Clean up old controller listeners
if (_activeController == pdfController && widget.controller == null) {
pdfController.removeListener(_onPdfLoaded);
pdfController.removeListener(_onPageChanged);
}
// Create new controller for internal use only
PdfViewerController? newController;
if (widget.controller == null) {
newController = PdfViewerController();
}
// Update state with new bytes and controller
setState(() {
_currentPdfBytes = Uint8List.fromList(updatedBytes);
_rebuiltCounter++;
if (widget.controller == null) {
_activeController = newController;
}
// Keep _isRotating true until navigation completes
});
// Wait for the new PDF to initialize and then navigate
await _waitForPdfAndNavigate(_savedPageBeforeRotation);
} catch (e) {
setState(() {
_isRotating = false;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to rotate page: ${e.toString()}'),
duration: const Duration(seconds: 3),
),
);
}
}
}
Future _waitForPdfAndNavigate(int targetPage) async {
// Wait for next frame to ensure widget is built
await Future.delayed(const Duration(milliseconds: 50));
if (!mounted) {
return;
}
final controllerToUse = _activeController;
if (controllerToUse == null) {
if (mounted) {
setState(() {
_isRotating = false;
});
}
return;
}
// Wait for PDF to be ready with timeout
int attempts = 0;
const maxAttempts = 100; // 10 seconds max
while (attempts < maxAttempts && mounted) {
try {
final pageCount = controllerToUse.pageCount;
if (pageCount > 0) {
break;
}
} catch (e) {
// Controller not ready yet, continue waiting
}
await Future.delayed(const Duration(milliseconds: 100));
attempts++;
}
if (attempts >= maxAttempts) {
if (mounted) {
setState(() {
_isRotating = false;
});
}
return;
}
if (!mounted) {
return;
}
// Additional delay to ensure rendering is complete
await Future.delayed(const Duration(milliseconds: 300));
if (!mounted) {
return;
}
try {
final pageCount = controllerToUse.pageCount;
final clampedPage = targetPage.clamp(1, pageCount);
// Don't await goToPage - just fire it and continue
// This prevents hanging if goToPage has issues
controllerToUse.goToPage(pageNumber: clampedPage);
// Wait a bit for navigation to take effect
await Future.delayed(const Duration(milliseconds: 500));
if (mounted) {
setState(() {
_currentPage = clampedPage;
_isRotating = false; // Restore button state HERE
});
// Set up page change listener
controllerToUse.removeListener(_onPageChanged);
controllerToUse.addListener(_onPageChanged);
// Verify after a delay
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) {
final actualPage = controllerToUse.pageNumber ?? _currentPage;
if (actualPage != clampedPage) {
controllerToUse.goToPage(pageNumber: clampedPage);
} else {}
}
});
}
} catch (e) {
if (mounted) {
// Fallback and restore button state
try {
final fallbackPage = controllerToUse.pageNumber ?? 1;
setState(() {
_currentPage = fallbackPage;
_isRotating = false; // Restore button state HERE too
});
} catch (_) {
setState(() {
_currentPage = 1;
_isRotating = false; // And HERE
});
}
controllerToUse.removeListener(_onPageChanged);
controllerToUse.addListener(_onPageChanged);
}
}
}
Timer? _scrollTimer;
void _handleKey(KeyEvent event) {
final controller = _activeController;
if (controller == null) return;
if (event is KeyDownEvent) {
_scrollTimer?.cancel();
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
_scrollTimer = Timer.periodic(const Duration(milliseconds: 800), (_) {
try {
final pageCount = controller.pageCount;
if (_currentPage < pageCount) {
_currentPage++;
controller.goToPage(pageNumber: _currentPage);
} else {
_scrollTimer?.cancel();
}
} catch (e) {
_scrollTimer?.cancel();
}
});
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
_scrollTimer = Timer.periodic(const Duration(milliseconds: 800), (_) {
try {
if (_currentPage > 1) {
_currentPage--;
controller.goToPage(pageNumber: _currentPage);
} else {
_scrollTimer?.cancel();
}
} catch (e) {
_scrollTimer?.cancel();
}
});
}
} else if (event is KeyUpEvent) {
_scrollTimer?.cancel();
}
}
void _onPdfLoaded() async {
if (!mounted) return;
final controller = widget.controller ?? pdfController;
try {
if (controller.isReady && controller.pageCount > 0) {
if (!mounted) return;
controller.removeListener(_onPdfLoaded);
controller.addListener(_onPageChanged);
}
} catch (e) {
// Controller not ready yet
}
}
/// Search only the target page for highlight texts - FAST single page search
Future _searchSinglePage(PdfDocument document, int targetPage, PdfViewerController controller) async {
final texts = widget.highlightTexts;
if (texts == null || texts.isEmpty) return;
final validTexts = texts.where((t) => t.trim().isNotEmpty).toList();
if (validTexts.isEmpty) return;
final pattern = RegExp(
validTexts.map((t) => RegExp.escape(t.trim())).join('|'),
caseSensitive: false,
);
try {
// Load text ONLY for target page
final page = document.pages[targetPage - 1];
final pageText = await page.loadStructuredText();
if (pageText.fullText.isEmpty) {
// Fallback to PdfTextSearcher if direct extraction fails (web)
_startFullSearch(controller);
return;
}
// Fast single-page search
final matches = await pageText.allMatches(pattern).toList();
if (matches.isEmpty || !mounted) return;
// Store as markers
_markers[targetPage] = matches
.map((m) => Marker(Colors.yellow, m))
.toList();
setState(() {});
controller.invalidate();
} catch (e) {
// Fallback to full search
_startFullSearch(controller);
}
}
/// Fallback: Full document search using PdfTextSearcher (slower but works on web)
void _startFullSearch(PdfViewerController controller) {
final texts = widget.highlightTexts;
if (texts == null || texts.isEmpty) return;
final validTexts = texts.where((t) => t.trim().isNotEmpty).toList();
if (validTexts.isEmpty) return;
final pattern = RegExp(
validTexts.map((t) => RegExp.escape(t.trim())).join('|'),
caseSensitive: false,
);
_textSearcher?.dispose();
_textSearcher = PdfTextSearcher(controller);
// Invalidate on each match found
_textSearcher!.addListener(() {
if (mounted) controller.invalidate();
});
_textSearcher!.startTextSearch(pattern, goToFirstMatch: false);
}
void _onPageChanged() {
final controller = _activeController;
if (controller == null) return;
final currentPage = controller.pageNumber ?? 1;
if (currentPage != _currentPage) {
setState(() {
_currentPage = currentPage;
});
}
}
@OverRide
void dispose() {
if (widget.controller != null) {
widget.controller!.removeListener(_onPdfLoaded);
widget.controller!.removeListener(_onPageChanged);
} else {
pdfController.removeListener(_onPdfLoaded);
pdfController.removeListener(_onPageChanged);
}
if (_activeController != null &&
_activeController != widget.controller &&
_activeController != pdfController) {
_activeController!.removeListener(_onPageChanged);
}
_scrollTimer?.cancel();
_textSearcher?.dispose();
super.dispose();
}
@OverRide
Widget build(BuildContext context) {
final controller =
_activeController ?? (widget.controller ?? pdfController);
return KeyboardListener(
focusNode: FocusNode()..requestFocus(),
autofocus: true,
onKeyEvent: _handleKey,
child: Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
actions: [
if (widget.isPrintAllowed != null)
IconButton(
icon: const Icon(Icons.print),
onPressed: () async {
await Printing.layoutPdf(
onLayout: (_) async {
return _currentPdfBytes ?? widget.pdfBytes;
},
);
},
),
IconButton(
icon: _isRotating
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.rotate_left),
onPressed:
_isRotating ? null : () => _rotateCurrentPage('ccw90'),
tooltip: _isRotating
? 'Rotating...'
: 'Rotate page $_currentPage left (90°)',
),
IconButton(
icon: _isRotating
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.rotate_right),
onPressed:
_isRotating ? null : () => _rotateCurrentPage('cw90'),
tooltip: _isRotating
? 'Rotating...'
: 'Rotate page $_currentPage right (90°)',
),
const VerticalDivider(),
IconButton(
icon: const Icon(Icons.zoom_in),
onPressed: controller.zoomUp,
),
IconButton(
icon: const Icon(Icons.zoom_out),
onPressed: controller.zoomDown,
),
IconButton(
icon: const Icon(Icons.first_page),
onPressed: () => controller.goToPage(pageNumber: 1),
),
IconButton(
icon: const Icon(Icons.last_page),
onPressed: () {
try {
final pageCount = controller.pageCount;
if (pageCount > 0) {
controller.goToPage(pageNumber: pageCount);
}
} catch (e) {
// Silent fail
}
},
),
],
),
body: PdfViewer.data(
key: ValueKey('pdf_$_rebuiltCounter'),
_currentPdfBytes ?? widget.pdfBytes,
sourceName: '${widget.uniqueId}_$_rebuiltCounter',
controller: controller,
params: PdfViewerParams(
maxScale: 8,
viewerOverlayBuilder: (context, size, handleLinkTap) => [
GestureDetector(
behavior: HitTestBehavior.translucent,
onTapUp: (details) {
handleLinkTap(details.localPosition);
},
onDoubleTap: () {
controller.zoomUp(loop: true);
},
child: IgnorePointer(
child:
SizedBox(width: size.width, height: size.height),
),
),
PdfViewerScrollThumb(
controller: controller,
orientation: ScrollbarOrientation.right,
thumbSize: const Size(40, 25),
thumbBuilder:
(context, thumbSize, pageNumber, controller) {
final currentPageFromController = pageNumber ?? 1;
if (currentPageFromController != _currentPage) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_currentPage = currentPageFromController;
});
}
});
}
return Container(
color: Colors.black,
child: Center(
child: Text(
currentPageFromController.toString(),
style: const TextStyle(color: Colors.white),
),
),
);
},
),
PdfViewerScrollThumb(
controller: controller,
orientation: ScrollbarOrientation.bottom,
thumbSize: const Size(80, 30),
thumbBuilder:
(context, thumbSize, pageNumber, controller) {
final currentPageFromController = pageNumber ?? 1;
return Container(
color: Colors.black,
child: Center(
child: Text(
currentPageFromController.toString(),
style: const TextStyle(color: Colors.white),
),
),
);
},
),
],
errorBannerBuilder: (context, error, stackTrace, documentRef) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
color: Colors.red.shade100,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.error_outline,
color: Colors.red.shade700,
size: 48,
),
const SizedBox(height: 8),
Text(
'Failed to load PDF',
style: TextStyle(
color: Colors.red.shade700,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'Error: ${error.toString()}',
style: TextStyle(
color: Colors.red.shade600,
fontSize: 14,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
ElevatedButton.icon(
onPressed: () {
setState(() {});
},
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade700,
foregroundColor: Colors.white,
),
),
],
),
);
},
loadingBannerBuilder: (context, bytesDownloaded, totalBytes) =>
Center(
child: CircularProgressIndicator(
value: totalBytes != null
? bytesDownloaded / totalBytes
: null,
backgroundColor: Colors.grey,
),
),
pagePaintCallbacks: [
_paintMarkers,
_paintTextSearchHighlights,
],
onDocumentChanged: (document) async {
if (document == null) {
_markers.clear();
_textSearcher?.dispose();
_textSearcher = null;
}
},
onViewerReady: (document, controller) async {
// Navigate to initial page FIRST
final targetPage = (widget.initialPage ?? 1).clamp(1, document.pages.length);
if (targetPage > 1) {
controller.goToPage(pageNumber: targetPage);
}
// Search ONLY the target page immediately
if (widget.highlightTexts != null && widget.highlightTexts!.isNotEmpty) {
if (mounted) {
_searchSinglePage(document, targetPage, controller);
}
}
controller.requestFocus();
}),
)),
);
}
/// Paint text search highlights using the built-in callback from PdfTextSearcher
void _paintTextSearchHighlights(Canvas canvas, Rect pageRect, PdfPage page) {
if (_textSearcher == null) return;
// Use the built-in paint callback which handles coordinates correctly
_textSearcher!.pageTextMatchPaintCallback(canvas, pageRect, page);
}
void _paintMarkers(Canvas canvas, Rect pageRect, PdfPage page) {
final markers = _markers[page.pageNumber];
if (markers == null) return;
for (final marker in markers) {
final paint = Paint()
..color = marker.color.withAlpha(100)
..style = PaintingStyle.fill;
final rect = marker.range.bounds.toRectInDocument(page: page, pageRect: pageRect);
canvas.drawRect(rect, paint);
}
}
}
this is my code and when i try to highlighttext , it dont appears immediately