diff --git a/Jugenddienst Stunden/Converter/EventArgsPassThroughConverter.cs b/Jugenddienst Stunden/Converter/EventArgsPassThroughConverter.cs new file mode 100644 index 0000000..f6b4545 --- /dev/null +++ b/Jugenddienst Stunden/Converter/EventArgsPassThroughConverter.cs @@ -0,0 +1,8 @@ +using System.Globalization; + +namespace Jugenddienst_Stunden.Converter; + +public sealed class EventArgsPassThroughConverter : IValueConverter { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => value; + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => value; +} diff --git a/Jugenddienst Stunden/Converter/InverseBoolConverter.cs b/Jugenddienst Stunden/Converter/InverseBoolConverter.cs new file mode 100644 index 0000000..8c4f506 --- /dev/null +++ b/Jugenddienst Stunden/Converter/InverseBoolConverter.cs @@ -0,0 +1,15 @@ +using System.Globalization; + +namespace Jugenddienst_Stunden.Converter; + +public sealed class InverseBoolConverter : IValueConverter { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { + if (value is bool b) return !b; + return true; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { + if (value is bool b) return !b; + return false; + } +} diff --git a/Jugenddienst Stunden/Infrastructure/ApiClient.cs b/Jugenddienst Stunden/Infrastructure/ApiClient.cs index fcea2cb..6f10a26 100644 --- a/Jugenddienst Stunden/Infrastructure/ApiClient.cs +++ b/Jugenddienst Stunden/Infrastructure/ApiClient.cs @@ -1,8 +1,9 @@ +using Jugenddienst_Stunden.Interfaces; using System.Net.Http.Headers; +using System.Net.Http.Json; using System.Text; using System.Text.Json; -using System.Net.Http.Json; -using Jugenddienst_Stunden.Interfaces; +using ZXing.Aztec.Internal; namespace Jugenddienst_Stunden.Infrastructure; @@ -12,51 +13,68 @@ internal sealed class ApiClient : IApiClient { private readonly ApiOptions _options; private readonly IAppSettings _settings; - public ApiClient(HttpClient http, ApiOptions options, ITokenProvider tokenProvider, IAppSettings settings) { - _http = http; - _options = options; - _settings = settings; + public ApiClient(HttpClient http, ApiOptions options, ITokenProvider tokenProvider, IAppSettings settings) { + _http = http; + _options = options; + _settings = settings; - _http.Timeout = options.Timeout; - if (!_http.DefaultRequestHeaders.Accept.Any()) - _http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + // Timeout nur einmalig beim Erstellen setzen – spätere Änderungen an HttpClient.Timeout + // nach der ersten Verwendung führen zu InvalidOperationException. + if (_http.Timeout != options.Timeout) + _http.Timeout = options.Timeout; + // Standardmäßig JSON akzeptieren; doppelte Einträge vermeiden + if (!_http.DefaultRequestHeaders.Accept.Any(h => h.MediaType?.Equals("application/json", StringComparison.OrdinalIgnoreCase) == true)) + _http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - var token = tokenProvider.GetToken(); - if (!string.IsNullOrWhiteSpace(token)) - _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + // KEINE globalen Header/Properties mehr dynamisch setzen. Authorization wird pro Request gesetzt. - _json = new JsonSerializerOptions { - PropertyNameCaseInsensitive = true, - WriteIndented = false, - DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull - }; + _json = new JsonSerializerOptions { + PropertyNameCaseInsensitive = true, + WriteIndented = false, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + }; // Globale Converter: erlauben numerische Felder auch als Strings (z.B. user.id) _json.Converters.Add(new Jugenddienst_Stunden.Models.JsonFlexibleInt32Converter()); _json.Converters.Add(new Jugenddienst_Stunden.Models.JsonFlexibleNullableInt32Converter()); - // Stelle sicher, dass die BaseAddress sofort aus den aktuellen Settings (Preferences) gesetzt wird - // und nicht erst beim ersten Request. Dadurch steht die ApiUrl ab Initialisierung zur Verfügung. - EnsureBaseAddress(); - } + // WICHTIG: HttpClient.BaseAddress NICHT dynamisch setzen oder ändern – das verursacht Exceptions, + // sobald bereits Requests gestartet wurden. Wir bauen stattdessen absolute URIs pro Request. + } public Task GetAsync(string path, IDictionary? query = null, CancellationToken ct = default) => SendAsync(HttpMethod.Get, path, null, query, ct); - public async Task SendAsync(HttpMethod method, string path, object? body = null, - IDictionary? query = null, CancellationToken ct = default) { - // Vor jedem Request sicherstellen, dass die (ggf. geänderte) BaseAddress gesetzt ist - EnsureBaseAddress(); - var uri = BuildUri(path, query); - using var req = new HttpRequestMessage(method, uri) { - Content = body is null ? null : JsonContent.Create(body, options: _json) - }; + public async Task SendAsync(HttpMethod method, string path, object? body = null, + IDictionary? query = null, CancellationToken ct = default) { + // Absolute URI aus aktuellem Settings‑BaseUrl bauen, ohne HttpClient.BaseAddress zu nutzen. + var uri = BuildAbsoluteUri(_settings.ApiUrl, path, query); + using var req = new HttpRequestMessage(method, uri); + // Authorization PRO REQUEST setzen (immer, wenn Token vorhanden ist) + // Hinweis: Das QR-Token kann RFC-unzulässige Zeichen (z. B. '|') enthalten. + // AuthenticationHeaderValue würde solche Werte ablehnen. Daher ohne Validierung setzen. + var currentToken = _settings.ApiKey; + if (!string.IsNullOrWhiteSpace(currentToken)) { + // Vorherige Header (falls vorhanden) entfernen, um Duplikate zu vermeiden + req.Headers.Remove("Authorization"); + req.Headers.TryAddWithoutValidation("Authorization", $"Bearer {currentToken}"); + } + if (body is HttpContent httpContent) { + req.Content = httpContent; + } else if (body is not null) { + req.Content = JsonContent.Create(body, options: _json); + } + + // Sicherstellen, dass Accept: application/json auch auf Request-Ebene vorhanden ist (z. B. für LoginWithToken GET) + if (!req.Headers.Accept.Any(h => h.MediaType?.Equals("application/json", StringComparison.OrdinalIgnoreCase) == true)) { + req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + } using var res = await _http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false); var text = await res.Content.ReadAsStringAsync(ct).ConfigureAwait(false); - //if (!res.IsSuccessStatusCode) - // throw ApiException.From(res.StatusCode, TryGetMessage(text), text); + if (!res.IsSuccessStatusCode) + throw ApiException.From(res.StatusCode, TryGetMessage(text), text); if (res.StatusCode != System.Net.HttpStatusCode.OK) { // Verhalten wie in BaseFunc: bei Fehlerstatus -> "message" aus Body lesen und mit dessen Inhalt eine Exception werfen. @@ -91,22 +109,7 @@ internal sealed class ApiClient : IApiClient { public Task DeleteAsync(string path, IDictionary? query = null, CancellationToken ct = default) => SendAsync(HttpMethod.Delete, path, null, query, ct); - private void EnsureBaseAddress() { - var baseUrl = _settings.ApiUrl; - - if (string.IsNullOrWhiteSpace(baseUrl)) { - throw new InvalidOperationException( - "ApiUrl ist leer. Bitte zuerst eine gültige Server-URL setzen (Preferences key 'apiUrl'), " + - "z.B. im Login/Setup, bevor API-Aufrufe stattfinden." - ); - } - - // nur setzen, wenn nötig (damit spätere Änderungen nach Login greifen) - if (_http.BaseAddress is null || !Uri.Equals(_http.BaseAddress, new Uri(baseUrl, UriKind.Absolute))) { - _http.BaseAddress = new Uri(baseUrl, UriKind.Absolute); - _http.Timeout = _options.Timeout; - } - } + // Entfernt: EnsureBaseAddress – wir ändern BaseAddress nicht mehr dynamisch. private static string TryGetMessage(string text) { try { @@ -119,15 +122,29 @@ internal sealed class ApiClient : IApiClient { return text; } - private static Uri BuildUri(string path, IDictionary? query) { - if (query is null || query.Count == 0) - return new Uri(path, UriKind.Relative); + private static Uri BuildAbsoluteUri(string baseUrl, string path, IDictionary? query) { + if (string.IsNullOrWhiteSpace(baseUrl)) + throw new InvalidOperationException( + "ApiUrl ist leer. Bitte zuerst eine gültige Server-URL setzen (Preferences key 'apiUrl')."); - var sb = new StringBuilder(path); - sb.Append(path.Contains('?') ? '&' : '?'); - sb.Append(string.Join('&', query - .Where(kv => kv.Value is not null) - .Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value!)}"))); - return new Uri(sb.ToString(), UriKind.Relative); - } + // Basis muss absolut sein (z. B. https://host/appapi/) + var baseUri = new Uri(baseUrl, UriKind.Absolute); + + // Pfad relativ zur Basis aufbauen + string relativePath = path ?? string.Empty; + if (query is not null && query.Count > 0) { + var sb = new StringBuilder(relativePath); + sb.Append(relativePath.Contains('?') ? '&' : '?'); + sb.Append(string.Join('&', query + .Where(kv => kv.Value is not null) + .Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value!)}"))); + relativePath = sb.ToString(); + } + + // Wenn path bereits absolut ist, direkt verwenden + if (Uri.TryCreate(relativePath, UriKind.Absolute, out var absoluteFromPath)) + return absoluteFromPath; + + return new Uri(baseUri, relativePath); + } } \ No newline at end of file diff --git a/Jugenddienst Stunden/Infrastructure/RequestLoggingHandler.cs b/Jugenddienst Stunden/Infrastructure/RequestLoggingHandler.cs new file mode 100644 index 0000000..5181cbc --- /dev/null +++ b/Jugenddienst Stunden/Infrastructure/RequestLoggingHandler.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Jugenddienst_Stunden.Infrastructure; +internal sealed class RequestLoggingHandler : DelegatingHandler { + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { + // Log outgoing request URI + headers + Debug.WriteLine($"[Http] Request: {request.Method} {request.RequestUri}"); + foreach (var h in request.Headers) { + Debug.WriteLine($"[Http] RequestHeader: {h.Key} = {string.Join(", ", h.Value)}"); + } + if (request.Content is not null) { + foreach (var h in request.Content.Headers) { + Debug.WriteLine($"[Http] ContentHeader: {h.Key} = {string.Join(", ", h.Value)}"); + } + } + + var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + + // Log response status + Location (bei Redirects) + final request URI used by handler + Debug.WriteLine($"[Http] Response: {(int)response.StatusCode} {response.ReasonPhrase}"); + if (response.Headers.Location is not null) + Debug.WriteLine($"[Http] Response Location: {response.Headers.Location}"); + if (response.RequestMessage?.RequestUri is not null) + Debug.WriteLine($"[Http] Final RequestUri: {response.RequestMessage.RequestUri}"); + + return response; + } +} diff --git a/Jugenddienst Stunden/Interfaces/IAuthService.cs b/Jugenddienst Stunden/Interfaces/IAuthService.cs new file mode 100644 index 0000000..e3ce995 --- /dev/null +++ b/Jugenddienst Stunden/Interfaces/IAuthService.cs @@ -0,0 +1,8 @@ +namespace Jugenddienst_Stunden.Interfaces; + +using Jugenddienst_Stunden.Types; + +public interface IAuthService { + Task LoginWithCredentials(string username, string password, string serverUrl, CancellationToken ct = default); + Task LoginWithToken(string token, CancellationToken ct = default); +} diff --git a/Jugenddienst Stunden/MauiProgram.cs b/Jugenddienst Stunden/MauiProgram.cs index bc8bc6c..560f694 100644 --- a/Jugenddienst Stunden/MauiProgram.cs +++ b/Jugenddienst Stunden/MauiProgram.cs @@ -8,6 +8,7 @@ using Jugenddienst_Stunden.Validators; using Microsoft.Extensions.Logging; using ZXing.Net.Maui.Controls; using System.Net.Http; +using Jugenddienst_Stunden.ViewModels; namespace Jugenddienst_Stunden; @@ -56,7 +57,16 @@ public static class MauiProgram { builder.Services.AddSingleton(); // HttpClient + ApiClient - builder.Services.AddSingleton(_ => new HttpClient()); + // Configure HttpClient with RequestLoggingHandler and disable automatic redirects for diagnosis + builder.Services.AddTransient(); + builder.Services.AddSingleton(sp => { + var nativeHandler = new HttpClientHandler { AllowAutoRedirect = false }; + var logging = sp.GetRequiredService(); + logging.InnerHandler = nativeHandler; + // HttpClient.Timeout will be adjusted by ApiClient if needed + return new HttpClient(logging, disposeHandler: true); + }); + builder.Services.AddSingleton(sp => { var alert = sp.GetRequiredService(); try { @@ -85,13 +95,16 @@ public static class MauiProgram { // DI: Validatoren builder.Services.AddSingleton(); - // DI: Services & Repositories - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + // DI: Services & Repositories + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - // DI: Views/ViewModels - builder.Services.AddTransient(); - builder.Services.AddTransient(); + // DI: Views/ViewModels + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); return builder.Build(); } diff --git a/Jugenddienst Stunden/Services/AuthService.cs b/Jugenddienst Stunden/Services/AuthService.cs new file mode 100644 index 0000000..539fc8b --- /dev/null +++ b/Jugenddienst Stunden/Services/AuthService.cs @@ -0,0 +1,94 @@ +using System.Net.Http; +using System.Text; +using Jugenddienst_Stunden.Infrastructure; +using Jugenddienst_Stunden.Interfaces; +using Jugenddienst_Stunden.Models; +using Jugenddienst_Stunden.Types; + +namespace Jugenddienst_Stunden.Services; + +internal sealed class AuthService : IAuthService { + private readonly IApiClient _api; + private readonly IAppSettings _settings; + private readonly IAlertService _alerts; + + public AuthService(IApiClient api, IAppSettings settings, IAlertService alerts) { + _api = api; + _settings = settings; + _alerts = alerts; + } + + public async Task LoginWithCredentials(string username, string password, string serverUrl, CancellationToken ct = default) { + if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) + throw new Exception("Benutzername und Passwort werden benötigt."); + + var apiBase = NormalizeApiUrl(serverUrl); + _settings.ApiUrl = apiBase; // BaseAddress für IApiClient setzen + + var content = new FormUrlEncodedContent(new[] { + new KeyValuePair("user", username), + new KeyValuePair("pass", password) + }); + + // POST ohne Pfad – die API erwartet /appapi + // Wichtig: Basis-URL hat garantiert einen abschließenden Slash (…/appapi/), + // sodass ein leerer Pfad nicht zu Redirects führt (die den POST in GET verwandeln könnten). + var res = await _api.SendAsync(HttpMethod.Post, string.Empty, content, null, ct).ConfigureAwait(false); + if (res.user is null) + throw new Exception(res.message ?? "Ungültige Antwort vom Server."); + + ApplyUser(res.user, apiBase); + return res.user; + } + + public async Task LoginWithToken(string token, CancellationToken ct = default) { + if (string.IsNullOrWhiteSpace(token)) throw new Exception("Kein Token erkannt."); + + // QR-Token enthält die URL – extrahiere sie + var td = new TokenData(token); + // URL aus dem Token ebenfalls normalisieren, damit sie auf "/appapi/" endet + _settings.ApiUrl = NormalizeApiUrl(td.Url); + _settings.ApiKey = token; + + var res = await _api.GetAsync(string.Empty, null, ct).ConfigureAwait(false); + if (res.user is null) + throw new Exception(res.message ?? "Ungültige Antwort vom Server."); + + ApplyUser(res.user, td.Url); + return res.user; + } + + private void ApplyUser(User user, string apiBase) { + _settings.ApiUrl = apiBase; + // Wenn der Server keinen Token im User zurückliefert (QR-Login-Fall), bestehenden Token beibehalten + var tokenToUse = string.IsNullOrWhiteSpace(user.Token) ? _settings.ApiKey : user.Token; + _settings.ApiKey = tokenToUse; + _settings.EmployeeId = user.Id; + _settings.Name = user.Name; + _settings.Surname = user.Surname; + } + + private static string NormalizeApiUrl(string input) { + if (string.IsNullOrWhiteSpace(input)) throw new Exception("Server-URL wird benötigt."); + var url = input.Trim(); + if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && + !url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { + url = "https://" + url; + } + // Sicherstellen, dass der Pfad auf "/appapi" endet + if (!url.EndsWith("/appapi", StringComparison.OrdinalIgnoreCase)) { + url = url.TrimEnd('/') + "/appapi"; + } + // WICHTIG: Einen abschließenden Slash erzwingen, damit relative Pfade korrekt angehängt werden + // und damit POST auf Basis-URL (leerem Pfad) nicht zu einem 301/302-Redirect führt, + // der den Body (user/pass) verlieren könnte. + //if (!url.EndsWith("/", StringComparison.Ordinal)) { + // url += "/"; + //} + + if (url.EndsWith("/", StringComparison.Ordinal)) { + url = url.Remove(url.Length - 1, 1); + } + return url; + } +} diff --git a/Jugenddienst Stunden/Types/User.cs b/Jugenddienst Stunden/Types/User.cs index 94e749b..6fc95fe 100644 --- a/Jugenddienst Stunden/Types/User.cs +++ b/Jugenddienst Stunden/Types/User.cs @@ -1,6 +1,6 @@ namespace Jugenddienst_Stunden.Types; -internal class User { +public class User { public int Id { get; set; } public string Name { get; set; } public string Surname { get; set; } diff --git a/Jugenddienst Stunden/ViewModels/LoginViewModel.cs b/Jugenddienst Stunden/ViewModels/LoginViewModel.cs index f34abaf..f05c76d 100644 --- a/Jugenddienst Stunden/ViewModels/LoginViewModel.cs +++ b/Jugenddienst Stunden/ViewModels/LoginViewModel.cs @@ -1,33 +1,145 @@ -namespace Jugenddienst_Stunden.ViewModels; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Jugenddienst_Stunden.Interfaces; + +namespace Jugenddienst_Stunden.ViewModels; /// -/// Die Loginseite +/// ViewModel für die Loginseite (MVVM) /// -public class LoginViewModel { - /// - /// Name der Anwendung - /// - public string AppTitle => AppInfo.Name; +public partial class LoginViewModel : ObservableObject { + private readonly IAuthService _auth; + private readonly IAppSettings _settings; + private readonly IAlertService? _alerts; + private DateTime _lastDetectionTime = DateTime.MinValue; + private readonly TimeSpan _detectionInterval = TimeSpan.FromSeconds(5); - /// - /// Programmversion - /// - public string Version => AppInfo.VersionString; + public event EventHandler? AlertEvent; + public event EventHandler? InfoEvent; - /// - /// Kurze Mitteilung für den Anwender - /// - public string Message { get; set; } = "Scanne den QR-Code von deinem Benutzerprofil auf der Stundenseite."; + /// + /// Name der Anwendung + /// + public string AppTitle => AppInfo.Name; - /// - /// Genutzer Server für die API - /// - public string Server { get; set; } = "Server: " + Preferences.Default.Get("apiUrl", "").Replace("/appapi", "") - .Replace("https://", "").Replace("http://", ""); + /// + /// Programmversion + /// + public string Version => AppInfo.VersionString; - /// - /// Titel der Seite - im Moment der aktuelle Anwender - /// - public string Title { get; set; } = Preferences.Default.Get("name", "Nicht") + " " + - Preferences.Default.Get("surname", "eingeloggt"); + [ObservableProperty] + private string message = "Scanne den QR-Code von deinem Benutzerprofil auf der Stundenseite."; + + [ObservableProperty] + private string? server; + + [ObservableProperty] + private string title = Preferences.Default.Get("name", "Nicht") + " " + Preferences.Default.Get("surname", "eingeloggt"); + + [ObservableProperty] + private string? username; + + [ObservableProperty] + private string? password; + + [ObservableProperty] + private bool isManualMode; + + [ObservableProperty] + private bool isBusy; + + [ObservableProperty] + private bool isDetecting; + + // Explizite Command-Property für den QR-Scanner-Event, damit das Binding in XAML zuverlässig greift + public IAsyncRelayCommand QrDetectedCommand { get; } + + public LoginViewModel(IAuthService auth, IAppSettings settings) { + _auth = auth; + _settings = settings; + + // gespeicherte Präferenz für Logintyp laden + var lt = Preferences.Default.Get("logintype", "qr"); + isManualMode = string.Equals(lt, "manual", StringComparison.OrdinalIgnoreCase); + // Scanner standardmäßig nur im QR-Modus aktivieren + IsDetecting = !isManualMode; + + // Serveranzeige vorbereiten + var apiUrl = Preferences.Default.Get("apiUrl", string.Empty); + if (!string.IsNullOrWhiteSpace(apiUrl)) { + Server = "Server: " + apiUrl.Replace("/appapi", "").Replace("https://", "").Replace("http://", ""); + } + + // Command initialisieren + QrDetectedCommand = new AsyncRelayCommand(QrDetectedAsync); + } + + // DI-Konstruktor: AlertService anbinden und Alerts an VM-Event weiterreichen (analog StundeViewModel) + internal LoginViewModel(IAuthService auth, IAppSettings settings, IAlertService alertService) : this(auth, settings) { + _alerts = alertService; + if (alertService is not null) { + alertService.AlertRaised += (s, msg) => AlertEvent?.Invoke(this, msg); + } + } + + partial void OnIsManualModeChanged(bool value) { + Preferences.Default.Set("logintype", value ? "manual" : "qr"); + // Scanner nur aktiv, wenn QR-Modus aktiv ist + IsDetecting = !value; + } + + [RelayCommand] + private async Task LoginAsync() { + if (IsBusy) return; + try { + IsBusy = true; + var user = await _auth.LoginWithCredentials(Username?.Trim() ?? string.Empty, + Password ?? string.Empty, + (Server ?? string.Empty).Replace("Server:", string.Empty).Trim()); + + Title = $"{user.Name} {user.Surname}"; + InfoEvent?.Invoke(this, "Login erfolgreich"); + + await Shell.Current.GoToAsync("//StundenPage"); + } catch (Exception ex) { + if (_alerts is not null) { + _alerts.Raise(ex.Message); + } else { + AlertEvent?.Invoke(this, ex.Message); + } + } finally { + IsBusy = false; + } + } + + private async Task QrDetectedAsync(object? args) { + var now = DateTime.Now; + if ((now - _lastDetectionTime) <= _detectionInterval) return; + _lastDetectionTime = now; + + try { + var token = ExtractFirstBarcodeValue(args); + if (string.IsNullOrWhiteSpace(token)) return; + + var user = await _auth.LoginWithToken(token); + Title = $"{user.Name} {user.Surname}"; + + await Shell.Current.GoToAsync("//StundenPage"); + } catch (Exception ex) { + if (_alerts is not null) { + _alerts.Raise(ex.Message); + } else { + AlertEvent?.Invoke(this, ex.Message); + } + } + } + + private static string? ExtractFirstBarcodeValue(object? args) { + try { + if (args is ZXing.Net.Maui.BarcodeDetectionEventArgs e && e.Results is not null) { + return e.Results.FirstOrDefault()?.Value; + } + } catch { } + return null; + } } \ No newline at end of file diff --git a/Jugenddienst Stunden/Views/LoginPage.xaml b/Jugenddienst Stunden/Views/LoginPage.xaml index 0d5b820..13537c9 100644 --- a/Jugenddienst Stunden/Views/LoginPage.xaml +++ b/Jugenddienst Stunden/Views/LoginPage.xaml @@ -10,13 +10,13 @@ Title="{Binding Title}"> - - - + + + @@ -39,7 +39,7 @@ @@ -47,25 +47,30 @@