Guilhermes

This commit is contained in:
Guilherme Gaspar
2026-03-23 17:42:36 +00:00
parent 2f1935bf94
commit 44f456eb29
57 changed files with 1916 additions and 0 deletions

View 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>

View 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);
}
}

View 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>

View 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; }
}

View 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>

View 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));
}
}

View 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>

View 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));
}
}

View 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>

View 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");
}
}
}

View 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>

View 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; }
}

View 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>

View 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");
}
}
}

View 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>

View 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;
}
}