diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index ef315c9..94f3dab 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,7 +1,6 @@ plugins { id("com.android.application") id("kotlin-android") - id("com.google.gms.google-services") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") } diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index e4e86fb..ca7fe06 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -21,7 +21,6 @@ plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("com.android.application") version "8.11.1" apply false id("org.jetbrains.kotlin.android") version "2.2.20" apply false - id("com.google.gms.google-services") version "4.4.2" apply false } include(":app") diff --git a/assets/videos/episodio_01.mp4 b/assets/videos/episodio_01.mp4 deleted file mode 100644 index 8f4fa64..0000000 Binary files a/assets/videos/episodio_01.mp4 and /dev/null differ diff --git a/assets/videos/episodio_02.mp4 b/assets/videos/episodio_02.mp4 deleted file mode 100644 index 56ef96e..0000000 Binary files a/assets/videos/episodio_02.mp4 and /dev/null differ diff --git a/assets/videos/episodio_03.mp4 b/assets/videos/episodio_03.mp4 deleted file mode 100644 index dfefd8b..0000000 Binary files a/assets/videos/episodio_03.mp4 and /dev/null differ diff --git a/assets/videos/episodio_04.mp4 b/assets/videos/episodio_04.mp4 deleted file mode 100644 index b411840..0000000 Binary files a/assets/videos/episodio_04.mp4 and /dev/null differ diff --git a/assets/videos/episodio_05.mp4 b/assets/videos/episodio_05.mp4 deleted file mode 100644 index 8490943..0000000 Binary files a/assets/videos/episodio_05.mp4 and /dev/null differ diff --git a/assets/videos/episodio_06.mp4 b/assets/videos/episodio_06.mp4 deleted file mode 100644 index cd95ece..0000000 Binary files a/assets/videos/episodio_06.mp4 and /dev/null differ diff --git a/assets/videos/episodio_07.mp4 b/assets/videos/episodio_07.mp4 deleted file mode 100644 index 76bc8b4..0000000 Binary files a/assets/videos/episodio_07.mp4 and /dev/null differ diff --git a/documentação/01-estrutura-do-projeto.md b/documentação/01-estrutura-do-projeto.md deleted file mode 100644 index 05f0a83..0000000 --- a/documentação/01-estrutura-do-projeto.md +++ /dev/null @@ -1,189 +0,0 @@ -# Estrutura do Projeto - Check Theeth Kids - -## Visão Geral - -O **Check Theeth Kids** é um aplicativo Flutter desenvolvido para ajudar crianças e pais a manterem uma boa saúde bucal através de educação interativa, quizzes e acompanhamento. - -## Estrutura de Pastas - -``` -check_theeth_kids/ -├── lib/ -│ ├── main.dart # Ponto de entrada da aplicação -│ ├── logged_home.dart # Tela principal para usuários logados -│ ├── auth_gate.dart # Gerenciamento de autenticação -│ ├── home_screen.dart # Tela inicial para não logados -│ ├── gates/ -│ │ └── debug_launch_gate.dart # Controle de inicialização -│ ├── quiz/ # Sistema de quiz educativo -│ │ ├── quiz1.dart # Quiz completo com 20 perguntas -│ │ ├── quiz2.dart # (obsoleto - integrado ao quiz1.dart) -│ │ ├── quiz3.dart # (obsoleto - integrado ao quiz1.dart) -│ │ ├── quiz4.dart # (obsoleto - integrado ao quiz1.dart) -│ │ ├── quiz5.dart # (obsoleto - integrado ao quiz1.dart) -│ │ ├── quiz_extended.dart # (obsoleto - integrado ao quiz1.dart) -│ │ ├── quiz_complete.dart # Backup do sistema completo -│ │ ├── quiz_question_screen.dart # Tela genérica de perguntas -│ │ ├── quiz_result.dart # Tela de resultados do quiz -│ │ ├── quiz_prefs.dart # Preferências e configurações -│ │ └── quiz_random.dart # Sistema de quiz aleatório -│ ├── screens/ -│ │ ├── hello_splash_screen.dart # Tela de splash inicial -│ │ ├── curiosidade_screen.dart # Tela de curiosidades -│ │ └── video_screen.dart # Tela de vídeos educativos -│ └── assets/ # Recursos estáticos -│ ├── images/ -│ ├── animations/ -│ └── videos/ -├── documentação/ # Documentação do projeto -├── pubspec.yaml # Dependências e configurações -└── README.md # Documentação geral -``` - -## Arquivos Principais - -### Arquivos de Navegação e Autenticação - -#### `main.dart` -- **Função**: Ponto de entrada da aplicação -- **Responsabilidade**: Inicializa Firebase e define o widget raiz `MyApp` -- **Importância**: Essencial para o funcionamento do app - -#### `auth_gate.dart` -- **Função**: Gerenciamento de estado de autenticação -- **Lógica**: - - Se usuário logado → `LoggedHomeScreen` - - Se não logado → `HomeScreen` -- **Dependências**: Firebase Auth - -#### `gates/debug_launch_gate.dart` -- **Função**: Controle de inicialização e splash screen -- **Responsabilidade**: Gerencia transição entre splash e autenticação - -### Tela Principal do Aplicativo - -#### `logged_home.dart` -- **Função**: Tela principal para usuários autenticados -- **Componentes**: - - AppBar animado com informações do usuário - - Sistema de perfil com upload de fotos - - Gerenciamento de crianças - - Seção de clínicas parceiras - - Biblioteca de vídeos educativos - - Sistema de quiz integrado - - Interface com animações Lottie -- **Estado**: StatefulWidget com múltiplos gerenciadores de estado - -### Sistema de Quiz - -#### `quiz/quiz1.dart` (ARQUIVO PRINCIPAL) -- **Função**: Sistema completo de quiz com 20 perguntas -- **Estrutura**: Contém todas as 20 perguntas em um único arquivo -- **Fluxo**: Quiz1 → Quiz2 → ... → Quiz20 → Resultados -- **Pontuação**: Máximo de 100 pontos (5 pontos por pergunta) - -#### `quiz/quiz_question_screen.dart` -- **Função**: Tela genérica reutilizável para perguntas -- **Componentes**: - - Exibição de perguntas e respostas - - Sistema de navegação (próximo/anterior) - - Feedback visual para respostas - - Controle de pontuação - -#### `quiz/quiz_result.dart` -- **Função**: Tela de resultados com feedback personalizado -- **Recursos**: - - Exibição de pontuação final - - Mensagens motivacionais baseadas no desempenho - - Opção de refazer o quiz - -### Tela de Conteúdo Educativo - -#### `screens/curiosidade_screen.dart` -- **Função**: Exibição de curiosidades sobre saúde bucal -- **Recursos**: Conteúdo educativo com imagens e textos - -#### `screens/video_screen.dart` -- **Função**: Reprodução de vídeos educativos -- **Integração**: YouTube Player para conteúdo em vídeo - -### Tela Inicial - -#### `home_screen.dart` -- **Função**: Tela de boas-vindas para usuários não autenticados -- **Componentes**: Botões de login e cadastro - -#### `screens/hello_splash_screen.dart` -- **Função**: Tela de splash inicial com animações -- **Duração**: Transição automática para tela principal - -## Fluxo da Aplicação - -1. **Inicialização**: `main.dart` → `DebugLaunchGate` -2. **Splash**: `HelloSplashScreen` (2-3 segundos) -3. **Autenticação**: `AuthGate` verifica estado do usuário -4. **Tela Principal**: - - Não logado → `HomeScreen` - - Logado → `LoggedHomeScreen` -5. **Navegação Interna**: - - Quiz → Sistema de 20 perguntas - - Vídeos → `VideoScreen` - - Curiosidades → `CuriosidadeScreen` - - Perfil → Sistema de gerenciamento - -## Dependências Principais - -### Firebase -- **firebase_core**: Configuração base -- **firebase_auth**: Autenticação de usuários -- **cloud_firestore**: Banco de dados -- **firebase_storage**: Armazenamento de imagens - -### UI e Animações -- **flutter/material.dart**: UI Material Design -- **lottie**: Animações vetoriais -- **youtube_player_flutter**: Reprodução de vídeos - -### Utilitários -- **image_picker**: Seleção de imagens da galeria/câmera -- **shared_preferences**: Armazenamento local de preferências - -## Configurações Importantes - -### Firebase -- **Projeto**: `check-theeth-kids-db` -- **Configuração**: Necessário arquivo `google-services.json` (Android) e `GoogleService-Info.plist` (iOS) - -### Assets -- **Imagens**: Configuradas em `pubspec.yaml` -- **Animações**: Arquivos Lottie na pasta `assets/animations/` -- **Vídeos**: Integrados via YouTube Player - -## Estado Atual do Projeto - -### ✅ Funcionalidades Completas -- Sistema de autenticação Firebase -- Tela principal com todas as funcionalidades -- Sistema de quiz com 20 perguntas -- Upload e gerenciamento de fotos de perfil -- Sistema de gerenciamento de crianças -- Biblioteca de vídeos educativos -- Sistema de resultados do quiz - -### ⚠️ Pontos de Atenção -- Configuração Firebase Web requer credenciais específicas -- Algumas dependências podem estar desatualizadas (43 packages com versões mais recentes) -- Sistema de quiz completamente integrado em um único arquivo - -### 🔄 Manutenção -- **Atualização de dependências**: Recomendado revisar packages desatualizados -- **Firebase Web**: Configurar credenciais para plataforma web -- **Testes**: Implementar testes unitários e de integração - -## Próximos Passos Recomendados - -1. **Atualização de Dependências**: Revisar e atualizar packages desatualizados -2. **Configuração Firebase Web**: Adicionar credenciais para plataforma web -3. **Testes Automatizados**: Implementar suíte de testes -4. **Otimização**: Revisar performance e otimizar carregamento -5. **Documentação de API**: Documentar endpoints e estruturas de dados diff --git a/documentação/02-restauracao-logged-home.md b/documentação/02-restauracao-logged-home.md deleted file mode 100644 index e0e5dc1..0000000 --- a/documentação/02-restauracao-logged-home.md +++ /dev/null @@ -1,221 +0,0 @@ -# Restauração do logged_home.dart - Processo Completo - -## Contexto Inicial - -O arquivo `logged_home.dart` sofreu corrupção durante tentativas de correção de erros de lint, resultando em perda de funcionalidades críticas. O usuário solicitou a restauração completa do arquivo para seu estado original, mantendo toda a funcionalidade e interface original. - -## Problema Identificado - -### Sintomas -- **Corrupção do arquivo**: Estrutura do código comprometida -- **Perda de funcionalidades**: Componentes principais ausentes -- **Erros de compilação**: Múltiplos erros de sintaxe e estrutura -- **Interface quebrada**: UI não correspondente ao design original - -### Causa Raiz -Tentativas de correção de erros de lint (`use_build_context_synchronously` e `unnecessary_underscores`) resultaram em modificações indevidas que comprometeram a estrutura do arquivo. - -## Processo de Restauração - -### Etapa 1: Backup e Análise -1. **Backup do arquivo corrompido**: Criado `logged_home.dart.backup` -2. **Análise do código original**: Identificação da estrutura completa fornecida pelo usuário -3. **Mapeamento de funcionalidades**: Lista de todos os componentes e recursos - -### Etapa 2: Restauração Completa -O arquivo foi completamente restaurado com as seguintes funcionalidades: - -#### Interface Principal -- **AppBar Animado**: Com informações do usuário e pontuação do quiz -- **BottomNavigationBar**: Navegação entre Home, Perfil, Configurações -- **Sistema de Abas**: Organização em múltiplas seções - -#### Sistema de Perfil -- **Gerenciamento de Foto de Perfil**: - - Upload via galeria ou câmera - - Armazenamento no Firebase Storage - - Exibição com tratamento de erros - - Sistema de loading durante upload - -#### Gerenciamento de Crianças -- **Cadastro de Crianças**: - - Formulário completo com nome, idade, e informações adicionais - - Validação de dados - - Sistema de loading e feedback - - Diálogos de confirmação - -- **Seleção de Criança Ativa**: - - Interface de seleção visual - - Persistência da seleção - - Atualização dinâmica da interface - -#### Sistema de Quiz -- **Integração Completa**: - - Acesso direto ao sistema de quiz - - Exibição de pontuações anteriores - - Histórico de resultados - - Botões de acesso rápido - -#### Biblioteca de Conteúdo -- **Seção de Vídeos**: - - Lista de vídeos educativos - - Player integrado - - Categorias organizadas - -- **Seção de Curiosidades**: - - Conteúdo educativo sobre saúde bucal - - Interface com imagens e textos explicativos - -#### Sistema de Clínicas -- **Clínicas Parceiras**: - - Lista de clínicas parceiras - - Informações de contato - - Sistema de localização - -### Etapa 3: Correção de Erros de Lint - -Após a restauração, foram identificados e corrigidos os seguintes erros: - -#### Erros de BuildContext -- **Problema**: `use_build_context_synchronously` -- **Causa**: Uso de `context` após operações assíncronas -- **Solução**: - - Adição de verificações `mounted` antes do uso do context - - Comentários `// ignore: use_build_context_synchronously` onde necessário - - Armazenamento do context em variáveis locais antes de operações assíncronas - -#### Erros de Underscores -- **Problema**: `unnecessary_underscores` -- **Causa**: Uso de `_`, `__`, `___` em parâmetros não utilizados -- **Solução**: Substituição por nomes descritivos como `context`, `error`, `stackTrace` - -#### Erros de Estrutura -- **Problema**: `curly_braces_in_flow_control_structures` -- **Causa**: Ausência de chaves em blocos if/else -- **Solução**: Adição de chaves em todas as estruturas de controle - -## Estrutura Final do Arquivo - -### Imports Principais -```dart -import 'dart:async'; -import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_storage/firebase_storage.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:lottie/lottie.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'quiz/quiz1.dart'; -import 'quiz/quiz_prefs.dart'; -import 'screens/curiosidade_screen.dart'; -import 'screens/video_screen.dart'; -``` - -### Classes Principais - -#### `LoggedHomeScreen` -- **Tipo**: StatefulWidget -- **Função**: Tela principal do aplicativo -- **Estado**: Gerencia múltiplos estados (perfil, crianças, quiz, etc.) - -#### `_HomeTabState`, `_PerfilTabState`, `_ConfigTabState` -- **Função**: Gerenciamento individual de cada aba -- **Estado**: Cada aba tem seu próprio estado e lógica - -#### `_AddChildSheet` -- **Função**: Modal para adicionar novas crianças -- **Componentes**: Formulário completo com validação - -### Funcionalidades Implementadas - -#### 1. Sistema de Autenticação -- Verificação de usuário logado -- Logout com confirmação -- Redirecionamento automático - -#### 2. Sistema de Perfil -- Upload de foto de perfil -- Exibição de informações do usuário -- Edição de dados pessoais - -#### 3. Sistema de Crianças -- Cadastro de múltiplas crianças -- Seleção de criança ativa -- Edição e exclusão de registros - -#### 4. Sistema de Quiz -- Acesso direto ao quiz -- Exibição de resultados anteriores -- Histórico completo - -#### 5. Biblioteca de Conteúdo -- Acesso a vídeos educativos -- Seção de curiosidades -- Conteúdo organizado por categorias - -## Resolução de Problemas Técnicos - -### Firebase Integration -- **Firestore**: Configuração correta de coleções e documentos -- **Storage**: Sistema de upload e recuperação de imagens -- **Auth**: Gerenciamento de sessão e autenticação - -### Tratamento de Erros -- **Try-catch blocks**: Em todas as operações assíncronas -- **Feedback visual**: Snackbars e diálogos informativos -- **Loading states**: Indicadores visuais durante operações - -### Performance -- **Lazy loading**: Carregamento sob demanda de imagens -- **Caching**: Armazenamento local de preferências -- **Optimized rebuilds**: Uso eficiente de setState - -## Validação Final - -### Testes Realizados -1. **Compilação**: `flutter analyze` sem erros -2. **Funcionalidade**: Todas as features originais restauradas -3. **Interface**: UI correspondente ao design original -4. **Performance**: Tempo de carregamento aceitável - -### Resultados Obtidos -- ✅ **100% das funcionalidades originais restauradas** -- ✅ **Interface idêntica à versão original** -- ✅ **Zero erros de lint** -- ✅ **Performance otimizada** -- ✅ **Código limpo e documentado** - -## Lições Aprendidas - -### Boas Práticas -1. **Backup antes de modificações**: Sempre criar backup antes de alterações significativas -2. **Testes incrementais**: Validar cada mudança antes de prosseguir -3. **Documentação**: Manter documentação atualizada das funcionalidades - -### Evitar Problemas Futuros -1. **Não modificar estrutura existente**: A menos que seja absolutamente necessário -2. **Uso cuidadoso de ferramentas automáticas**: Verificar resultados de correções automáticas -3. **Testes completos**: Validar todas as funcionalidades após modificações - -## Arquivos Relacionados - -### Principais -- `lib/logged_home.dart` - Arquivo principal restaurado -- `lib/logged_home.dart.backup` - Backup do estado corrompido - -### Dependências -- `lib/quiz/quiz1.dart` - Sistema de quiz -- `lib/screens/curiosidade_screen.dart` - Tela de curiosidades -- `lib/screens/video_screen.dart` - Tela de vídeos - -### Configuração -- `pubspec.yaml` - Dependências do projeto -- Firebase configuration files - -## Conclusão - -A restauração do `logged_home.dart` foi um sucesso completo, recuperando 100% da funcionalidade original enquanto corrigia os problemas de lint que motivaram as modificações iniciais. O arquivo agora está estável, funcional e pronto para uso em produção. - -O processo demonstrou a importância de backups cuidadosos e validação incremental durante modificações de código crítico. diff --git a/documentação/03-expansao-quiz-20-perguntas.md b/documentação/03-expansao-quiz-20-perguntas.md deleted file mode 100644 index d639fd8..0000000 --- a/documentação/03-expansao-quiz-20-perguntas.md +++ /dev/null @@ -1,298 +0,0 @@ -# Expansão do Quiz para 20 Perguntas - Documentação Completa - -## Visão Geral - -O sistema de quiz do aplicativo foi expandido de 5 para 20 perguntas completas, reorganizando a estrutura existente para proporcionar uma experiência educativa mais abrangente sobre saúde bucal infantil. - -## Estrutura Anterior vs Nova - -### Sistema Original (5 perguntas) -``` -Quiz 1/5 → Quiz 2/5 → Quiz 3/5 → Quiz 4/5 → Quiz 5/5 → Resultados -``` -- **Arquivos**: `quiz1.dart`, `quiz2.dart`, `quiz3.dart`, `quiz4.dart`, `quiz5.dart` -- **Pontuação**: Máximo 25 pontos (5 pontos por pergunta) -- **Tópicos**: Básicos de higiene bucal - -### Sistema Expandido (20 perguntas) -``` -Quiz 1/20 → Quiz 2/20 → ... → Quiz 20/20 → Resultados -``` -- **Arquivo**: `quiz1.dart` (consolidado) -- **Pontuação**: Máximo 100 pontos (5 pontos por pergunta) -- **Tópicos**: Abrangentes (avançados → básicos) - -## Processo de Expansão - -### Etapa 1: Análise da Estrutura Existente - -#### Arquivos Identificados -- `quiz1.dart` - `quiz5.dart`: Perguntas básicas -- `quiz_extended.dart`: Perguntas 6-20 -- `quiz_question_screen.dart`: Tela genérica de perguntas -- `quiz_result.dart`: Tela de resultados - -#### Problema Identificado -- **Fragmentação**: Múltiplos arquivos para perguntas relacionadas -- **Fluxo confuso**: Dois sistemas separados (básico + extendido) -- **Manutenção complexa**: Dificuldade em gerenciar conteúdo disperso - -### Etapa 2: Reorganização do Conteúdo - -#### Nova Sequência Lógica -1. **Quiz 1-15**: Tópicos avançados (antigas perguntas 6-20) -2. **Quiz 16-20**: Tópicos básicos (antigas perguntas 1-5) - -#### Justificativa da Reorganização -- **Progressão educativa**: Começa com tópicos mais complexos e específicos -- **Engajamento**: Conteúdo mais interessante no início -- **Retenção**: Informações básicas no final reforçam aprendizado - -### Etapa 3: Consolidação do Código - -#### Estrutura Final -```dart -class Quiz1Screen extends StatelessWidget { ... } -class Quiz2Screen extends StatelessWidget { ... } -... -class Quiz20Screen extends StatelessWidget { ... } -``` - -#### Benefícios da Consolidação -- **Manutenção simplificada**: Um único arquivo para todo o sistema -- **Fluxo unificado**: Navegação contínua sem quebras -- **Performance**: Redução de imports e carregamento - -## Conteúdo das Perguntas - -### Quiz 1-15: Tópicos Avançados - -#### Quiz 1: Tipos de Escova -- **Pergunta**: "Qual tipo de escova é mais recomendada para crianças?" -- **Respostas**: Escova macia, escova dura, escova elétrica -- **Foco**: Equipamentos adequados para crianças - -#### Quiz 2: Alimentos Prejudiciais -- **Pergunta**: "Qual alimento é mais prejudicial para os dentes?" -- **Respostas**: Balas/chicletes, frutas, vegetais -- **Foco**: Nutrição e saúde bucal - -#### Quiz 3: Primeira Visita ao Dentista -- **Pergunta**: "Qual a idade ideal para a primeira visita ao dentista?" -- **Respostas**: 1 ano, 6 anos, só com dor -- **Foco**: Prevenção e cuidado precoce - -#### Quiz 4: Frequência de Fio Dental -- **Pergunta**: "Com que frequência crianças devem usar fio dental?" -- **Respostas**: Diariamente, só se juntos, semanalmente -- **Foco**: Higiene completa - -#### Quiz 5: Segurança do Flúor -- **Pergunta**: "O flúor é seguro para crianças?" -- **Respostas**: Sim (quantidade correta), não, só após 12 anos -- **Foco**: Prevenção de cáries - -#### Quiz 6-15: Tópicos Especializados -- Chupetas e mamadeiras -- Bebidas e dentição -- Hábitos noturnos -- Traumatismos dentários -- Selantes dentários -- Aparelhos ortodônticos -- Respiração bucal -- Saúde gengival -- Lanches escolares -- Medo do dentista - -### Quiz 16-20: Tópicos Básicos - -#### Quiz 16: Tempo de Escovação -- **Pergunta**: "Qual é o tempo ideal para escovar os dentes?" -- **Respostas**: 2 minutos, 30 segundos, 5 minutos -- **Foco**: Fundamentos da higiene - -#### Quiz 17: Troca da Escova -- **Pergunta**: "Quando devo trocar a escova de dentes?" -- **Respostas**: 3 meses, só quebrar, mensalmente -- **Foco**: Manutenção de equipamentos - -#### Quiz 18: Quantidade de Pasta -- **Pergunta**: "Qual a quantidade ideal de pasta de dente para crianças?" -- **Respostas**: Grão de arroz/ervilha, cobrir escova, sem pasta -- **Foco**: Dosagem correta - -#### Quiz 19: Horário do Fio Dental -- **Pergunta**: "Qual é o melhor horário para usar fio dental?" -- **Respostas**: Diário (geralmente noite), só preso, após refeições -- **Foco**: Rotina de higiene - -#### Quiz 20: Prevenção de Cáries -- **Pergunta**: "O que ajuda mais a prevenir cáries no dia a dia?" -- **Respostas**: Escovar+flúor+reduzir açúcar, só enxaguante, evitar dentista -- **Foco**: Prevenção integrada - -## Implementação Técnica - -### Arquivo Principal: `quiz1.dart` - -#### Estrutura Completa -```dart -import 'package:flutter/material.dart'; -import 'quiz_question_screen.dart'; -import 'quiz_result.dart'; - -// Quiz 1: Tipos de Escova -class Quiz1Screen extends StatelessWidget { - const Quiz1Screen({super.key, this.currentScore = 0, this.scopeId}); - - final int currentScore; - final String? scopeId; - - @override - Widget build(BuildContext context) { - return QuizQuestionScreen( - title: 'Quiz 1/20', - question: 'Qual tipo de escova é mais recomendada para crianças?', - answers: const [ - QuizAnswer(title: '...', description: '...', weight: 2), - QuizAnswer(title: '...', description: '...', weight: 5), - QuizAnswer(title: '...', description: '...', weight: 3), - ], - currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( - builder: (_) => Quiz2Screen(currentScore: nextScore, scopeId: scopeId), - ), - showBackButton: false, - ); - } -} - -// Quiz 2-20: Estrutura similar... -``` - -### Sistema de Navegação - -#### Fluxo Contínuo -- **Quiz 1**: Sem botão "voltar" (início) -- **Quiz 2-19**: Com botão "voltar" (navegação livre) -- **Quiz 20**: Com botão "voltar" e marcação "final" - -#### Sistema de Pontuação -- **Cálculo**: 5 pontos por pergunta × 20 perguntas = 100 pontos -- **Pesos**: Resposta correta (2 pontos), parcialmente correta (3 pontos), incorreta (5 pontos) -- **Feedback**: Mensagens baseadas na pontuação final - -### Integração com o Sistema Principal - -#### Acesso via logged_home.dart -```dart -Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const Quiz1Screen(), - ), -); -``` - -#### Resultados e Persistência -- **Firebase Firestore**: Armazenamento de resultados -- **Shared Preferences**: Cache local de pontuações -- **Histórico**: Registro de tentativas anteriores - -## Arquivos Modificados e Criados - -### Arquivos Principais -- `lib/quiz/quiz1.dart` - **MODIFICADO**: Sistema completo com 20 perguntas -- `lib/quiz/quiz_complete.dart` - **CRIADO**: Backup do sistema completo - -### Arquivos Obsoletos -- `lib/quiz/quiz2.dart` - **INTEGRADO**: Conteúdo movido para quiz1.dart -- `lib/quiz/quiz3.dart` - **INTEGRADO**: Conteúdo movido para quiz1.dart -- `lib/quiz/quiz4.dart` - **INTEGRADO**: Conteúdo movido para quiz1.dart -- `lib/quiz/quiz5.dart` - **INTEGRADO**: Conteúdo movido para quiz1.dart -- `lib/quiz/quiz_extended.dart` - **INTEGRADO**: Conteúdo movido para quiz1.dart - -### Arquivos Mantidos -- `lib/quiz/quiz_question_screen.dart` - Tela genérica de perguntas -- `lib/quiz/quiz_result.dart` - Tela de resultados -- `lib/quiz/quiz_prefs.dart` - Preferências e configurações -- `lib/quiz/quiz_random.dart` - Sistema de quiz aleatório - -## Benefícios da Expansão - -### Educacionais -- **Conteúdo abrangente**: Cobertura completa de saúde bucal infantil -- **Progressão lógica**: Do complexo ao básico para melhor retenção -- **Diversidade de tópicos**: Desde equipamentos até psicologia - -### Técnicos -- **Manutenção simplificada**: Um único arquivo para todo o conteúdo -- **Performance otimizada**: Redução de imports e carregamento -- **Fluxo unificado**: Experiência contínua sem interrupções - -###用户体验 -- **Engajamento aumentado**: Mais conteúdo para explorar -- **Retenção melhorada**: Reforço de conceitos básicos no final -- **Satisfação**: Sensação de progresso com 20 perguntas - -## Validação e Testes - -### Testes Realizados -1. **Compilação**: `flutter analyze` sem erros -2. **Fluxo completo**: Navegação Quiz 1→20→Resultados -3. **Pontuação**: Sistema correto de 100 pontos -4. **Interface**: Todas as telas funcionando corretamente - -### Resultados Obtidos -- ✅ **20 perguntas funcionais** -- ✅ **Fluxo contínuo sem quebras** -- ✅ **Sistema de pontuação correto** -- ✅ **Interface responsiva** -- ✅ **Zero erros de compilação** - -## Desempenho e Otimização - -### Métricas -- **Tempo de carregamento**: < 2 segundos para primeira pergunta -- **Memória**: Uso otimizado com carregamento sob demanda -- **Navegação**: Transições suaves entre perguntas - -### Otimizações Implementadas -- **Lazy loading**: Carregamento de conteúdo apenas quando necessário -- **Cache local**: Armazenamento de preferências e resultados -- **Efficient rebuilds**: Uso otimizado de StatefulWidget - -## Manutenção Futura - -### Adição de Novas Perguntas -- **Localização**: Adicionar novas classes no final do arquivo `quiz1.dart` -- **Numeração**: Continuar sequência (Quiz21, Quiz22, etc.) -- **Integração**: Atualizar sistema de navegação e pontuação - -### Atualização de Conteúdo -- **Edição simples**: Modificar diretamente as perguntas existentes -- **Validação**: Testar fluxo completo após modificações -- **Documentação**: Manter registro das alterações - -### Expansão de Funcionalidades -- **Categorias**: Possível implementação de categorias de perguntas -- **Dificuldade**: Sistema de níveis de dificuldade -- **Personalização**: Quiz adaptativo baseado no perfil do usuário - -## Impacto no Sistema - -### Mudanças Necessárias -- **Interface**: Atualização de indicadores de progresso (5→20) -- **Resultados**: Ajuste de sistema de pontuação (25→100 pontos) -- **Histórico**: Modificação de estrutura de armazenamento - -### Compatibilidade -- **Backward compatibility**: Mantidos sistemas antigos como backup -- **Gradual migration**: Possível retorno ao sistema anterior se necessário -- **Data migration**: Sistema de conversão de resultados antigos - -## Conclusão - -A expansão do quiz para 20 perguntas representa um avanço significativo na capacidade educacional do aplicativo. A reorganização do conteúdo proporciona uma experiência mais coesa e abrangente, enquanto a consolidação do código simplifica a manutenção e melhora o desempenho. - -O novo sistema está pronto para uso em produção e oferece uma base sólida para futuras expansões e melhorias. diff --git a/documentação/04-correcoes-lint-erros.md b/documentação/04-correcoes-lint-erros.md deleted file mode 100644 index cf11b47..0000000 --- a/documentação/04-correcoes-lint-erros.md +++ /dev/null @@ -1,312 +0,0 @@ -# Correções de Lint e Erros - Documentação Completa - -## Visão Geral - -Durante o processo de desenvolvimento e restauração do projeto, diversos erros de lint e compilação foram identificados e corrigidos. Este documento detalha todos os problemas encontrados e as soluções implementadas. - -## Erros de Lint Principais - -### 1. `use_build_context_synchronously` - -#### Descrição do Problema -O erro ocorre quando `BuildContext` é usado após uma operação assíncrona sem verificação adequada se o widget ainda está montado. - -#### Causa -```dart -// Código problemático -final result = await showDialog(...); -ScaffoldMessenger.of(context).showSnackBar(...); // Context pode ser inválido -``` - -#### Soluções Implementadas - -##### Solução 1: Verificação `mounted` -```dart -final result = await showDialog(...); -if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(...); -} -``` - -##### Solução 2: Armazenamento do Context -```dart -final dialogContext = context; -final result = await showDialog(...); -ScaffoldMessenger.of(dialogContext).showSnackBar(...); -``` - -##### Solução 3: Comentário Ignore (casos especiais) -```dart -// ignore: use_build_context_synchronously -final result = await showDialog(...); -``` - -#### Arquivos Corrigidos -- `lib/logged_home.dart` - Linhas 1055, 1077, 1155, 1180 - -### 2. `unnecessary_underscores` - -#### Descrição do Problema -Uso de underscores (`_`, `__`, `___`) em parâmetros que poderiam ter nomes descritivos. - -#### Causa -```dart -// Código problemático -errorBuilder: (context, _, __) => Icon(Icons.error), -``` - -#### Solução Implementada -```dart -// Código corrigido -errorBuilder: (context, error, stackTrace) => Icon(Icons.error), -``` - -#### Arquivos Corrigidos -- `lib/logged_home.dart` - Linha 902 - -### 3. `curly_braces_in_flow_control_structures` - -#### Descrição do Problema -Ausência de chaves em estruturas de controle que contêm apenas uma instrução. - -#### Causa -```dart -// Código problemático -if (condition) - return something; -``` - -#### Solução Implementada -```dart -// Código corrigido -if (condition) { - return something; -} -``` - -#### Arquivos Corrigidos -- `lib/logged_home.dart` - Linhas 1551, 1686 - -## Erros de Compilação - -### 1. Firebase Configuration - -#### Problema -``` -FirebaseOptions cannot be null when creating the default app. -``` - -#### Causa -Configuração do Firebase incompleta para plataforma web. - -#### Solução -- **Android**: Adicionar `google-services.json` em `android/app/` -- **iOS**: Adicionar `GoogleService-Info.plist` em `ios/Runner/` -- **Web**: Configurar credenciais no `index.html` - -#### Status -- ⚠️ **Parcialmente resolvido**: Android/iOS funcionam, web precisa configuração - -### 2. Import Errors - -#### Problema -``` -Unused import: 'quiz_extended.dart' -``` - -#### Causa -Imports de arquivos que foram consolidados ou removidos. - -#### Solução -Remover imports não utilizados: -```dart -// Removido -import 'quiz_extended.dart'; - -// Mantido apenas o necessário -import 'quiz1.dart'; -``` - -### 3. Class/Function Not Found - -#### Problema -``` -The method 'QuizExtendedScreen' isn't defined -``` - -#### Causa -Referência a classes que foram renomeadas ou movidas. - -#### Solução -Atualizar referências: -```dart -// Antigo -QuizExtendedScreen(currentScore: nextScore, scopeId: scopeId) - -// Novo -Quiz7Screen(currentScore: nextScore, scopeId: scopeId) -``` - -## Processo de Correção - -### Etapa 1: Identificação -```bash -flutter analyze -``` - -#### Resultados Típicos -``` -info - Don't use 'BuildContext's across async gaps - lib\logged_home.dart:1055:9 -info - Unnecessary underscores in parameter names - lib\logged_home.dart:902:9 -info - Curly braces in flow control structures - lib\logged_home.dart:1551:9 -``` - -### Etapa 2: Priorização -1. **Alta prioridade**: Erros que impedem compilação -2. **Média prioridade**: Warnings de lint -3. **Baixa prioridade**: Sugestões de estilo - -### Etapa 3: Correção Sistemática - -#### Para `use_build_context_synchronously` -1. Identificar todos os usos de context após `await` -2. Adicionar verificação `mounted` antes do uso -3. Testar o fluxo completo -4. Adicionar `// ignore` apenas se necessário - -#### Para `unnecessary_underscores` -1. Encontrar parâmetros com underscores -2. Substituir por nomes descritivos -3. Verificar se o parâmetro é realmente usado -4. Remover se não utilizado - -#### Para `curly_braces_in_flow_control_structures` -1. Localizar estruturas if/else sem chaves -2. Adicionar chaves em todos os casos -3. Manter consistência no estilo - -### Etapa 4: Validação -```bash -flutter analyze -flutter run --dry-run -``` - -## Padrões de Correção Estabelecidos - -### 1. BuildContext Seguro -```dart -// Padrão estabelecido -if (!mounted) return; -final result = await someAsyncOperation(); -if (!mounted) return; -ScaffoldMessenger.of(context).showSnackBar(...); -``` - -### 2. Nomenclatura Descritiva -```dart -// Padrão estabelecido -errorBuilder: (context, error, stackTrace) => ..., // ✅ -errorBuilder: (context, _, __) => ..., // ❌ -``` - -### 3. Estruturas de Controle -```dart -// Padrão estabelecido -if (condition) { - return value; -} -``` - -## Ferramentas Utilizadas - -### 1. Flutter Analyzer -```bash -flutter analyze -flutter analyze --fatal-infos -``` - -### 2. Formatação Automática -```bash -dart format . -dart format --set-exit-if-changed . -``` - -### 3. Verificação de Dependências -```bash -flutter pub deps -flutter pub outdated -``` - -## Boas Práticas Implementadas - -### 1. Verificação `mounted` -Sempre verificar se o widget está montado antes de usar context após operações assíncronas. - -### 2. Nomenclatura Descritiva -Usar nomes descritivos para parâmetros, evitando underscores não necessários. - -### 3. Estrutura Consistente -Manter chaves em todas as estruturas de controle para consistência. - -### 4. Imports Limpes -Remover imports não utilizados e organizar imports em grupos. - -## Problemas Recorrentes - -### 1. BuildContext em Operações Assíncronas -**Solução**: Sempre usar verificação `mounted` ou armazenar context antes da operação. - -### 2. Parâmetros Não Utilizados -**Solução**: Usar `_` para parâmetros realmente não utilizados ou nomes descritivos. - -### 3. Estruturas de Controle -**Solução**: Manter chaves em todas as estruturas para consistência e futuras manutenções. - -## Validação Final - -### Testes Realizados -1. **Análise estática**: `flutter analyze` sem erros -2. **Compilação**: `flutter run` bem-sucedido -3. **Funcionalidade**: Todas as features funcionando -4. **Performance**: Sem degradação de performance - -### Resultados Obtidos -- ✅ **Zero erros de lint** -- ✅ **Zero erros de compilação** -- ✅ **Código limpo e consistente** -- ✅ **Performance mantida** -- ✅ **Funcionalidade preservada** - -## Lições Aprendidas - -### 1. Prevenção é Melhor que Correção -- Usar verificação `mounted` desde o início -- Adotar nomenclatura descritiva sempre -- Manter estrutura consistente - -### 2. Validação Incremental -- Executar `flutter analyze` após cada mudança significativa -- Testar funcionalidades imediatamente após correções -- Manter histórico de alterações - -### 3. Documentação de Padrões -- Documentar padrões de correção -- Criar guias de estilo -- Manter exemplos de código correto - -## Referências - -### Documentação Flutter -- [Flutter Lint Rules](https://dart.dev/guides/language/analysis-options) -- [BuildContext Best Practices](https://api.flutter.dev/flutter/widgets/BuildContext-class.html) - -### Ferramentas Recomendadas -- **Flutter Analyzer**: Análise estática -- **Dart Format**: Formatação de código -- **IDE Extensions**: Suporte para lint em tempo real - -## Conclusão - -O processo de correção de lint e erros foi fundamental para garantir a estabilidade e qualidade do código. A implementação de padrões consistentes e a validação sistemática resultaram em um código limpo, funcional e maintainable. - -As correções não apenas resolveram os problemas imediatos, mas também estabeleceram bases sólidas para desenvolvimento futuro, prevenindo recorrência dos mesmos problemas. diff --git a/documentação/05-dependências-configuracoes.md b/documentação/05-dependências-configuracoes.md deleted file mode 100644 index 55c39cf..0000000 --- a/documentação/05-dependências-configuracoes.md +++ /dev/null @@ -1,62 +0,0 @@ -# Dependências e Configurações - -## Requisitos -- **Flutter**: 3.38.8 (Stable) -- **Plataformas**: Android API 21+, iOS 11.0+, Web, Windows 10+ - -## pubspec.yaml Principal -```yaml -dependencies: - flutter: - sdk: flutter - firebase_core: ^3.15.2 - firebase_auth: ^5.7.0 - cloud_firestore: ^5.6.12 - firebase_storage: ^12.4.10 - lottie: ^3.3.2 - youtube_player_flutter: ^8.1.2 - image_picker: ^1.2.1 - shared_preferences: ^2.5.4 -``` - -## Firebase -- **Projeto**: `check-theeth-kids-db` -- **Android**: `google-services.json` em `android/app/` -- **iOS**: `GoogleService-Info.plist` em `ios/Runner/` -- **Web**: Configuração em `index.html` - -## Assets Configurados -``` -assets/ -├── images/ -├── animations/ -├── videos/ -└── icons/ -``` - -## Permissões Android -```xml - - - -``` - -## Permissões iOS -```xml -NSCameraUsageDescription -Este app precisa acessar a câmera para fotos de perfil -``` - -## Scripts de Desenvolvimento -```bash -flutter clean && flutter pub get -flutter analyze -flutter test -flutter build apk --release -``` - -## Status Atual -- ✅ Dependências atualizadas -- ✅ Firebase configurado (Android/iOS) -- ⚠️ Web precisa credenciais -- ✅ Assets configurados diff --git a/documentação/06-guia-desenvolvimento-manutencao.md b/documentação/06-guia-desenvolvimento-manutencao.md deleted file mode 100644 index 9e21ee3..0000000 --- a/documentação/06-guia-desenvolvimento-manutencao.md +++ /dev/null @@ -1,356 +0,0 @@ -# Guia de Desenvolvimento e Manutenção - -## Setup do Ambiente - -### 1. Pré-requisitos -```bash -# Instalar Flutter -flutter doctor - -# Verificar ambiente -flutter devices -``` - -### 2. Clonar e Configurar -```bash -git clone -cd check_theeth_kids -flutter pub get -``` - -### 3. Configurar Firebase -- Baixar arquivos de configuração do console Firebase -- Adicionar `google-services.json` (Android) e `GoogleService-Info.plist` (iOS) - -## Fluxo de Desenvolvimento - -### Branches -- `main`: Produção -- `develop`: Desenvolvimento -- `feature/*`: Novas funcionalidades -- `bugfix/*`: Correções de bugs - -### Comandos Diários -```bash -# Limpar e atualizar -flutter clean && flutter pub get - -# Verificar código -flutter analyze -dart format . - -# Rodar testes -flutter test - -# Build para teste -flutter build apk --debug -``` - -## Padrões de Código - -### 1. BuildContext Seguro -```dart -// ✅ Correto -if (!mounted) return; -final result = await someAsyncOperation(); -if (!mounted) return; -ScaffoldMessenger.of(context).showSnackBar(...); - -// ❌ Incorreto -final result = await someAsyncOperation(); -ScaffoldMessenger.of(context).showSnackBar(...); // Pode causar erro -``` - -### 2. Nomenclatura -```dart -// ✅ Descritivo -errorBuilder: (context, error, stackTrace) => ... - -// ❌ Underscores desnecessários -errorBuilder: (context, _, __) => ... -``` - -### 3. Estruturas de Controle -```dart -// ✅ Com chaves -if (condition) { - return value; -} - -// ❌ Sem chaves -if (condition) - return value; -``` - -## Manutenção do Quiz - -### Adicionar Nova Pergunta -```dart -// Em lib/quiz/quiz1.dart -class Quiz21Screen extends StatelessWidget { - const Quiz21Screen({super.key, required this.currentScore, this.scopeId}); - - // ... implementação - - @override - Widget build(BuildContext context) { - return QuizQuestionScreen( - title: 'Quiz 21/21', - question: 'Nova pergunta aqui...', - answers: const [ - QuizAnswer(title: 'Opção 1', description: '...', weight: 2), - QuizAnswer(title: 'Opção 2', description: '...', weight: 5), - QuizAnswer(title: 'Opção 3', description: '...', weight: 3), - ], - currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( - builder: (_) => QuizResultScreen(finalScore: nextScore, maxScore: 105, scopeId: scopeId), - ), - isFinal: true, - showBackButton: true, - ); - } -} -``` - -### Atualizar Quiz Anterior -```dart -// No Quiz20Screen, atualizar nextRoute -nextRoute: (context, nextScore) => MaterialPageRoute( - builder: (_) => Quiz21Screen(currentScore: nextScore, scopeId: scopeId), -), -``` - -## Manutenção do logged_home.dart - -### Adicionar Nova Funcionalidade -1. Criar widget específico -2. Adicionar ao `_HomeTabState` ou aba correspondente -3. Testar com diferentes estados -4. Verificar lint - -### Corrigir Erros Comuns -```bash -# Verificar problemas -flutter analyze - -# Corrigir automaticamente -dart fix --apply -``` - -## Deploy - -### Android -```bash -# Build release -flutter build apk --release - -# Upload para Play Store -# Usar Android Studio ou Google Play Console -``` - -### iOS -```bash -# Build release -flutter build ios --release - -# Upload para App Store -# Usar Xcode → Product → Archive -``` - -### Web -```bash -# Build web -flutter build web - -# Deploy para Firebase Hosting ou similar -firebase deploy --only hosting -``` - -## Troubleshooting Comum - -### Firebase Issues -```bash -# Limpar cache do Firebase -flutter clean -cd android && ./gradlew clean && cd .. -cd ios && rm -rf Pods Podfile.lock && pod install && cd .. -``` - -### Build Issues -```bash -# Limpar completamente -flutter clean -flutter pub cache repair -flutter pub get -``` - -### Emulator Issues -```bash -# Limpar dados do emulador -flutter emulators --clean -flutter emulators --launch -``` - -## Performance - -### Monitoramento -```dart -// Usar Flutter DevTools -flutter run --profile -# Abrir: http://localhost:port/devtools/ -``` - -### Otimizações -- Usar `const` widgets onde possível -- Evitar rebuilds desnecessários -- Usar `ListView.builder` para listas longas -- Implementar lazy loading para imagens - -## Segurança - -### Firebase Rules -```javascript -// Exemplo: firestore.rules -rules_version = '2'; -service cloud.firestore { - match /databases/{database}/documents { - match /users/{userId} { - allow read, write: if request.auth != null && request.auth.uid == userId; - } - } -} -``` - -### Best Practices -- Nunca expor API keys no código -- Validar dados no cliente e servidor -- Usar HTTPS para todas as comunicações -- Implementar rate limiting - -## Backup e Recuperação - -### Backup Automático -```bash -# Script de backup -#!/bin/bash -DATE=$(date +%Y%m%d) -tar -czf "backup_$DATE.tar.gz" --exclude='.git' --exclude='build' . -``` - -### Recuperação de Desastres -1. Restaurar do backup mais recente -2. Rodar `flutter pub get` -3. Testar funcionalidades críticas -4. Deploy para produção - -## Monitoramento - -### Logs e Erros -```dart -// Implementar logging -import 'dart:developer' as developer; - -developer.log('Erro ao carregar dados', error: error); -``` - -### Analytics -- Configurar Firebase Analytics -- Monitorar eventos importantes -- Acompanhar performance do app - -## Atualizações de Dependências - -### Processo Seguro -```bash -# Verificar atualizações -flutter pub outdated - -# Atualizar uma por vez -flutter pub upgrade package_name - -# Testar após cada atualização -flutter test -flutter analyze -``` - -### Versões Críticas -- Firebase: Verificar breaking changes -- Flutter: Aguardar estabilidade antes de atualizar -- Packages: Verificar compatibilidade - -## Documentação - -### Manter Documentação Atualizada -- Atualizar README.md após mudanças significativas -- Documentar novas funcionalidades -- Manter changelog - -### Code Comments -```dart -/// Widget principal do quiz com 20 perguntas -/// -/// Responsável por gerenciar o fluxo completo do quiz, -/// desde a primeira pergunta até o resultado final. -class Quiz1Screen extends StatelessWidget { - // ... -} -``` - -## Testes - -### Unit Tests -```dart -// test/quiz_test.dart -void main() { - test('Quiz calculation should work correctly', () { - // Implementar testes - }); -} -``` - -### Integration Tests -```dart -// integration_test/app_test.dart -void main() { - testWidgets('Quiz flow smoke test', (WidgetTester tester) async { - // Testar fluxo completo - }); -} -``` - -## Contato e Suporte - -### Equipe de Desenvolvimento -- Desenvolvedor Principal: [Nome] -- Firebase Admin: [Nome] -- UI/UX Designer: [Nome] - -### Recursos Externos -- [Flutter Documentation](https://docs.flutter.dev/) -- [Firebase Documentation](https://firebase.google.com/docs) -- [Dart Style Guide](https://dart.dev/guides/language/effective-dart/style) - -## Checklist de Release - -### Antes do Deploy -- [ ] `flutter analyze` sem erros -- [ ] Todos os testes passando -- [ ] Versão atualizada no pubspec.yaml -- [ ] Changelog atualizado -- [ ] Backup criado -- [ ] Testado em múltiplos dispositivos -- [ ] Performance verificada -- [ ] Segurança revisada - -### Pós-Deploy -- [ ] Monitorar logs de erro -- [ ] Verificar analytics -- [ ] Coletar feedback dos usuários -- [ ] Preparar hotfix se necessário - -## Conclusão - -Este guia serve como referência para desenvolvimento e manutenção contínua do projeto. Siga os padrões estabelecidos para garantir qualidade e consistência no código. - -Para dúvidas ou sugestões de melhoria deste guia, consulte a equipe de desenvolvimento. diff --git a/documentação/README.md b/documentação/README.md deleted file mode 100644 index 2bf1e64..0000000 --- a/documentação/README.md +++ /dev/null @@ -1,178 +0,0 @@ -# Documentação - Check Theeth Kids - -## Visão Geral - -Esta pasta contém a documentação completa do projeto Check Theeth Kids, incluindo todas as modificações, correções e melhorias implementadas durante o desenvolvimento. - -## Estrutura da Documentação - -### 📁 [01-estrutura-do-projeto.md](./01-estrutura-do-projeto.md) -**Conteúdo**: Visão geral da arquitetura do projeto -- Estrutura de pastas e arquivos -- Fluxo da aplicação -- Dependências principais -- Componentes e funcionalidades - -### 📁 [02-restauracao-logged-home.md](./02-restauracao-logged-home.md) -**Conteúdo**: Processo completo de restauração do logged_home.dart -- Problema identificado e causa raiz -- Processo de restauração passo a passo -- Funcionalidades recuperadas -- Lições aprendidas e boas práticas - -### 📁 [03-expansao-quiz-20-perguntas.md](./03-expansao-quiz-20-perguntas.md) -**Conteúdo**: Expansão do sistema de quiz para 20 perguntas -- Sistema original vs expandido -- Reorganização do conteúdo -- Implementação técnica -- Benefícios e validação - -### 📁 [04-correcoes-lint-erros.md](./04-correcoes-lint-erros.md) -**Conteúdo**: Detalhamento de todas as correções de lint e erros -- Erros de `use_build_context_synchronously` -- Problemas de `unnecessary_underscores` -- Correções de estrutura -- Padrões estabelecidos - -### 📁 [05-dependências-configuracoes.md](./05-dependências-configuracoes.md) -**Conteúdo**: Configurações técnicas e dependências -- Requisitos do sistema -- Firebase configuration -- Assets e permissões -- Scripts de desenvolvimento - -### 📁 [06-guia-desenvolvimento-manutencao.md](./06-guia-desenvolvimento-manutencao.md) -**Conteúdo**: Guia completo para desenvolvedores -- Setup do ambiente -- Padrões de código -- Processos de deploy -- Troubleshooting - -## Resumo das Principais Realizações - -### ✅ Restauração Completa do logged_home.dart -- **100% das funcionalidades originais recuperadas** -- Interface idêntica à versão original -- Zero erros de lint -- Performance otimizada - -### ✅ Expansão do Quiz para 20 Perguntas -- **Sistema consolidado em um único arquivo** -- Reorganização lógica do conteúdo (avançado → básico) -- Sistema de pontuação expandido (100 pontos) -- Fluxo contínuo e melhorado - -### ✅ Correções Técnicas -- **Zero erros de lint** (`flutter analyze` limpo) -- BuildContext seguro em operações assíncronas -- Código limpo e consistente -- Padrões estabelecidos para futuro - -### ✅ Documentação Completa -- **6 arquivos de documentação detalhados** -- Processos documentados passo a passo -- Guia de desenvolvimento e manutenção -- Referência para futuros desenvolvedores - -## Estado Atual do Projeto - -### 🟢 Funcionalidades Completas -- ✅ Sistema de autenticação Firebase -- ✅ Tela principal com todas as funcionalidades -- ✅ Sistema de quiz com 20 perguntas -- ✅ Upload e gerenciamento de fotos -- ✅ Sistema de gerenciamento de crianças -- ✅ Biblioteca de vídeos educativos -- ✅ Sistema de resultados do quiz - -### 🟡 Pontos de Atenção -- ⚠️ Configuração Firebase Web requer credenciais específicas -- ⚠️ 43 packages com versões mais recentes disponíveis -- ⚠️ Implementação de testes automatizados recomendada - -### 🔧 Manutenção Recomendada -- 📋 Atualização de dependências -- 📋 Configuração Firebase Web -- 📋 Implementação de testes -- 📋 Otimização de performance - -## Como Usar Esta Documentação - -### Para Novos Desenvolvedores -1. Comece com **[01-estrutura-do-projeto.md](./01-estrutura-do-projeto.md)** -2. Leia **[06-guia-desenvolvimento-manutencao.md](./06-guia-desenvolvimento-manutencao.md)** -3. Configure o ambiente seguindo as instruções - -### Para Manutenção -1. Consulte **[05-dependências-configuracoes.md](./05-dependências-configuracoes.md)** para configurações -2. Use **[04-correcoes-lint-erros.md](./04-correcoes-lint-erros.md)** como referência de padrões -3. Siga **[06-guia-desenvolvimento-manutencao.md](./06-guia-desenvolvimento-manutencao.md)** para processos - -### Para Troubleshooting -1. Verifique **[02-restauracao-logged-home.md](./02-restauracao-logged-home.md)** para issues do logged_home -2. Consulte **[03-expansao-quiz-20-perguntas.md](./03-expansao-quiz-20-perguntas.md)** para issues do quiz -3. Use **[04-correcoes-lint-erros.md](./04-correcoes-lint-erros.md)** para correções de lint - -## Comandos Rápidos - -### Desenvolvimento -```bash -# Limpar e atualizar -flutter clean && flutter pub get - -# Verificar código -flutter analyze -dart format . - -# Rodar aplicação -flutter run - -# Build para produção -flutter build apk --release -``` - -### Testes -```bash -# Rodar todos os testes -flutter test - -# Testar cobertura -flutter test --coverage -``` - -### Firebase -```bash -# Deploy web (se configurado) -firebase deploy --only hosting -``` - -## Contato e Suporte - -### Para Dúvidas Técnicas -- Consulte o guia de desenvolvimento -- Verifique os logs de erro -- Use Flutter DevTools para debugging - -### Para Novas Funcionalidades -- Siga os padrões estabelecidos -- Documente as mudanças -- Teste completamente antes do deploy - -## Histórico de Versões - -### Versão Atual (Documentada) -- **Quiz**: Expandido para 20 perguntas -- **logged_home.dart**: Restaurado e otimizado -- **Lint**: Zero erros -- **Documentação**: Completa e detalhada - -### Versões Anteriores -- Quiz com 5 perguntas (básico) -- Quiz com 15 perguntas (extendido) -- logged_home.dart corrompido (restaurado) - -## Conclusão - -Esta documentação representa o estado completo e atualizado do projeto Check Theeth Kids. Todas as funcionalidades estão operacionais, o código está limpo e otimizado, e os processos estão bem documentados para manutenção futura. - -O projeto está pronto para uso em produção e para futuras expansões seguindo os padrões estabelecidos. diff --git a/lib/auth_gate.dart b/lib/auth_gate.dart index 1af5112..5bc609e 100644 --- a/lib/auth_gate.dart +++ b/lib/auth_gate.dart @@ -1,6 +1,7 @@ -import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'main.dart' show supabase; import 'home_screen.dart'; import 'logged_home.dart'; @@ -14,10 +15,11 @@ class AuthGate extends StatelessWidget { return ValueListenableBuilder( valueListenable: forceHomeScreen, builder: (context, forcedHome, _) { - return StreamBuilder( - stream: FirebaseAuth.instance.authStateChanges(), + return StreamBuilder( + stream: supabase.auth.onAuthStateChange, + initialData: AuthState(AuthChangeEvent.initialSession, supabase.auth.currentSession), builder: (context, snapshot) { - final user = snapshot.data; + final user = snapshot.data?.session?.user; final Widget child; if (snapshot.connectionState == ConnectionState.waiting) { diff --git a/lib/home_screen.dart b/lib/home_screen.dart index 1033778..e8809d9 100644 --- a/lib/home_screen.dart +++ b/lib/home_screen.dart @@ -66,29 +66,6 @@ class _HomeScreenState extends State { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - Container( - width: 80, - height: 80, - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: const Color( - 0xFF2F9E94, - ).withValues(alpha: 0.18), - blurRadius: 24, - offset: const Offset(0, 8), - ), - ], - ), - child: const Icon( - Icons.medical_services_rounded, - size: 38, - color: Color(0xFF2F9E94), - ), - ), - const SizedBox(height: 18), const Text( 'Check-Teeth Kids', textAlign: TextAlign.center, diff --git a/lib/logged_home.dart b/lib/logged_home.dart index e105a99..3766a4b 100644 --- a/lib/logged_home.dart +++ b/lib/logged_home.dart @@ -1,17 +1,18 @@ -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:lottie/lottie.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; import 'dart:async'; import 'dart:math' as math; import 'dart:io'; import 'package:shared_preferences/shared_preferences.dart'; +import 'main.dart' show supabase; import 'quiz/quiz1.dart'; import 'quiz/quiz_prefs.dart'; +import 'screens/settings_screen.dart'; import 'screens/video_screen.dart'; +import 'widgets/app_dialogs.dart'; class LoggedHomeScreen extends StatefulWidget { const LoggedHomeScreen({super.key}); @@ -38,6 +39,7 @@ class _LoggedHomeScreenState extends State int? _lastMaxScore; String _cachedUserName = 'Sem nome'; + String? _cachedPhotoUrl; @override void initState() { @@ -81,41 +83,29 @@ class _LoggedHomeScreenState extends State } Future _loadInitialProfile() async { - final uid = (FirebaseAuth.instance.currentUser?.uid ?? '').trim(); + final uid = (supabase.auth.currentUser?.id ?? '').trim(); if (uid.isEmpty) return; try { - final userDoc = await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .get(); - final data = userDoc.data(); - final storedName = (data?['name'] ?? '').toString().trim(); + final userDoc = await supabase + .from('profiles') + .select() + .eq('id', uid) + .maybeSingle(); + final storedName = (userDoc?['name'] ?? '').toString().trim(); + final storedPhotoUrl = (userDoc?['photo_url'] ?? '').toString().trim(); - QuerySnapshot> childrenSnap; - try { - childrenSnap = await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .collection('children') - .orderBy('createdAt', descending: false) - .limit(1) - .get(); - } catch (_) { - childrenSnap = await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .collection('children') - .limit(1) - .get(); - } + final childrenSnap = await supabase + .from('children') + .select() + .eq('owner_id', uid) + .order('created_at') + .limit(1); String? childName; String? scopeId; - if (childrenSnap.docs.isNotEmpty) { - final c = childrenSnap.docs.first.data(); - final childId = (c['id'] ?? childrenSnap.docs.first.id) - .toString() - .trim(); + if (childrenSnap.isNotEmpty) { + final c = childrenSnap.first; + final childId = (c['id'] ?? '').toString().trim(); childName = (c['name'] ?? '').toString().trim(); scopeId = '${uid}_$childId'; } @@ -123,6 +113,7 @@ class _LoggedHomeScreenState extends State if (!mounted) return; setState(() { _cachedUserName = storedName.isNotEmpty ? storedName : _cachedUserName; + if (storedPhotoUrl.isNotEmpty) _cachedPhotoUrl = storedPhotoUrl; if ((_selectedChildName ?? '').trim().isEmpty && (childName ?? '').trim().isNotEmpty) { _selectedChildName = childName; @@ -140,7 +131,7 @@ class _LoggedHomeScreenState extends State Future _loadQuizResult() async { final scope = (_selectedChildScopeId ?? '').trim(); - final uid = FirebaseAuth.instance.currentUser?.uid; + final uid = supabase.auth.currentUser?.id; final String? userId = (uid ?? '').trim().isEmpty ? null : uid; int? score; @@ -152,15 +143,13 @@ class _LoggedHomeScreenState extends State : ''; if (childId.trim().isNotEmpty) { try { - final childDoc = await FirebaseFirestore.instance - .collection('users') - .doc(userId) - .collection('children') - .doc(childId) - .get(); - final data = childDoc.data(); - final s = data?['lastScore']; - final m = data?['lastMaxScore']; + final childDoc = await supabase + .from('children') + .select() + .eq('id', childId) + .maybeSingle(); + final s = childDoc?['last_score']; + final m = childDoc?['last_max_score']; if (s is int && m is int) { score = s; max = m; @@ -196,6 +185,25 @@ class _LoggedHomeScreenState extends State }); } + void selectChild(String? name, String? scopeId) { + setState(() { + _selectedChildName = name; + _selectedChildScopeId = scopeId; + }); + _loadQuizResult(); + } + + void updateCachedPhoto(String url) { + setState(() => _cachedPhotoUrl = url); + } + + String _greeting() { + final hour = DateTime.now().hour; + if (hour < 12) return 'Bom dia'; + if (hour < 18) return 'Boa tarde'; + return 'Boa noite'; + } + @override Widget build(BuildContext context) { final size = MediaQuery.sizeOf(context); @@ -203,7 +211,11 @@ class _LoggedHomeScreenState extends State ? _expandedAppBarHeight : _collapsedAppBarHeight; final double toolbarHeight = _index == 0 ? kToolbarHeight : appBarHeight; - final String title = _index == 0 ? '' : 'Perfil'; + final String title = _index == 0 + ? '' + : _index == 1 + ? 'Perfil' + : 'Configurações'; final ShapeBorder appBarShape = _index == 0 ? const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(bottom: Radius.circular(24)), @@ -296,13 +308,65 @@ class _LoggedHomeScreenState extends State child: _index == 0 ? Padding( padding: const EdgeInsets.only(left: 16, right: 10), - child: Text( - 'Olá, $shownName', - textAlign: TextAlign.left, - style: const TextStyle( - fontWeight: FontWeight.w900, - color: Colors.white, - fontSize: 20, + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(30), + onTap: () => setState(() => _index = 1), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 4, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + CircleAvatar( + radius: 20, + backgroundColor: Colors.white.withValues( + alpha: 0.25, + ), + backgroundImage: + (_cachedPhotoUrl ?? '').isNotEmpty + ? NetworkImage(_cachedPhotoUrl!) + : null, + child: (_cachedPhotoUrl ?? '').isEmpty + ? const Icon( + Icons.person_rounded, + color: Colors.white, + ) + : null, + ), + const SizedBox(width: 10), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _greeting(), + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.white.withValues( + alpha: 0.85, + ), + fontSize: 12, + ), + ), + Text( + shownName, + textAlign: TextAlign.left, + style: const TextStyle( + fontWeight: FontWeight.w900, + color: Colors.white, + fontSize: 19, + ), + ), + ], + ), + ], + ), + ), ), ), ) @@ -358,7 +422,8 @@ class _LoggedHomeScreenState extends State padding: EdgeInsets.fromLTRB(16, bodyTopPadding, 16, 16), child: _index == 0 ? _InicioTab(onQuizClosed: _loadQuizResult) - : _PerfilTab( + : _index == 1 + ? _PerfilTab( selectedChildIndex: _selectedChildIndex, onChildSelected: (index, name, scopeId) { setState(() { @@ -368,7 +433,8 @@ class _LoggedHomeScreenState extends State }); _loadQuizResult(); }, - ), + ) + : const SettingsBody(), ), ), ), @@ -390,6 +456,10 @@ class _LoggedHomeScreenState extends State icon: Icon(Icons.person_rounded), label: 'Perfil', ), + BottomNavigationBarItem( + icon: Icon(Icons.settings_rounded), + label: 'Ajustes', + ), ], ), ); @@ -501,14 +571,52 @@ class _InicioTab extends StatelessWidget { final VoidCallback onQuizClosed; + Future _startQuiz(BuildContext context) async { + final uid = (supabase.auth.currentUser?.id ?? '').trim(); + if (uid.isEmpty) return; + + List> children = const []; + try { + children = await supabase + .from('children') + .select() + .eq('owner_id', uid) + .order('created_at'); + } catch (_) { + // segue com lista vazia; tratado abaixo + } + + if (!context.mounted) return; + + Map? chosen; + if (children.isEmpty) { + chosen = await _requireFirstChild(context, uid); + if (chosen == null) return; + } else if (children.length == 1) { + chosen = children.first; + } else { + if (!context.mounted) return; + chosen = await _pickChildSheet(context, children); + if (chosen == null) return; + } + + final childId = (chosen['id'] ?? '').toString().trim(); + final childName = (chosen['name'] ?? '').toString().trim(); + final scopeId = childId.isEmpty ? uid : '${uid}_$childId'; + + if (!context.mounted) return; + final state = context.findAncestorStateOfType<_LoggedHomeScreenState>(); + state?.selectChild(childName, scopeId); + + await Navigator.of(context).push( + MaterialPageRoute(builder: (_) => Quiz1Screen(scopeId: scopeId)), + ); + onQuizClosed(); + } + @override Widget build(BuildContext context) { - final uid = (FirebaseAuth.instance.currentUser?.uid ?? '').trim(); final state = context.findAncestorStateOfType<_LoggedHomeScreenState>(); - final childScope = (state?._selectedChildScopeId ?? '').trim(); - final scopeId = childScope.isNotEmpty - ? childScope - : (uid.isNotEmpty ? uid : null); final selectedChildName = (state?._selectedChildName ?? '').trim(); return Align( @@ -524,15 +632,7 @@ class _InicioTab extends StatelessWidget { children: [ _HeroQuizCard( childName: selectedChildName, - onStartQuiz: () { - Navigator.of(context) - .push( - MaterialPageRoute( - builder: (_) => Quiz1Screen(scopeId: scopeId), - ), - ) - .then((_) => onQuizClosed()); - }, + onStartQuiz: () => _startQuiz(context), ), const SizedBox(height: 16), _VideoLibraryCard( @@ -554,6 +654,138 @@ class _InicioTab extends StatelessWidget { } } +Future?> _createChildViaSheet( + BuildContext context, + String uid, +) async { + final result = await showModalBottomSheet?>( + context: context, + isScrollControlled: true, + showDragHandle: true, + backgroundColor: const Color(0xFFFFE6F1), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + builder: (ctx) => const _AddChildSheet(), + ); + + if (result == null) return null; + if (!context.mounted) return null; + + try { + final inserted = await supabase + .from('children') + .insert({...result, 'owner_id': uid}) + .select() + .single(); + return inserted; + } catch (e) { + if (!context.mounted) return null; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Erro ao adicionar criança: $e'))); + return null; + } +} + +Future?> _requireFirstChild( + BuildContext context, + String uid, +) async { + final proceed = await showConfirmDialog( + context, + title: 'Cadastre uma criança', + message: 'Antes de iniciar o quiz, adicione uma criança ao seu perfil.', + confirmLabel: 'Adicionar criança', + ); + + if (proceed != true) return null; + if (!context.mounted) return null; + return _createChildViaSheet(context, uid); +} + +Future?> _pickChildSheet( + BuildContext context, + List> children, +) { + const Color teal = Color(0xFF2F9E94); + return showModalBottomSheet?>( + context: context, + isScrollControlled: true, + showDragHandle: true, + backgroundColor: const Color(0xFFFFE6F1), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + builder: (ctx) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(18, 6, 18, 18), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + 'Para qual criança é o quiz?', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w900, + color: Color(0xFFFF55A7), + ), + ), + const SizedBox(height: 14), + ...children.map((c) { + final name = (c['name'] ?? '').toString(); + final age = c['age']; + final label = age != null ? '$name • $age anos' : name; + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Material( + color: Colors.white.withValues(alpha: 0.85), + borderRadius: BorderRadius.circular(16), + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () => Navigator.of(ctx).pop(c), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + child: Row( + children: [ + Expanded( + child: Text( + label, + style: const TextStyle( + fontWeight: FontWeight.w800, + ), + ), + ), + const Icon( + Icons.chevron_right_rounded, + color: teal, + ), + ], + ), + ), + ), + ), + ); + }), + const SizedBox(height: 4), + TextButton( + onPressed: () => Navigator.of(ctx).pop(null), + child: const Text('Cancelar'), + ), + ], + ), + ), + ); + }, + ); +} + class _HeroQuizCard extends StatelessWidget { const _HeroQuizCard({required this.childName, required this.onStartQuiz}); @@ -651,7 +883,10 @@ class _VideoLibraryCard extends StatelessWidget { @override Widget build(BuildContext context) { - const Object? item = null; + final featured = videoList.firstWhere( + (v) => v.videoPath != null, + orElse: () => videoList.first, + ); return Material( color: Colors.white, @@ -664,53 +899,42 @@ class _VideoLibraryCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (item == null) - Container( + ClipRRect( + borderRadius: const BorderRadius.vertical( + top: Radius.circular(24), + ), + child: SizedBox( height: 160, - decoration: BoxDecoration( - color: const Color(0xFFFFE6F1), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(24), - ), - ), - ) - else - ClipRRect( - borderRadius: const BorderRadius.vertical( - top: Radius.circular(24), - ), - child: SizedBox( - height: 160, - child: Stack( - fit: StackFit.expand, - children: [ - Container( - color: const Color(0xFF2F9E94).withValues(alpha: 0.14), - ), - Container( + child: Stack( + fit: StackFit.expand, + children: [ + Container( + color: const Color(0xFF2F9E94).withValues(alpha: 0.14), + ), + VideoThumbnail( + video: featured, + borderRadius: 0, + iconSize: 54, + ), + IgnorePointer( + child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - Colors.black.withValues(alpha: 0.05), - Colors.black.withValues(alpha: 0.45), + Colors.transparent, + Colors.black.withValues(alpha: 0.35), ], + stops: const [0.6, 1.0], ), ), ), - const Align( - alignment: Alignment.center, - child: Icon( - Icons.play_circle_fill_rounded, - size: 54, - color: Colors.white, - ), - ), - ], - ), + ), + ], ), ), + ), Padding( padding: const EdgeInsets.fromLTRB(18, 14, 14, 14), child: Row( @@ -785,6 +1009,47 @@ class _PerfilTabState extends State<_PerfilTab> { bool _addingChild = false; bool _updatingPhoto = false; + bool _initialLoading = true; + Map? _profileData; + List> _children = const []; + + @override + void initState() { + super.initState(); + _loadPerfilData().whenComplete(() { + if (mounted) setState(() => _initialLoading = false); + }); + } + + // Busca simples (sem Realtime): o Realtime do Supabase pode travar depois de + // várias trocas de aba/reconexões, deixando a lista de filhos e a foto sem + // atualizar. Como só o próprio usuário edita esses dados, buscamos uma vez + // e recarregamos manualmente após cada ação (adicionar/remover filho, trocar + // foto), o que é bem mais confiável. + Future _loadPerfilData() async { + final uid = (supabase.auth.currentUser?.id ?? '').trim(); + if (uid.isEmpty) return; + try { + final profile = await supabase + .from('profiles') + .select() + .eq('id', uid) + .maybeSingle(); + final children = await supabase + .from('children') + .select() + .eq('owner_id', uid) + .order('created_at'); + if (!mounted) return; + setState(() { + _profileData = profile; + _children = children; + }); + } catch (_) { + // mantém os dados já carregados; usuário pode tentar de novo + } + } + Future<(int?, int?)> _loadScoreForScope(String scopeId) async { final score = await QuizPrefs.getLastScoreForScope(scopeId); final max = await QuizPrefs.getLastMaxScoreForScope(scopeId); @@ -800,10 +1065,14 @@ class _PerfilTabState extends State<_PerfilTab> { final source = await showModalBottomSheet( context: context, showDragHandle: true, + backgroundColor: const Color(0xFFFFE6F1), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), builder: (ctx) { return SafeArea( child: Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), + padding: const EdgeInsets.fromLTRB(18, 6, 18, 18), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -811,12 +1080,22 @@ class _PerfilTabState extends State<_PerfilTab> { const Text( 'Foto de perfil', textAlign: TextAlign.center, - style: TextStyle(fontWeight: FontWeight.w900, fontSize: 16), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w900, + color: Color(0xFFFF55A7), + ), ), - const SizedBox(height: 12), + const SizedBox(height: 14), SizedBox( height: 46, child: FilledButton( + style: FilledButton.styleFrom( + backgroundColor: const Color(0xFF2F9E94), + foregroundColor: Colors.white, + shape: const StadiumBorder(), + textStyle: const TextStyle(fontWeight: FontWeight.w900), + ), onPressed: () => Navigator.of(ctx).pop(ImageSource.camera), child: const Text('Câmera'), ), @@ -825,14 +1104,23 @@ class _PerfilTabState extends State<_PerfilTab> { SizedBox( height: 46, child: FilledButton( + style: FilledButton.styleFrom( + backgroundColor: const Color(0xFF2F9E94), + foregroundColor: Colors.white, + shape: const StadiumBorder(), + textStyle: const TextStyle(fontWeight: FontWeight.w900), + ), onPressed: () => Navigator.of(ctx).pop(ImageSource.gallery), child: const Text('Galeria'), ), ), - const SizedBox(height: 10), + const SizedBox(height: 8), SizedBox( height: 42, child: TextButton( + style: TextButton.styleFrom( + foregroundColor: const Color(0xFF2F9E94), + ), onPressed: () => Navigator.of(ctx).pop(), child: const Text('Cancelar'), ), @@ -857,17 +1145,26 @@ class _PerfilTabState extends State<_PerfilTab> { setState(() => _updatingPhoto = true); try { final file = File(picked.path); - final ref = FirebaseStorage.instance - .ref() - .child('users') - .child(uid) - .child('profile.jpg'); - await ref.putFile(file); - final url = await ref.getDownloadURL(); + final path = '$uid/profile.jpg'; + await supabase.storage + .from('photos') + .upload( + path, + file, + fileOptions: const FileOptions(upsert: true), + ); + final publicUrl = supabase.storage.from('photos').getPublicUrl(path); + final url = '$publicUrl?t=${DateTime.now().millisecondsSinceEpoch}'; - await FirebaseFirestore.instance.collection('users').doc(uid).set({ - 'photoUrl': url, - }, SetOptions(merge: true)); + await supabase.from('profiles').upsert({ + 'id': uid, + 'photo_url': url, + }); + + await _loadPerfilData(); + if (context.mounted) { + context.findAncestorStateOfType<_LoggedHomeScreenState>()?.updateCachedPhoto(url); + } } catch (e) { if (!context.mounted) return; ScaffoldMessenger.of( @@ -878,6 +1175,38 @@ class _PerfilTabState extends State<_PerfilTab> { } } + Future _confirmDeleteChild( + BuildContext context, { + required String childId, + required String childName, + }) async { + final messenger = ScaffoldMessenger.of(context); + final confirmed = await showConfirmDialog( + context, + title: 'Remover criança', + message: + 'Tem certeza que deseja remover "$childName"? Essa ação não pode ser desfeita.', + confirmLabel: 'Remover', + confirmColor: const Color(0xFFFF55A7), + ); + + if (confirmed != true) return; + if (!context.mounted) return; + + try { + await supabase.from('children').delete().eq('id', childId); + widget.onChildSelected(0, null, null); + await _loadPerfilData(); + messenger.showSnackBar( + const SnackBar(content: Text('Criança removida')), + ); + } catch (e) { + messenger.showSnackBar( + SnackBar(content: Text('Erro ao remover: $e')), + ); + } + } + Future _addAnotherChild(BuildContext context, String uid) async { if (_addingChild) return; final messenger = ScaffoldMessenger.of(context); @@ -896,30 +1225,20 @@ class _PerfilTabState extends State<_PerfilTab> { if (result == null) return; - final childId = FirebaseFirestore.instance - .collection('users') - .doc(uid) - .collection('children') - .doc() - .id; - final childMap = { ...result, - 'id': childId, - 'createdAt': FieldValue.serverTimestamp(), + 'owner_id': uid, }; setState(() => _addingChild = true); try { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .collection('children') - .doc(childId) - .set(childMap, SetOptions(merge: true)) + await supabase + .from('children') + .insert(childMap) .timeout(const Duration(seconds: 20)); if (!mounted) return; + await _loadPerfilData(); messenger.showSnackBar( const SnackBar(content: Text('Criança adicionada')), ); @@ -930,24 +1249,12 @@ class _PerfilTabState extends State<_PerfilTab> { if (!mounted) return; if (!mounted) return; - final addMore = await showDialog( + final addMore = await showConfirmDialog( // ignore: use_build_context_synchronously - context: context, - builder: (ctx) { - return AlertDialog( - title: const Text('Adicionar outra criança?'), - actions: [ - TextButton( - onPressed: () => Navigator.of(ctx).pop(false), - child: const Text('Agora não'), - ), - FilledButton( - onPressed: () => Navigator.of(ctx).pop(true), - child: const Text('Adicionar outra'), - ), - ], - ); - }, + context, + title: 'Adicionar outra criança?', + cancelLabel: 'Agora não', + confirmLabel: 'Adicionar outra', ); if (!mounted) return; @@ -976,9 +1283,9 @@ class _PerfilTabState extends State<_PerfilTab> { @override Widget build(BuildContext context) { - final user = FirebaseAuth.instance.currentUser; - final uid = (user?.uid ?? '').trim(); - final name = (user?.displayName ?? '').trim(); + final user = supabase.auth.currentUser; + final uid = (user?.id ?? '').trim(); + final name = (user?.userMetadata?['name'] ?? '').toString().trim(); final email = (user?.email ?? '').trim(); final shownName = name.isNotEmpty ? name : 'Sem nome'; @@ -986,39 +1293,33 @@ class _PerfilTabState extends State<_PerfilTab> { return const SizedBox.shrink(); } - return StreamBuilder>>( - stream: FirebaseFirestore.instance - .collection('users') - .doc(uid) - .snapshots(), - builder: (context, userSnapshot) { - final data = userSnapshot.data?.data(); - final storedName = (data?['name'] ?? '').toString().trim(); - final profileName = storedName.isNotEmpty ? storedName : shownName; - final photoUrl = (data?['photoUrl'] ?? '').toString().trim(); - final storedEmail = (data?['email'] ?? '').toString().trim(); - final profileEmail = storedEmail.isNotEmpty ? storedEmail : email; + if (_initialLoading) { + return const Center( + child: Padding( + padding: EdgeInsets.only(top: 60), + child: CircularProgressIndicator(color: Color(0xFF2F9E94)), + ), + ); + } - return StreamBuilder>>( - stream: FirebaseFirestore.instance - .collection('users') - .doc(uid) - .collection('children') - .orderBy('createdAt', descending: false) - .snapshots(), - builder: (context, childSnapshot) { - final docs = childSnapshot.data?.docs ?? const []; - final children = docs.map((d) => d.data()).toList(); - final int selectedIndex = children.isEmpty - ? 0 - : widget.selectedChildIndex.clamp( - 0, - (children.length - 1).clamp(0, 999999), - ); + final data = _profileData; + final storedName = (data?['name'] ?? '').toString().trim(); + final profileName = storedName.isNotEmpty ? storedName : shownName; + final photoUrl = (data?['photo_url'] ?? '').toString().trim(); + final storedEmail = (data?['email'] ?? '').toString().trim(); + final profileEmail = storedEmail.isNotEmpty ? storedEmail : email; - return Align( - alignment: Alignment.topCenter, - child: Padding( + final children = _children; + final int selectedIndex = children.isEmpty + ? 0 + : widget.selectedChildIndex.clamp( + 0, + (children.length - 1).clamp(0, 999999), + ); + + return Align( + alignment: Alignment.topCenter, + child: Padding( padding: const EdgeInsets.only(top: 10), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 560), @@ -1233,10 +1534,7 @@ class _PerfilTabState extends State<_PerfilTab> { ...children.asMap().entries.map((entry) { final i = entry.key; final c = entry.value; - final childId = - (c['id'] ?? '').toString().trim().isEmpty - ? docs[i].id - : (c['id'] ?? '').toString().trim(); + final childId = (c['id'] ?? '').toString().trim(); final childName = (c['name'] ?? '') .toString() .trim(); @@ -1347,6 +1645,20 @@ class _PerfilTabState extends State<_PerfilTab> { ); }, ), + const SizedBox(width: 6), + IconButton( + onPressed: () => _confirmDeleteChild( + context, + childId: childId, + childName: title, + ), + icon: const Icon( + Icons.delete_outline_rounded, + color: Color(0xFFFF55A7), + ), + tooltip: 'Remover', + visualDensity: VisualDensity.compact, + ), ], ), ), @@ -1387,7 +1699,7 @@ class _PerfilTabState extends State<_PerfilTab> { ), ), onPressed: () async { - await FirebaseAuth.instance.signOut(); + await supabase.auth.signOut(); }, icon: const Icon(Icons.logout_rounded), label: const Text('Sair'), @@ -1400,119 +1712,9 @@ class _PerfilTabState extends State<_PerfilTab> { ), ), ); - }, - ); - }, - ); } } -class _AddChildDialog extends StatefulWidget { - const _AddChildDialog(); - - @override - State<_AddChildDialog> createState() => _AddChildDialogState(); -} - -class _AddChildDialogState extends State<_AddChildDialog> { - final _formKey = GlobalKey(); - final _nameController = TextEditingController(); - final _ageController = TextEditingController(); - String? _gender; - - @override - void dispose() { - _nameController.dispose(); - _ageController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: const Text('Adicionar outra criança'), - content: SingleChildScrollView( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 420), - child: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - controller: _nameController, - textInputAction: TextInputAction.next, - decoration: const InputDecoration( - labelText: 'Nome da criança', - ), - validator: (v) { - final value = (v ?? '').trim(); - if (value.isEmpty) return 'Informe o nome'; - if (value.length < 2) return 'Nome muito curto'; - return null; - }, - ), - TextFormField( - controller: _ageController, - keyboardType: TextInputType.number, - textInputAction: TextInputAction.next, - decoration: const InputDecoration(labelText: 'Idade'), - validator: (v) { - final raw = (v ?? '').trim(); - if (raw.isEmpty) return 'Informe a idade'; - final age = int.tryParse(raw); - if (age == null) return 'Idade inválida'; - if (age < 0 || age > 25) return 'Idade inválida'; - return null; - }, - ), - DropdownButtonFormField( - initialValue: _gender, - items: const [ - DropdownMenuItem( - value: 'Masculino', - child: Text('Masculino'), - ), - DropdownMenuItem( - value: 'Feminino', - child: Text('Feminino'), - ), - DropdownMenuItem(value: 'Outro', child: Text('Outro')), - ], - onChanged: (v) => setState(() => _gender = v), - decoration: const InputDecoration(labelText: 'Gênero'), - validator: (v) { - if (v == null || v.trim().isEmpty) { - return 'Selecione o gênero'; - } - return null; - }, - ), - ], - ), - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(null), - child: const Text('Cancelar'), - ), - FilledButton( - onPressed: () { - if (!(_formKey.currentState?.validate() ?? false)) return; - Navigator.of(context).pop({ - 'name': _nameController.text.trim(), - 'age': int.parse(_ageController.text.trim()), - 'gender': (_gender ?? '').trim(), - }); - }, - child: const Text('Adicionar'), - ), - ], - ); - } -} class _AddChildSheet extends StatefulWidget { const _AddChildSheet(); diff --git a/lib/login_register/login_sheet.dart b/lib/login_register/login_sheet.dart index 2fb2531..dd53ae6 100644 --- a/lib/login_register/login_sheet.dart +++ b/lib/login_register/login_sheet.dart @@ -1,94 +1,21 @@ -import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; -import 'package:lottie/lottie.dart'; -import 'dart:math' as math; +import 'package:supabase_flutter/supabase_flutter.dart'; + +import '../main.dart' show supabase; Future showLoginSheet(BuildContext context) { return showModalBottomSheet( context: context, isScrollControlled: true, - backgroundColor: Colors.transparent, + showDragHandle: true, + backgroundColor: const Color(0xFFFFE6F1), shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), - builder: (ctx) => const _AnimatedAuthSheet(child: LoginBottomSheet()), + builder: (ctx) => const LoginBottomSheet(), ); } -class _AnimatedAuthSheet extends StatelessWidget { - const _AnimatedAuthSheet({required this.child}); - - final Widget child; - - @override - Widget build(BuildContext context) { - final size = MediaQuery.sizeOf(context); - const topRadius = Radius.circular(20); - - return TweenAnimationBuilder( - tween: Tween(begin: 0.0, end: 1.0), - duration: const Duration(milliseconds: 260), - curve: Curves.easeOutCubic, - builder: (context, t, w) { - return Opacity( - opacity: t, - child: Transform.translate( - offset: Offset(0, (1 - t) * 12), - child: w, - ), - ); - }, - child: ClipRRect( - borderRadius: const BorderRadius.vertical(top: topRadius), - child: Material( - color: Colors.transparent, - child: Stack( - clipBehavior: Clip.none, - children: [ - Positioned.fill( - child: Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Color(0xFFFFE6F1), - Color(0xFFFFC9DF), - ], - ), - ), - ), - ), - Positioned( - left: -size.width * 0.38, - bottom: -size.width * 0.45, - child: IgnorePointer( - child: SizedBox( - width: size.width * 1.05, - height: size.width * 1.05, - child: Transform.rotate( - angle: 28 * math.pi / 180, - child: Opacity( - opacity: 0.95, - child: Lottie.asset( - 'lottie/Liquid waves.json', - fit: BoxFit.cover, - repeat: true, - ), - ), - ), - ), - ), - ), - child, - ], - ), - ), - ), - ); - } -} - class LoginBottomSheet extends StatefulWidget { const LoginBottomSheet({super.key}); @@ -113,119 +40,106 @@ class _LoginBottomSheetState extends State { @override Widget build(BuildContext context) { - final viewInsets = MediaQuery.viewInsetsOf(context); - const accentPink = Color(0xFFFF55A7); - const primaryTeal = Color(0xFF2F9E94); - final underlineBorder = UnderlineInputBorder( - borderSide: BorderSide(color: Colors.black.withValues(alpha: 0.20)), - ); - - return Padding( - padding: EdgeInsets.only( - left: 16, - right: 16, - top: 12, - bottom: 16 + viewInsets.bottom, - ), - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 520), - child: SingleChildScrollView( - child: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, + final bottomInset = MediaQuery.viewInsetsOf(context).bottom; + return SafeArea( + child: Padding( + padding: EdgeInsets.fromLTRB(18, 6, 18, 18 + bottomInset), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + 'Entrar', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w900, + color: Color(0xFFFF55A7), + ), + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.82), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.black.withValues(alpha: 0.08)), + ), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + controller: _emailController, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.next, + decoration: const InputDecoration(labelText: 'Email'), + validator: (v) { + final value = (v ?? '').trim(); + if (value.isEmpty) return 'Informe seu email'; + if (!value.contains('@')) return 'Email inválido'; + return null; + }, + ), + TextFormField( + controller: _passwordController, + obscureText: true, + textInputAction: TextInputAction.done, + decoration: const InputDecoration(labelText: 'Senha'), + validator: (v) { + final value = (v ?? ''); + if (value.isEmpty) return 'Informe sua senha'; + if (value.length < 6) return 'Mínimo de 6 caracteres'; + return null; + }, + ), + ], + ), + ), + ), + const SizedBox(height: 14), + Row( children: [ - Center( - child: Container( - width: 46, - height: 5, - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.10), - borderRadius: BorderRadius.circular(99), + Expanded( + child: SizedBox( + height: 44, + child: TextButton( + onPressed: _loading + ? null + : () => Navigator.of(context).pop(), + child: const Text('Cancelar'), ), ), ), - const SizedBox(height: 14), - const Text( - 'Entrar', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w800, - color: accentPink, - ), - ), - const SizedBox(height: 16), - TextFormField( - controller: _emailController, - keyboardType: TextInputType.emailAddress, - textInputAction: TextInputAction.next, - decoration: InputDecoration( - labelText: 'Email', - border: underlineBorder, - enabledBorder: underlineBorder, - focusedBorder: underlineBorder.copyWith( - borderSide: const BorderSide(color: primaryTeal, width: 1.6), + const SizedBox(width: 10), + Expanded( + child: SizedBox( + height: 44, + child: FilledButton( + style: FilledButton.styleFrom( + backgroundColor: const Color(0xFF2F9E94), + foregroundColor: Colors.white, + shape: const StadiumBorder(), + textStyle: const TextStyle(fontWeight: FontWeight.w900), + ), + onPressed: _loading ? null : _submit, + child: _loading + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text('Entrar'), ), - floatingLabelStyle: const TextStyle(color: primaryTeal, fontWeight: FontWeight.w700), ), - validator: (v) { - final value = (v ?? '').trim(); - if (value.isEmpty) return 'Informe seu email'; - if (!value.contains('@')) return 'Email inválido'; - return null; - }, - ), - const SizedBox(height: 12), - TextFormField( - controller: _passwordController, - obscureText: true, - textInputAction: TextInputAction.done, - decoration: InputDecoration( - labelText: 'Senha', - border: underlineBorder, - enabledBorder: underlineBorder, - focusedBorder: underlineBorder.copyWith( - borderSide: const BorderSide(color: primaryTeal, width: 1.6), - ), - floatingLabelStyle: const TextStyle(color: primaryTeal, fontWeight: FontWeight.w700), - ), - validator: (v) { - final value = (v ?? ''); - if (value.isEmpty) return 'Informe sua senha'; - if (value.length < 6) return 'Mínimo de 6 caracteres'; - return null; - }, - ), - const SizedBox(height: 16), - SizedBox( - height: 46, - child: FilledButton( - style: FilledButton.styleFrom( - backgroundColor: primaryTeal, - foregroundColor: const Color.fromARGB(255, 255, 255, 255), - shape: const StadiumBorder(), - textStyle: const TextStyle(fontWeight: FontWeight.w800), - ), - onPressed: _loading ? null : _submit, - child: _loading - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Text('Entrar'), - ), - ), - const SizedBox(height: 8), - TextButton( - onPressed: _loading ? null : () => Navigator.of(context).pop(), - child: const Text('Fechar'), ), ], ), - ), + ], ), ), ); @@ -239,17 +153,14 @@ class _LoginBottomSheetState extends State { final email = _emailController.text.trim(); final password = _passwordController.text; - await FirebaseAuth.instance.signInWithEmailAndPassword( - email: email, - password: password, - ); + await supabase.auth.signInWithPassword(email: email, password: password); if (!mounted) return; Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Login efetuado')), ); - } on FirebaseAuthException catch (e) { + } on AuthException catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(_friendlyAuthError(e))), @@ -264,18 +175,14 @@ class _LoginBottomSheetState extends State { } } - String _friendlyAuthError(FirebaseAuthException e) { + String _friendlyAuthError(AuthException e) { switch (e.code) { - case 'invalid-email': - return 'Email inválido.'; - case 'user-disabled': - return 'Usuário desativado.'; - case 'user-not-found': - case 'wrong-password': - case 'invalid-credential': + case 'invalid_credentials': return 'Email ou senha incorretos.'; + case 'user_not_found': + return 'Usuário não encontrado.'; default: - return e.message ?? 'Falha de autenticação.'; + return e.message; } } } diff --git a/lib/login_register/register_sheet.dart b/lib/login_register/register_sheet.dart index 71d356c..d0af41e 100644 --- a/lib/login_register/register_sheet.dart +++ b/lib/login_register/register_sheet.dart @@ -1,90 +1,22 @@ -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; -import 'package:lottie/lottie.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; import 'dart:async'; -import 'dart:math' as math; + +import '../main.dart' show supabase; Future showRegisterSheet(BuildContext context) { return showModalBottomSheet( context: context, isScrollControlled: true, - backgroundColor: Colors.transparent, + showDragHandle: true, + backgroundColor: const Color(0xFFFFE6F1), shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), - builder: (ctx) => const _AnimatedAuthSheet(child: RegisterBottomSheet()), + builder: (ctx) => const RegisterBottomSheet(), ); } -class _AnimatedAuthSheet extends StatelessWidget { - const _AnimatedAuthSheet({required this.child}); - - final Widget child; - - @override - Widget build(BuildContext context) { - final size = MediaQuery.sizeOf(context); - const topRadius = Radius.circular(20); - - return TweenAnimationBuilder( - tween: Tween(begin: 0.0, end: 1.0), - duration: const Duration(milliseconds: 260), - curve: Curves.easeOutCubic, - builder: (context, t, w) { - return Opacity( - opacity: t, - child: Transform.translate(offset: Offset(0, (1 - t) * 12), child: w), - ); - }, - child: ClipRRect( - borderRadius: const BorderRadius.vertical(top: topRadius), - child: Material( - color: Colors.transparent, - child: Stack( - clipBehavior: Clip.none, - children: [ - Positioned.fill( - child: Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Color(0xFFFFE6F1), Color(0xFFFFC9DF)], - ), - ), - ), - ), - Positioned( - left: -size.width * 0.38, - bottom: -size.width * 0.45, - child: IgnorePointer( - child: SizedBox( - width: size.width * 1.05, - height: size.width * 1.05, - child: Transform.rotate( - angle: 28 * math.pi / 180, - child: Opacity( - opacity: 0.95, - child: Lottie.asset( - 'lottie/Liquid waves.json', - fit: BoxFit.cover, - repeat: true, - ), - ), - ), - ), - ), - ), - child, - ], - ), - ), - ), - ); - } -} - class RegisterBottomSheet extends StatefulWidget { const RegisterBottomSheet({super.key}); @@ -106,15 +38,11 @@ class _RegisterBottomSheetState extends State { required String name, required String email, }) async { - await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .set({ - 'name': name, - 'email': email, - 'createdAt': FieldValue.serverTimestamp(), - }, SetOptions(merge: true)) - .timeout(const Duration(seconds: 20)); + await supabase.from('profiles').upsert({ + 'id': uid, + 'name': name, + 'email': email, + }).timeout(const Duration(seconds: 20)); } @override @@ -127,162 +55,120 @@ class _RegisterBottomSheetState extends State { @override Widget build(BuildContext context) { - final viewInsets = MediaQuery.viewInsetsOf(context); - const accentPink = Color(0xFFFF55A7); - const primaryTeal = Color(0xFF2F9E94); - final underlineBorder = UnderlineInputBorder( - borderSide: BorderSide(color: Colors.black.withValues(alpha: 0.20)), - ); - - return Padding( - padding: EdgeInsets.only( - left: 16, - right: 16, - top: 12, - bottom: 16 + viewInsets.bottom, - ), - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 560), - child: SingleChildScrollView( - child: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, + final bottomInset = MediaQuery.viewInsetsOf(context).bottom; + return SafeArea( + child: Padding( + padding: EdgeInsets.fromLTRB(18, 6, 18, 18 + bottomInset), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + 'Criar conta', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w900, + color: Color(0xFFFF55A7), + ), + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.82), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.black.withValues(alpha: 0.08)), + ), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + controller: _nameController, + textInputAction: TextInputAction.next, + decoration: const InputDecoration(labelText: 'Nome'), + validator: (v) { + if (v == null || v.trim().isEmpty) { + return 'Informe seu nome'; + } + if (v.trim().length < 2) { + return 'Nome muito curto'; + } + return null; + }, + ), + TextFormField( + controller: _emailController, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.next, + decoration: const InputDecoration(labelText: 'Email'), + validator: (v) { + final value = (v ?? '').trim(); + if (value.isEmpty) return 'Informe seu email'; + if (!value.contains('@')) return 'Email inválido'; + return null; + }, + ), + TextFormField( + controller: _passwordController, + obscureText: true, + textInputAction: TextInputAction.done, + decoration: const InputDecoration(labelText: 'Senha'), + validator: (v) { + final value = (v ?? ''); + if (value.isEmpty) return 'Informe sua senha'; + if (value.length < 6) return 'Mínimo de 6 caracteres'; + return null; + }, + ), + ], + ), + ), + ), + const SizedBox(height: 14), + Row( children: [ - Center( - child: Container( - width: 46, - height: 5, - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.10), - borderRadius: BorderRadius.circular(99), + Expanded( + child: SizedBox( + height: 44, + child: TextButton( + onPressed: _loading + ? null + : () => Navigator.of(context).pop(), + child: const Text('Cancelar'), ), ), ), - const SizedBox(height: 14), - const Text( - 'Criar conta', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w800, - color: accentPink, - ), - ), - const SizedBox(height: 16), - TextFormField( - controller: _nameController, - textInputAction: TextInputAction.next, - decoration: InputDecoration( - labelText: 'Nome', - border: underlineBorder, - enabledBorder: underlineBorder, - focusedBorder: underlineBorder.copyWith( - borderSide: const BorderSide( - color: primaryTeal, - width: 1.6, + const SizedBox(width: 10), + Expanded( + child: SizedBox( + height: 44, + child: FilledButton( + style: FilledButton.styleFrom( + backgroundColor: const Color(0xFF2F9E94), + foregroundColor: Colors.white, + shape: const StadiumBorder(), + textStyle: const TextStyle(fontWeight: FontWeight.w900), ), - ), - floatingLabelStyle: const TextStyle( - color: primaryTeal, - fontWeight: FontWeight.w700, + onPressed: _loading ? null : _submit, + child: _loading + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text('Registrar'), ), ), - validator: (v) { - if (v == null || v.trim().isEmpty) { - return 'Informe seu nome'; - } - if (v.trim().length < 2) { - return 'Nome muito curto'; - } - return null; - }, - ), - const SizedBox(height: 12), - TextFormField( - controller: _emailController, - keyboardType: TextInputType.emailAddress, - textInputAction: TextInputAction.next, - decoration: InputDecoration( - labelText: 'Email', - border: underlineBorder, - enabledBorder: underlineBorder, - focusedBorder: underlineBorder.copyWith( - borderSide: const BorderSide( - color: primaryTeal, - width: 1.6, - ), - ), - floatingLabelStyle: const TextStyle( - color: primaryTeal, - fontWeight: FontWeight.w700, - ), - ), - validator: (v) { - final value = (v ?? '').trim(); - if (value.isEmpty) return 'Informe seu email'; - if (!value.contains('@')) return 'Email inválido'; - return null; - }, - ), - const SizedBox(height: 12), - TextFormField( - controller: _passwordController, - obscureText: true, - textInputAction: TextInputAction.done, - decoration: InputDecoration( - labelText: 'Senha', - border: underlineBorder, - enabledBorder: underlineBorder, - focusedBorder: underlineBorder.copyWith( - borderSide: const BorderSide( - color: primaryTeal, - width: 1.6, - ), - ), - floatingLabelStyle: const TextStyle( - color: primaryTeal, - fontWeight: FontWeight.w700, - ), - ), - validator: (v) { - final value = (v ?? ''); - if (value.isEmpty) return 'Informe sua senha'; - if (value.length < 6) return 'Mínimo de 6 caracteres'; - return null; - }, - ), - const SizedBox(height: 16), - SizedBox( - height: 46, - child: FilledButton( - style: FilledButton.styleFrom( - backgroundColor: primaryTeal, - foregroundColor: Colors.white, - shape: const StadiumBorder(), - textStyle: const TextStyle(fontWeight: FontWeight.w800), - ), - onPressed: _loading ? null : _submit, - child: _loading - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Text('Registrar'), - ), - ), - const SizedBox(height: 8), - TextButton( - onPressed: _loading - ? null - : () => Navigator.of(context).pop(), - child: const Text('Fechar'), ), ], ), - ), + ], ), ), ); @@ -297,21 +183,25 @@ class _RegisterBottomSheetState extends State { final email = _emailController.text.trim(); final password = _passwordController.text; - final credential = await FirebaseAuth.instance - .createUserWithEmailAndPassword(email: email, password: password) + final response = await supabase.auth + .signUp( + email: email, + password: password, + data: {'name': name}, + ) .timeout(const Duration(seconds: 20)); - final user = credential.user; + final user = response.user; if (user == null) { throw StateError('Usuário não encontrado após criar conta.'); } - final uid = user.uid; + final uid = user.id; if (!mounted) return; // Fecha o sheet imediatamente após autenticar. - // As gravações no Firestore seguem em background para não travar a UI. + // As gravações no banco seguem em background para não travar a UI. Navigator.of(context).pop(); unawaited( @@ -321,7 +211,7 @@ class _RegisterBottomSheetState extends State { email: email, ).catchError((_) {}), ); - } on FirebaseAuthException catch (e) { + } on AuthException catch (e) { if (!mounted) return; ScaffoldMessenger.of( context, @@ -345,16 +235,15 @@ class _RegisterBottomSheetState extends State { } } - String _friendlyAuthError(FirebaseAuthException e) { + String _friendlyAuthError(AuthException e) { switch (e.code) { - case 'invalid-email': - return 'Email inválido.'; - case 'email-already-in-use': + case 'email_exists': + case 'user_already_exists': return 'Este email já está em uso.'; - case 'weak-password': + case 'weak_password': return 'Senha fraca. Use pelo menos 6 caracteres.'; default: - return e.message ?? 'Falha de autenticação.'; + return e.message; } } } diff --git a/lib/main.dart b/lib/main.dart index 4355eb0..1233bed 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,16 @@ -import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; import 'dart:async'; import 'gates/debug_launch_gate.dart'; +const String supabaseUrl = 'https://mannjismlhlwaqqqnvog.supabase.co'; +const String supabaseAnonKey = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im1hbm5qaXNtbGhsd2FxcXFudm9nIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzk3MjU3MjEsImV4cCI6MjA5NTMwMTcyMX0.hF5tD1n7t_WSDLIesu1xcBHhzHouAVUCP8tUDDwUmDw'; + +SupabaseClient get supabase => Supabase.instance.client; + Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -14,7 +20,7 @@ Future main() async { }; runZonedGuarded(() async { - await Firebase.initializeApp(); + await Supabase.initialize(url: supabaseUrl, publishableKey: supabaseAnonKey); runApp(const MyApp()); }, (error, stack) { debugPrint('UNCAUGHT: $error'); diff --git a/lib/quiz/README_QUIZ.md b/lib/quiz/README_QUIZ.md deleted file mode 100644 index 2066eaa..0000000 --- a/lib/quiz/README_QUIZ.md +++ /dev/null @@ -1,140 +0,0 @@ -# Sistema de Quiz Extendido - Check-Teeth Kids - -## Novos Arquivos Criados - -### 1. `quiz_extended.dart` -Contém 15 novas telas de quiz sequenciais (Quiz 6-20) com temas educativos sobre saúde bucal: - -- **Quiz 6**: Tipos de escova para crianças -- **Quiz 7**: Alimentos que causam cáries -- **Quiz 8**: Primeira visita ao dentista -- **Quiz 9**: Uso de chupeta -- **Quiz 10**: Flúor na água -- **Quiz 11**: Escovação noturna -- **Quiz 12**: Bebidas ácidas -- **Quiz 13**: Importância dos dentes de leite -- **Quiz 14**: Técnica de escovação -- **Quiz 15**: Enxaguante bucal infantil -- **Quiz 16**: Lanches escolares saudáveis -- **Quiz 17**: Traumas dentários -- **Quiz 18**: Problemas na mordida -- **Quiz 19**: Gengivas sangrando -- **Quiz 20**: Selantes dentários - -### 2. `quiz_random.dart` -Sistema de quiz aleatório com 15 perguntas selecionadas aleatoriamente a cada sessão. - -## Como Usar - -### Para Quiz Sequencial Extendido (20 perguntas): -```dart -import 'quiz_extended.dart'; - -// Para iniciar do Quiz 6: -Navigator.push( - context, - MaterialPageRoute(builder: (_) => const Quiz6Screen()), -); - -// Para conectar ao final do Quiz 5, modifique quiz5.dart: -nextRoute: (context, nextScore) => MaterialPageRoute( - builder: (_) => Quiz6Screen(currentScore: nextScore, scopeId: scopeId), -), -``` - -### Para Quiz Aleatório (15 perguntas): -```dart -import 'quiz_random.dart'; - -Navigator.push( - context, - MaterialPageRoute(builder: (_) => const QuizRandomScreen()), -); -``` - -## Sistema de Pontuação - -- **Quiz Sequencial**: 20 perguntas × 5 pontos = máximo 100 pontos -- **Quiz Aleatório**: 15 perguntas × 5 pontos = máximo 75 pontos -- **Sistema de pesos**: 2 (melhor) a 5 (pior) pontos - -## Estrutura das Perguntas - -Cada quiz segue o padrão: -```dart -QuizQuestionScreen( - title: 'Quiz X/20', - question: 'Pergunta educativa...', - answers: [ - QuizAnswer(title: 'Resposta A', description: 'Explicação...', weight: 2), - QuizAnswer(title: 'Resposta B', description: 'Explicação...', weight: 5), - QuizAnswer(title: 'Resposta C', description: 'Explicação...', weight: 3), - ], - currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute(...), - showBackButton: true, -); -``` - -## Temas Abordados - -### 🦷 Higiene Oral -- Tempo e técnica de escovação -- Tipos de escova e pasta de dente -- Uso de fio dental e enxaguante - -### 🍎 Nutrição e Saúde -- Alimentos prejudiciais e benéficos -- Bebidas ácidas vs neutras -- Lanches escolares saudáveis - -### 👶 Desenvolvimento Infantil -- Dentes de leite e permanentes -- Hábitos como chupeta e sucção -- Primeira visita ao dentista - -### 🔬 Prevenção e Tratamento -- Flúor e selantes -- Traumas dentários -- Problemas gengivais - -## Integração com Sistema Existente - -Os novos quizzes são totalmente compatíveis com: -- ✅ Sistema de pontuação existente -- ✅ Tela de resultados (`QuizResultScreen`) -- ✅ Navegação e animações -- ✅ Design e cores do app -- ✅ Firebase (scopeId) - -## Personalização - -Para modificar o quiz aleatório: -```dart -// Em quiz_random.dart, altere o número de perguntas: -final List _selectedQuestions = _allQuestions.take(10).toList(); // 10 perguntas -``` - -Para adicionar novas perguntas: -```dart -// Adicione ao final da lista _allQuestions em quiz_random.dart -QuizQuestion( - id: 16, - title: 'Quiz 16/15', - question: 'Nova pergunta...', - answers: [...], -), -``` - -## Teste e Validação - -Os arquivos foram testados com: -- ✅ `flutter analyze` - sem erros -- ✅ Estrutura compatível com código existente -- ✅ Importações corretas -- ✅ Navegação funcional - ---- - -*Criado em 01/05/2026* -*Total de perguntas: 35 (5 originais + 15 sequenciais + 15 aleatórias)* diff --git a/lib/quiz/quiz1.dart b/lib/quiz/quiz1.dart index de88520..1464f55 100644 --- a/lib/quiz/quiz1.dart +++ b/lib/quiz/quiz1.dart @@ -14,43 +14,29 @@ class Quiz1Screen extends StatelessWidget { Widget build(BuildContext context) { return QuizQuestionScreen( title: 'Quiz 1/26', - question: - 'Qual das seguintes imagens se assemelha à face do seu filho/a?', + question: 'O rosto do seu filho/a se parece com o desta imagem?', + questionImagePaths: const ['assets/mockup_images/2.jpeg'], + suggestedYoutubeId: '', // TODO: colar o ID do YouTube do Episódio 1 + suggestedVideoTitle: 'Ver vídeo: Episódio 1', answers: const [ QuizAnswer( - title: 'Opção A', - description: - 'Selecione se a imagem se assemelha à face do seu filho/a', + title: 'Sim', + description: 'O rosto se assemelha à imagem', weight: 2, - imagePath: 'assets/images/face_a.png', + value: 'sim', ), QuizAnswer( - title: 'Opção B', - description: - 'Selecione se a imagem se assemelha à face do seu filho/a', - weight: 2, - imagePath: 'assets/images/face_b.png', - ), - QuizAnswer( - title: 'Opção C', - description: - 'Selecione se a imagem se assemelha à face do seu filho/a', - weight: 2, - imagePath: 'assets/images/face_c.png', - ), - QuizAnswer( - title: 'Opção D', - description: - 'Selecione se a imagem se assemelha à face do seu filho/a', - weight: 2, - imagePath: 'assets/images/face_d.png', + title: 'Não', + description: 'O rosto não se assemelha à imagem', + weight: 1, + value: 'nao', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz2Screen(currentScore: nextScore, scopeId: scopeId), ), - answerType: QuizAnswerType.image, + answerType: QuizAnswerType.yesNo, showBackButton: false, ); } @@ -68,42 +54,29 @@ class Quiz2Screen extends StatelessWidget { return QuizQuestionScreen( title: 'Quiz 2/26', question: - 'Qual das seguintes imagens se assemelha à boca do seu filho/a?', + 'A boca do seu filho/a fica habitualmente na posição desta imagem (entreaberta)?', + questionImagePaths: const ['assets/mockup_images/4.jpeg'], + suggestedYoutubeId: '', // TODO: colar o ID do YouTube do Episódio 2 + suggestedVideoTitle: 'Ver vídeo: Episódio 2', answers: const [ QuizAnswer( - title: 'Opção A', - description: - 'Selecione se a imagem se assemelha à boca do seu filho/a', + title: 'Sim', + description: 'A boca fica habitualmente entreaberta', weight: 2, - imagePath: 'assets/images/mouth_a.png', + value: 'sim', ), QuizAnswer( - title: 'Opção B', - description: - 'Selecione se a imagem se assemelha à boca do seu filho/a', - weight: 2, - imagePath: 'assets/images/mouth_b.png', - ), - QuizAnswer( - title: 'Opção C', - description: - 'Selecione se a imagem se assemelha à boca do seu filho/a', - weight: 2, - imagePath: 'assets/images/mouth_c.png', - ), - QuizAnswer( - title: 'Opção D', - description: - 'Selecione se a imagem se assemelha à boca do seu filho/a', - weight: 2, - imagePath: 'assets/images/mouth_d.png', + title: 'Não', + description: 'A boca fica habitualmente fechada', + weight: 1, + value: 'nao', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz3Screen(currentScore: nextScore, scopeId: scopeId), ), - answerType: QuizAnswerType.image, + answerType: QuizAnswerType.yesNo, showBackButton: true, ); } @@ -120,43 +93,29 @@ class Quiz3Screen extends StatelessWidget { Widget build(BuildContext context) { return QuizQuestionScreen( title: 'Quiz 3/26', - question: - 'Qual das seguintes imagens se assemelha às olheiras do seu filho/a?', + question: 'O seu filho/a tem olheiras semelhantes às desta imagem?', + questionImagePaths: const ['assets/mockup_images/8.jpeg'], + suggestedYoutubeId: '', // TODO: colar o ID do YouTube do Episódio 3 + suggestedVideoTitle: 'Ver vídeo: Episódio 3', answers: const [ QuizAnswer( - title: 'Opção A', - description: - 'Selecione se a imagem se assemelha às olheiras do seu filho/a', + title: 'Sim', + description: 'Tem olheiras semelhantes à imagem', weight: 2, - imagePath: 'assets/images/dark_circles_a.png', + value: 'sim', ), QuizAnswer( - title: 'Opção B', - description: - 'Selecione se a imagem se assemelha às olheiras do seu filho/a', - weight: 2, - imagePath: 'assets/images/dark_circles_b.png', - ), - QuizAnswer( - title: 'Opção C', - description: - 'Selecione se a imagem se assemelha às olheiras do seu filho/a', - weight: 2, - imagePath: 'assets/images/dark_circles_c.png', - ), - QuizAnswer( - title: 'Opção D', - description: - 'Selecione se a imagem se assemelha às olheiras do seu filho/a', - weight: 2, - imagePath: 'assets/images/dark_circles_d.png', + title: 'Não', + description: 'Não tem olheiras semelhantes à imagem', + weight: 1, + value: 'nao', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz4Screen(currentScore: nextScore, scopeId: scopeId), ), - answerType: QuizAnswerType.image, + answerType: QuizAnswerType.yesNo, showBackButton: true, ); } @@ -174,42 +133,29 @@ class Quiz4Screen extends StatelessWidget { return QuizQuestionScreen( title: 'Quiz 4/26', question: - 'Qual das seguintes imagens se assemelha ao queixo do seu filho/a com a boca fechada?', + 'Com a boca fechada, o queixo do seu filho/a se parece com o desta imagem?', + questionImagePaths: const ['assets/mockup_images/6.jpeg'], + suggestedYoutubeId: '', // TODO: colar o ID do YouTube do Episódio 4 + suggestedVideoTitle: 'Ver vídeo: Episódio 4', answers: const [ QuizAnswer( - title: 'Opção A', - description: - 'Selecione se a imagem se assemelha ao queixo do seu filho/a', + title: 'Sim', + description: 'O queixo se assemelha à imagem', weight: 2, - imagePath: 'assets/images/chin_a.png', + value: 'sim', ), QuizAnswer( - title: 'Opção B', - description: - 'Selecione se a imagem se assemelha ao queixo do seu filho/a', - weight: 2, - imagePath: 'assets/images/chin_b.png', - ), - QuizAnswer( - title: 'Opção C', - description: - 'Selecione se a imagem se assemelha ao queixo do seu filho/a', - weight: 2, - imagePath: 'assets/images/chin_c.png', - ), - QuizAnswer( - title: 'Opção D', - description: - 'Selecione se a imagem se assemelha ao queixo do seu filho/a', - weight: 2, - imagePath: 'assets/images/chin_d.png', + title: 'Não', + description: 'O queixo não se assemelha à imagem', + weight: 1, + value: 'nao', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz5Screen(currentScore: nextScore, scopeId: scopeId), ), - answerType: QuizAnswerType.image, + answerType: QuizAnswerType.yesNo, showBackButton: true, ); } @@ -272,43 +218,29 @@ class Quiz7Screen extends StatelessWidget { Widget build(BuildContext context) { return QuizQuestionScreen( title: 'Quiz 7/26', - question: - 'Qual das seguintes imagens se assemelha à boca do seu filho/a?', + question: 'A boca do seu filho/a se parece com a desta imagem?', + questionImagePaths: const ['assets/mockup_images/14.jpeg'], + suggestedYoutubeId: '', // TODO: colar o ID do YouTube do Episódio 5 + suggestedVideoTitle: 'Ver vídeo: Episódio 5', answers: const [ QuizAnswer( - title: 'Opção A', - description: - 'Selecione se a imagem se assemelha à boca do seu filho/a', + title: 'Sim', + description: 'A boca se assemelha à imagem', weight: 2, - imagePath: 'assets/images/mouth2_a.png', + value: 'sim', ), QuizAnswer( - title: 'Opção B', - description: - 'Selecione se a imagem se assemelha à boca do seu filho/a', - weight: 2, - imagePath: 'assets/images/mouth2_b.png', - ), - QuizAnswer( - title: 'Opção C', - description: - 'Selecione se a imagem se assemelha à boca do seu filho/a', - weight: 2, - imagePath: 'assets/images/mouth2_c.png', - ), - QuizAnswer( - title: 'Opção D', - description: - 'Selecione se a imagem se assemelha à boca do seu filho/a', - weight: 2, - imagePath: 'assets/images/mouth2_d.png', + title: 'Não', + description: 'A boca não se assemelha à imagem', + weight: 1, + value: 'nao', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz8Screen(currentScore: nextScore, scopeId: scopeId), ), - answerType: QuizAnswerType.image, + answerType: QuizAnswerType.yesNo, showBackButton: true, ); } @@ -326,42 +258,29 @@ class Quiz8Screen extends StatelessWidget { return QuizQuestionScreen( title: 'Quiz 8/26', question: - 'Qual das seguintes imagens se assemelha ao freio do seu filho/a?', + 'O frénulo (freio) da língua do seu filho/a se parece com o desta imagem?', + questionImagePaths: const ['assets/mockup_images/17.png'], + suggestedYoutubeId: '', // TODO: colar o ID do YouTube do Episódio 6 + suggestedVideoTitle: 'Ver vídeo: Episódio 6', answers: const [ QuizAnswer( - title: 'Opção A', - description: - 'Selecione se a imagem se assemelha ao freio do seu filho/a', + title: 'Sim', + description: 'O frénulo se assemelha à imagem', weight: 2, - imagePath: 'assets/images/frenulum_a.png', + value: 'sim', ), QuizAnswer( - title: 'Opção B', - description: - 'Selecione se a imagem se assemelha ao freio do seu filho/a', - weight: 2, - imagePath: 'assets/images/frenulum_b.png', - ), - QuizAnswer( - title: 'Opção C', - description: - 'Selecione se a imagem se assemelha ao freio do seu filho/a', - weight: 2, - imagePath: 'assets/images/frenulum_c.png', - ), - QuizAnswer( - title: 'Opção D', - description: - 'Selecione se a imagem se assemelha ao freio do seu filho/a', - weight: 2, - imagePath: 'assets/images/frenulum_d.png', + title: 'Não', + description: 'O frénulo não se assemelha à imagem', + weight: 1, + value: 'nao', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz9Screen(currentScore: nextScore, scopeId: scopeId), ), - answerType: QuizAnswerType.image, + answerType: QuizAnswerType.yesNo, showBackButton: true, ); } diff --git a/lib/quiz/quiz_question_screen.dart b/lib/quiz/quiz_question_screen.dart index c41eb6a..bee7f6f 100644 --- a/lib/quiz/quiz_question_screen.dart +++ b/lib/quiz/quiz_question_screen.dart @@ -3,6 +3,8 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:lottie/lottie.dart'; +import '../screens/video_screen.dart'; + typedef QuizNextBuilder = Route Function(BuildContext context, int nextScore); @@ -37,6 +39,9 @@ class QuizQuestionScreen extends StatefulWidget { this.showBackButton = false, this.answerType = QuizAnswerType.text, this.questionImagePaths = const [], + this.suggestedVideoPath, + this.suggestedYoutubeId, + this.suggestedVideoTitle, }); final String title; @@ -49,6 +54,9 @@ class QuizQuestionScreen extends StatefulWidget { final bool showBackButton; final QuizAnswerType answerType; final List questionImagePaths; + final String? suggestedVideoPath; + final String? suggestedYoutubeId; + final String? suggestedVideoTitle; @override State createState() => _QuizQuestionScreenState(); @@ -58,6 +66,7 @@ class _QuizQuestionScreenState extends State { int? _selected; TextEditingController? _numberController; int? _numberValue; + bool _navigating = false; @override void initState() { @@ -76,9 +85,9 @@ class _QuizQuestionScreenState extends State { @override Widget build(BuildContext context) { final size = MediaQuery.sizeOf(context); - bool canProceed = _selected != null; + bool canProceed = _selected != null && !_navigating; if (widget.answerType == QuizAnswerType.number) { - canProceed = _numberValue != null && _numberValue! >= 0; + canProceed = _numberValue != null && _numberValue! >= 0 && !_navigating; } return Scaffold( @@ -145,6 +154,35 @@ class _QuizQuestionScreenState extends State { ), const SizedBox(height: 10), ], + if (widget.suggestedVideoPath != null || + widget.suggestedYoutubeId != null) ...[ + TextButton.icon( + onPressed: () => showVideoPlayerDialog( + context, + VideoData( + id: 0, + title: + widget.suggestedVideoTitle ?? 'Vídeo', + description: '', + videoPath: widget.suggestedVideoPath, + youtubeId: widget.suggestedYoutubeId, + ), + ), + icon: const Icon( + Icons.play_circle_outline_rounded, + color: Color(0xFF2F9E94), + ), + label: Text( + widget.suggestedVideoTitle ?? + 'Ver vídeo (opcional)', + style: const TextStyle( + color: Color(0xFF2F9E94), + fontWeight: FontWeight.w800, + ), + ), + ), + const SizedBox(height: 4), + ], Text( widget.question, textAlign: TextAlign.center, @@ -239,6 +277,7 @@ class _QuizQuestionScreenState extends State { onPressed: !canProceed ? null : () { + setState(() => _navigating = true); int nextScore = widget.currentScore; if (widget.answerType == QuizAnswerType.number) { @@ -454,7 +493,7 @@ class _QuizAnswerTile extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ if (answer.imagePath != null) ...[ ClipRRect( @@ -478,19 +517,14 @@ class _QuizAnswerTile extends StatelessWidget { ), const SizedBox(height: 10), ], - Row( - children: [ - Expanded( - child: Text( - answer.title, - style: const TextStyle( - fontWeight: FontWeight.w900, - fontSize: 15, - color: Color(0xFF2F9E94), - ), - ), - ), - ], + Text( + answer.title, + textAlign: TextAlign.center, + style: const TextStyle( + fontWeight: FontWeight.w900, + fontSize: 15, + color: Color(0xFF2F9E94), + ), ), ], ), @@ -516,6 +550,7 @@ class _QuestionReferenceImages extends StatelessWidget { child: Image.asset( paths.first, fit: BoxFit.cover, + cacheWidth: 800, errorBuilder: (context, error, stackTrace) => _placeholder(), ), ), @@ -534,6 +569,7 @@ class _QuestionReferenceImages extends StatelessWidget { borderRadius: BorderRadius.circular(12), child: Image.asset( paths[i], + cacheWidth: 300, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => _placeholder(), ), diff --git a/lib/quiz/quiz_result.dart b/lib/quiz/quiz_result.dart index c49ab23..1ba30a9 100644 --- a/lib/quiz/quiz_result.dart +++ b/lib/quiz/quiz_result.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; import 'dart:async'; +import '../main.dart' show supabase; import 'quiz_prefs.dart'; class QuizResultScreen extends StatefulWidget { @@ -40,7 +39,7 @@ class _QuizResultScreenState extends State { maxScore: widget.maxScore, ); } else { - final uid = FirebaseAuth.instance.currentUser?.uid; + final uid = supabase.auth.currentUser?.id; if (uid != null && uid.trim().isNotEmpty) { await QuizPrefs.saveLastResultForUser( userId: uid, @@ -55,25 +54,23 @@ class _QuizResultScreenState extends State { } } - final uid = FirebaseAuth.instance.currentUser?.uid; + final uid = supabase.auth.currentUser?.id; final userId = (uid ?? '').trim(); if (userId.isNotEmpty && scope.isNotEmpty && scope.startsWith('${userId}_')) { final childId = scope.substring(userId.length + 1).trim(); if (childId.isNotEmpty) { - // Fire-and-forget: avoid blocking UI on Firestore (may hang offline). + // Fire-and-forget: avoid blocking UI on erros de rede. unawaited( - FirebaseFirestore.instance - .collection('users') - .doc(userId) - .collection('children') - .doc(childId) - .set({ - 'lastScore': widget.finalScore, - 'lastMaxScore': widget.maxScore, - 'lastQuizAt': FieldValue.serverTimestamp(), - }, SetOptions(merge: true)) + supabase + .from('children') + .update({ + 'last_score': widget.finalScore, + 'last_max_score': widget.maxScore, + 'last_quiz_at': DateTime.now().toIso8601String(), + }) + .eq('id', childId) .catchError((_) {}), ); } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart new file mode 100644 index 0000000..8971ea9 --- /dev/null +++ b/lib/screens/settings_screen.dart @@ -0,0 +1,212 @@ +import 'package:flutter/material.dart'; + +import '../main.dart' show supabase; +import '../widgets/app_dialogs.dart'; +import 'terms_screen.dart'; + +const Color _teal = Color(0xFF2F9E94); +const Color _accentPink = Color(0xFFFF55A7); + +/// Conteúdo da aba de Configurações, para ser embutido na bottom navigation +/// do LoggedHomeScreen (sem Scaffold/AppBar próprios). +class SettingsBody extends StatefulWidget { + const SettingsBody({super.key}); + + @override + State createState() => _SettingsBodyState(); +} + +class _SettingsBodyState extends State { + bool _deletingAccount = false; + + Future _signOut() async { + await supabase.auth.signOut(); + if (!mounted) return; + Navigator.of(context).popUntil((route) => route.isFirst); + } + + Future _confirmDeleteAccountData() async { + final messenger = ScaffoldMessenger.of(context); + final confirmed = await showConfirmDialog( + context, + title: 'Apagar dados da conta', + message: + 'Isso remove permanentemente seu perfil, crianças cadastradas e ' + 'fotos. Essa ação não pode ser desfeita. Deseja continuar?', + confirmLabel: 'Apagar', + confirmColor: _accentPink, + ); + + if (confirmed != true) return; + + final uid = supabase.auth.currentUser?.id; + if (uid == null) return; + + setState(() => _deletingAccount = true); + try { + await supabase.from('children').delete().eq('owner_id', uid); + await supabase.from('profiles').delete().eq('id', uid); + await supabase.auth.signOut(); + if (!mounted) return; + Navigator.of(context).popUntil((route) => route.isFirst); + } catch (e) { + messenger.showSnackBar(SnackBar(content: Text('Erro ao apagar: $e'))); + } finally { + if (mounted) setState(() => _deletingAccount = false); + } + } + + @override + Widget build(BuildContext context) { + final user = supabase.auth.currentUser; + final name = (user?.userMetadata?['name'] ?? '').toString().trim(); + final email = (user?.email ?? '').trim(); + + return ListView( + padding: const EdgeInsets.all(16), + children: [ + _SectionLabel('Conta'), + _SettingsCard( + children: [ + _InfoTile( + icon: Icons.person_outline_rounded, + title: name.isEmpty ? 'Sem nome' : name, + subtitle: email, + ), + const Divider(height: 1), + _ActionTile( + icon: Icons.logout_rounded, + title: 'Sair', + onTap: _signOut, + ), + ], + ), + const SizedBox(height: 20), + _SectionLabel('Sobre'), + _SettingsCard( + children: [ + _ActionTile( + icon: Icons.description_outlined, + title: 'Termos de Serviço', + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const TermsScreen()), + ), + ), + const Divider(height: 1), + const _InfoTile( + icon: Icons.info_outline_rounded, + title: 'Versão do app', + subtitle: '1.0.0', + ), + ], + ), + const SizedBox(height: 20), + _SectionLabel('Zona de risco'), + _SettingsCard( + children: [ + _ActionTile( + icon: Icons.delete_forever_rounded, + title: 'Apagar dados da conta', + titleColor: _accentPink, + loading: _deletingAccount, + onTap: _deletingAccount ? null : _confirmDeleteAccountData, + ), + ], + ), + const SizedBox(height: 12), + ], + ); + } +} + +class _SectionLabel extends StatelessWidget { + const _SectionLabel(this.text); + + final String text; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 4, bottom: 8), + child: Text( + text, + style: const TextStyle( + color: _teal, + fontWeight: FontWeight.w900, + fontSize: 14, + ), + ), + ); + } +} + +class _SettingsCard extends StatelessWidget { + const _SettingsCard({required this.children}); + + final List children; + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.white, + borderRadius: BorderRadius.circular(18), + elevation: 6, + shadowColor: Colors.black.withValues(alpha: 0.12), + child: Column(children: children), + ); + } +} + +class _InfoTile extends StatelessWidget { + const _InfoTile({required this.icon, required this.title, this.subtitle}); + + final IconData icon; + final String title; + final String? subtitle; + + @override + Widget build(BuildContext context) { + return ListTile( + leading: Icon(icon, color: _teal), + title: Text(title, style: const TextStyle(fontWeight: FontWeight.w800)), + subtitle: (subtitle == null || subtitle!.isEmpty) + ? null + : Text(subtitle!), + ); + } +} + +class _ActionTile extends StatelessWidget { + const _ActionTile({ + required this.icon, + required this.title, + required this.onTap, + this.titleColor, + this.loading = false, + }); + + final IconData icon; + final String title; + final VoidCallback? onTap; + final Color? titleColor; + final bool loading; + + @override + Widget build(BuildContext context) { + return ListTile( + leading: Icon(icon, color: titleColor ?? _teal), + title: Text( + title, + style: TextStyle(fontWeight: FontWeight.w800, color: titleColor), + ), + trailing: loading + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.chevron_right_rounded), + onTap: onTap, + ); + } +} diff --git a/lib/screens/terms_screen.dart b/lib/screens/terms_screen.dart new file mode 100644 index 0000000..b6e7aec --- /dev/null +++ b/lib/screens/terms_screen.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; + +class TermsScreen extends StatelessWidget { + const TermsScreen({super.key}); + + static const Color _teal = Color(0xFF2F9E94); + static const Color _accentPink = Color(0xFFFF55A7); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: _teal, + foregroundColor: Colors.white, + elevation: 0, + title: const Text( + 'Termos de Serviço', + style: TextStyle(fontWeight: FontWeight.w900), + ), + ), + body: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFFFFE6F1), Color(0xFFFFC9DF)], + ), + ), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + 'Termos de Serviço', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w900, + color: _accentPink, + ), + ), + const SizedBox(height: 16), + Expanded( + child: Container( + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.85), + borderRadius: BorderRadius.circular(20), + ), + child: const SingleChildScrollView( + child: Text( + 'Conteúdo dos Termos de Serviço em breve.\n\n' + 'Este espaço será preenchido com os termos de uso e ' + 'condições do Check-Teeth Kids.', + style: TextStyle( + fontSize: 14, + height: 1.5, + color: Colors.black87, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/video_screen.dart b/lib/screens/video_screen.dart index 8e0678a..cf69434 100644 --- a/lib/screens/video_screen.dart +++ b/lib/screens/video_screen.dart @@ -4,19 +4,24 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:lottie/lottie.dart'; import 'package:video_player/video_player.dart'; +import 'package:youtube_player_flutter/youtube_player_flutter.dart'; -// Video data structure - easily editable for future updates +// Video data structure - easily editable for future updates. +// Episódios 1-7 tocam via YouTube (não listado); preencha youtubeId ao subir +// cada vídeo. Episódios 8-13 continuam embutidos no app (assets/videos). class VideoData { final int id; final String title; final String description; - final String videoPath; + final String? videoPath; + final String? youtubeId; VideoData({ required this.id, required this.title, required this.description, - required this.videoPath, + this.videoPath, + this.youtubeId, }); } @@ -26,43 +31,43 @@ final List videoList = [ id: 1, title: 'Episódio 1', description: 'Aprenda sobre saúde bucal neste episódio', - videoPath: 'assets/videos/episodio_01.mp4', + youtubeId: '', // TODO: colar o ID do vídeo do YouTube (não listado) ), VideoData( id: 2, title: 'Episódio 2', description: 'Aprenda sobre saúde bucal neste episódio', - videoPath: 'assets/videos/episodio_02.mp4', + youtubeId: '', // TODO: colar o ID do vídeo do YouTube (não listado) ), VideoData( id: 3, title: 'Episódio 3', description: 'Aprenda sobre saúde bucal neste episódio', - videoPath: 'assets/videos/episodio_03.mp4', + youtubeId: '', // TODO: colar o ID do vídeo do YouTube (não listado) ), VideoData( id: 4, title: 'Episódio 4', description: 'Aprenda sobre saúde bucal neste episódio', - videoPath: 'assets/videos/episodio_04.mp4', + youtubeId: '', // TODO: colar o ID do vídeo do YouTube (não listado) ), VideoData( id: 5, title: 'Episódio 5', description: 'Aprenda sobre saúde bucal neste episódio', - videoPath: 'assets/videos/episodio_05.mp4', + youtubeId: '', // TODO: colar o ID do vídeo do YouTube (não listado) ), VideoData( id: 6, title: 'Episódio 6', description: 'Aprenda sobre saúde bucal neste episódio', - videoPath: 'assets/videos/episodio_06.mp4', + youtubeId: '', // TODO: colar o ID do vídeo do YouTube (não listado) ), VideoData( id: 7, title: 'Episódio 7', description: 'Aprenda sobre saúde bucal neste episódio', - videoPath: 'assets/videos/episodio_07.mp4', + youtubeId: '', // TODO: colar o ID do vídeo do YouTube (não listado) ), VideoData( id: 8, @@ -105,6 +110,25 @@ final List videoList = [ // Cache for video controllers to avoid re-initializing final Map _videoControllerCache = {}; +Future showVideoPlayerDialog(BuildContext context, VideoData video) { + if (video.youtubeId != null) { + if (video.youtubeId!.isEmpty) { + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Vídeo ainda não disponível'))); + return Future.value(); + } + return showDialog( + context: context, + builder: (context) => _YoutubePlayerDialog(video: video), + ); + } + return showDialog( + context: context, + builder: (context) => _VideoPlayerDialog(video: video), + ); +} + class VideoScreen extends StatefulWidget { const VideoScreen({super.key}); @@ -268,29 +292,41 @@ class _VideoScreenState extends State { } } -class _VideoButton extends StatefulWidget { - const _VideoButton({required this.video}); +/// Preview de um vídeo (frame local ou thumbnail do YouTube), reutilizável +/// em qualquer card que precise mostrar "a cara" de um episódio. +class VideoThumbnail extends StatefulWidget { + const VideoThumbnail({ + super.key, + required this.video, + this.borderRadius = 12, + this.iconSize = 48, + }); final VideoData video; + final double borderRadius; + final double iconSize; @override - State<_VideoButton> createState() => _VideoButtonState(); + State createState() => _VideoThumbnailState(); } -class _VideoButtonState extends State<_VideoButton> { +class _VideoThumbnailState extends State { VideoPlayerController? _controller; bool _isInitialized = false; + bool get _isYoutube => widget.video.youtubeId != null; + @override void initState() { super.initState(); - _initializeVideo(); + if (!_isYoutube) _initializeVideo(); } Future _initializeVideo() async { + final path = widget.video.videoPath!; // Check if controller exists in cache - if (_videoControllerCache.containsKey(widget.video.videoPath)) { - _controller = _videoControllerCache[widget.video.videoPath]; + if (_videoControllerCache.containsKey(path)) { + _controller = _videoControllerCache[path]; await _controller!.seekTo(const Duration(seconds: 2)); await _controller!.pause(); if (mounted) { @@ -302,12 +338,12 @@ class _VideoButtonState extends State<_VideoButton> { } // Create new controller and cache it - _controller = VideoPlayerController.asset(widget.video.videoPath); + _controller = VideoPlayerController.asset(path); try { await _controller!.initialize(); await _controller!.seekTo(const Duration(seconds: 2)); await _controller!.pause(); - _videoControllerCache[widget.video.videoPath] = _controller!; + _videoControllerCache[path] = _controller!; if (mounted) { setState(() { _isInitialized = true; @@ -332,6 +368,73 @@ class _VideoButtonState extends State<_VideoButton> { super.dispose(); } + @override + Widget build(BuildContext context) { + if (_isYoutube) { + final youtubeId = widget.video.youtubeId!; + if (youtubeId.isEmpty) { + return Center( + child: Icon( + Icons.hourglass_top_rounded, + size: widget.iconSize * 0.85, + color: Colors.black26, + ), + ); + } + return ClipRRect( + borderRadius: BorderRadius.circular(widget.borderRadius), + child: Image.network( + 'https://img.youtube.com/vi/$youtubeId/hqdefault.jpg', + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: (context, error, stackTrace) => Center( + child: Icon( + Icons.play_circle_fill_rounded, + size: widget.iconSize, + color: VideoScreen._accentPink, + ), + ), + ), + ); + } + return _isInitialized && _controller != null + ? ClipRRect( + borderRadius: BorderRadius.circular(widget.borderRadius), + child: FittedBox( + fit: BoxFit.cover, + child: SizedBox( + width: _controller!.value.size.width, + height: _controller!.value.size.height, + child: VideoPlayer(_controller!), + ), + ), + ) + : Center( + child: Icon( + Icons.play_circle_fill_rounded, + size: widget.iconSize, + color: VideoScreen._accentPink, + ), + ); + } +} + +class _VideoButton extends StatelessWidget { + const _VideoButton({required this.video}); + + final VideoData video; + + void _showVideoPlayer(BuildContext context, VideoData video) { + if (video.youtubeId == null && + _videoControllerCache.containsKey(video.videoPath)) { + // Dispose the cached controller to avoid codec conflict with dialog controller + _videoControllerCache[video.videoPath]!.dispose(); + _videoControllerCache.remove(video.videoPath); + } + showVideoPlayerDialog(context, video); + } + @override Widget build(BuildContext context) { return Material( @@ -341,7 +444,7 @@ class _VideoButtonState extends State<_VideoButton> { color: Colors.white, child: InkWell( borderRadius: BorderRadius.circular(16), - onTap: () => _showVideoPlayer(context, widget.video), + onTap: () => _showVideoPlayer(context, video), child: Padding( padding: const EdgeInsets.all(12), child: Column( @@ -353,29 +456,11 @@ class _VideoButtonState extends State<_VideoButton> { color: const Color(0xFFFFE6F1), borderRadius: BorderRadius.circular(12), ), - child: _isInitialized && _controller != null - ? ClipRRect( - borderRadius: BorderRadius.circular(12), - child: FittedBox( - fit: BoxFit.cover, - child: SizedBox( - width: _controller!.value.size.width, - height: _controller!.value.size.height, - child: VideoPlayer(_controller!), - ), - ), - ) - : const Center( - child: Icon( - Icons.play_circle_fill_rounded, - size: 48, - color: VideoScreen._accentPink, - ), - ), + child: VideoThumbnail(video: video), ), const SizedBox(height: 10), Text( - widget.video.title, + video.title, style: const TextStyle( fontWeight: FontWeight.w900, fontSize: 14, @@ -386,7 +471,7 @@ class _VideoButtonState extends State<_VideoButton> { ), const SizedBox(height: 4), Text( - widget.video.description, + video.description, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, @@ -401,17 +486,69 @@ class _VideoButtonState extends State<_VideoButton> { ), ); } +} - void _showVideoPlayer(BuildContext context, VideoData video) { - // Dispose the cached controller to avoid codec conflict with dialog controller - if (_videoControllerCache.containsKey(video.videoPath)) { - _videoControllerCache[video.videoPath]!.dispose(); - _videoControllerCache.remove(video.videoPath); - } - _controller = null; - showDialog( - context: context, - builder: (context) => _VideoPlayerDialog(video: video), +class _YoutubePlayerDialog extends StatefulWidget { + const _YoutubePlayerDialog({required this.video}); + + final VideoData video; + + @override + State<_YoutubePlayerDialog> createState() => _YoutubePlayerDialogState(); +} + +class _YoutubePlayerDialogState extends State<_YoutubePlayerDialog> { + late final YoutubePlayerController _controller; + + @override + void initState() { + super.initState(); + _controller = YoutubePlayerController( + initialVideoId: widget.video.youtubeId!, + flags: const YoutubePlayerFlags(autoPlay: true, mute: false), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.sizeOf(context); + return Dialog( + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.all(16), + child: Container( + decoration: BoxDecoration( + color: VideoScreen._accentPink.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: VideoScreen._accentPink, width: 3), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(21), + child: SizedBox( + width: size.width * 0.9, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + YoutubePlayer(controller: _controller), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + color: VideoScreen._accentPink.withValues(alpha: 0.15), + alignment: Alignment.centerRight, + child: IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: () => Navigator.of(context).pop(), + ), + ), + ], + ), + ), + ), + ), ); } } @@ -436,7 +573,7 @@ class _VideoPlayerDialogState extends State<_VideoPlayerDialog> { } Future _initializeVideo() async { - _controller = VideoPlayerController.asset(widget.video.videoPath); + _controller = VideoPlayerController.asset(widget.video.videoPath!); try { await _controller.initialize(); if (mounted) { @@ -502,7 +639,7 @@ class _VideoPlayerDialogState extends State<_VideoPlayerDialog> { if (_isInitialized) _VideoControls( controller: _controller, - videoPath: widget.video.videoPath, + videoPath: widget.video.videoPath!, onClose: () => Navigator.of(context).pop(), ), ], diff --git a/lib/widgets/app_dialogs.dart b/lib/widgets/app_dialogs.dart new file mode 100644 index 0000000..3046715 --- /dev/null +++ b/lib/widgets/app_dialogs.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +const Color _teal = Color(0xFF2F9E94); +const Color _accentPink = Color(0xFFFF55A7); + +/// Diálogo de confirmação com a identidade visual do app (título rosa, +/// botões em pílula), usado para todas as confirmações destrutivas/decisórias. +Future showConfirmDialog( + BuildContext context, { + required String title, + String? message, + String cancelLabel = 'Cancelar', + required String confirmLabel, + Color confirmColor = _teal, +}) { + return showDialog( + context: context, + builder: (ctx) { + return AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: Text( + title, + style: const TextStyle(fontWeight: FontWeight.w900, color: _accentPink), + ), + content: message == null ? null : Text(message), + actions: [ + TextButton( + style: TextButton.styleFrom(foregroundColor: _teal), + onPressed: () => Navigator.of(ctx).pop(false), + child: Text(cancelLabel), + ), + FilledButton( + style: FilledButton.styleFrom( + backgroundColor: confirmColor, + foregroundColor: Colors.white, + shape: const StadiumBorder(), + textStyle: const TextStyle(fontWeight: FontWeight.w800), + ), + onPressed: () => Navigator.of(ctx).pop(true), + child: Text(confirmLabel), + ), + ], + ); + }, + ); +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 64a0ece..e12c657 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,17 @@ #include "generated_plugin_registrant.h" #include +#include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2db3c22..4453582 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,8 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux + gtk + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 765592c..5cc240e 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,22 +5,18 @@ import FlutterMacOS import Foundation -import cloud_firestore +import app_links import file_selector_macos -import firebase_auth -import firebase_core -import firebase_storage import flutter_inappwebview_macos import shared_preferences_foundation +import url_launcher_macos import video_player_avfoundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) + AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) - FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) - FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) - FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 3df9986..081dbc6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,14 +1,38 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - _flutterfire_internals: + app_links: dependency: transitive description: - name: _flutterfire_internals - sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11 + name: app_links + sha256: "3462d9defc61565fde4944858b59bec5be2b9d5b05f20aed190adb3ad08a7abc" url: "https://pub.dev" source: hosted - version: "1.3.59" + version: "7.0.0" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" archive: dependency: transitive description: @@ -45,10 +69,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -73,30 +97,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" - cloud_firestore: - dependency: "direct main" - description: - name: cloud_firestore - sha256: "2d33da4465bdb81b6685c41b535895065adcb16261beb398f5f3bbc623979e9c" - url: "https://pub.dev" - source: hosted - version: "5.6.12" - cloud_firestore_platform_interface: - dependency: transitive - description: - name: cloud_firestore_platform_interface - sha256: "413c4e01895cf9cb3de36fa5c219479e06cd4722876274ace5dfc9f13ab2e39b" - url: "https://pub.dev" - source: hosted - version: "6.6.12" - cloud_firestore_web: - dependency: transitive - description: - name: cloud_firestore_web - sha256: c1e30fc4a0fcedb08723fb4b1f12ee4e56d937cbf9deae1bda43cbb6367bb4cf - url: "https://pub.dev" - source: hosted - version: "4.4.12" collection: dependency: transitive description: @@ -105,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" cross_file: dependency: transitive description: @@ -137,6 +145,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dart_jsonwebtoken: + dependency: transitive + description: + name: dart_jsonwebtoken + sha256: ad84e60181696513d04d5f2078e0bbc20365b911f46f647797317414bdc88fbe + url: "https://pub.dev" + source: hosted + version: "3.4.1" fake_async: dependency: transitive description: @@ -193,78 +209,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3+5" - firebase_auth: - dependency: "direct main" - description: - name: firebase_auth - sha256: "0fed2133bee1369ee1118c1fef27b2ce0d84c54b7819a2b17dada5cfec3b03ff" - url: "https://pub.dev" - source: hosted - version: "5.7.0" - firebase_auth_platform_interface: - dependency: transitive - description: - name: firebase_auth_platform_interface - sha256: "871c9df4ec9a754d1a793f7eb42fa3b94249d464cfb19152ba93e14a5966b386" - url: "https://pub.dev" - source: hosted - version: "7.7.3" - firebase_auth_web: - dependency: transitive - description: - name: firebase_auth_web - sha256: d9ada769c43261fd1b18decf113186e915c921a811bd5014f5ea08f4cf4bc57e - url: "https://pub.dev" - source: hosted - version: "5.15.3" - firebase_core: - dependency: "direct main" - description: - name: firebase_core - sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5" - url: "https://pub.dev" - source: hosted - version: "3.15.2" - firebase_core_platform_interface: - dependency: transitive - description: - name: firebase_core_platform_interface - sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64 - url: "https://pub.dev" - source: hosted - version: "6.0.2" - firebase_core_web: - dependency: transitive - description: - name: firebase_core_web - sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37" - url: "https://pub.dev" - source: hosted - version: "2.24.1" - firebase_storage: - dependency: "direct main" - description: - name: firebase_storage - sha256: "958fc88a7ef0b103e694d30beed515c8f9472dde7e8459b029d0e32b8ff03463" - url: "https://pub.dev" - source: hosted - version: "12.4.10" - firebase_storage_platform_interface: - dependency: transitive - description: - name: firebase_storage_platform_interface - sha256: d2661c05293c2a940c8ea4bc0444e1b5566c79dd3202c2271140c082c8cd8dd4 - url: "https://pub.dev" - source: hosted - version: "5.2.10" - firebase_storage_web: - dependency: transitive - description: - name: firebase_storage_web - sha256: "629a557c5e1ddb97a3666cbf225e97daa0a66335dbbfdfdce113ef9f881e833f" - url: "https://pub.dev" - source: hosted - version: "3.10.17" flutter: dependency: "direct main" description: flutter @@ -368,6 +312,30 @@ packages: description: flutter source: sdk version: "0.0.0" + functions_client: + dependency: transitive + description: + name: functions_client + sha256: e9685e9ab852a8b8e0579c867f6c9155da27317b294b3df796a536b8b8e0d253 + url: "https://pub.dev" + source: hosted + version: "2.6.4" + gotrue: + dependency: transitive + description: + name: gotrue + sha256: ac6b664b35304aece2034c66bc09ce761f9f7c8fb6066bbb6b2932aa87d82653 + url: "https://pub.dev" + source: hosted + version: "2.25.0" + gtk: + dependency: transitive + description: + name: gtk + sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5" + url: "https://pub.dev" + source: hosted + version: "2.2.0" html: dependency: transitive description: @@ -472,6 +440,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.9.0" + jwt_decode: + dependency: transitive + description: + name: jwt_decode + sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb + url: "https://pub.dev" + source: hosted + version: "0.3.1" leak_tracker: dependency: transitive description: @@ -504,6 +480,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" lottie: dependency: "direct main" description: @@ -516,18 +500,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: @@ -544,6 +528,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + passkeys_platform_interface: + dependency: transitive + description: + name: passkeys_platform_interface + sha256: "9610bd136b3382500390912ddd8517ee99505228b1af7b507b9c907f7e99a47d" + url: "https://pub.dev" + source: hosted + version: "2.8.0" path: dependency: transitive description: @@ -600,6 +592,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" + url: "https://pub.dev" + source: hosted + version: "4.0.0" posix: dependency: transitive description: @@ -608,6 +608,38 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.3" + postgrest: + dependency: transitive + description: + name: postgrest + sha256: "10e3f195e131eec944fa872644aed89b33b5be998f3fc77c87325db3927bc646" + url: "https://pub.dev" + source: hosted + version: "2.8.0" + realtime_client: + dependency: transitive + description: + name: realtime_client + sha256: cf816d406248a6286bf7aa2053b406583ff88bd0842ab7f3f3127d53f6f1c68b + url: "https://pub.dev" + source: hosted + version: "2.10.0" + retry: + dependency: transitive + description: + name: retry + sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" shared_preferences: dependency: "direct main" description: @@ -685,6 +717,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.12.1" + storage_client: + dependency: transitive + description: + name: storage_client + sha256: "221263cfbe0c01575b7d7fe7543e44abff380eb4d38d936372844a1caa85ba12" + url: "https://pub.dev" + source: hosted + version: "2.6.0" stream_channel: dependency: transitive description: @@ -701,6 +741,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + supabase: + dependency: transitive + description: + name: supabase + sha256: "6933e60614652fa10749eb29fdcc6f2adb6515f5638e5ce28376ae08a626d745" + url: "https://pub.dev" + source: hosted + version: "2.13.4" + supabase_flutter: + dependency: "direct main" + description: + name: supabase_flutter + sha256: b6d34558565c14f1b44ff7bc9ca2979ed38ee2907b80ed73e699150cec08b739 + url: "https://pub.dev" + source: hosted + version: "2.15.4" term_glyph: dependency: transitive description: @@ -713,10 +769,10 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.7" typed_data: dependency: transitive description: @@ -725,6 +781,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c" + url: "https://pub.dev" + source: hosted + version: "6.3.30" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" vector_math: dependency: transitive description: @@ -789,6 +909,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" xdg_directories: dependency: transitive description: @@ -813,6 +949,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.3" + yet_another_json_isolate: + dependency: transitive + description: + name: yet_another_json_isolate + sha256: eaa26beb5990b25a49d942374fd5a0c5aa67a837e03b14b4c26134aaa1ed01a9 + url: "https://pub.dev" + source: hosted + version: "2.1.1" youtube_player_flutter: dependency: "direct main" description: @@ -823,4 +967,4 @@ packages: version: "9.1.3" sdks: dart: ">=3.10.4 <4.0.0" - flutter: ">=3.38.0" + flutter: ">=3.38.1" diff --git a/pubspec.yaml b/pubspec.yaml index 362acdf..bd98eaa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,10 +34,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 - firebase_core: ^3.15.0 - firebase_auth: ^5.7.0 - cloud_firestore: ^5.6.10 - firebase_storage: ^12.4.10 + supabase_flutter: ^2.8.0 image_picker: ^1.1.2 lottie: ^3.3.1 shared_preferences: ^2.3.2 @@ -72,9 +69,14 @@ flutter: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg - lottie/ - - assets/ + - assets/Check-theeth.png - assets/mockup_images/ - - assets/videos/ + - assets/videos/episodio_08.mp4 + - assets/videos/episodio_09.mp4 + - assets/videos/episodio_10.mp4 + - assets/videos/episodio_11.mp4 + - assets/videos/episodio_12.mp4 + - assets/videos/episodio_13.mp4 flutter_launcher_icons: android: true diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index a98e2e1..e826e00 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,24 +6,18 @@ #include "generated_plugin_registrant.h" -#include +#include #include -#include -#include -#include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { - CloudFirestorePluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("CloudFirestorePluginCApi")); + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); - FirebaseAuthPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); - FirebaseCorePluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); - FirebaseStoragePluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FirebaseStoragePluginCApi")); FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index d7ac8dd..dd4fcd4 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,12 +3,10 @@ # list(APPEND FLUTTER_PLUGIN_LIST - cloud_firestore + app_links file_selector_windows - firebase_auth - firebase_core - firebase_storage flutter_inappwebview_windows + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST