Atualização geral de optimazação e desing
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
id("com.google.gms.google-services")
|
|
||||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
id("dev.flutter.flutter-gradle-plugin")
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ plugins {
|
|||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "8.11.1" apply false
|
id("com.android.application") version "8.11.1" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "2.2.20" 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")
|
include(":app")
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
|
||||||
@@ -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.
|
|
||||||
@@ -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<void>(
|
|
||||||
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<void>(
|
|
||||||
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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
|
||||||
```
|
|
||||||
|
|
||||||
## Permissões iOS
|
|
||||||
```xml
|
|
||||||
<key>NSCameraUsageDescription</key>
|
|
||||||
<string>Este app precisa acessar a câmera para fotos de perfil</string>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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
|
|
||||||
@@ -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 <repository-url>
|
|
||||||
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<void>(
|
|
||||||
builder: (_) => QuizResultScreen(finalScore: nextScore, maxScore: 105, scopeId: scopeId),
|
|
||||||
),
|
|
||||||
isFinal: true,
|
|
||||||
showBackButton: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Atualizar Quiz Anterior
|
|
||||||
```dart
|
|
||||||
// No Quiz20Screen, atualizar nextRoute
|
|
||||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
|
||||||
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 <emulator_id>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:firebase_auth/firebase_auth.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
|
import 'main.dart' show supabase;
|
||||||
import 'home_screen.dart';
|
import 'home_screen.dart';
|
||||||
import 'logged_home.dart';
|
import 'logged_home.dart';
|
||||||
|
|
||||||
@@ -14,10 +15,11 @@ class AuthGate extends StatelessWidget {
|
|||||||
return ValueListenableBuilder<bool>(
|
return ValueListenableBuilder<bool>(
|
||||||
valueListenable: forceHomeScreen,
|
valueListenable: forceHomeScreen,
|
||||||
builder: (context, forcedHome, _) {
|
builder: (context, forcedHome, _) {
|
||||||
return StreamBuilder<User?>(
|
return StreamBuilder<AuthState>(
|
||||||
stream: FirebaseAuth.instance.authStateChanges(),
|
stream: supabase.auth.onAuthStateChange,
|
||||||
|
initialData: AuthState(AuthChangeEvent.initialSession, supabase.auth.currentSession),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final user = snapshot.data;
|
final user = snapshot.data?.session?.user;
|
||||||
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
|||||||
@@ -66,29 +66,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
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(
|
const Text(
|
||||||
'Check-Teeth Kids',
|
'Check-Teeth Kids',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
|||||||
@@ -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:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:lottie/lottie.dart';
|
import 'package:lottie/lottie.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import 'main.dart' show supabase;
|
||||||
import 'quiz/quiz1.dart';
|
import 'quiz/quiz1.dart';
|
||||||
import 'quiz/quiz_prefs.dart';
|
import 'quiz/quiz_prefs.dart';
|
||||||
|
import 'screens/settings_screen.dart';
|
||||||
import 'screens/video_screen.dart';
|
import 'screens/video_screen.dart';
|
||||||
|
import 'widgets/app_dialogs.dart';
|
||||||
|
|
||||||
class LoggedHomeScreen extends StatefulWidget {
|
class LoggedHomeScreen extends StatefulWidget {
|
||||||
const LoggedHomeScreen({super.key});
|
const LoggedHomeScreen({super.key});
|
||||||
@@ -38,6 +39,7 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
|
|||||||
int? _lastMaxScore;
|
int? _lastMaxScore;
|
||||||
|
|
||||||
String _cachedUserName = 'Sem nome';
|
String _cachedUserName = 'Sem nome';
|
||||||
|
String? _cachedPhotoUrl;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -81,41 +83,29 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadInitialProfile() async {
|
Future<void> _loadInitialProfile() async {
|
||||||
final uid = (FirebaseAuth.instance.currentUser?.uid ?? '').trim();
|
final uid = (supabase.auth.currentUser?.id ?? '').trim();
|
||||||
if (uid.isEmpty) return;
|
if (uid.isEmpty) return;
|
||||||
try {
|
try {
|
||||||
final userDoc = await FirebaseFirestore.instance
|
final userDoc = await supabase
|
||||||
.collection('users')
|
.from('profiles')
|
||||||
.doc(uid)
|
.select()
|
||||||
.get();
|
.eq('id', uid)
|
||||||
final data = userDoc.data();
|
.maybeSingle();
|
||||||
final storedName = (data?['name'] ?? '').toString().trim();
|
final storedName = (userDoc?['name'] ?? '').toString().trim();
|
||||||
|
final storedPhotoUrl = (userDoc?['photo_url'] ?? '').toString().trim();
|
||||||
|
|
||||||
QuerySnapshot<Map<String, dynamic>> childrenSnap;
|
final childrenSnap = await supabase
|
||||||
try {
|
.from('children')
|
||||||
childrenSnap = await FirebaseFirestore.instance
|
.select()
|
||||||
.collection('users')
|
.eq('owner_id', uid)
|
||||||
.doc(uid)
|
.order('created_at')
|
||||||
.collection('children')
|
.limit(1);
|
||||||
.orderBy('createdAt', descending: false)
|
|
||||||
.limit(1)
|
|
||||||
.get();
|
|
||||||
} catch (_) {
|
|
||||||
childrenSnap = await FirebaseFirestore.instance
|
|
||||||
.collection('users')
|
|
||||||
.doc(uid)
|
|
||||||
.collection('children')
|
|
||||||
.limit(1)
|
|
||||||
.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
String? childName;
|
String? childName;
|
||||||
String? scopeId;
|
String? scopeId;
|
||||||
if (childrenSnap.docs.isNotEmpty) {
|
if (childrenSnap.isNotEmpty) {
|
||||||
final c = childrenSnap.docs.first.data();
|
final c = childrenSnap.first;
|
||||||
final childId = (c['id'] ?? childrenSnap.docs.first.id)
|
final childId = (c['id'] ?? '').toString().trim();
|
||||||
.toString()
|
|
||||||
.trim();
|
|
||||||
childName = (c['name'] ?? '').toString().trim();
|
childName = (c['name'] ?? '').toString().trim();
|
||||||
scopeId = '${uid}_$childId';
|
scopeId = '${uid}_$childId';
|
||||||
}
|
}
|
||||||
@@ -123,6 +113,7 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_cachedUserName = storedName.isNotEmpty ? storedName : _cachedUserName;
|
_cachedUserName = storedName.isNotEmpty ? storedName : _cachedUserName;
|
||||||
|
if (storedPhotoUrl.isNotEmpty) _cachedPhotoUrl = storedPhotoUrl;
|
||||||
if ((_selectedChildName ?? '').trim().isEmpty &&
|
if ((_selectedChildName ?? '').trim().isEmpty &&
|
||||||
(childName ?? '').trim().isNotEmpty) {
|
(childName ?? '').trim().isNotEmpty) {
|
||||||
_selectedChildName = childName;
|
_selectedChildName = childName;
|
||||||
@@ -140,7 +131,7 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
|
|||||||
|
|
||||||
Future<void> _loadQuizResult() async {
|
Future<void> _loadQuizResult() async {
|
||||||
final scope = (_selectedChildScopeId ?? '').trim();
|
final scope = (_selectedChildScopeId ?? '').trim();
|
||||||
final uid = FirebaseAuth.instance.currentUser?.uid;
|
final uid = supabase.auth.currentUser?.id;
|
||||||
final String? userId = (uid ?? '').trim().isEmpty ? null : uid;
|
final String? userId = (uid ?? '').trim().isEmpty ? null : uid;
|
||||||
|
|
||||||
int? score;
|
int? score;
|
||||||
@@ -152,15 +143,13 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
|
|||||||
: '';
|
: '';
|
||||||
if (childId.trim().isNotEmpty) {
|
if (childId.trim().isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
final childDoc = await FirebaseFirestore.instance
|
final childDoc = await supabase
|
||||||
.collection('users')
|
.from('children')
|
||||||
.doc(userId)
|
.select()
|
||||||
.collection('children')
|
.eq('id', childId)
|
||||||
.doc(childId)
|
.maybeSingle();
|
||||||
.get();
|
final s = childDoc?['last_score'];
|
||||||
final data = childDoc.data();
|
final m = childDoc?['last_max_score'];
|
||||||
final s = data?['lastScore'];
|
|
||||||
final m = data?['lastMaxScore'];
|
|
||||||
if (s is int && m is int) {
|
if (s is int && m is int) {
|
||||||
score = s;
|
score = s;
|
||||||
max = m;
|
max = m;
|
||||||
@@ -196,6 +185,25 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final size = MediaQuery.sizeOf(context);
|
final size = MediaQuery.sizeOf(context);
|
||||||
@@ -203,7 +211,11 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
|
|||||||
? _expandedAppBarHeight
|
? _expandedAppBarHeight
|
||||||
: _collapsedAppBarHeight;
|
: _collapsedAppBarHeight;
|
||||||
final double toolbarHeight = _index == 0 ? kToolbarHeight : appBarHeight;
|
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
|
final ShapeBorder appBarShape = _index == 0
|
||||||
? const RoundedRectangleBorder(
|
? const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.vertical(bottom: Radius.circular(24)),
|
borderRadius: BorderRadius.vertical(bottom: Radius.circular(24)),
|
||||||
@@ -296,13 +308,65 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
|
|||||||
child: _index == 0
|
child: _index == 0
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: const EdgeInsets.only(left: 16, right: 10),
|
padding: const EdgeInsets.only(left: 16, right: 10),
|
||||||
child: Text(
|
child: Material(
|
||||||
'Olá, $shownName',
|
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,
|
textAlign: TextAlign.left,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 20,
|
fontSize: 19,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -358,7 +422,8 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
|
|||||||
padding: EdgeInsets.fromLTRB(16, bodyTopPadding, 16, 16),
|
padding: EdgeInsets.fromLTRB(16, bodyTopPadding, 16, 16),
|
||||||
child: _index == 0
|
child: _index == 0
|
||||||
? _InicioTab(onQuizClosed: _loadQuizResult)
|
? _InicioTab(onQuizClosed: _loadQuizResult)
|
||||||
: _PerfilTab(
|
: _index == 1
|
||||||
|
? _PerfilTab(
|
||||||
selectedChildIndex: _selectedChildIndex,
|
selectedChildIndex: _selectedChildIndex,
|
||||||
onChildSelected: (index, name, scopeId) {
|
onChildSelected: (index, name, scopeId) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -368,7 +433,8 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
|
|||||||
});
|
});
|
||||||
_loadQuizResult();
|
_loadQuizResult();
|
||||||
},
|
},
|
||||||
),
|
)
|
||||||
|
: const SettingsBody(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -390,6 +456,10 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
|
|||||||
icon: Icon(Icons.person_rounded),
|
icon: Icon(Icons.person_rounded),
|
||||||
label: 'Perfil',
|
label: 'Perfil',
|
||||||
),
|
),
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
icon: Icon(Icons.settings_rounded),
|
||||||
|
label: 'Ajustes',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -501,14 +571,52 @@ class _InicioTab extends StatelessWidget {
|
|||||||
|
|
||||||
final VoidCallback onQuizClosed;
|
final VoidCallback onQuizClosed;
|
||||||
|
|
||||||
|
Future<void> _startQuiz(BuildContext context) async {
|
||||||
|
final uid = (supabase.auth.currentUser?.id ?? '').trim();
|
||||||
|
if (uid.isEmpty) return;
|
||||||
|
|
||||||
|
List<Map<String, dynamic>> 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<String, dynamic>? 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<void>(builder: (_) => Quiz1Screen(scopeId: scopeId)),
|
||||||
|
);
|
||||||
|
onQuizClosed();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final uid = (FirebaseAuth.instance.currentUser?.uid ?? '').trim();
|
|
||||||
final state = context.findAncestorStateOfType<_LoggedHomeScreenState>();
|
final state = context.findAncestorStateOfType<_LoggedHomeScreenState>();
|
||||||
final childScope = (state?._selectedChildScopeId ?? '').trim();
|
|
||||||
final scopeId = childScope.isNotEmpty
|
|
||||||
? childScope
|
|
||||||
: (uid.isNotEmpty ? uid : null);
|
|
||||||
final selectedChildName = (state?._selectedChildName ?? '').trim();
|
final selectedChildName = (state?._selectedChildName ?? '').trim();
|
||||||
|
|
||||||
return Align(
|
return Align(
|
||||||
@@ -524,15 +632,7 @@ class _InicioTab extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
_HeroQuizCard(
|
_HeroQuizCard(
|
||||||
childName: selectedChildName,
|
childName: selectedChildName,
|
||||||
onStartQuiz: () {
|
onStartQuiz: () => _startQuiz(context),
|
||||||
Navigator.of(context)
|
|
||||||
.push(
|
|
||||||
MaterialPageRoute<void>(
|
|
||||||
builder: (_) => Quiz1Screen(scopeId: scopeId),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.then((_) => onQuizClosed());
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_VideoLibraryCard(
|
_VideoLibraryCard(
|
||||||
@@ -554,6 +654,138 @@ class _InicioTab extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>?> _createChildViaSheet(
|
||||||
|
BuildContext context,
|
||||||
|
String uid,
|
||||||
|
) async {
|
||||||
|
final result = await showModalBottomSheet<Map<String, dynamic>?>(
|
||||||
|
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<Map<String, dynamic>?> _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<Map<String, dynamic>?> _pickChildSheet(
|
||||||
|
BuildContext context,
|
||||||
|
List<Map<String, dynamic>> children,
|
||||||
|
) {
|
||||||
|
const Color teal = Color(0xFF2F9E94);
|
||||||
|
return showModalBottomSheet<Map<String, dynamic>?>(
|
||||||
|
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 {
|
class _HeroQuizCard extends StatelessWidget {
|
||||||
const _HeroQuizCard({required this.childName, required this.onStartQuiz});
|
const _HeroQuizCard({required this.childName, required this.onStartQuiz});
|
||||||
|
|
||||||
@@ -651,7 +883,10 @@ class _VideoLibraryCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
const Object? item = null;
|
final featured = videoList.firstWhere(
|
||||||
|
(v) => v.videoPath != null,
|
||||||
|
orElse: () => videoList.first,
|
||||||
|
);
|
||||||
|
|
||||||
return Material(
|
return Material(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -664,17 +899,6 @@ class _VideoLibraryCard extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
if (item == null)
|
|
||||||
Container(
|
|
||||||
height: 160,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFFFFE6F1),
|
|
||||||
borderRadius: const BorderRadius.vertical(
|
|
||||||
top: Radius.circular(24),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: const BorderRadius.vertical(
|
borderRadius: const BorderRadius.vertical(
|
||||||
top: Radius.circular(24),
|
top: Radius.circular(24),
|
||||||
@@ -687,25 +911,25 @@ class _VideoLibraryCard extends StatelessWidget {
|
|||||||
Container(
|
Container(
|
||||||
color: const Color(0xFF2F9E94).withValues(alpha: 0.14),
|
color: const Color(0xFF2F9E94).withValues(alpha: 0.14),
|
||||||
),
|
),
|
||||||
Container(
|
VideoThumbnail(
|
||||||
|
video: featured,
|
||||||
|
borderRadius: 0,
|
||||||
|
iconSize: 54,
|
||||||
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: [
|
colors: [
|
||||||
Colors.black.withValues(alpha: 0.05),
|
Colors.transparent,
|
||||||
Colors.black.withValues(alpha: 0.45),
|
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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -785,6 +1009,47 @@ class _PerfilTabState extends State<_PerfilTab> {
|
|||||||
bool _addingChild = false;
|
bool _addingChild = false;
|
||||||
bool _updatingPhoto = false;
|
bool _updatingPhoto = false;
|
||||||
|
|
||||||
|
bool _initialLoading = true;
|
||||||
|
Map<String, dynamic>? _profileData;
|
||||||
|
List<Map<String, dynamic>> _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<void> _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 {
|
Future<(int?, int?)> _loadScoreForScope(String scopeId) async {
|
||||||
final score = await QuizPrefs.getLastScoreForScope(scopeId);
|
final score = await QuizPrefs.getLastScoreForScope(scopeId);
|
||||||
final max = await QuizPrefs.getLastMaxScoreForScope(scopeId);
|
final max = await QuizPrefs.getLastMaxScoreForScope(scopeId);
|
||||||
@@ -800,10 +1065,14 @@ class _PerfilTabState extends State<_PerfilTab> {
|
|||||||
final source = await showModalBottomSheet<ImageSource>(
|
final source = await showModalBottomSheet<ImageSource>(
|
||||||
context: context,
|
context: context,
|
||||||
showDragHandle: true,
|
showDragHandle: true,
|
||||||
|
backgroundColor: const Color(0xFFFFE6F1),
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||||
|
),
|
||||||
builder: (ctx) {
|
builder: (ctx) {
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
|
padding: const EdgeInsets.fromLTRB(18, 6, 18, 18),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
@@ -811,12 +1080,22 @@ class _PerfilTabState extends State<_PerfilTab> {
|
|||||||
const Text(
|
const Text(
|
||||||
'Foto de perfil',
|
'Foto de perfil',
|
||||||
textAlign: TextAlign.center,
|
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(
|
SizedBox(
|
||||||
height: 46,
|
height: 46,
|
||||||
child: FilledButton(
|
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),
|
onPressed: () => Navigator.of(ctx).pop(ImageSource.camera),
|
||||||
child: const Text('Câmera'),
|
child: const Text('Câmera'),
|
||||||
),
|
),
|
||||||
@@ -825,14 +1104,23 @@ class _PerfilTabState extends State<_PerfilTab> {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
height: 46,
|
height: 46,
|
||||||
child: FilledButton(
|
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),
|
onPressed: () => Navigator.of(ctx).pop(ImageSource.gallery),
|
||||||
child: const Text('Galeria'),
|
child: const Text('Galeria'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 8),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 42,
|
height: 42,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: const Color(0xFF2F9E94),
|
||||||
|
),
|
||||||
onPressed: () => Navigator.of(ctx).pop(),
|
onPressed: () => Navigator.of(ctx).pop(),
|
||||||
child: const Text('Cancelar'),
|
child: const Text('Cancelar'),
|
||||||
),
|
),
|
||||||
@@ -857,17 +1145,26 @@ class _PerfilTabState extends State<_PerfilTab> {
|
|||||||
setState(() => _updatingPhoto = true);
|
setState(() => _updatingPhoto = true);
|
||||||
try {
|
try {
|
||||||
final file = File(picked.path);
|
final file = File(picked.path);
|
||||||
final ref = FirebaseStorage.instance
|
final path = '$uid/profile.jpg';
|
||||||
.ref()
|
await supabase.storage
|
||||||
.child('users')
|
.from('photos')
|
||||||
.child(uid)
|
.upload(
|
||||||
.child('profile.jpg');
|
path,
|
||||||
await ref.putFile(file);
|
file,
|
||||||
final url = await ref.getDownloadURL();
|
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({
|
await supabase.from('profiles').upsert({
|
||||||
'photoUrl': url,
|
'id': uid,
|
||||||
}, SetOptions(merge: true));
|
'photo_url': url,
|
||||||
|
});
|
||||||
|
|
||||||
|
await _loadPerfilData();
|
||||||
|
if (context.mounted) {
|
||||||
|
context.findAncestorStateOfType<_LoggedHomeScreenState>()?.updateCachedPhoto(url);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(
|
||||||
@@ -878,6 +1175,38 @@ class _PerfilTabState extends State<_PerfilTab> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _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<void> _addAnotherChild(BuildContext context, String uid) async {
|
Future<void> _addAnotherChild(BuildContext context, String uid) async {
|
||||||
if (_addingChild) return;
|
if (_addingChild) return;
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
@@ -896,30 +1225,20 @@ class _PerfilTabState extends State<_PerfilTab> {
|
|||||||
|
|
||||||
if (result == null) return;
|
if (result == null) return;
|
||||||
|
|
||||||
final childId = FirebaseFirestore.instance
|
|
||||||
.collection('users')
|
|
||||||
.doc(uid)
|
|
||||||
.collection('children')
|
|
||||||
.doc()
|
|
||||||
.id;
|
|
||||||
|
|
||||||
final childMap = {
|
final childMap = {
|
||||||
...result,
|
...result,
|
||||||
'id': childId,
|
'owner_id': uid,
|
||||||
'createdAt': FieldValue.serverTimestamp(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setState(() => _addingChild = true);
|
setState(() => _addingChild = true);
|
||||||
try {
|
try {
|
||||||
await FirebaseFirestore.instance
|
await supabase
|
||||||
.collection('users')
|
.from('children')
|
||||||
.doc(uid)
|
.insert(childMap)
|
||||||
.collection('children')
|
|
||||||
.doc(childId)
|
|
||||||
.set(childMap, SetOptions(merge: true))
|
|
||||||
.timeout(const Duration(seconds: 20));
|
.timeout(const Duration(seconds: 20));
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
await _loadPerfilData();
|
||||||
messenger.showSnackBar(
|
messenger.showSnackBar(
|
||||||
const SnackBar(content: Text('Criança adicionada')),
|
const SnackBar(content: Text('Criança adicionada')),
|
||||||
);
|
);
|
||||||
@@ -930,24 +1249,12 @@ class _PerfilTabState extends State<_PerfilTab> {
|
|||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final addMore = await showDialog<bool>(
|
final addMore = await showConfirmDialog(
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
context: context,
|
context,
|
||||||
builder: (ctx) {
|
title: 'Adicionar outra criança?',
|
||||||
return AlertDialog(
|
cancelLabel: 'Agora não',
|
||||||
title: const Text('Adicionar outra criança?'),
|
confirmLabel: 'Adicionar outra',
|
||||||
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'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -976,9 +1283,9 @@ class _PerfilTabState extends State<_PerfilTab> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final user = FirebaseAuth.instance.currentUser;
|
final user = supabase.auth.currentUser;
|
||||||
final uid = (user?.uid ?? '').trim();
|
final uid = (user?.id ?? '').trim();
|
||||||
final name = (user?.displayName ?? '').trim();
|
final name = (user?.userMetadata?['name'] ?? '').toString().trim();
|
||||||
final email = (user?.email ?? '').trim();
|
final email = (user?.email ?? '').trim();
|
||||||
final shownName = name.isNotEmpty ? name : 'Sem nome';
|
final shownName = name.isNotEmpty ? name : 'Sem nome';
|
||||||
|
|
||||||
@@ -986,29 +1293,23 @@ class _PerfilTabState extends State<_PerfilTab> {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
return StreamBuilder<DocumentSnapshot<Map<String, dynamic>>>(
|
if (_initialLoading) {
|
||||||
stream: FirebaseFirestore.instance
|
return const Center(
|
||||||
.collection('users')
|
child: Padding(
|
||||||
.doc(uid)
|
padding: EdgeInsets.only(top: 60),
|
||||||
.snapshots(),
|
child: CircularProgressIndicator(color: Color(0xFF2F9E94)),
|
||||||
builder: (context, userSnapshot) {
|
),
|
||||||
final data = userSnapshot.data?.data();
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = _profileData;
|
||||||
final storedName = (data?['name'] ?? '').toString().trim();
|
final storedName = (data?['name'] ?? '').toString().trim();
|
||||||
final profileName = storedName.isNotEmpty ? storedName : shownName;
|
final profileName = storedName.isNotEmpty ? storedName : shownName;
|
||||||
final photoUrl = (data?['photoUrl'] ?? '').toString().trim();
|
final photoUrl = (data?['photo_url'] ?? '').toString().trim();
|
||||||
final storedEmail = (data?['email'] ?? '').toString().trim();
|
final storedEmail = (data?['email'] ?? '').toString().trim();
|
||||||
final profileEmail = storedEmail.isNotEmpty ? storedEmail : email;
|
final profileEmail = storedEmail.isNotEmpty ? storedEmail : email;
|
||||||
|
|
||||||
return StreamBuilder<QuerySnapshot<Map<String, dynamic>>>(
|
final children = _children;
|
||||||
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
|
final int selectedIndex = children.isEmpty
|
||||||
? 0
|
? 0
|
||||||
: widget.selectedChildIndex.clamp(
|
: widget.selectedChildIndex.clamp(
|
||||||
@@ -1233,10 +1534,7 @@ class _PerfilTabState extends State<_PerfilTab> {
|
|||||||
...children.asMap().entries.map((entry) {
|
...children.asMap().entries.map((entry) {
|
||||||
final i = entry.key;
|
final i = entry.key;
|
||||||
final c = entry.value;
|
final c = entry.value;
|
||||||
final childId =
|
final childId = (c['id'] ?? '').toString().trim();
|
||||||
(c['id'] ?? '').toString().trim().isEmpty
|
|
||||||
? docs[i].id
|
|
||||||
: (c['id'] ?? '').toString().trim();
|
|
||||||
final childName = (c['name'] ?? '')
|
final childName = (c['name'] ?? '')
|
||||||
.toString()
|
.toString()
|
||||||
.trim();
|
.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 {
|
onPressed: () async {
|
||||||
await FirebaseAuth.instance.signOut();
|
await supabase.auth.signOut();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.logout_rounded),
|
icon: const Icon(Icons.logout_rounded),
|
||||||
label: const Text('Sair'),
|
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<FormState>();
|
|
||||||
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<String>(
|
|
||||||
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 {
|
class _AddChildSheet extends StatefulWidget {
|
||||||
const _AddChildSheet();
|
const _AddChildSheet();
|
||||||
|
|||||||
@@ -1,94 +1,21 @@
|
|||||||
import 'package:firebase_auth/firebase_auth.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lottie/lottie.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'dart:math' as math;
|
|
||||||
|
import '../main.dart' show supabase;
|
||||||
|
|
||||||
Future<void> showLoginSheet(BuildContext context) {
|
Future<void> showLoginSheet(BuildContext context) {
|
||||||
return showModalBottomSheet<void>(
|
return showModalBottomSheet<void>(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
backgroundColor: Colors.transparent,
|
showDragHandle: true,
|
||||||
|
backgroundColor: const Color(0xFFFFE6F1),
|
||||||
shape: const RoundedRectangleBorder(
|
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<double>(
|
|
||||||
tween: Tween<double>(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 {
|
class LoginBottomSheet extends StatefulWidget {
|
||||||
const LoginBottomSheet({super.key});
|
const LoginBottomSheet({super.key});
|
||||||
|
|
||||||
@@ -113,63 +40,41 @@ class _LoginBottomSheetState extends State<LoginBottomSheet> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final viewInsets = MediaQuery.viewInsetsOf(context);
|
final bottomInset = MediaQuery.viewInsetsOf(context).bottom;
|
||||||
const accentPink = Color(0xFFFF55A7);
|
return SafeArea(
|
||||||
const primaryTeal = Color(0xFF2F9E94);
|
child: Padding(
|
||||||
final underlineBorder = UnderlineInputBorder(
|
padding: EdgeInsets.fromLTRB(18, 6, 18, 18 + bottomInset),
|
||||||
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(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Center(
|
|
||||||
child: Container(
|
|
||||||
width: 46,
|
|
||||||
height: 5,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.black.withValues(alpha: 0.10),
|
|
||||||
borderRadius: BorderRadius.circular(99),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 14),
|
|
||||||
const Text(
|
const Text(
|
||||||
'Entrar',
|
'Entrar',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w900,
|
||||||
color: accentPink,
|
color: Color(0xFFFF55A7),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
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(
|
TextFormField(
|
||||||
controller: _emailController,
|
controller: _emailController,
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
decoration: InputDecoration(
|
decoration: const InputDecoration(labelText: 'Email'),
|
||||||
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) {
|
validator: (v) {
|
||||||
final value = (v ?? '').trim();
|
final value = (v ?? '').trim();
|
||||||
if (value.isEmpty) return 'Informe seu email';
|
if (value.isEmpty) return 'Informe seu email';
|
||||||
@@ -177,20 +82,11 @@ class _LoginBottomSheetState extends State<LoginBottomSheet> {
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
decoration: InputDecoration(
|
decoration: const InputDecoration(labelText: 'Senha'),
|
||||||
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) {
|
validator: (v) {
|
||||||
final value = (v ?? '');
|
final value = (v ?? '');
|
||||||
if (value.isEmpty) return 'Informe sua senha';
|
if (value.isEmpty) return 'Informe sua senha';
|
||||||
@@ -198,34 +94,52 @@ class _LoginBottomSheetState extends State<LoginBottomSheet> {
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
],
|
||||||
SizedBox(
|
),
|
||||||
height: 46,
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 44,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: _loading
|
||||||
|
? null
|
||||||
|
: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Cancelar'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 44,
|
||||||
child: FilledButton(
|
child: FilledButton(
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
backgroundColor: primaryTeal,
|
backgroundColor: const Color(0xFF2F9E94),
|
||||||
foregroundColor: const Color.fromARGB(255, 255, 255, 255),
|
foregroundColor: Colors.white,
|
||||||
shape: const StadiumBorder(),
|
shape: const StadiumBorder(),
|
||||||
textStyle: const TextStyle(fontWeight: FontWeight.w800),
|
textStyle: const TextStyle(fontWeight: FontWeight.w900),
|
||||||
),
|
),
|
||||||
onPressed: _loading ? null : _submit,
|
onPressed: _loading ? null : _submit,
|
||||||
child: _loading
|
child: _loading
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 18,
|
width: 18,
|
||||||
height: 18,
|
height: 18,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: const Text('Entrar'),
|
: 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<LoginBottomSheet> {
|
|||||||
final email = _emailController.text.trim();
|
final email = _emailController.text.trim();
|
||||||
final password = _passwordController.text;
|
final password = _passwordController.text;
|
||||||
|
|
||||||
await FirebaseAuth.instance.signInWithEmailAndPassword(
|
await supabase.auth.signInWithPassword(email: email, password: password);
|
||||||
email: email,
|
|
||||||
password: password,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Login efetuado')),
|
const SnackBar(content: Text('Login efetuado')),
|
||||||
);
|
);
|
||||||
} on FirebaseAuthException catch (e) {
|
} on AuthException catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(_friendlyAuthError(e))),
|
SnackBar(content: Text(_friendlyAuthError(e))),
|
||||||
@@ -264,18 +175,14 @@ class _LoginBottomSheetState extends State<LoginBottomSheet> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _friendlyAuthError(FirebaseAuthException e) {
|
String _friendlyAuthError(AuthException e) {
|
||||||
switch (e.code) {
|
switch (e.code) {
|
||||||
case 'invalid-email':
|
case 'invalid_credentials':
|
||||||
return 'Email inválido.';
|
|
||||||
case 'user-disabled':
|
|
||||||
return 'Usuário desativado.';
|
|
||||||
case 'user-not-found':
|
|
||||||
case 'wrong-password':
|
|
||||||
case 'invalid-credential':
|
|
||||||
return 'Email ou senha incorretos.';
|
return 'Email ou senha incorretos.';
|
||||||
|
case 'user_not_found':
|
||||||
|
return 'Usuário não encontrado.';
|
||||||
default:
|
default:
|
||||||
return e.message ?? 'Falha de autenticação.';
|
return e.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:flutter/material.dart';
|
||||||
import 'package:lottie/lottie.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
|
||||||
|
import '../main.dart' show supabase;
|
||||||
|
|
||||||
Future<void> showRegisterSheet(BuildContext context) {
|
Future<void> showRegisterSheet(BuildContext context) {
|
||||||
return showModalBottomSheet<void>(
|
return showModalBottomSheet<void>(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
backgroundColor: Colors.transparent,
|
showDragHandle: true,
|
||||||
|
backgroundColor: const Color(0xFFFFE6F1),
|
||||||
shape: const RoundedRectangleBorder(
|
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<double>(
|
|
||||||
tween: Tween<double>(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 {
|
class RegisterBottomSheet extends StatefulWidget {
|
||||||
const RegisterBottomSheet({super.key});
|
const RegisterBottomSheet({super.key});
|
||||||
|
|
||||||
@@ -106,15 +38,11 @@ class _RegisterBottomSheetState extends State<RegisterBottomSheet> {
|
|||||||
required String name,
|
required String name,
|
||||||
required String email,
|
required String email,
|
||||||
}) async {
|
}) async {
|
||||||
await FirebaseFirestore.instance
|
await supabase.from('profiles').upsert({
|
||||||
.collection('users')
|
'id': uid,
|
||||||
.doc(uid)
|
|
||||||
.set({
|
|
||||||
'name': name,
|
'name': name,
|
||||||
'email': email,
|
'email': email,
|
||||||
'createdAt': FieldValue.serverTimestamp(),
|
}).timeout(const Duration(seconds: 20));
|
||||||
}, SetOptions(merge: true))
|
|
||||||
.timeout(const Duration(seconds: 20));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -127,68 +55,40 @@ class _RegisterBottomSheetState extends State<RegisterBottomSheet> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final viewInsets = MediaQuery.viewInsetsOf(context);
|
final bottomInset = MediaQuery.viewInsetsOf(context).bottom;
|
||||||
const accentPink = Color(0xFFFF55A7);
|
return SafeArea(
|
||||||
const primaryTeal = Color(0xFF2F9E94);
|
child: Padding(
|
||||||
final underlineBorder = UnderlineInputBorder(
|
padding: EdgeInsets.fromLTRB(18, 6, 18, 18 + bottomInset),
|
||||||
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(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Center(
|
|
||||||
child: Container(
|
|
||||||
width: 46,
|
|
||||||
height: 5,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.black.withValues(alpha: 0.10),
|
|
||||||
borderRadius: BorderRadius.circular(99),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 14),
|
|
||||||
const Text(
|
const Text(
|
||||||
'Criar conta',
|
'Criar conta',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w900,
|
||||||
color: accentPink,
|
color: Color(0xFFFF55A7),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
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(
|
TextFormField(
|
||||||
controller: _nameController,
|
controller: _nameController,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
decoration: InputDecoration(
|
decoration: const InputDecoration(labelText: 'Nome'),
|
||||||
labelText: 'Nome',
|
|
||||||
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) {
|
validator: (v) {
|
||||||
if (v == null || v.trim().isEmpty) {
|
if (v == null || v.trim().isEmpty) {
|
||||||
return 'Informe seu nome';
|
return 'Informe seu nome';
|
||||||
@@ -199,26 +99,11 @@ class _RegisterBottomSheetState extends State<RegisterBottomSheet> {
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _emailController,
|
controller: _emailController,
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
decoration: InputDecoration(
|
decoration: const InputDecoration(labelText: 'Email'),
|
||||||
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) {
|
validator: (v) {
|
||||||
final value = (v ?? '').trim();
|
final value = (v ?? '').trim();
|
||||||
if (value.isEmpty) return 'Informe seu email';
|
if (value.isEmpty) return 'Informe seu email';
|
||||||
@@ -226,26 +111,11 @@ class _RegisterBottomSheetState extends State<RegisterBottomSheet> {
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
decoration: InputDecoration(
|
decoration: const InputDecoration(labelText: 'Senha'),
|
||||||
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) {
|
validator: (v) {
|
||||||
final value = (v ?? '');
|
final value = (v ?? '');
|
||||||
if (value.isEmpty) return 'Informe sua senha';
|
if (value.isEmpty) return 'Informe sua senha';
|
||||||
@@ -253,36 +123,52 @@ class _RegisterBottomSheetState extends State<RegisterBottomSheet> {
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
],
|
||||||
SizedBox(
|
),
|
||||||
height: 46,
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 44,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: _loading
|
||||||
|
? null
|
||||||
|
: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Cancelar'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 44,
|
||||||
child: FilledButton(
|
child: FilledButton(
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
backgroundColor: primaryTeal,
|
backgroundColor: const Color(0xFF2F9E94),
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
shape: const StadiumBorder(),
|
shape: const StadiumBorder(),
|
||||||
textStyle: const TextStyle(fontWeight: FontWeight.w800),
|
textStyle: const TextStyle(fontWeight: FontWeight.w900),
|
||||||
),
|
),
|
||||||
onPressed: _loading ? null : _submit,
|
onPressed: _loading ? null : _submit,
|
||||||
child: _loading
|
child: _loading
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 18,
|
width: 18,
|
||||||
height: 18,
|
height: 18,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: const Text('Registrar'),
|
: 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<RegisterBottomSheet> {
|
|||||||
final email = _emailController.text.trim();
|
final email = _emailController.text.trim();
|
||||||
final password = _passwordController.text;
|
final password = _passwordController.text;
|
||||||
|
|
||||||
final credential = await FirebaseAuth.instance
|
final response = await supabase.auth
|
||||||
.createUserWithEmailAndPassword(email: email, password: password)
|
.signUp(
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
data: {'name': name},
|
||||||
|
)
|
||||||
.timeout(const Duration(seconds: 20));
|
.timeout(const Duration(seconds: 20));
|
||||||
|
|
||||||
final user = credential.user;
|
final user = response.user;
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw StateError('Usuário não encontrado após criar conta.');
|
throw StateError('Usuário não encontrado após criar conta.');
|
||||||
}
|
}
|
||||||
|
|
||||||
final uid = user.uid;
|
final uid = user.id;
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
// Fecha o sheet imediatamente após autenticar.
|
// 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();
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
unawaited(
|
unawaited(
|
||||||
@@ -321,7 +211,7 @@ class _RegisterBottomSheetState extends State<RegisterBottomSheet> {
|
|||||||
email: email,
|
email: email,
|
||||||
).catchError((_) {}),
|
).catchError((_) {}),
|
||||||
);
|
);
|
||||||
} on FirebaseAuthException catch (e) {
|
} on AuthException catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(
|
||||||
context,
|
context,
|
||||||
@@ -345,16 +235,15 @@ class _RegisterBottomSheetState extends State<RegisterBottomSheet> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _friendlyAuthError(FirebaseAuthException e) {
|
String _friendlyAuthError(AuthException e) {
|
||||||
switch (e.code) {
|
switch (e.code) {
|
||||||
case 'invalid-email':
|
case 'email_exists':
|
||||||
return 'Email inválido.';
|
case 'user_already_exists':
|
||||||
case 'email-already-in-use':
|
|
||||||
return 'Este email já está em uso.';
|
return 'Este email já está em uso.';
|
||||||
case 'weak-password':
|
case 'weak_password':
|
||||||
return 'Senha fraca. Use pelo menos 6 caracteres.';
|
return 'Senha fraca. Use pelo menos 6 caracteres.';
|
||||||
default:
|
default:
|
||||||
return e.message ?? 'Falha de autenticação.';
|
return e.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import 'package:firebase_core/firebase_core.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'gates/debug_launch_gate.dart';
|
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<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
@@ -14,7 +20,7 @@ Future<void> main() async {
|
|||||||
};
|
};
|
||||||
|
|
||||||
runZonedGuarded(() async {
|
runZonedGuarded(() async {
|
||||||
await Firebase.initializeApp();
|
await Supabase.initialize(url: supabaseUrl, publishableKey: supabaseAnonKey);
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
}, (error, stack) {
|
}, (error, stack) {
|
||||||
debugPrint('UNCAUGHT: $error');
|
debugPrint('UNCAUGHT: $error');
|
||||||
|
|||||||
@@ -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<void>(
|
|
||||||
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<QuizQuestion> _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)*
|
|
||||||
@@ -14,43 +14,29 @@ class Quiz1Screen extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return QuizQuestionScreen(
|
return QuizQuestionScreen(
|
||||||
title: 'Quiz 1/26',
|
title: 'Quiz 1/26',
|
||||||
question:
|
question: 'O rosto do seu filho/a se parece com o desta imagem?',
|
||||||
'Qual das seguintes imagens se assemelha à face do seu filho/a?',
|
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 [
|
answers: const [
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
title: 'Opção A',
|
title: 'Sim',
|
||||||
description:
|
description: 'O rosto se assemelha à imagem',
|
||||||
'Selecione se a imagem se assemelha à face do seu filho/a',
|
|
||||||
weight: 2,
|
weight: 2,
|
||||||
imagePath: 'assets/images/face_a.png',
|
value: 'sim',
|
||||||
),
|
),
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
title: 'Opção B',
|
title: 'Não',
|
||||||
description:
|
description: 'O rosto não se assemelha à imagem',
|
||||||
'Selecione se a imagem se assemelha à face do seu filho/a',
|
weight: 1,
|
||||||
weight: 2,
|
value: 'nao',
|
||||||
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',
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||||
builder: (_) => Quiz2Screen(currentScore: nextScore, scopeId: scopeId),
|
builder: (_) => Quiz2Screen(currentScore: nextScore, scopeId: scopeId),
|
||||||
),
|
),
|
||||||
answerType: QuizAnswerType.image,
|
answerType: QuizAnswerType.yesNo,
|
||||||
showBackButton: false,
|
showBackButton: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -68,42 +54,29 @@ class Quiz2Screen extends StatelessWidget {
|
|||||||
return QuizQuestionScreen(
|
return QuizQuestionScreen(
|
||||||
title: 'Quiz 2/26',
|
title: 'Quiz 2/26',
|
||||||
question:
|
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 [
|
answers: const [
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
title: 'Opção A',
|
title: 'Sim',
|
||||||
description:
|
description: 'A boca fica habitualmente entreaberta',
|
||||||
'Selecione se a imagem se assemelha à boca do seu filho/a',
|
|
||||||
weight: 2,
|
weight: 2,
|
||||||
imagePath: 'assets/images/mouth_a.png',
|
value: 'sim',
|
||||||
),
|
),
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
title: 'Opção B',
|
title: 'Não',
|
||||||
description:
|
description: 'A boca fica habitualmente fechada',
|
||||||
'Selecione se a imagem se assemelha à boca do seu filho/a',
|
weight: 1,
|
||||||
weight: 2,
|
value: 'nao',
|
||||||
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',
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||||
builder: (_) => Quiz3Screen(currentScore: nextScore, scopeId: scopeId),
|
builder: (_) => Quiz3Screen(currentScore: nextScore, scopeId: scopeId),
|
||||||
),
|
),
|
||||||
answerType: QuizAnswerType.image,
|
answerType: QuizAnswerType.yesNo,
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -120,43 +93,29 @@ class Quiz3Screen extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return QuizQuestionScreen(
|
return QuizQuestionScreen(
|
||||||
title: 'Quiz 3/26',
|
title: 'Quiz 3/26',
|
||||||
question:
|
question: 'O seu filho/a tem olheiras semelhantes às desta imagem?',
|
||||||
'Qual das seguintes imagens se assemelha às olheiras do seu filho/a?',
|
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 [
|
answers: const [
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
title: 'Opção A',
|
title: 'Sim',
|
||||||
description:
|
description: 'Tem olheiras semelhantes à imagem',
|
||||||
'Selecione se a imagem se assemelha às olheiras do seu filho/a',
|
|
||||||
weight: 2,
|
weight: 2,
|
||||||
imagePath: 'assets/images/dark_circles_a.png',
|
value: 'sim',
|
||||||
),
|
),
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
title: 'Opção B',
|
title: 'Não',
|
||||||
description:
|
description: 'Não tem olheiras semelhantes à imagem',
|
||||||
'Selecione se a imagem se assemelha às olheiras do seu filho/a',
|
weight: 1,
|
||||||
weight: 2,
|
value: 'nao',
|
||||||
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',
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||||
builder: (_) => Quiz4Screen(currentScore: nextScore, scopeId: scopeId),
|
builder: (_) => Quiz4Screen(currentScore: nextScore, scopeId: scopeId),
|
||||||
),
|
),
|
||||||
answerType: QuizAnswerType.image,
|
answerType: QuizAnswerType.yesNo,
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -174,42 +133,29 @@ class Quiz4Screen extends StatelessWidget {
|
|||||||
return QuizQuestionScreen(
|
return QuizQuestionScreen(
|
||||||
title: 'Quiz 4/26',
|
title: 'Quiz 4/26',
|
||||||
question:
|
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 [
|
answers: const [
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
title: 'Opção A',
|
title: 'Sim',
|
||||||
description:
|
description: 'O queixo se assemelha à imagem',
|
||||||
'Selecione se a imagem se assemelha ao queixo do seu filho/a',
|
|
||||||
weight: 2,
|
weight: 2,
|
||||||
imagePath: 'assets/images/chin_a.png',
|
value: 'sim',
|
||||||
),
|
),
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
title: 'Opção B',
|
title: 'Não',
|
||||||
description:
|
description: 'O queixo não se assemelha à imagem',
|
||||||
'Selecione se a imagem se assemelha ao queixo do seu filho/a',
|
weight: 1,
|
||||||
weight: 2,
|
value: 'nao',
|
||||||
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',
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||||
builder: (_) => Quiz5Screen(currentScore: nextScore, scopeId: scopeId),
|
builder: (_) => Quiz5Screen(currentScore: nextScore, scopeId: scopeId),
|
||||||
),
|
),
|
||||||
answerType: QuizAnswerType.image,
|
answerType: QuizAnswerType.yesNo,
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -272,43 +218,29 @@ class Quiz7Screen extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return QuizQuestionScreen(
|
return QuizQuestionScreen(
|
||||||
title: 'Quiz 7/26',
|
title: 'Quiz 7/26',
|
||||||
question:
|
question: 'A boca do seu filho/a se parece com a desta imagem?',
|
||||||
'Qual das seguintes imagens se assemelha à boca do seu filho/a?',
|
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 [
|
answers: const [
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
title: 'Opção A',
|
title: 'Sim',
|
||||||
description:
|
description: 'A boca se assemelha à imagem',
|
||||||
'Selecione se a imagem se assemelha à boca do seu filho/a',
|
|
||||||
weight: 2,
|
weight: 2,
|
||||||
imagePath: 'assets/images/mouth2_a.png',
|
value: 'sim',
|
||||||
),
|
),
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
title: 'Opção B',
|
title: 'Não',
|
||||||
description:
|
description: 'A boca não se assemelha à imagem',
|
||||||
'Selecione se a imagem se assemelha à boca do seu filho/a',
|
weight: 1,
|
||||||
weight: 2,
|
value: 'nao',
|
||||||
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',
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||||
builder: (_) => Quiz8Screen(currentScore: nextScore, scopeId: scopeId),
|
builder: (_) => Quiz8Screen(currentScore: nextScore, scopeId: scopeId),
|
||||||
),
|
),
|
||||||
answerType: QuizAnswerType.image,
|
answerType: QuizAnswerType.yesNo,
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -326,42 +258,29 @@ class Quiz8Screen extends StatelessWidget {
|
|||||||
return QuizQuestionScreen(
|
return QuizQuestionScreen(
|
||||||
title: 'Quiz 8/26',
|
title: 'Quiz 8/26',
|
||||||
question:
|
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 [
|
answers: const [
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
title: 'Opção A',
|
title: 'Sim',
|
||||||
description:
|
description: 'O frénulo se assemelha à imagem',
|
||||||
'Selecione se a imagem se assemelha ao freio do seu filho/a',
|
|
||||||
weight: 2,
|
weight: 2,
|
||||||
imagePath: 'assets/images/frenulum_a.png',
|
value: 'sim',
|
||||||
),
|
),
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
title: 'Opção B',
|
title: 'Não',
|
||||||
description:
|
description: 'O frénulo não se assemelha à imagem',
|
||||||
'Selecione se a imagem se assemelha ao freio do seu filho/a',
|
weight: 1,
|
||||||
weight: 2,
|
value: 'nao',
|
||||||
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',
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||||
builder: (_) => Quiz9Screen(currentScore: nextScore, scopeId: scopeId),
|
builder: (_) => Quiz9Screen(currentScore: nextScore, scopeId: scopeId),
|
||||||
),
|
),
|
||||||
answerType: QuizAnswerType.image,
|
answerType: QuizAnswerType.yesNo,
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import 'dart:math' as math;
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lottie/lottie.dart';
|
import 'package:lottie/lottie.dart';
|
||||||
|
|
||||||
|
import '../screens/video_screen.dart';
|
||||||
|
|
||||||
typedef QuizNextBuilder =
|
typedef QuizNextBuilder =
|
||||||
Route<void> Function(BuildContext context, int nextScore);
|
Route<void> Function(BuildContext context, int nextScore);
|
||||||
|
|
||||||
@@ -37,6 +39,9 @@ class QuizQuestionScreen extends StatefulWidget {
|
|||||||
this.showBackButton = false,
|
this.showBackButton = false,
|
||||||
this.answerType = QuizAnswerType.text,
|
this.answerType = QuizAnswerType.text,
|
||||||
this.questionImagePaths = const [],
|
this.questionImagePaths = const [],
|
||||||
|
this.suggestedVideoPath,
|
||||||
|
this.suggestedYoutubeId,
|
||||||
|
this.suggestedVideoTitle,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
@@ -49,6 +54,9 @@ class QuizQuestionScreen extends StatefulWidget {
|
|||||||
final bool showBackButton;
|
final bool showBackButton;
|
||||||
final QuizAnswerType answerType;
|
final QuizAnswerType answerType;
|
||||||
final List<String> questionImagePaths;
|
final List<String> questionImagePaths;
|
||||||
|
final String? suggestedVideoPath;
|
||||||
|
final String? suggestedYoutubeId;
|
||||||
|
final String? suggestedVideoTitle;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<QuizQuestionScreen> createState() => _QuizQuestionScreenState();
|
State<QuizQuestionScreen> createState() => _QuizQuestionScreenState();
|
||||||
@@ -58,6 +66,7 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
|
|||||||
int? _selected;
|
int? _selected;
|
||||||
TextEditingController? _numberController;
|
TextEditingController? _numberController;
|
||||||
int? _numberValue;
|
int? _numberValue;
|
||||||
|
bool _navigating = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -76,9 +85,9 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final size = MediaQuery.sizeOf(context);
|
final size = MediaQuery.sizeOf(context);
|
||||||
bool canProceed = _selected != null;
|
bool canProceed = _selected != null && !_navigating;
|
||||||
if (widget.answerType == QuizAnswerType.number) {
|
if (widget.answerType == QuizAnswerType.number) {
|
||||||
canProceed = _numberValue != null && _numberValue! >= 0;
|
canProceed = _numberValue != null && _numberValue! >= 0 && !_navigating;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -145,6 +154,35 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
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(
|
Text(
|
||||||
widget.question,
|
widget.question,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
@@ -239,6 +277,7 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
|
|||||||
onPressed: !canProceed
|
onPressed: !canProceed
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
|
setState(() => _navigating = true);
|
||||||
int nextScore = widget.currentScore;
|
int nextScore = widget.currentScore;
|
||||||
if (widget.answerType ==
|
if (widget.answerType ==
|
||||||
QuizAnswerType.number) {
|
QuizAnswerType.number) {
|
||||||
@@ -454,7 +493,7 @@ class _QuizAnswerTile extends StatelessWidget {
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (answer.imagePath != null) ...[
|
if (answer.imagePath != null) ...[
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
@@ -478,20 +517,15 @@ class _QuizAnswerTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
],
|
],
|
||||||
Row(
|
Text(
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
answer.title,
|
answer.title,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: Color(0xFF2F9E94),
|
color: Color(0xFF2F9E94),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -516,6 +550,7 @@ class _QuestionReferenceImages extends StatelessWidget {
|
|||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
paths.first,
|
paths.first,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
cacheWidth: 800,
|
||||||
errorBuilder: (context, error, stackTrace) => _placeholder(),
|
errorBuilder: (context, error, stackTrace) => _placeholder(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -534,6 +569,7 @@ class _QuestionReferenceImages extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
paths[i],
|
paths[i],
|
||||||
|
cacheWidth: 300,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (context, error, stackTrace) => _placeholder(),
|
errorBuilder: (context, error, stackTrace) => _placeholder(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import '../main.dart' show supabase;
|
||||||
import 'quiz_prefs.dart';
|
import 'quiz_prefs.dart';
|
||||||
|
|
||||||
class QuizResultScreen extends StatefulWidget {
|
class QuizResultScreen extends StatefulWidget {
|
||||||
@@ -40,7 +39,7 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
|
|||||||
maxScore: widget.maxScore,
|
maxScore: widget.maxScore,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final uid = FirebaseAuth.instance.currentUser?.uid;
|
final uid = supabase.auth.currentUser?.id;
|
||||||
if (uid != null && uid.trim().isNotEmpty) {
|
if (uid != null && uid.trim().isNotEmpty) {
|
||||||
await QuizPrefs.saveLastResultForUser(
|
await QuizPrefs.saveLastResultForUser(
|
||||||
userId: uid,
|
userId: uid,
|
||||||
@@ -55,25 +54,23 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final uid = FirebaseAuth.instance.currentUser?.uid;
|
final uid = supabase.auth.currentUser?.id;
|
||||||
final userId = (uid ?? '').trim();
|
final userId = (uid ?? '').trim();
|
||||||
if (userId.isNotEmpty &&
|
if (userId.isNotEmpty &&
|
||||||
scope.isNotEmpty &&
|
scope.isNotEmpty &&
|
||||||
scope.startsWith('${userId}_')) {
|
scope.startsWith('${userId}_')) {
|
||||||
final childId = scope.substring(userId.length + 1).trim();
|
final childId = scope.substring(userId.length + 1).trim();
|
||||||
if (childId.isNotEmpty) {
|
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(
|
unawaited(
|
||||||
FirebaseFirestore.instance
|
supabase
|
||||||
.collection('users')
|
.from('children')
|
||||||
.doc(userId)
|
.update({
|
||||||
.collection('children')
|
'last_score': widget.finalScore,
|
||||||
.doc(childId)
|
'last_max_score': widget.maxScore,
|
||||||
.set({
|
'last_quiz_at': DateTime.now().toIso8601String(),
|
||||||
'lastScore': widget.finalScore,
|
})
|
||||||
'lastMaxScore': widget.maxScore,
|
.eq('id', childId)
|
||||||
'lastQuizAt': FieldValue.serverTimestamp(),
|
|
||||||
}, SetOptions(merge: true))
|
|
||||||
.catchError((_) {}),
|
.catchError((_) {}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
212
lib/screens/settings_screen.dart
Normal file
212
lib/screens/settings_screen.dart
Normal file
@@ -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<SettingsBody> createState() => _SettingsBodyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsBodyState extends State<SettingsBody> {
|
||||||
|
bool _deletingAccount = false;
|
||||||
|
|
||||||
|
Future<void> _signOut() async {
|
||||||
|
await supabase.auth.signOut();
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _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<void>(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<Widget> 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
lib/screens/terms_screen.dart
Normal file
72
lib/screens/terms_screen.dart
Normal file
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,19 +4,24 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:lottie/lottie.dart';
|
import 'package:lottie/lottie.dart';
|
||||||
import 'package:video_player/video_player.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 {
|
class VideoData {
|
||||||
final int id;
|
final int id;
|
||||||
final String title;
|
final String title;
|
||||||
final String description;
|
final String description;
|
||||||
final String videoPath;
|
final String? videoPath;
|
||||||
|
final String? youtubeId;
|
||||||
|
|
||||||
VideoData({
|
VideoData({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.description,
|
required this.description,
|
||||||
required this.videoPath,
|
this.videoPath,
|
||||||
|
this.youtubeId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,43 +31,43 @@ final List<VideoData> videoList = [
|
|||||||
id: 1,
|
id: 1,
|
||||||
title: 'Episódio 1',
|
title: 'Episódio 1',
|
||||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
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(
|
VideoData(
|
||||||
id: 2,
|
id: 2,
|
||||||
title: 'Episódio 2',
|
title: 'Episódio 2',
|
||||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
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(
|
VideoData(
|
||||||
id: 3,
|
id: 3,
|
||||||
title: 'Episódio 3',
|
title: 'Episódio 3',
|
||||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
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(
|
VideoData(
|
||||||
id: 4,
|
id: 4,
|
||||||
title: 'Episódio 4',
|
title: 'Episódio 4',
|
||||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
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(
|
VideoData(
|
||||||
id: 5,
|
id: 5,
|
||||||
title: 'Episódio 5',
|
title: 'Episódio 5',
|
||||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
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(
|
VideoData(
|
||||||
id: 6,
|
id: 6,
|
||||||
title: 'Episódio 6',
|
title: 'Episódio 6',
|
||||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
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(
|
VideoData(
|
||||||
id: 7,
|
id: 7,
|
||||||
title: 'Episódio 7',
|
title: 'Episódio 7',
|
||||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
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(
|
VideoData(
|
||||||
id: 8,
|
id: 8,
|
||||||
@@ -105,6 +110,25 @@ final List<VideoData> videoList = [
|
|||||||
// Cache for video controllers to avoid re-initializing
|
// Cache for video controllers to avoid re-initializing
|
||||||
final Map<String, VideoPlayerController> _videoControllerCache = {};
|
final Map<String, VideoPlayerController> _videoControllerCache = {};
|
||||||
|
|
||||||
|
Future<void> 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<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _YoutubePlayerDialog(video: video),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _VideoPlayerDialog(video: video),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
class VideoScreen extends StatefulWidget {
|
class VideoScreen extends StatefulWidget {
|
||||||
const VideoScreen({super.key});
|
const VideoScreen({super.key});
|
||||||
|
|
||||||
@@ -268,29 +292,41 @@ class _VideoScreenState extends State<VideoScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _VideoButton extends StatefulWidget {
|
/// Preview de um vídeo (frame local ou thumbnail do YouTube), reutilizável
|
||||||
const _VideoButton({required this.video});
|
/// 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 VideoData video;
|
||||||
|
final double borderRadius;
|
||||||
|
final double iconSize;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_VideoButton> createState() => _VideoButtonState();
|
State<VideoThumbnail> createState() => _VideoThumbnailState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _VideoButtonState extends State<_VideoButton> {
|
class _VideoThumbnailState extends State<VideoThumbnail> {
|
||||||
VideoPlayerController? _controller;
|
VideoPlayerController? _controller;
|
||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
|
|
||||||
|
bool get _isYoutube => widget.video.youtubeId != null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_initializeVideo();
|
if (!_isYoutube) _initializeVideo();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initializeVideo() async {
|
Future<void> _initializeVideo() async {
|
||||||
|
final path = widget.video.videoPath!;
|
||||||
// Check if controller exists in cache
|
// Check if controller exists in cache
|
||||||
if (_videoControllerCache.containsKey(widget.video.videoPath)) {
|
if (_videoControllerCache.containsKey(path)) {
|
||||||
_controller = _videoControllerCache[widget.video.videoPath];
|
_controller = _videoControllerCache[path];
|
||||||
await _controller!.seekTo(const Duration(seconds: 2));
|
await _controller!.seekTo(const Duration(seconds: 2));
|
||||||
await _controller!.pause();
|
await _controller!.pause();
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -302,12 +338,12 @@ class _VideoButtonState extends State<_VideoButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create new controller and cache it
|
// Create new controller and cache it
|
||||||
_controller = VideoPlayerController.asset(widget.video.videoPath);
|
_controller = VideoPlayerController.asset(path);
|
||||||
try {
|
try {
|
||||||
await _controller!.initialize();
|
await _controller!.initialize();
|
||||||
await _controller!.seekTo(const Duration(seconds: 2));
|
await _controller!.seekTo(const Duration(seconds: 2));
|
||||||
await _controller!.pause();
|
await _controller!.pause();
|
||||||
_videoControllerCache[widget.video.videoPath] = _controller!;
|
_videoControllerCache[path] = _controller!;
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
@@ -332,6 +368,73 @@ class _VideoButtonState extends State<_VideoButton> {
|
|||||||
super.dispose();
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return Material(
|
||||||
@@ -341,7 +444,7 @@ class _VideoButtonState extends State<_VideoButton> {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
onTap: () => _showVideoPlayer(context, widget.video),
|
onTap: () => _showVideoPlayer(context, video),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -353,29 +456,11 @@ class _VideoButtonState extends State<_VideoButton> {
|
|||||||
color: const Color(0xFFFFE6F1),
|
color: const Color(0xFFFFE6F1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: _isInitialized && _controller != null
|
child: VideoThumbnail(video: video),
|
||||||
? 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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text(
|
Text(
|
||||||
widget.video.title,
|
video.title,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@@ -386,7 +471,7 @@ class _VideoButtonState extends State<_VideoButton> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
widget.video.description,
|
video.description,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.w600,
|
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(
|
class _YoutubePlayerDialog extends StatefulWidget {
|
||||||
context: context,
|
const _YoutubePlayerDialog({required this.video});
|
||||||
builder: (context) => _VideoPlayerDialog(video: 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<void> _initializeVideo() async {
|
Future<void> _initializeVideo() async {
|
||||||
_controller = VideoPlayerController.asset(widget.video.videoPath);
|
_controller = VideoPlayerController.asset(widget.video.videoPath!);
|
||||||
try {
|
try {
|
||||||
await _controller.initialize();
|
await _controller.initialize();
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -502,7 +639,7 @@ class _VideoPlayerDialogState extends State<_VideoPlayerDialog> {
|
|||||||
if (_isInitialized)
|
if (_isInitialized)
|
||||||
_VideoControls(
|
_VideoControls(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
videoPath: widget.video.videoPath,
|
videoPath: widget.video.videoPath!,
|
||||||
onClose: () => Navigator.of(context).pop(),
|
onClose: () => Navigator.of(context).pop(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
46
lib/widgets/app_dialogs.dart
Normal file
46
lib/widgets/app_dialogs.dart
Normal file
@@ -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<bool?> showConfirmDialog(
|
||||||
|
BuildContext context, {
|
||||||
|
required String title,
|
||||||
|
String? message,
|
||||||
|
String cancelLabel = 'Cancelar',
|
||||||
|
required String confirmLabel,
|
||||||
|
Color confirmColor = _teal,
|
||||||
|
}) {
|
||||||
|
return showDialog<bool>(
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,9 +7,17 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <file_selector_linux/file_selector_plugin.h>
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
|
#include <gtk/gtk_plugin.h>
|
||||||
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
file_selector_linux
|
file_selector_linux
|
||||||
|
gtk
|
||||||
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|||||||
@@ -5,22 +5,18 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import cloud_firestore
|
import app_links
|
||||||
import file_selector_macos
|
import file_selector_macos
|
||||||
import firebase_auth
|
|
||||||
import firebase_core
|
|
||||||
import firebase_storage
|
|
||||||
import flutter_inappwebview_macos
|
import flutter_inappwebview_macos
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
import url_launcher_macos
|
||||||
import video_player_avfoundation
|
import video_player_avfoundation
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
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"))
|
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"))
|
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin"))
|
VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
362
pubspec.lock
362
pubspec.lock
@@ -1,14 +1,38 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
_flutterfire_internals:
|
app_links:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _flutterfire_internals
|
name: app_links
|
||||||
sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11
|
sha256: "3462d9defc61565fde4944858b59bec5be2b9d5b05f20aed190adb3ad08a7abc"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
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:
|
archive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -45,10 +69,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.0"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -73,30 +97,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
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:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -105,6 +105,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.19.1"
|
||||||
|
convert:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: convert
|
||||||
|
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.2"
|
||||||
cross_file:
|
cross_file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -137,6 +145,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
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:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -193,78 +209,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.3+5"
|
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:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -368,6 +312,30 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
html:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -472,6 +440,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.9.0"
|
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:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -504,6 +480,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.0.0"
|
||||||
|
logging:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: logging
|
||||||
|
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
lottie:
|
lottie:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -516,18 +500,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.19"
|
version: "0.12.17"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.0"
|
version: "0.11.1"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -544,6 +528,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
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:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -600,6 +592,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
|
pointycastle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pointycastle
|
||||||
|
sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.0"
|
||||||
posix:
|
posix:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -608,6 +608,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.3"
|
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:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -685,6 +717,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.12.1"
|
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:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -701,6 +741,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
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:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -713,10 +769,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.10"
|
version: "0.7.7"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -725,6 +781,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
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:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -789,6 +909,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
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:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -813,6 +949,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
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:
|
youtube_player_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -823,4 +967,4 @@ packages:
|
|||||||
version: "9.1.3"
|
version: "9.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.10.4 <4.0.0"
|
dart: ">=3.10.4 <4.0.0"
|
||||||
flutter: ">=3.38.0"
|
flutter: ">=3.38.1"
|
||||||
|
|||||||
14
pubspec.yaml
14
pubspec.yaml
@@ -34,10 +34,7 @@ dependencies:
|
|||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
firebase_core: ^3.15.0
|
supabase_flutter: ^2.8.0
|
||||||
firebase_auth: ^5.7.0
|
|
||||||
cloud_firestore: ^5.6.10
|
|
||||||
firebase_storage: ^12.4.10
|
|
||||||
image_picker: ^1.1.2
|
image_picker: ^1.1.2
|
||||||
lottie: ^3.3.1
|
lottie: ^3.3.1
|
||||||
shared_preferences: ^2.3.2
|
shared_preferences: ^2.3.2
|
||||||
@@ -72,9 +69,14 @@ flutter:
|
|||||||
# - images/a_dot_burr.jpeg
|
# - images/a_dot_burr.jpeg
|
||||||
# - images/a_dot_ham.jpeg
|
# - images/a_dot_ham.jpeg
|
||||||
- lottie/
|
- lottie/
|
||||||
- assets/
|
- assets/Check-theeth.png
|
||||||
- assets/mockup_images/
|
- 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:
|
flutter_launcher_icons:
|
||||||
android: true
|
android: true
|
||||||
|
|||||||
@@ -6,24 +6,18 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <cloud_firestore/cloud_firestore_plugin_c_api.h>
|
#include <app_links/app_links_plugin_c_api.h>
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <firebase_auth/firebase_auth_plugin_c_api.h>
|
|
||||||
#include <firebase_core/firebase_core_plugin_c_api.h>
|
|
||||||
#include <firebase_storage/firebase_storage_plugin_c_api.h>
|
|
||||||
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
|
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
|
||||||
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
CloudFirestorePluginCApiRegisterWithRegistrar(
|
AppLinksPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("CloudFirestorePluginCApi"));
|
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
||||||
FileSelectorWindowsRegisterWithRegistrar(
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
FirebaseAuthPluginCApiRegisterWithRegistrar(
|
|
||||||
registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi"));
|
|
||||||
FirebaseCorePluginCApiRegisterWithRegistrar(
|
|
||||||
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
|
|
||||||
FirebaseStoragePluginCApiRegisterWithRegistrar(
|
|
||||||
registry->GetRegistrarForPlugin("FirebaseStoragePluginCApi"));
|
|
||||||
FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(
|
FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
|
registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
|
||||||
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,10 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
cloud_firestore
|
app_links
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
firebase_auth
|
|
||||||
firebase_core
|
|
||||||
firebase_storage
|
|
||||||
flutter_inappwebview_windows
|
flutter_inappwebview_windows
|
||||||
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|||||||
Reference in New Issue
Block a user