MakerBook/books_viewer/lib/pages/login_page.dart

363 lines
13 KiB
Dart

// pages/login_page.dart
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
//import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'package:just_audio/just_audio.dart';
import 'package:path_provider/path_provider.dart';
import 'package:MakerBook/pages/epub_page.dart';
import 'package:MakerBook/pages/qr_code_scanner.dart';
import 'package:MakerBook/utils/encrypt.dart';
import 'package:MakerBook/widgets/web_viewer.dart';
import 'package:MakerBook/utils/global.dart';
import '../widgets/animated_button.dart';
import 'pdf_page.dart';
import 'home_page.dart';
import 'package:crypto/crypto.dart';
class LoginPage extends StatefulWidget {
const LoginPage({Key? key}) : super(key: key);
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final TextEditingController _codeController = TextEditingController();
bool loading = false;
Future<void> _login() async {
if (loading) return;
if (_codeController.text.isNotEmpty) {
setState(() => loading = true);
final url = Uri.parse('$apiEndpoint/auth/loginBook');
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'accesskey': _codeController.text}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body)['results'][1][0];
if (data['success'] == 1) {
final ext = data['ext'];
final book_id = data['courseId'];
final courseName = data['courseName'];
final modifiedAt = data['modifiedAt'];
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/$book_id$ext');
await baixarOuAtualizarArquivo(book_id, bookPath, ext, modifiedAt);
//final player = AudioPlayer();
//await player.play(AssetSource('sounds/open_book.mp3'));
final player = AudioPlayer();
await player.setAsset('assets/sounds/open_book.mp3');
await player.play();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ext == ".pdf"
? PdfPage(pdfBytes: file.readAsBytesSync(), courseName: courseName, book_id: book_id,)
: ext == ".epub"
? EpubPage(epubBytes: [], book_id: book_id, courseName: courseName)
: ext == ".web"
? WebViewer(book_id: book_id.toString())
: const LoginPage(),
),
).then((_) async {
final directory = await getTemporaryDirectory();
final tempFile = File('${directory.path}/${book_id}_temp$ext');
if (await tempFile.exists()) {
await tempFile.delete();
}
});
} else {
_showSnackBar('Login falhou');
}
} else {
_showSnackBar('Erro na requisição');
}
} else {
_showSnackBar('Por favor, insira o código de acesso');
}
setState(() => loading = false);
}
Future<void> baixarOuAtualizarArquivo(
int bookId,
String bookPath,
String ext,
String? serverModifiedAtStr,
) async {
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/$bookId$ext');
final jsonFile = File('${directory.path}/$bookId.json');
print("[LOG] Diretorio Json: ${'${directory.path}/$bookId.json'}");
DateTime? serverModifiedAt = DateTime.tryParse(serverModifiedAtStr ?? "");
// Verifica se já existe o JSON com a modifiedAt local
if (await jsonFile.exists()) {
final jsonData = jsonDecode(await jsonFile.readAsString());
final localModifiedAt = DateTime.tryParse(jsonData['modifiedAt'] ?? '');
if (serverModifiedAt != null && localModifiedAt != null) {
if (!serverModifiedAt.isAfter(localModifiedAt)) {
print("[LOG] Arquivo já está atualizado (por modifiedAt).");
return;
} else {
print("[LOG] Arquivo modificado no servidor. Baixando...");
}
}
}
// Baixa o arquivo
final bookUrl = Uri.parse("$bookPath/course_$bookId$ext");
final resBook = await http.get(bookUrl);
if (resBook.statusCode != 200) {
_showSnackBar('[LOG] Falha ao carregar o livro');
return;
}
Uint8List conteudoProtegidoBytes;
if (ext == ".epub") {
String protegido = await protectFile(resBook.bodyBytes, null);
conteudoProtegidoBytes = Uint8List.fromList(utf8.encode(protegido));
} else {
conteudoProtegidoBytes = resBook.bodyBytes;
}
bool arquivoMudou = true;
// Compara com arquivo local, se existir
if (await file.exists()) {
final localBytes = await file.readAsBytes();
final hashNovo = md5.convert(conteudoProtegidoBytes);
final hashLocal = md5.convert(localBytes);
if (hashNovo == hashLocal) {
arquivoMudou = false;
print("[LOG] Hashs iguais. Nada a fazer.");
} else {
print("[LOG] Arquivo realmente mudou. Substituindo.");
}
} else {
print("[LOG] Arquivo ainda não existe. Salvando.");
}
if (arquivoMudou) {
// Salva o arquivo EPUB
await file.writeAsBytes(conteudoProtegidoBytes);
}
// Atualiza o JSON com nova modifiedAt, mantendo o lastLocation se já existir
Map<String, dynamic> newJson = {
"modifiedAt": serverModifiedAtStr,
"lastLocation": null,
};
if (await jsonFile.exists()) {
final conteudo = await jsonFile.readAsString();
print("[LOG] Conteudo Json Original: ${conteudo}");
final oldJson = jsonDecode(conteudo);
if (oldJson['lastLocation'] != null) {
newJson['lastLocation'] = oldJson['lastLocation'];
}
}
await jsonFile.writeAsString(jsonEncode(newJson));
print("[LOG] Arquivo e JSON atualizados.");
}
void _showSnackBar(String msg) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
}
Future<void> _scanQRCode() async {
if (!loading) {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const QRCodeScannerPage(),
),
);
if (result != null) {
setState(() {
_codeController.text = result;
});
}
}
}
@override
Widget build(BuildContext context) {
final alturaTela = MediaQuery.of(context).size.height;
return Scaffold(
body: Stack(
children: [
// Fundo
Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/background.png'),
fit: BoxFit.cover,
),
),
),
SafeArea(
child: Stack(
children: [
// Conteúdo com rolagem e centralização
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: alturaTela * 0.9),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 40),
Image.asset('assets/images/icon.png', width: 160),
const SizedBox(height: 40),
// Campo de código
Container(
width: 300,
padding: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
border: Border.all(color: Colors.lightBlueAccent, width: 2),
borderRadius: BorderRadius.circular(25),
color: Colors.white,
),
child: TextField(
enabled: !loading,
controller: _codeController,
textAlign: TextAlign.center,
style: GoogleFonts.fredoka(
fontSize: 20,
fontWeight: FontWeight.bold,
),
decoration: const InputDecoration(
hintText: 'DIGITE O CÓDIGO DO LIVRO',
border: InputBorder.none,
),
),
),
const SizedBox(height: 20),
// Botão abrir livro com mãozinha
Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
AnimatedButton(
onTap: loading ? () {} : _login,
child: ElevatedButton(
onPressed: _login,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 16),
),
child: Text(
loading ? 'ENTRANDO...' : 'ABRIR LIVRO',
style: GoogleFonts.fredoka(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
Positioned(
right: -30,
bottom: -25,
child: const AnimatedHand(size: 50),
)
],
),
const SizedBox(height: 40),
Text(
'PREFERE USAR A CÂMERA?',
style: GoogleFonts.fredoka(
fontSize: 19,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 20),
// Botão QR
AnimatedButton(
onTap: _scanQRCode,
child: ElevatedButton.icon(
onPressed: _scanQRCode,
icon: const Icon(Icons.qr_code, color: Colors.black),
label: Text(
'ESCANEAR QR CODE',
style: GoogleFonts.fredoka(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
side: const BorderSide(color: Colors.lightBlueAccent, width: 2),
),
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 14),
),
),
),
const SizedBox(height: 30),
if (loading) const CircularProgressIndicator(),
const SizedBox(height: 40),
],
),
),
),
),
// 👉 Botão de voltar por cima
Positioned(
top: 10,
left: 10,
child: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const HomePage()),
);
},
),
),
],
),
),
],
),
);
}
}