import 'dart:async'; import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:open_filex/open_filex.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_svg/svg.dart'; import 'package:generp/Utils/commonWidgets.dart'; import 'package:path_provider/path_provider.dart'; import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:flutter_pdfview/flutter_pdfview.dart'; import 'package:http/http.dart' as http; import 'dart:typed_data'; import 'package:photo_view/photo_view.dart'; import '../../Utils/app_colors.dart'; class Fileviewer extends StatefulWidget { final String fileName; final String fileUrl; final bool downloadEnable;// use this to show download button or not at const Fileviewer({super.key, required this.fileName, required this.fileUrl, required this.downloadEnable}); @override State createState() => _FileviewerState(); } class _FileviewerState extends State { final Completer _controller = Completer(); var empId = ""; var sessionId = ""; bool isLoading = true; InAppWebViewController? webViewController; PullToRefreshController? pullToRefreshController; PullToRefreshSettings pullToRefreshSettings = PullToRefreshSettings( color: AppColors.app_blue, ); bool pullToRefreshEnabled = true; final GlobalKey webViewKey = GlobalKey(); // Zoom control variables PhotoViewController _photoViewController = PhotoViewController(); PhotoViewScaleStateController _scaleStateController = PhotoViewScaleStateController(); String getFileExtension(String fileName) { print(widget.fileUrl); return fileName.split('.').last.toLowerCase(); } Future _launchUrl(String url) async { final Uri uri = Uri.parse(url); if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); } else { throw 'Could not launch $url'; } } var Finalurl; @override void initState() { super.initState(); // Create pullToRefreshController only for non-web platforms if (!kIsWeb) { pullToRefreshController = PullToRefreshController( settings: pullToRefreshSettings, onRefresh: () async { // Guard: don't run if widget disposed if (!mounted) return; try { if (defaultTargetPlatform == TargetPlatform.android) { await webViewController?.reload(); } else if (defaultTargetPlatform == TargetPlatform.iOS) { final currentUrl = await webViewController?.getUrl(); if (currentUrl != null) { await webViewController?.loadUrl( urlRequest: URLRequest(url: currentUrl), ); } } } catch (e) { // ignore errors during refresh } finally { // ensure refresh animation stops even if reload fails safeEndRefreshing(); } }, ); } else { pullToRefreshController = null; } // Initialize photo view controllers _photoViewController = PhotoViewController(); _scaleStateController = PhotoViewScaleStateController(); } /// Safe helper to end pull-to-refresh only when mounted and controller exists. void safeEndRefreshing() { if (!mounted) return; final ctrl = pullToRefreshController; if (ctrl != null) { try { ctrl.endRefreshing(); } catch (e) { // In some rare cases controller may already be disposed — ignore. } } } @override void dispose() { // Dispose photo controllers first _photoViewController.dispose(); _scaleStateController.dispose(); // Dispose pullToRefreshController and null it to avoid later use try { pullToRefreshController?.dispose(); } catch (e) { // ignore dispose errors } pullToRefreshController = null; webViewController = null; super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: true, appBar: AppBar( backgroundColor: Color(0xFFFFFFFF), automaticallyImplyLeading: false, title: SizedBox( child: Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ InkResponse( onTap: () => Navigator.pop(context, true), child: SvgPicture.asset( "assets/svg/appbar_back_button.svg", height: 25, ), ), SizedBox(width: 10), InkResponse( onTap: () => Navigator.pop(context, true), child: Text( "File Viewer", style: TextStyle( fontSize: 16, height: 1.1, fontFamily: "JakartaSemiBold", color: AppColors.semi_black, ), ), ), Spacer(), /// DOWNLOAD BUTTON (NEW) /// ----------------------------- if (widget.downloadEnable) IconButton( icon: Icon(Icons.download_rounded, color: AppColors.app_blue), onPressed: () async { await _downloadFile(widget.fileUrl, widget.fileName); }, ), ], ), ), ), body: SafeArea( child: Center( child: fileWidget(context) ), ), ); } Future _downloadFile(String url, String fileName) async { try { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Downloading...")), ); final http.Response response = await http.get(Uri.parse(url)); if (response.statusCode == 200) { final bytes = response.bodyBytes; // Get storage directory final directory = await getApplicationDocumentsDirectory(); final filePath = "${directory.path}/$fileName"; final file = File(filePath); await file.writeAsBytes(bytes); // Download success ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("File downloaded successfully \n At this: ${directory.path}")), ); // ASK USER TO OPEN THE DOWNLOADED FILE // ------------------------------------- showDialog( context: context, builder: (context) { return Dialog( backgroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), elevation: 0, child: Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 20, offset: const Offset(0, 4), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Success Icon Container( width: 60, height: 60, decoration: BoxDecoration( color: Colors.green.withOpacity(0.1), shape: BoxShape.circle, ), child: const Icon( Icons.check_circle_rounded, color: Colors.green, size: 36, ), ), const SizedBox(height: 16), // Title const Text( "Download Complete", style: TextStyle( fontSize: 18, fontFamily: "Plus Jakarta Sans", fontWeight: FontWeight.w700, color: Colors.black87, ), ), const SizedBox(height: 8), // Description Text( "Do you want to open the file?", textAlign: TextAlign.center, style: TextStyle( fontSize: 14, fontFamily: "Plus Jakarta Sans", fontWeight: FontWeight.w400, color: Colors.grey[600], height: 1.4, ), ), const SizedBox(height: 24), // Buttons Row Row( children: [ // Cancel Button Expanded( child: OutlinedButton( onPressed: () => Navigator.pop(context), style: OutlinedButton.styleFrom( backgroundColor: Colors.transparent, foregroundColor: Colors.grey[600], side: BorderSide( color: Colors.grey[300]!, width: 1.5, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.symmetric(vertical: 14), elevation: 0, ), child: Text( "Cancel", style: TextStyle( fontFamily: "Plus Jakarta Sans", fontWeight: FontWeight.w600, fontSize: 14, color: Colors.grey[700], ), ), ), ), const SizedBox(width: 12), // Open Button Expanded( child: ElevatedButton( onPressed: () { Navigator.pop(context); OpenFilex.open(filePath); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF008CDE), foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.symmetric(vertical: 14), elevation: 0, shadowColor: Colors.transparent, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.open_in_new, size: 18), const SizedBox(width: 6), Text( "Open", style: TextStyle( fontFamily: "Plus Jakarta Sans", fontWeight: FontWeight.w600, fontSize: 14, color: Colors.white, ), ), ], ), ), ), ], ), ], ), ), ); }, ); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Failed to download file")), ); } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Error: $e")), ); } } Widget fileWidget(BuildContext context) { final extension = getFileExtension(widget.fileName); switch (extension) { case 'jpg': case 'jpeg': case 'png': case 'gif': case 'bmp': case 'webp': return _buildImageViewer(); case 'pdf': return _buildPdfViewer(); case 'doc': case 'docx': case 'xls': case 'xlsx': case 'ppt': case 'pptx': return _buildDocumentViewer(); default: return _buildUnsupportedViewer(); } } Widget _buildImageViewer() { return PhotoView( imageProvider: CachedNetworkImageProvider(widget.fileUrl), loadingBuilder: (context, event) => Center( child: Container( width: 40, height: 40, child: CircularProgressIndicator( value: event == null ? 0 : event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1), ), ), ), errorBuilder: (context, error, stackTrace) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, color: Colors.red, size: 50), SizedBox(height: 10), Text( 'Failed to load image', style: TextStyle(fontSize: 16, color: Colors.grey), ), ], ), ), backgroundDecoration: BoxDecoration(color: Colors.white), minScale: PhotoViewComputedScale.contained * 0.5, maxScale: PhotoViewComputedScale.covered * 4.0, initialScale: PhotoViewComputedScale.contained, basePosition: Alignment.center, scaleStateController: _scaleStateController, controller: _photoViewController, enableRotation: true, gestureDetectorBehavior: HitTestBehavior.deferToChild, filterQuality: FilterQuality.high, ); } Widget _buildPdfViewer() { return SfPdfViewer.network( widget.fileUrl, key: GlobalKey(), canShowScrollHead: true, canShowPaginationDialog: true, pageLayoutMode: PdfPageLayoutMode.single, interactionMode: PdfInteractionMode.pan, enableDoubleTapZooming: true, enableTextSelection: true, onZoomLevelChanged: (PdfZoomDetails details) { // Use the correct property name //print('Zoom level changed: ${details.zoomLevel}'); }, ); } Widget _buildDocumentViewer() { return Stack( children: [ InAppWebView( key: webViewKey, initialUrlRequest: URLRequest(url: WebUri(widget.fileUrl)), androidOnGeolocationPermissionsShowPrompt: ( InAppWebViewController controller, String origin, ) async { return GeolocationPermissionShowPromptResponse( origin: origin, allow: true, retain: true, ); }, initialOptions: InAppWebViewGroupOptions( crossPlatform: InAppWebViewOptions( useShouldOverrideUrlLoading: true, mediaPlaybackRequiresUserGesture: false, javaScriptEnabled: true, clearCache: true, supportZoom: true, ), android: AndroidInAppWebViewOptions( useWideViewPort: true, loadWithOverviewMode: true, allowContentAccess: true, geolocationEnabled: true, allowFileAccess: true, databaseEnabled: true, domStorageEnabled: true, builtInZoomControls: true, displayZoomControls: false, safeBrowsingEnabled: true, clearSessionCache: true, supportMultipleWindows: false, ), ios: IOSInAppWebViewOptions( allowsInlineMediaPlayback: true, allowsAirPlayForMediaPlayback: true, allowsPictureInPictureMediaPlayback: true, allowsBackForwardNavigationGestures: true, allowsLinkPreview: true, isFraudulentWebsiteWarningEnabled: true, ), ), androidOnPermissionRequest: ( InAppWebViewController controller, String origin, List resources, ) async { return PermissionRequestResponse( resources: resources, action: PermissionRequestResponseAction.GRANT, ); }, onWebViewCreated: (controller) { webViewController = controller; _controller.complete(controller); }, pullToRefreshController: pullToRefreshController, onLoadStart: (controller, url) { setState(() { isLoading = true; }); }, onLoadStop: (controller, url) { pullToRefreshController?.endRefreshing(); setState(() { isLoading = false; }); // Enable zooming in WebView controller.evaluateJavascript(source: """ var meta = document.createElement('meta'); meta.name = 'viewport'; meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=4.0, user-scalable=yes'; document.getElementsByTagName('head')[0].appendChild(meta); """); }, onReceivedError: (controller, request, error) { pullToRefreshController?.endRefreshing(); setState(() { isLoading = false; }); }, onProgressChanged: (controller, progress) { if (progress == 100) { pullToRefreshController?.endRefreshing(); } }, onConsoleMessage: (controller, consoleMessage) { if (kDebugMode) { debugPrint("consoleMessage: ${consoleMessage.message}"); } }, ), // Loading indicator for documents if (isLoading) Positioned.fill( child: Container( color: Colors.black.withOpacity(0.3), child: Center( child: Container( padding: EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( color: Colors.black26, blurRadius: 10, ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(AppColors.app_blue), ), SizedBox(height: 10), Text( 'Loading Document...', style: TextStyle( fontSize: 14, color: Colors.grey[700], ), ), ], ), ), ), ), ), ], ); } Widget _buildUnsupportedViewer() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.insert_drive_file, size: 64, color: Colors.grey[400], ), SizedBox(height: 16), Text( 'Unsupported File Format', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.grey[600], ), ), SizedBox(height: 8), Text( 'Format: ${getFileExtension(widget.fileName).toUpperCase()}', style: TextStyle( fontSize: 14, color: Colors.grey[500], ), ), SizedBox(height: 16), ElevatedButton.icon( onPressed: () { _launchUrl(widget.fileUrl); }, icon: Icon(Icons.open_in_new), label: Text('Open in External App'), style: ElevatedButton.styleFrom( backgroundColor: AppColors.app_blue, foregroundColor: Colors.white, ), ), ], ), ); } Future _loadPdf(String url) async { try { final response = await http.get(Uri.parse(url)); if (response.statusCode == 200) { return response.bodyBytes; } } catch (e) { print('Error loading PDF: $e'); } return null; } }