a version da app estavel e pronto para producao

This commit is contained in:
Gelson-do-Souto 2026-03-09 13:12:37 +01:00
parent f009368bf5
commit 1c9d43f19e
3 changed files with 112 additions and 45 deletions

View File

@ -4,6 +4,7 @@ import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
// Importações originais mantidas
import 'package:kzeduca_app/models/transaction.dart'; import 'package:kzeduca_app/models/transaction.dart';
import 'package:kzeduca_app/widgets/transaction_calculator.dart'; import 'package:kzeduca_app/widgets/transaction_calculator.dart';
import 'package:kzeduca_app/services/i18n_service.dart'; import 'package:kzeduca_app/services/i18n_service.dart';
@ -96,6 +97,10 @@ class _AddTransactionScreenState extends State<AddTransactionScreen> {
void _onAmountChanged(double newAmount) { void _onAmountChanged(double newAmount) {
setState(() { setState(() {
_currentAmount = newAmount; _currentAmount = newAmount;
// UX: Garante que a calculadora esteja visível se o valor for alterado.
if (newAmount > 0.0) {
_showCalculator = true;
}
}); });
} }
@ -133,18 +138,20 @@ class _AddTransactionScreenState extends State<AddTransactionScreen> {
} }
void _saveTransaction() async { void _saveTransaction() async {
// Usamos 'listen: false' para evitar rebuilds desnecessários no método.
final i18n = Provider.of<I18nService>(context, listen: false); final i18n = Provider.of<I18nService>(context, listen: false);
if (_currentAmount <= 0) { if (_currentAmount <= 0) {
_showSnackBar(i18n.t('error_enter_amount'), isError: true); _showSnackBar(i18n.t('error campo de entrada valor monetario'), isError: true);
return; return;
} }
if (_titleController.text.trim().isEmpty) { if (_titleController.text.trim().isEmpty) {
_showSnackBar(i18n.t('error_enter_title'), isError: true); _showSnackBar(i18n.t('erro campo entrada titulo '), isError: true);
return; return;
} }
// A classe Transaction deve vir do seu arquivo 'models/transaction.dart'
final newTransaction = Transaction( final newTransaction = Transaction(
title: _titleController.text.trim(), title: _titleController.text.trim(),
amount: _currentAmount, amount: _currentAmount,
@ -159,10 +166,10 @@ class _AddTransactionScreenState extends State<AddTransactionScreen> {
await txnList.addTransaction(newTransaction); await txnList.addTransaction(newTransaction);
final userStateService = Provider.of<UserStateService>(context, listen: false); final userStateService = Provider.of<UserStateService>(context, listen: false);
await userStateService.recalculateBalanceFromHive(); await userStateService.recalculateBalanceFromHive();
_showSnackBar(i18n.t('transaction_saved_success')); _showSnackBar(i18n.t('transaction salva com success'));
Navigator.pop(context, true); Navigator.pop(context, true);
} catch (e) { } catch (e) {
_showSnackBar('${i18n.t('transaction_save_error')}: $e', isError: true); _showSnackBar('${i18n.t('erro ao salvar transaction')}: $e', isError: true);
} }
} }
@ -171,9 +178,10 @@ class _AddTransactionScreenState extends State<AddTransactionScreen> {
SnackBar( SnackBar(
content: Text( content: Text(
message, message,
style: GoogleFonts.montserrat(color: Colors.white), style: GoogleFonts.montserrat(color: Colors.white, fontWeight: FontWeight.w600),
), ),
backgroundColor: isError ? _kExpenseColor : _kIncomeColor, // UX: Usar a cor de destaque para sucesso e a cor de erro para falha
backgroundColor: isError ? _kExpenseColor.withOpacity(0.9) : _kIncomeColor.withOpacity(0.9),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
), ),
@ -249,23 +257,25 @@ class _AddTransactionScreenState extends State<AddTransactionScreen> {
), ),
), ),
), ),
AnimatedOpacity( // MELHORIA UX: Usar AnimatedSize para transição mais suave da Calculadora
AnimatedSize(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
opacity: _showCalculator ? 1.0 : 0.0, curve: Curves.easeOut,
child: Visibility( child: _showCalculator
visible: _showCalculator, ? Column(
child: Column( children: [
children: [ TransactionCalculator(
TransactionCalculator( // A calculadora é responsável por chamar _onAmountChanged
onAmountChanged: _onAmountChanged, onAmountChanged: _onAmountChanged,
onSave: _saveTransaction, onSave: _saveTransaction,
), ),
const SizedBox(height: 15), const SizedBox(height: 15),
_buildSaveButton(i18n), ],
], )
), : const SizedBox(height: 0), // Oculta a calculadora
),
), ),
// O botão Salvar aparece sempre, exceto se for integrado na calculadora
_buildSaveButton(i18n),
], ],
), ),
), ),
@ -278,6 +288,13 @@ class _AddTransactionScreenState extends State<AddTransactionScreen> {
} }
Widget _buildSaveButton(I18nService i18n) { Widget _buildSaveButton(I18nService i18n) {
// Se a calculadora estiver visível, ela deve ter seu próprio botão de salvar
// A implementação do seu código original a colocava fora do bloco da calculadora, mantive assim para consistência
// No entanto, se o TransactionCalculator tiver um botão "Salvar" interno, este aqui pode ser oculto
// Se você não quiser o botão de salvar duplicado quando a calculadora estiver visível:
// if (_showCalculator) return const SizedBox.shrink();
return ElevatedButton( return ElevatedButton(
onPressed: _saveTransaction, onPressed: _saveTransaction,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -326,9 +343,13 @@ class _AddTransactionScreenState extends State<AddTransactionScreen> {
onTap: () { onTap: () {
setState(() { setState(() {
_selectedType = type; _selectedType = type;
// Garante que a categoria inicial do novo tipo seja selecionada
_selectedCategory = type == TransactionType.expense _selectedCategory = type == TransactionType.expense
? _expenseCategoryNames.keys.first ? _expenseCategoryNames.keys.first
: _incomeCategoryNames.keys.first; : _incomeCategoryNames.keys.first;
// UX: Zera o valor ao mudar o tipo de transação
_currentAmount = 0.0;
_showCalculator = false;
}); });
}, },
child: AnimatedContainer( child: AnimatedContainer(
@ -371,6 +392,7 @@ class _AddTransactionScreenState extends State<AddTransactionScreen> {
Widget _buildAmountDisplay(I18nService i18n) { Widget _buildAmountDisplay(I18nService i18n) {
return GestureDetector( return GestureDetector(
// UX: Clicar no valor alterna a visibilidade da calculadora
onTap: () { onTap: () {
setState(() { setState(() {
_showCalculator = !_showCalculator; _showCalculator = !_showCalculator;
@ -421,11 +443,17 @@ class _AddTransactionScreenState extends State<AddTransactionScreen> {
required IconData icon, required IconData icon,
int maxLines = 1, int maxLines = 1,
}) { }) {
// MELHORIA UX: Cores de texto, ícone e hint ajustadas para o tema escuro.
const Color lightTextColor = Colors.white;
final Color iconColor = lightTextColor.withOpacity(0.7);
final Color hintColor = lightTextColor.withOpacity(0.5);
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1), color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
border: Border.all(color: const Color.fromARGB(255, 20, 19, 19).withOpacity(0.2)), // MELHORIA UX: Borda consistente com o restante da interface
border: Border.all(color: Colors.white.withOpacity(0.2)),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.2), color: Colors.black.withOpacity(0.2),
@ -438,13 +466,16 @@ class _AddTransactionScreenState extends State<AddTransactionScreen> {
controller: controller, controller: controller,
maxLines: maxLines, maxLines: maxLines,
textCapitalization: TextCapitalization.sentences, textCapitalization: TextCapitalization.sentences,
style: GoogleFonts.montserrat(color: const Color.fromARGB(255, 0, 0, 0), fontWeight: FontWeight.w500), // MELHORIA UX: Cor do texto digitado ajustada para branco
style: GoogleFonts.montserrat(color: lightTextColor, fontWeight: FontWeight.w500),
decoration: InputDecoration( decoration: InputDecoration(
labelText: label, labelText: label,
hintText: hint, hintText: hint,
prefixIcon: Icon(icon, color: const Color.fromARGB(255, 15, 14, 14).withOpacity(0.7)), // MELHORIA UX: Cor do ícone ajustada para o tema escuro
labelStyle: GoogleFonts.montserrat(color: const Color.fromARGB(255, 0, 0, 0).withOpacity(0.7)), prefixIcon: Icon(icon, color: iconColor),
hintStyle: GoogleFonts.montserrat(color: const Color.fromARGB(255, 0, 0, 0).withOpacity(0.5)), // MELHORIA UX: Cor do label e hint ajustada para o tema escuro
labelStyle: GoogleFonts.montserrat(color: iconColor),
hintStyle: GoogleFonts.montserrat(color: hintColor),
border: InputBorder.none, border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15), contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
), ),
@ -561,4 +592,4 @@ class _AddTransactionScreenState extends State<AddTransactionScreen> {
), ),
); );
} }
} }

View File

@ -8,11 +8,14 @@ import 'package:provider/provider.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
import 'package:chewie/chewie.dart'; import 'package:chewie/chewie.dart';
import 'dart:async'; import 'dart:async';
import 'dart:io' show Platform;
import '../models/app_models.dart'; import '../models/app_models.dart';
import '../services/i18n_service.dart'; import '../services/i18n_service.dart';
import '../services/toast_service.dart'; import '../services/toast_service.dart';
import '../screens/pdf_viewer_screen.dart'; import '../screens/pdf_viewer_screen.dart';
import 'package:path_provider/path_provider.dart';
class LessonDetailSheet extends StatefulWidget { class LessonDetailSheet extends StatefulWidget {
final QuickLesson lesson; final QuickLesson lesson;
@ -302,9 +305,10 @@ class _LessonDetailSheetState extends State<LessonDetailSheet> with WidgetsBindi
Future<void> _downloadFile(String url, String fileName) async { Future<void> _downloadFile(String url, String fileName) async {
final i18n = Provider.of<I18nService>(context, listen: false); final i18n = Provider.of<I18nService>(context, listen: false);
// 1. Bloqueio de YouTube
if (yt_iframe.YoutubePlayerController.convertUrlToId(url) != null) { if (yt_iframe.YoutubePlayerController.convertUrlToId(url) != null) {
ToastService.show( ToastService.show(
message: 'Não é possível fazer o download de vídeos do YouTube diretamente.', message: i18n.t('Não é possível fazer o download de vídeos do YouTube diretamente.'),
type: ToastType.error, type: ToastType.error,
); );
return; return;
@ -312,50 +316,82 @@ class _LessonDetailSheetState extends State<LessonDetailSheet> with WidgetsBindi
if (url.isEmpty) { if (url.isEmpty) {
ToastService.show( ToastService.show(
message: 'A URL do recurso não está disponível.', message: i18n.t('A URL do recurso não está disponível.'),
type: ToastType.error, type: ToastType.error,
); );
return; return;
} }
if (Theme.of(context).platform == TargetPlatform.android || // 2. Lógica para Dispositivos (Android/iOS)
Theme.of(context).platform == TargetPlatform.iOS) { if (Platform.isAndroid || Platform.isIOS) {
// SOLUÇÃO: Pede permissão e obtém o caminho de salvamento
final status = await Permission.storage.request(); final status = await Permission.storage.request();
if (status.isGranted) { if (status.isGranted) {
await FlutterDownloader.enqueue(
url: url, String? externalStorageDir;
savedDir: '/storage/emulated/0/Download',
fileName: fileName, if (Platform.isAndroid) {
showNotification: true, // Em Android modernos, usamos getExternalStorageDirectory()
openFileFromNotification: true, // ou getDownloadsDirectory() que é específico para downloads no Android SDK 29+.
); final dir = await getDownloadsDirectory();
ToastService.show(message: 'Download iniciado'); externalStorageDir = dir?.path;
} else if (Platform.isIOS) {
// Para iOS, usa o diretório de documentos do aplicativo
final dir = await getApplicationDocumentsDirectory();
externalStorageDir = dir.path;
}
if (externalStorageDir != null) {
try {
await FlutterDownloader.enqueue(
url: url,
savedDir: externalStorageDir, // <--- NOVO CAMINHO CORRETO
fileName: fileName,
showNotification: true,
openFileFromNotification: true,
// saveInPublicStorage: true, // Remova este se causar erro, nem sempre é necessário.
);
ToastService.show(message: i18n.t('Download iniciado'));
} catch (e) {
ToastService.show(
message: i18n.t('Falha ao iniciar o download: $e'),
type: ToastType.error,
);
}
} else {
ToastService.show(
message: i18n.t('Não foi possível encontrar o diretório de download.'),
type: ToastType.error,
);
}
} else { } else {
ToastService.show( ToastService.show(
message: 'Permissão de armazenamento negada', message: i18n.t('Permissão de armazenamento negada'),
type: ToastType.error, type: ToastType.error,
); );
} }
} else { } else {
// (Lógica da Web / Desktop)
try { try {
if (await canLaunchUrl(Uri.parse(url))) { if (await canLaunchUrl(Uri.parse(url))) {
await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
ToastService.show(message: 'Download iniciado no navegador'); ToastService.show(message: i18n.t('Download iniciado no navegador'));
} else { } else {
ToastService.show( ToastService.show(
message: 'Não foi possível iniciar o download. Verifique a URL.', message: i18n.t('Não foi possível iniciar o download. Verifique a URL.'),
type: ToastType.error, type: ToastType.error,
); );
} }
} catch (e) { } catch (e) {
ToastService.show( ToastService.show(
message: 'Ocorreu um erro ao iniciar o download: $e', message: i18n.t('Ocorreu um erro ao iniciar o download: $e'),
type: ToastType.error, type: ToastType.error,
); );
} }
} }
} }
// --- Widgets de UI --- // --- Widgets de UI ---
Widget _buildLessonHeader(I18nService i18n) { Widget _buildLessonHeader(I18nService i18n) {