a version da app estavel e pronto para producao
This commit is contained in:
parent
f009368bf5
commit
1c9d43f19e
|
|
@ -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),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
||||||
|
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(
|
await FlutterDownloader.enqueue(
|
||||||
url: url,
|
url: url,
|
||||||
savedDir: '/storage/emulated/0/Download',
|
savedDir: externalStorageDir, // <--- NOVO CAMINHO CORRETO
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
showNotification: true,
|
showNotification: true,
|
||||||
openFileFromNotification: true,
|
openFileFromNotification: true,
|
||||||
|
// saveInPublicStorage: true, // Remova este se causar erro, nem sempre é necessário.
|
||||||
);
|
);
|
||||||
ToastService.show(message: 'Download iniciado');
|
ToastService.show(message: i18n.t('Download iniciado'));
|
||||||
} else {
|
} catch (e) {
|
||||||
ToastService.show(
|
ToastService.show(
|
||||||
message: 'Permissão de armazenamento negada',
|
message: i18n.t('Falha ao iniciar o download: $e'),
|
||||||
type: ToastType.error,
|
type: ToastType.error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
ToastService.show(
|
||||||
|
message: i18n.t('Não foi possível encontrar o diretório de download.'),
|
||||||
|
type: ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ToastService.show(
|
||||||
|
message: i18n.t('Permissão de armazenamento negada'),
|
||||||
|
type: ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} 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) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue