Guilhermes
This commit is contained in:
20
GuilhermesApp/Pages/ChatPage.xaml
Normal file
20
GuilhermesApp/Pages/ChatPage.xaml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="GuilhermesApp.Pages.ChatPage"
|
||||
Shell.NavBarIsVisible="False"
|
||||
Title="Chat">
|
||||
|
||||
<Grid RowDefinitions="*, Auto" Padding="15">
|
||||
|
||||
<ScrollView x:Name="ChatScroll" Grid.Row="0">
|
||||
<VerticalStackLayout x:Name="ChatStack" Spacing="10" Padding="0,0,0,15" />
|
||||
</ScrollView>
|
||||
|
||||
<Grid Grid.Row="1" ColumnDefinitions="*, Auto" ColumnSpacing="10" Margin="0,10,0,0">
|
||||
<Entry x:Name="MensagemEntry" Grid.Column="0" Placeholder="Escreve aqui..." ReturnType="Send" />
|
||||
<Button x:Name="EnviarBtn" Grid.Column="1" Text="Enviar" Clicked="OnEnviarClicked" />
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</ContentPage>
|
||||
94
GuilhermesApp/Pages/ChatPage.xaml.cs
Normal file
94
GuilhermesApp/Pages/ChatPage.xaml.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Maui.Controls.Shapes;
|
||||
|
||||
namespace GuilhermesApp.Pages;
|
||||
|
||||
public partial class ChatPage : ContentPage
|
||||
{
|
||||
private const string ApiKey = "AIzaSyBfQIHWK57GDhk4N-xYg_bEVWyQ6X1EmwA";
|
||||
private const string Url = $"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={ApiKey}";
|
||||
private HttpClient _httpClient = new HttpClient();
|
||||
|
||||
public ChatPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
AdicionarBalao("Olá! Sou o assistente da GuilhermesApp. Como te posso ajudar hoje?", false);
|
||||
}
|
||||
|
||||
private async void OnEnviarClicked(object sender, EventArgs e)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(MensagemEntry.Text)) return;
|
||||
|
||||
string textoUser = MensagemEntry.Text;
|
||||
MensagemEntry.Text = "";
|
||||
AdicionarBalao(textoUser, true);
|
||||
|
||||
EnviarBtn.IsEnabled = false;
|
||||
|
||||
try
|
||||
{
|
||||
var requestBody = new { contents = new[] { new { parts = new[] { new { text = textoUser } } } } };
|
||||
var content = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await _httpClient.PostAsync(Url, content);
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
|
||||
// Verifica se o pedido teve sucesso
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var respostaBot = doc.RootElement
|
||||
.GetProperty("candidates")[0]
|
||||
.GetProperty("content")
|
||||
.GetProperty("parts")[0]
|
||||
.GetProperty("text").GetString();
|
||||
|
||||
AdicionarBalao(respostaBot, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Lê a mensagem de erro real da Google
|
||||
string erroGoogle = "Erro desconhecido.";
|
||||
if (doc.RootElement.TryGetProperty("error", out var errorElement) &&
|
||||
errorElement.TryGetProperty("message", out var msgElement))
|
||||
{
|
||||
erroGoogle = msgElement.GetString() ?? "Erro desconhecido";
|
||||
}
|
||||
AdicionarBalao($"A API recusou: {erroGoogle}", false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AdicionarBalao($"Erro interno: {ex.Message}", false);
|
||||
}
|
||||
|
||||
EnviarBtn.IsEnabled = true;
|
||||
}
|
||||
// Cria os balões visuais
|
||||
private void AdicionarBalao(string? texto, bool isUser)
|
||||
{
|
||||
var label = new Label
|
||||
{
|
||||
Text = texto,
|
||||
TextColor = Colors.Black,
|
||||
Padding = 15
|
||||
};
|
||||
|
||||
var border = new Border
|
||||
{
|
||||
StrokeShape = new RoundRectangle { CornerRadius = new CornerRadius(15) },
|
||||
BackgroundColor = isUser ? Color.FromArgb("#D1E8FF") : Color.FromArgb("#F3F4F6"),
|
||||
Stroke = Colors.Transparent,
|
||||
Margin = new Thickness(isUser ? 50 : 0, 5, isUser ? 0 : 50, 5),
|
||||
HorizontalOptions = isUser ? LayoutOptions.End : LayoutOptions.Start,
|
||||
Content = label
|
||||
};
|
||||
|
||||
ChatStack.Children.Add(border);
|
||||
|
||||
// Faz scroll automático para o fundo
|
||||
ChatScroll.ScrollToAsync(ChatStack, ScrollToPosition.End, true);
|
||||
}
|
||||
}
|
||||
33
GuilhermesApp/Pages/FormsHistoryPage.xaml
Normal file
33
GuilhermesApp/Pages/FormsHistoryPage.xaml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="GuilhermesApp.Pages.FormsHistoryPage"
|
||||
Shell.NavBarIsVisible="False"
|
||||
xmlns:local="clr-namespace:GuilhermesApp.Pages"
|
||||
Title="O Meu Histórico">
|
||||
|
||||
<VerticalStackLayout Padding="20" Spacing="15">
|
||||
|
||||
<Label Text="Registos Anteriores" FontSize="24" FontAttributes="Bold" HorizontalOptions="Center" />
|
||||
|
||||
<ActivityIndicator x:Name="LoadingIndicator" IsRunning="True" Color="Blue" HorizontalOptions="Center" />
|
||||
|
||||
<Label x:Name="EmptyLabel" Text="Ainda não tens formulários submetidos." IsVisible="False" HorizontalOptions="Center" TextColor="Black" />
|
||||
|
||||
<CollectionView x:Name="FormsCollection" IsVisible="False" SelectionMode="Single" SelectionChanged="OnFormSelected">
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="local:FormResult">
|
||||
<Border Margin="0,5" Padding="15" StrokeShape="RoundRectangle 10" BackgroundColor="#F3F4F6" Stroke="Transparent">
|
||||
<VerticalStackLayout Spacing="5">
|
||||
<Label Text="{Binding Data}" FontAttributes="Bold" FontSize="16" TextColor="Black" />
|
||||
<Label Text="{Binding Humor, StringFormat='Humor: {0}'}" TextColor="Black"/>
|
||||
<Label Text="{Binding Stress, StringFormat='Nível de Stress: {0}/10'}" TextColor="Black"/>
|
||||
<Label Text="Clica para ver tudo..." FontSize="12" TextColor="Black" Margin="0,5,0,0" />
|
||||
</VerticalStackLayout>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
</CollectionView>
|
||||
|
||||
</VerticalStackLayout>
|
||||
</ContentPage>
|
||||
83
GuilhermesApp/Pages/FormsHistoryPage.xaml.cs
Normal file
83
GuilhermesApp/Pages/FormsHistoryPage.xaml.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using GuilhermesApp.Helpers;
|
||||
using Firebase.Database.Query;
|
||||
|
||||
namespace GuilhermesApp.Pages;
|
||||
|
||||
public partial class FormsHistoryPage : ContentPage
|
||||
{
|
||||
private FirebaseService _firebaseService = new FirebaseService();
|
||||
|
||||
public FormsHistoryPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await CarregarHistorico();
|
||||
}
|
||||
|
||||
private async Task CarregarHistorico()
|
||||
{
|
||||
try
|
||||
{
|
||||
var user = _firebaseService.AuthClient.User;
|
||||
if (user == null) return;
|
||||
|
||||
// Vai buscar todos os registos na pasta Forms do utilizador
|
||||
var forms = await _firebaseService.DbClient
|
||||
.Child("Forms")
|
||||
.Child(user.Uid)
|
||||
.OnceAsync<FormResult>();
|
||||
|
||||
LoadingIndicator.IsRunning = false;
|
||||
LoadingIndicator.IsVisible = false;
|
||||
|
||||
if (forms.Count == 0)
|
||||
{
|
||||
EmptyLabel.IsVisible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Converte os dados, inverte a ordem (para o mais recente ficar no topo) e mostra na lista
|
||||
var listaOrdenada = forms.Select(f => f.Object).Reverse().ToList();
|
||||
FormsCollection.ItemsSource = listaOrdenada;
|
||||
FormsCollection.IsVisible = true;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
LoadingIndicator.IsRunning = false;
|
||||
LoadingIndicator.IsVisible = false;
|
||||
await DisplayAlert("Erro", "Não foi possível carregar o histórico.", "OK");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnFormSelected(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (e.CurrentSelection.FirstOrDefault() is FormResult form)
|
||||
{
|
||||
string exercicio = form.FezExercicio ? "Sim" : "Não";
|
||||
string mensagem = $"Humor: {form.Humor}\nStress: {form.Stress}/10\nSono: {form.Sono}\nExercício: {exercicio}\nNotas: {form.Notas}";
|
||||
|
||||
await DisplayAlert($"Detalhes de {form.Data}", mensagem, "Fechar");
|
||||
|
||||
// Limpa a seleção para permitir clicar novamente no mesmo
|
||||
((CollectionView)sender).SelectedItem = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Classe de apoio para mapear os dados do Firebase
|
||||
public class FormResult
|
||||
{
|
||||
public string? Data { get; set; }
|
||||
public string? Humor { get; set; }
|
||||
public double Stress { get; set; }
|
||||
public string? Sono { get; set; }
|
||||
public bool FezExercicio { get; set; }
|
||||
public string? Notas { get; set; }
|
||||
}
|
||||
16
GuilhermesApp/Pages/FormsMenuPage.xaml
Normal file
16
GuilhermesApp/Pages/FormsMenuPage.xaml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="GuilhermesApp.Pages.FormsMenuPage"
|
||||
Shell.NavBarIsVisible="False"
|
||||
Title="Formulários">
|
||||
|
||||
<VerticalStackLayout Spacing="20" Padding="30" VerticalOptions="Center">
|
||||
|
||||
<Label Text="Gestão de Formulários" FontSize="28" HorizontalOptions="Center" FontAttributes="Bold" Margin="0,0,0,20" />
|
||||
|
||||
<Button Text="Preencher Novo Formulário" Clicked="OnNewFormClicked" HeightRequest="60" />
|
||||
<Button Text="Ver Histórico" Clicked="OnHistoryClicked" HeightRequest="60" />
|
||||
|
||||
</VerticalStackLayout>
|
||||
</ContentPage>
|
||||
19
GuilhermesApp/Pages/FormsMenuPage.xaml.cs
Normal file
19
GuilhermesApp/Pages/FormsMenuPage.xaml.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace GuilhermesApp.Pages;
|
||||
|
||||
public partial class FormsMenuPage : ContentPage
|
||||
{
|
||||
public FormsMenuPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private async void OnNewFormClicked(object sender, EventArgs e)
|
||||
{
|
||||
await Shell.Current.GoToAsync(nameof(NewFormPage));
|
||||
}
|
||||
|
||||
private async void OnHistoryClicked(object sender, EventArgs e)
|
||||
{
|
||||
await Shell.Current.GoToAsync(nameof(FormsHistoryPage));
|
||||
}
|
||||
}
|
||||
32
GuilhermesApp/Pages/LoginPage.xaml
Normal file
32
GuilhermesApp/Pages/LoginPage.xaml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="GuilhermesApp.Pages.LoginPage"
|
||||
Shell.NavBarIsVisible="False"
|
||||
Shell.FlyoutBehavior="Disabled"
|
||||
Title="Login">
|
||||
|
||||
<ScrollView>
|
||||
<VerticalStackLayout Spacing="20" Padding="30" VerticalOptions="Center">
|
||||
|
||||
<Image Source="logo.png" HeightRequest="240" HorizontalOptions="Center" />
|
||||
|
||||
<Label Text="Bem-vindo!" FontSize="32" HorizontalOptions="Center" FontAttributes="Bold" />
|
||||
|
||||
<Entry x:Name="EmailEntry" Placeholder="Email" Keyboard="Email" />
|
||||
<Entry x:Name="PasswordEntry" Placeholder="Password" IsPassword="True" />
|
||||
|
||||
<Button Text="Entrar" Clicked="OnLoginClicked" />
|
||||
|
||||
<Label Text="Ainda não tens conta? Regista-te"
|
||||
TextColor="#FF888890"
|
||||
TextDecorations="Underline"
|
||||
HorizontalOptions="Center">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="OnRegisterClicked" />
|
||||
</Label.GestureRecognizers>
|
||||
</Label>
|
||||
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
</ContentPage>
|
||||
42
GuilhermesApp/Pages/LoginPage.xaml.cs
Normal file
42
GuilhermesApp/Pages/LoginPage.xaml.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using GuilhermesApp.Helpers;
|
||||
|
||||
namespace GuilhermesApp.Pages;
|
||||
|
||||
public partial class LoginPage : ContentPage
|
||||
{
|
||||
private FirebaseService _firebaseService = new FirebaseService();
|
||||
|
||||
public LoginPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private async void OnLoginClicked(object sender, EventArgs e)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(EmailEntry.Text) || string.IsNullOrWhiteSpace(PasswordEntry.Text))
|
||||
{
|
||||
await DisplayAlert("Erro", "Preenche o email e a password.", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Tenta fazer o login no Firebase
|
||||
var user = await _firebaseService.AuthClient.SignInWithEmailAndPasswordAsync(EmailEntry.Text, PasswordEntry.Text);
|
||||
|
||||
// Se correr bem, vai para a página de Perfil
|
||||
// O "//" define a HomePage como a nova raiz e impede de voltar atrás
|
||||
await Shell.Current.GoToAsync("//MainTabs");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await DisplayAlert("Erro", "Login falhou. Verifica as credenciais.", "OK");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnRegisterClicked(object sender, EventArgs e)
|
||||
{
|
||||
// Vai para a página de registo
|
||||
await Shell.Current.GoToAsync(nameof(RegisterPage));
|
||||
}
|
||||
}
|
||||
51
GuilhermesApp/Pages/NewFormPage.xaml
Normal file
51
GuilhermesApp/Pages/NewFormPage.xaml
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="GuilhermesApp.Pages.NewFormPage"
|
||||
Shell.NavBarIsVisible="False"
|
||||
Title="Novo Formulário">
|
||||
|
||||
<ScrollView>
|
||||
<VerticalStackLayout Spacing="20" Padding="30">
|
||||
|
||||
<Label Text="Bem-estar Diário" FontSize="24" FontAttributes="Bold" HorizontalOptions="Center" />
|
||||
|
||||
<Label Text="1. Como te sentes hoje?" FontAttributes="Bold" />
|
||||
<Picker x:Name="HumorPicker" Title="Seleciona uma opção">
|
||||
<Picker.ItemsSource>
|
||||
<x:Array Type="{x:Type x:String}">
|
||||
<x:String>Muito Bem</x:String>
|
||||
<x:String>Bem</x:String>
|
||||
<x:String>Neutro</x:String>
|
||||
<x:String>Mal</x:String>
|
||||
<x:String>Muito Mal</x:String>
|
||||
</x:Array>
|
||||
</Picker.ItemsSource>
|
||||
</Picker>
|
||||
|
||||
<Label Text="2. Nível de stress (1 a 10)?" FontAttributes="Bold" />
|
||||
<Slider x:Name="StressSlider" Minimum="1" Maximum="10" Value="5" />
|
||||
<Label x:DataType="Slider" Text="{Binding Source={x:Reference StressSlider}, Path=Value, StringFormat='{0:F0}'}" HorizontalOptions="Center" />
|
||||
|
||||
<Label Text="3. Dormiste bem?" FontAttributes="Bold" />
|
||||
<Picker x:Name="SonoPicker" Title="Seleciona uma opção">
|
||||
<Picker.ItemsSource>
|
||||
<x:Array Type="{x:Type x:String}">
|
||||
<x:String>Sim, perfeitamente</x:String>
|
||||
<x:String>Mais ou menos</x:String>
|
||||
<x:String>Não, dormi mal</x:String>
|
||||
</x:Array>
|
||||
</Picker.ItemsSource>
|
||||
</Picker>
|
||||
|
||||
<Label Text="4. Fizeste exercício hoje?" FontAttributes="Bold" />
|
||||
<Switch x:Name="ExercicioSwitch" HorizontalOptions="Start" />
|
||||
|
||||
<Label Text="5. Notas adicionais:" FontAttributes="Bold" />
|
||||
<Entry x:Name="NotasEntry" Placeholder="Escreve aqui..." />
|
||||
|
||||
<Button Text="Submeter" Clicked="OnSubmitClicked" Margin="0,20,0,0" />
|
||||
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
</ContentPage>
|
||||
56
GuilhermesApp/Pages/NewFormPage.xaml.cs
Normal file
56
GuilhermesApp/Pages/NewFormPage.xaml.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using GuilhermesApp.Helpers;
|
||||
using Firebase.Database.Query;
|
||||
|
||||
namespace GuilhermesApp.Pages;
|
||||
|
||||
public partial class NewFormPage : ContentPage
|
||||
{
|
||||
private FirebaseService _firebaseService = new FirebaseService();
|
||||
|
||||
public NewFormPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private async void OnSubmitClicked(object sender, EventArgs e)
|
||||
{
|
||||
// Obriga a responder a algumas
|
||||
if (HumorPicker.SelectedIndex == -1 || SonoPicker.SelectedIndex == -1)
|
||||
{
|
||||
await DisplayAlert("Erro", "Responde pelo menos às perguntas de humor e sono.", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var user = _firebaseService.AuthClient.User;
|
||||
if (user == null) return;
|
||||
|
||||
// Prepara os dados do formulário
|
||||
var novoFormulario = new
|
||||
{
|
||||
Data = DateTime.Now.ToString("dd/MM/yyyy HH:mm"),
|
||||
Humor = HumorPicker.SelectedItem.ToString(),
|
||||
Stress = Math.Round(StressSlider.Value),
|
||||
Sono = SonoPicker.SelectedItem.ToString(),
|
||||
FezExercicio = ExercicioSwitch.IsToggled,
|
||||
Notas = string.IsNullOrWhiteSpace(NotasEntry.Text) ? "Sem notas" : NotasEntry.Text
|
||||
};
|
||||
|
||||
// Guarda na Realtime Database na pasta "Forms" -> "ID do Utilizador"
|
||||
await _firebaseService.DbClient
|
||||
.Child("Forms")
|
||||
.Child(user.Uid)
|
||||
.PostAsync(novoFormulario); // PostAsync cria um novo registo na lista
|
||||
|
||||
await DisplayAlert("Sucesso", "Formulário submetido e guardado!", "OK");
|
||||
|
||||
// Volta à página anterior
|
||||
await Shell.Current.GoToAsync("..");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await DisplayAlert("Erro", $"Não foi possível guardar. Erro: {ex.Message}", "OK");
|
||||
}
|
||||
}
|
||||
}
|
||||
24
GuilhermesApp/Pages/ProfilePage.xaml
Normal file
24
GuilhermesApp/Pages/ProfilePage.xaml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="GuilhermesApp.Pages.ProfilePage"
|
||||
Shell.NavBarIsVisible="False"
|
||||
Title="Perfil">
|
||||
|
||||
<VerticalStackLayout Spacing="20" Padding="30" VerticalOptions="Center">
|
||||
|
||||
<Label Text="O Meu Perfil" FontSize="32" HorizontalOptions="Center" FontAttributes="Bold" />
|
||||
|
||||
<ActivityIndicator x:Name="LoadingIndicator" IsRunning="True" HorizontalOptions="Center" Color="Blue" />
|
||||
|
||||
<VerticalStackLayout x:Name="ProfileInfoLayout" IsVisible="False" Spacing="10" HorizontalOptions="Center">
|
||||
<Label x:Name="NomeLabel" FontSize="22" FontAttributes="Bold" HorizontalOptions="Center" />
|
||||
<Label x:Name="EmailLabel" FontSize="16" TextColor="Gray" HorizontalOptions="Center" />
|
||||
<Label x:Name="TelemovelLabel" FontSize="16" HorizontalOptions="Center" />
|
||||
<Label x:Name="GeneroLabel" FontSize="16" HorizontalOptions="Center" />
|
||||
</VerticalStackLayout>
|
||||
|
||||
<Button Text="Terminar Sessão" Clicked="OnLogoutClicked" BackgroundColor="Red" TextColor="White" Margin="0,30,0,0" />
|
||||
|
||||
</VerticalStackLayout>
|
||||
</ContentPage>
|
||||
70
GuilhermesApp/Pages/ProfilePage.xaml.cs
Normal file
70
GuilhermesApp/Pages/ProfilePage.xaml.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using GuilhermesApp.Helpers;
|
||||
using Firebase.Database.Query;
|
||||
|
||||
namespace GuilhermesApp.Pages;
|
||||
|
||||
public partial class ProfilePage : ContentPage
|
||||
{
|
||||
private FirebaseService _firebaseService = new FirebaseService();
|
||||
|
||||
public ProfilePage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
// Este método corre sempre que a página aparece no ecrã
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await CarregarPerfil();
|
||||
}
|
||||
|
||||
private async Task CarregarPerfil()
|
||||
{
|
||||
try
|
||||
{
|
||||
var user = _firebaseService.AuthClient.User;
|
||||
if (user != null)
|
||||
{
|
||||
// Vai buscar os dados à Realtime Database
|
||||
var perfil = await _firebaseService.DbClient
|
||||
.Child("Users")
|
||||
.Child(user.Uid)
|
||||
.OnceSingleAsync<UserProfile>();
|
||||
|
||||
if (perfil != null)
|
||||
{
|
||||
NomeLabel.Text = perfil.Nome;
|
||||
EmailLabel.Text = perfil.Email;
|
||||
TelemovelLabel.Text = $"Telemóvel: {perfil.Telemovel}";
|
||||
GeneroLabel.Text = $"Género: {perfil.Genero}";
|
||||
|
||||
// Esconde o loading e mostra os dados
|
||||
LoadingIndicator.IsRunning = false;
|
||||
LoadingIndicator.IsVisible = false;
|
||||
ProfileInfoLayout.IsVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await DisplayAlert("Erro", "Não foi possível carregar o perfil.", "OK");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnLogoutClicked(object sender, EventArgs e)
|
||||
{
|
||||
_firebaseService.AuthClient.SignOut();
|
||||
// Volta para o Login e limpa o histórico
|
||||
await Shell.Current.GoToAsync("//LoginPage");
|
||||
}
|
||||
}
|
||||
|
||||
// Classe simples no final do ficheiro para ajudar a ler os dados do Firebase
|
||||
public class UserProfile
|
||||
{
|
||||
public string? Nome { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Telemovel { get; set; }
|
||||
public string? Genero { get; set; }
|
||||
}
|
||||
36
GuilhermesApp/Pages/RegisterPage.xaml
Normal file
36
GuilhermesApp/Pages/RegisterPage.xaml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="GuilhermesApp.Pages.RegisterPage"
|
||||
Shell.NavBarIsVisible="False"
|
||||
Shell.FlyoutBehavior="Disabled"
|
||||
Title="Registar">
|
||||
|
||||
<ScrollView>
|
||||
<VerticalStackLayout Spacing="20" Padding="30" VerticalOptions="Center">
|
||||
|
||||
<Label Text="Criar Conta" FontSize="32" HorizontalOptions="Center" FontAttributes="Bold" />
|
||||
|
||||
<Entry x:Name="NomeEntry" Placeholder="Nome Completo" />
|
||||
<Entry x:Name="TelemovelEntry" Placeholder="Telemóvel" Keyboard="Telephone" />
|
||||
|
||||
<Picker x:Name="GeneroPicker" Title="Seleciona o Género">
|
||||
<Picker.ItemsSource>
|
||||
<x:Array Type="{x:Type x:String}">
|
||||
<x:String>Masculino</x:String>
|
||||
<x:String>Feminino</x:String>
|
||||
<x:String>Outro</x:String>
|
||||
<x:String>Prefiro não dizer</x:String>
|
||||
</x:Array>
|
||||
</Picker.ItemsSource>
|
||||
</Picker>
|
||||
|
||||
<Entry x:Name="EmailEntry" Placeholder="Email" Keyboard="Email" />
|
||||
<Entry x:Name="PasswordEntry" Placeholder="Password (mín. 6 caracteres)" IsPassword="True" />
|
||||
<Entry x:Name="ConfirmPasswordEntry" Placeholder="Confirmar Password" IsPassword="True" />
|
||||
|
||||
<Button Text="Registar" Clicked="OnRegisterAccountClicked" />
|
||||
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
</ContentPage>
|
||||
68
GuilhermesApp/Pages/RegisterPage.xaml.cs
Normal file
68
GuilhermesApp/Pages/RegisterPage.xaml.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using GuilhermesApp.Helpers;
|
||||
using Firebase.Database.Query;
|
||||
|
||||
namespace GuilhermesApp.Pages;
|
||||
|
||||
public partial class RegisterPage : ContentPage
|
||||
{
|
||||
private FirebaseService _firebaseService = new FirebaseService();
|
||||
|
||||
public RegisterPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private async void OnRegisterAccountClicked(object sender, EventArgs e)
|
||||
{
|
||||
// 1. Validar se está tudo preenchido
|
||||
if (string.IsNullOrWhiteSpace(NomeEntry.Text) || string.IsNullOrWhiteSpace(TelemovelEntry.Text) ||
|
||||
GeneroPicker.SelectedIndex == -1 || string.IsNullOrWhiteSpace(EmailEntry.Text) ||
|
||||
string.IsNullOrWhiteSpace(PasswordEntry.Text))
|
||||
{
|
||||
await DisplayAlert("Erro", "Preenche todos os campos.", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Validar passwords
|
||||
if (PasswordEntry.Text != ConfirmPasswordEntry.Text)
|
||||
{
|
||||
await DisplayAlert("Erro", "As passwords não coincidem.", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
if (PasswordEntry.Text.Length < 6)
|
||||
{
|
||||
await DisplayAlert("Erro", "A password tem de ter pelo menos 6 caracteres.", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 3. Cria a conta na Autenticação
|
||||
var userCredential = await _firebaseService.AuthClient.CreateUserWithEmailAndPasswordAsync(EmailEntry.Text, PasswordEntry.Text);
|
||||
var uid = userCredential.User.Uid; // O ID único do utilizador
|
||||
|
||||
// 4. Guarda os restantes dados na Realtime Database, na pasta "Users"
|
||||
await _firebaseService.DbClient
|
||||
.Child("Users")
|
||||
.Child(uid)
|
||||
.PutAsync(new
|
||||
{
|
||||
Nome = NomeEntry.Text,
|
||||
Telemovel = TelemovelEntry.Text,
|
||||
Genero = GeneroPicker.SelectedItem.ToString(),
|
||||
Email = EmailEntry.Text
|
||||
});
|
||||
|
||||
await DisplayAlert("Sucesso", "Conta criada com sucesso!", "OK");
|
||||
|
||||
// Volta para a página de Login
|
||||
await Shell.Current.GoToAsync("..");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Mostra o erro exato que vem do Firebase
|
||||
await DisplayAlert("Erro", $"Não foi possível criar a conta. Detalhe: {ex.Message}", "OK");
|
||||
}
|
||||
}
|
||||
}
|
||||
27
GuilhermesApp/Pages/SensorPage.xaml
Normal file
27
GuilhermesApp/Pages/SensorPage.xaml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="GuilhermesApp.Pages.SensorPage"
|
||||
Shell.NavBarIsVisible="False"
|
||||
Title="Sensor">
|
||||
|
||||
<VerticalStackLayout Spacing="30" Padding="30" VerticalOptions="Center">
|
||||
|
||||
<Label Text="Controlo ESP32-C6" FontSize="28" FontAttributes="Bold" HorizontalOptions="Center" />
|
||||
|
||||
<Border Padding="20" StrokeShape="RoundRectangle 15" BackgroundColor="#F3F4F6" Stroke="Transparent">
|
||||
<VerticalStackLayout Spacing="10" HorizontalOptions="Center">
|
||||
<Label Text="Estado do LED" FontSize="18" HorizontalOptions="Center" TextColor="Black"/>
|
||||
<Button x:Name="LedBtn" Text="LIGAR LED" Clicked="OnLedClicked" BackgroundColor="DarkSlateGray" TextColor="White"/>
|
||||
</VerticalStackLayout>
|
||||
</Border>
|
||||
|
||||
<Border Padding="20" StrokeShape="RoundRectangle 15" BackgroundColor="#D1E8FF" Stroke="Transparent">
|
||||
<VerticalStackLayout Spacing="10" HorizontalOptions="Center">
|
||||
<Label Text="Botão no Arduino" FontSize="18" HorizontalOptions="Center" TextColor="Black"/>
|
||||
<Label x:Name="StatusLabel" Text="A aguardar sinal..." FontAttributes="Bold" FontSize="20" TextColor="Black" HorizontalOptions="Center" />
|
||||
</VerticalStackLayout>
|
||||
</Border>
|
||||
|
||||
</VerticalStackLayout>
|
||||
</ContentPage>
|
||||
83
GuilhermesApp/Pages/SensorPage.xaml.cs
Normal file
83
GuilhermesApp/Pages/SensorPage.xaml.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using MQTTnet;
|
||||
using System.Text;
|
||||
using System.Buffers;
|
||||
|
||||
namespace GuilhermesApp.Pages;
|
||||
|
||||
public partial class SensorPage : ContentPage
|
||||
{
|
||||
private IMqttClient? _mqttClient;
|
||||
private MqttClientOptions? _options;
|
||||
private bool _isLedOn = false;
|
||||
|
||||
public SensorPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
ConfigurarMqtt();
|
||||
}
|
||||
|
||||
private async void ConfigurarMqtt()
|
||||
{
|
||||
try
|
||||
{
|
||||
// TENTATIVA A: MqttClientFactory (Novo padrão em algumas versões v5)
|
||||
// Se der erro aqui, tenta mudar para: var factory = new MqttFactory();
|
||||
var factory = new MqttClientFactory();
|
||||
_mqttClient = factory.CreateMqttClient();
|
||||
|
||||
_options = new MqttClientOptionsBuilder()
|
||||
.WithTcpServer("broker.hivemq.com", 1883)
|
||||
.Build();
|
||||
|
||||
_mqttClient.ApplicationMessageReceivedAsync += e =>
|
||||
{
|
||||
// Extração manual de bytes para evitar o erro do ToArray
|
||||
var buffer = e.ApplicationMessage.Payload;
|
||||
byte[] payloadBytes = new byte[buffer.Length];
|
||||
buffer.CopyTo(payloadBytes);
|
||||
|
||||
string payload = Encoding.UTF8.GetString(payloadBytes);
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
if (payload == "PRESSIONADO")
|
||||
{
|
||||
StatusLabel.Text = $"Último clique: {DateTime.Now:HH:mm:ss}";
|
||||
StatusLabel.TextColor = Colors.Green;
|
||||
}
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
await _mqttClient.ConnectAsync(_options);
|
||||
await _mqttClient.SubscribeAsync("guilherme/app/botao");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Erro crítico MQTT: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnLedClicked(object sender, EventArgs e)
|
||||
{
|
||||
if (_mqttClient == null || !_mqttClient.IsConnected)
|
||||
{
|
||||
await DisplayAlert("Erro", "O MQTT não está ligado.", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
_isLedOn = !_isLedOn;
|
||||
string comando = _isLedOn ? "ON" : "OFF";
|
||||
|
||||
var message = new MqttApplicationMessageBuilder()
|
||||
.WithTopic("guilherme/app/led")
|
||||
.WithPayload(comando)
|
||||
.Build();
|
||||
|
||||
await _mqttClient.PublishAsync(message);
|
||||
|
||||
LedBtn.Text = _isLedOn ? "DESLIGAR LED" : "LIGAR LED";
|
||||
LedBtn.BackgroundColor = _isLedOn ? Colors.Red : Colors.DarkSlateGray;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user