// widgets/pdf_viewer.dart import 'dart:convert'; import 'package:MakerBook/secure_screen/ios_secure_screen.dart'; import 'package:MakerBook/utils/global.dart'; import 'package:flutter/material.dart'; import 'package:flutter_pdfview/flutter_pdfview.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:io'; import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; class PdfViewer extends StatefulWidget { final List pdfBytes; final String courseName; final int book_id; const PdfViewer({super.key, required this.pdfBytes, required this.courseName, required this.book_id}); @override _PdfViewerState createState() => _PdfViewerState(); } class _PdfViewerState extends State { String? localPath; PDFViewController? _controller; int _totalPages = 0; int _currentPage = 0; String _courseName = ""; bool setaTutorial = false; bool _mostrarControles = true; @override void initState() { super.initState(); IosSecureScreen.enable(); // <- liga a “tela segura” no iOS _loadPdf(); _verificaSeMostraTutorial(); } @override void dispose() { _saveLastPage(); IosSecureScreen.enable(); // <- liga a “tela segura” no iOS super.dispose(); } Future _loadPdf() async { final tempDir = await getTemporaryDirectory(); final tempFile = File('${tempDir.path}/temp.pdf'); await tempFile.writeAsBytes(widget.pdfBytes, flush: true); setState(() { localPath = tempFile.path; _courseName = widget.courseName; }); } Future _saveLastPage() async { try { int page = await _controller!.getCurrentPage() ?? 1; await saveLastLocation(widget.book_id, page.toString()); } catch (ex) { print("Erro ao salvar lastLocation no PDF"); } } void _goToPage(int page) { if (_controller != null && page >= 0 && page < _totalPages) { _controller!.setPage(page); print("[LOG] Página do PDF definida para ${page}"); } } void _showPageInputDialog() { final TextEditingController _pageController = TextEditingController(); showDialog( context: context, barrierDismissible: false, // Para manter o foco builder: (context) { return Dialog( backgroundColor: const Color(0xFFFDF6E4), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), child: SizedBox( width: 300, child: Column( mainAxisSize: MainAxisSize.min, children: [ // Título com lupa Stack( children: [ Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 16), decoration: const BoxDecoration( color: Color(0xFF84D1F0), borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: Center( child: Text( 'PESQUISA DETALHADA!', style: GoogleFonts.fredoka( fontSize: 17, fontWeight: FontWeight.bold, color: Colors.black ), ), ), ), Positioned( top: 0, right: 0, child: Image.asset( 'assets/images/zoom.png', width: 50, height: 50, ), ), ], ), const SizedBox(height: 24), // Campo de texto Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: TextField( controller: _pageController, keyboardType: TextInputType.number, textAlign: TextAlign.center, style: GoogleFonts.fredoka( fontSize: 17, fontWeight: FontWeight.bold, ), decoration: InputDecoration( hintText: 'NÚMERO DA PÁGINA', hintStyle: GoogleFonts.fredoka( fontSize: 17, fontWeight: FontWeight.bold, ), contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(25), borderSide: const BorderSide(color: Colors.lightBlueAccent, width: 2), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(25), borderSide: const BorderSide(color: Colors.blue, width: 2), ), ), ), ), const SizedBox(height: 24), // Botões Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // Botão cancelar ElevatedButton( onPressed: () => Navigator.pop(context), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFFFB1B1), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), ), child: Text( 'CANCELAR', style: GoogleFonts.fredoka( fontSize: 15, fontWeight: FontWeight.bold, color: Colors.black ), ), ), // Botão pesquisar ElevatedButton( onPressed: () { final int? page = int.tryParse(_pageController.text); if (page != null && page > 0 && page <= _totalPages) { _goToPage(page - 1); } Navigator.pop(context); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF94A38F), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), ), child: Text( 'PESQUISAR', style: GoogleFonts.fredoka( fontSize: 15, fontWeight: FontWeight.bold, color: Colors.black ), ), ), ], ), ) ], ), ), ); }, ); } void _showAnnotationDialog() async { final TextEditingController _noteController = TextEditingController(); final directory = await getApplicationDocumentsDirectory(); final file = File('${directory.path}/${widget.book_id}.json'); Map data = {}; int currentPage = _currentPage + 1; // Carregar anotação atual if (await file.exists()) { data = jsonDecode(await file.readAsString()); final notes = data['notes'] ?? {}; if (notes.containsKey('$currentPage')) { _noteController.text = notes['$currentPage']; } } int numLinhas = 5; double alturaLinha = 24; showDialog( context: context, builder: (context) { return Dialog( backgroundColor: const Color(0xFFFDF6E4), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), child: SizedBox( width: 300, child: Column( mainAxisSize: MainAxisSize.min, children: [ // Header Stack( children: [ Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 16), decoration: const BoxDecoration( color: Color(0xFFF79C5E), borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: Center( child: Text( 'ÁREA DE ANOTAÇÕES', style: GoogleFonts.fredoka( fontSize: 18, fontWeight: FontWeight.bold, ), ), ), ), Positioned( top: 2, right: -0, child: Image.asset( 'assets/images/lamp.png', width: 50, height: 50, ), ), ], ), const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Stack( children: [ // Fundo com linhas estilo caderno CustomPaint( size: Size(double.infinity, numLinhas * alturaLinha), // 5 * 35 linhas painter: CadernoPainter(lineCount: numLinhas, lineHeight: alturaLinha), ), // Campo de texto por cima TextField( controller: _noteController, keyboardType: TextInputType.multiline, maxLines: numLinhas, textAlign: TextAlign.center, style: GoogleFonts.fredoka( fontSize: 15, fontWeight: FontWeight.bold, ), decoration: const InputDecoration( hintText: 'REGISTRE AQUI SUAS IDEIAS!', hintStyle: TextStyle(fontWeight: FontWeight.bold), border: InputBorder.none, ), ), ], ), ), const SizedBox(height: 10), // Botões Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ ElevatedButton( onPressed: () => Navigator.pop(context), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFFFB1B1), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), ), child: Text( 'FECHAR', style: GoogleFonts.fredoka( fontSize: 14, fontWeight: FontWeight.bold, ), ), ), ElevatedButton( onPressed: () async { data['notes'] ??= {}; data['notes']['$currentPage'] = _noteController.text; await file.writeAsString(jsonEncode(data)); Navigator.pop(context); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF94A38F), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), ), child: Text( 'SALVAR', style: GoogleFonts.fredoka( fontSize: 14, fontWeight: FontWeight.bold, ), ), ), ], ), ), ], ), ), ); }, ); } List targets = []; GlobalKey lupaKey = GlobalKey(); GlobalKey anotacaoKey = GlobalKey(); GlobalKey rolagemKey = GlobalKey(); void _verificaSeMostraTutorial() async { final prefs = await SharedPreferences.getInstance(); bool jaViu = prefs.getBool('tutorialPdfVisto') ?? false; //bool jaViu = false; setState(() { setaTutorial = !jaViu; }); if (!jaViu) { WidgetsBinding.instance.addPostFrameCallback((_) { _mostrarTutorial(); }); prefs.setBool('tutorialPdfVisto', true); } } void _mostrarTutorial() { final targets = [ // Passo anotacao (sem alterações) TargetFocus( identify: "lupa", keyTarget: lupaKey, contents: [ TargetContent( align: ContentAlign.bottom, child: const Text( "Use a lupa para realizar uma pesquisa detalhada!", style: TextStyle(color: Colors.white, fontSize: 18), ), ), ], ), // Passo anotacao (sem alterações) TargetFocus( identify: "anotacao", keyTarget: anotacaoKey, contents: [ TargetContent( align: ContentAlign.bottom, child: const Text( "Registre as suas anotações aqui!", style: TextStyle(color: Colors.white, fontSize: 18), ), ), ], ), // Passo rolagem TargetFocus( identify: "rolagem", keyTarget: rolagemKey, contents: [ TargetContent( align: ContentAlign.bottom, child: const Text( "Para visualizar o conteúdo, role a página!", style: TextStyle(color: Colors.white, fontSize: 18), ), ), ], ), ]; TutorialCoachMark( targets: targets, colorShadow: Colors.black, textSkip: "Pular", paddingFocus: 10, opacityShadow: 0.8, onFinish: () { print("Tutorial concluído!"); setState(() { setaTutorial = false; }); }, ).show( context: context ); } @override Widget build(BuildContext context) { return Scaffold( appBar: _mostrarControles ? AppBar( backgroundColor: Colors.blueAccent, foregroundColor: Colors.black, title: Text(_courseName), actions: [ IconButton( key: lupaKey, icon: const Icon(Icons.find_in_page), tooltip: 'Ir para página', onPressed: _totalPages > 0 ? _showPageInputDialog : null, ), IconButton( key: anotacaoKey, icon: const Icon(Icons.note_alt_outlined), tooltip: 'Anotações', onPressed: _showAnnotationDialog, ), const SizedBox(width: 8), ], ) : PreferredSize(preferredSize: Size.zero, child: SizedBox.shrink()), body: localPath == null ? const Center(child: CircularProgressIndicator()) : setaTutorial ? Center( child: Image.asset( key: rolagemKey, 'assets/images/seta.png', width: 80, height: 80, ), ) : GestureDetector( onTap: () { setState(() { _mostrarControles = !_mostrarControles; }); }, child: Stack( children: [ PDFView( filePath: localPath!, onRender: (_pages) { setState(() { _totalPages = _pages ?? 0; }); }, onViewCreated: (controller) async { _controller = controller; await Future.delayed(const Duration(milliseconds: 500)); _currentPage = int.parse(await getLastLocation(widget.book_id) ?? "1"); _goToPage(_currentPage); }, onPageChanged: (page, total) { setState(() { _currentPage = page ?? 0; }); }, ), // 🔸 Detector de toque invisível cobrindo tudo Positioned.fill( child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { setState(() { _mostrarControles = !_mostrarControles; }); }, ), ), // 🔸 Controles inferiores (visíveis só se ativo) if (_mostrarControles) Positioned( bottom: 16, left: 16, right: 16, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: Colors.black.withOpacity(0.6), borderRadius: BorderRadius.circular(16), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( icon: const Icon(Icons.chevron_left, color: Colors.white), onPressed: _currentPage > 0 ? () => _goToPage(_currentPage - 1) : null, ), Text( 'Página ${_currentPage + 1} de $_totalPages', style: const TextStyle(color: Colors.white), ), IconButton( icon: const Icon(Icons.chevron_right, color: Colors.white), onPressed: _currentPage < _totalPages - 1 ? () => _goToPage(_currentPage + 1) : null, ), ], ), ), ), ], ) ), ); } } class CadernoPainter extends CustomPainter { final int lineCount; final double lineHeight; CadernoPainter({this.lineCount = 6, this.lineHeight = 45}); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.black26 ..strokeWidth = 1; for (int i = 1; i <= lineCount; i++) { final y = i * lineHeight; canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); } } @override bool shouldRepaint(CustomPainter oldDelegate) => false; }