MakerBook/books_viewer/lib/widgets/pdf_viewer.dart

606 lines
21 KiB
Dart

// 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<int> 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<PdfViewer> {
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<void> _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<void> _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<String, dynamic> 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<TargetFocus> 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;
}