import 'dart:async'; import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:percent_indicator/circular_percent_indicator.dart'; import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; import 'package:path_provider/path_provider.dart'; import 'dart:io'; class FilePdf extends StatefulWidget { final String pdf; final String? searchTerm; final String title; const FilePdf({ super.key, required this.pdf, required this.title, this.searchTerm, }); @override // ignore: library_private_types_in_public_api _FilePdfState createState() => _FilePdfState(); } class _FilePdfState extends State { late Future _pdfPathFuture; late PdfViewerController _pdfViewerController; late PdfTextSearchResult _pdfTextSearchResult; final _baseUrl = dotenv.env['BASE_URL']!; final _token = dotenv.env['TOKEN']!; bool isDownloading = true; ValueNotifier downloadProgressNotifier = ValueNotifier(0); bool isSearching = false; int downloadProgress = 0; Timer? _debounceTimer; @override void initState() { super.initState(); _pdfPathFuture = _fetchPdf(); _pdfViewerController = PdfViewerController(); _pdfTextSearchResult = PdfTextSearchResult(); } @override void dispose() { _pdfTextSearchResult.removeListener(() {}); _pdfTextSearchResult.dispose(); _pdfViewerController.dispose(); _debounceTimer?.cancel(); super.dispose(); } Future _fetchPdf() async { final String pdfFileName = widget.pdf; final String baseUrl = '$_baseUrl/assets/$pdfFileName?access_token=$_token'; final appDir = await getApplicationDocumentsDirectory(); final pdfDir = Directory( '${appDir.path}/LGCC_Search/${context.locale.toString()}/library/'); if (!await pdfDir.exists()) { await pdfDir.create(recursive: true); } final String filePath = '${pdfDir.path}/$pdfFileName.pdf'; if (await File(filePath).exists()) { return filePath; } else { try { await Dio().download(baseUrl, filePath, onReceiveProgress: (actualBytes, totalBytes) { final progress = (actualBytes / totalBytes * 100).floor(); setState(() { downloadProgress = progress; downloadProgressNotifier.value = progress; }); }); setState(() { isDownloading = false; }); return filePath; } catch (e) { if (kDebugMode) { print('Error downloading file: $e'); } setState(() { isDownloading = false; downloadProgress = 0; downloadProgressNotifier.value = 0; }); return ''; } } } Future saveDoc() async { final List savedBytes = await _pdfViewerController.saveDocument(); _saveDocument(savedBytes); } void _performSearch(String searchTerm, {bool immediate = false}) { if (mounted && searchTerm.isEmpty) { setState(() { isSearching = false; }); } if (immediate) { _performSearchAction(searchTerm); } else { _debounceTimer?.cancel(); _debounceTimer = Timer(const Duration(milliseconds: 300), () { _performSearchAction(searchTerm); }); } } void _performSearchAction(String searchTerm) { _pdfTextSearchResult = _pdfViewerController.searchText(searchTerm); _pdfTextSearchResult.addListener(() { if (mounted && _pdfTextSearchResult.isSearchCompleted) { setState(() {}); } }); } Future _saveDocument(List dataBytes) async { final appDir = await getApplicationDocumentsDirectory(); final directory = Directory( '${appDir.path}/LGCC_Search/${context.locale.toString()}/library/'); final String path = directory.path; final File file = File('$path/${widget.pdf}.pdf'); await file.writeAsBytes(dataBytes); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: isSearching ? TextField( onChanged: (val) => _performSearch(val), ) : Text(widget.title), backgroundColor: const Color(0xFFe0e6d1), actions: [ Container( margin: const EdgeInsets.only(right: 12), child: Row( children: [ if (_pdfTextSearchResult.hasResult && _pdfTextSearchResult.totalInstanceCount > 1) Text( '${_pdfTextSearchResult.currentInstanceIndex} / ${_pdfTextSearchResult.totalInstanceCount}'), if (_pdfTextSearchResult.hasResult && _pdfTextSearchResult.totalInstanceCount > 1) IconButton( icon: const Icon(Icons.chevron_left), onPressed: (_pdfTextSearchResult.currentInstanceIndex > 1) ? () { _pdfTextSearchResult.previousInstance(); } : null, ), if (_pdfTextSearchResult.hasResult && _pdfTextSearchResult.totalInstanceCount > 1) IconButton( icon: const Icon(Icons.chevron_right), onPressed: (_pdfTextSearchResult.currentInstanceIndex < _pdfTextSearchResult.totalInstanceCount) ? () { _pdfTextSearchResult.nextInstance(); } : null, ), IconButton( icon: Icon(isSearching ? Icons.close : Icons.search), onPressed: () { setState(() { isSearching = !isSearching; }); }, ), ], ), ) ]), body: FutureBuilder( future: _pdfPathFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center( child: CircularPercentIndicator( backgroundColor: Colors.grey.shade400, progressColor: const Color(0xFF6b8e23), lineWidth: 3, radius: 32, center: Text(downloadProgress.toString(), style: const TextStyle(fontSize: 14, color: Colors.black)), percent: downloadProgress / 100, ), ); } else if (snapshot.hasError) { return Center(child: Text('no_results'.tr())); } else if (snapshot.hasData) { return SfPdfViewer.file( File(snapshot.data!), controller: _pdfViewerController, onDocumentLoaded: (PdfDocumentLoadedDetails details) { if (widget.searchTerm != null) { _performSearch(widget.searchTerm!, immediate: true); } }, onAnnotationAdded: (annotation) => saveDoc(), ); } else { return const Center(child: Text('Error')); } }, ), ); } }