From 1c9d43f19ee93b7e710eb04aabcf124583b8d7ff Mon Sep 17 00:00:00 2001 From: Gelson-do-Souto Date: Mon, 9 Mar 2026 13:12:37 +0100 Subject: [PATCH] a version da app estavel e pronto para producao --- ...kotlin-compiler-6616858073753132730.salive | 0 lib/screens/add_transaction_screen.dart | 85 +++++++++++++------ lib/widgets/lesson_detail_sheet.dart | 72 ++++++++++++---- 3 files changed, 112 insertions(+), 45 deletions(-) create mode 100644 android/.kotlin/sessions/kotlin-compiler-6616858073753132730.salive diff --git a/android/.kotlin/sessions/kotlin-compiler-6616858073753132730.salive b/android/.kotlin/sessions/kotlin-compiler-6616858073753132730.salive new file mode 100644 index 0000000..e69de29 diff --git a/lib/screens/add_transaction_screen.dart b/lib/screens/add_transaction_screen.dart index 2d50d7d..68639e6 100755 --- a/lib/screens/add_transaction_screen.dart +++ b/lib/screens/add_transaction_screen.dart @@ -4,6 +4,7 @@ import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:google_fonts/google_fonts.dart'; +// Importações originais mantidas import 'package:kzeduca_app/models/transaction.dart'; import 'package:kzeduca_app/widgets/transaction_calculator.dart'; import 'package:kzeduca_app/services/i18n_service.dart'; @@ -96,6 +97,10 @@ class _AddTransactionScreenState extends State { void _onAmountChanged(double newAmount) { setState(() { _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 { } void _saveTransaction() async { + // Usamos 'listen: false' para evitar rebuilds desnecessários no método. final i18n = Provider.of(context, listen: false); if (_currentAmount <= 0) { - _showSnackBar(i18n.t('error_enter_amount'), isError: true); + _showSnackBar(i18n.t('error campo de entrada valor monetario'), isError: true); return; } if (_titleController.text.trim().isEmpty) { - _showSnackBar(i18n.t('error_enter_title'), isError: true); + _showSnackBar(i18n.t('erro campo entrada titulo '), isError: true); return; } + // A classe Transaction deve vir do seu arquivo 'models/transaction.dart' final newTransaction = Transaction( title: _titleController.text.trim(), amount: _currentAmount, @@ -159,10 +166,10 @@ class _AddTransactionScreenState extends State { await txnList.addTransaction(newTransaction); final userStateService = Provider.of(context, listen: false); await userStateService.recalculateBalanceFromHive(); - _showSnackBar(i18n.t('transaction_saved_success')); + _showSnackBar(i18n.t('transaction salva com success')); Navigator.pop(context, true); } 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 { SnackBar( content: Text( 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)), behavior: SnackBarBehavior.floating, ), @@ -249,23 +257,25 @@ class _AddTransactionScreenState extends State { ), ), ), - AnimatedOpacity( + // MELHORIA UX: Usar AnimatedSize para transição mais suave da Calculadora + AnimatedSize( duration: const Duration(milliseconds: 300), - opacity: _showCalculator ? 1.0 : 0.0, - child: Visibility( - visible: _showCalculator, - child: Column( - children: [ - TransactionCalculator( - onAmountChanged: _onAmountChanged, - onSave: _saveTransaction, - ), - const SizedBox(height: 15), - _buildSaveButton(i18n), - ], - ), - ), + curve: Curves.easeOut, + child: _showCalculator + ? Column( + children: [ + TransactionCalculator( + // A calculadora é responsável por chamar _onAmountChanged + onAmountChanged: _onAmountChanged, + onSave: _saveTransaction, + ), + const SizedBox(height: 15), + ], + ) + : 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 { } 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( onPressed: _saveTransaction, style: ElevatedButton.styleFrom( @@ -326,9 +343,13 @@ class _AddTransactionScreenState extends State { onTap: () { setState(() { _selectedType = type; + // Garante que a categoria inicial do novo tipo seja selecionada _selectedCategory = type == TransactionType.expense ? _expenseCategoryNames.keys.first : _incomeCategoryNames.keys.first; + // UX: Zera o valor ao mudar o tipo de transação + _currentAmount = 0.0; + _showCalculator = false; }); }, child: AnimatedContainer( @@ -371,6 +392,7 @@ class _AddTransactionScreenState extends State { Widget _buildAmountDisplay(I18nService i18n) { return GestureDetector( + // UX: Clicar no valor alterna a visibilidade da calculadora onTap: () { setState(() { _showCalculator = !_showCalculator; @@ -421,11 +443,17 @@ class _AddTransactionScreenState extends State { required IconData icon, 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( decoration: BoxDecoration( color: Colors.white.withOpacity(0.1), 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( color: Colors.black.withOpacity(0.2), @@ -438,13 +466,16 @@ class _AddTransactionScreenState extends State { controller: controller, maxLines: maxLines, 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( labelText: label, hintText: hint, - prefixIcon: Icon(icon, color: const Color.fromARGB(255, 15, 14, 14).withOpacity(0.7)), - labelStyle: GoogleFonts.montserrat(color: const Color.fromARGB(255, 0, 0, 0).withOpacity(0.7)), - hintStyle: GoogleFonts.montserrat(color: const Color.fromARGB(255, 0, 0, 0).withOpacity(0.5)), + // MELHORIA UX: Cor do ícone ajustada para o tema escuro + prefixIcon: Icon(icon, color: iconColor), + // 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, contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15), ), @@ -561,4 +592,4 @@ class _AddTransactionScreenState extends State { ), ); } -} +} \ No newline at end of file diff --git a/lib/widgets/lesson_detail_sheet.dart b/lib/widgets/lesson_detail_sheet.dart index 22c5099..bdf10c7 100755 --- a/lib/widgets/lesson_detail_sheet.dart +++ b/lib/widgets/lesson_detail_sheet.dart @@ -8,11 +8,14 @@ import 'package:provider/provider.dart'; import 'package:video_player/video_player.dart'; import 'package:chewie/chewie.dart'; import 'dart:async'; +import 'dart:io' show Platform; import '../models/app_models.dart'; import '../services/i18n_service.dart'; import '../services/toast_service.dart'; import '../screens/pdf_viewer_screen.dart'; +import 'package:path_provider/path_provider.dart'; + class LessonDetailSheet extends StatefulWidget { final QuickLesson lesson; @@ -302,9 +305,10 @@ class _LessonDetailSheetState extends State with WidgetsBindi Future _downloadFile(String url, String fileName) async { final i18n = Provider.of(context, listen: false); + // 1. Bloqueio de YouTube if (yt_iframe.YoutubePlayerController.convertUrlToId(url) != null) { 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, ); return; @@ -312,50 +316,82 @@ class _LessonDetailSheetState extends State with WidgetsBindi if (url.isEmpty) { 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, ); return; } - if (Theme.of(context).platform == TargetPlatform.android || - Theme.of(context).platform == TargetPlatform.iOS) { + // 2. Lógica para Dispositivos (Android/iOS) + if (Platform.isAndroid || Platform.isIOS) { + + // SOLUÇÃO: Pede permissão e obtém o caminho de salvamento final status = await Permission.storage.request(); if (status.isGranted) { - await FlutterDownloader.enqueue( - url: url, - savedDir: '/storage/emulated/0/Download', - fileName: fileName, - showNotification: true, - openFileFromNotification: true, - ); - ToastService.show(message: 'Download iniciado'); + + String? externalStorageDir; + + if (Platform.isAndroid) { + // Em Android modernos, usamos getExternalStorageDirectory() + // ou getDownloadsDirectory() que é específico para downloads no Android SDK 29+. + final dir = await getDownloadsDirectory(); + 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 { ToastService.show( - message: 'Permissão de armazenamento negada', + message: i18n.t('Permissão de armazenamento negada'), type: ToastType.error, ); } } else { + // (Lógica da Web / Desktop) try { if (await canLaunchUrl(Uri.parse(url))) { 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 { 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, ); } } catch (e) { 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, ); } } - } - +} // --- Widgets de UI --- Widget _buildLessonHeader(I18nService i18n) {