LoginFixes
This commit is contained in:
@@ -13,62 +13,62 @@ internal sealed class ApiClient : IApiClient {
|
|||||||
private readonly ApiOptions _options;
|
private readonly ApiOptions _options;
|
||||||
private readonly IAppSettings _settings;
|
private readonly IAppSettings _settings;
|
||||||
|
|
||||||
public ApiClient(HttpClient http, ApiOptions options, ITokenProvider tokenProvider, IAppSettings settings) {
|
public ApiClient(HttpClient http, ApiOptions options, ITokenProvider tokenProvider, IAppSettings settings) {
|
||||||
_http = http;
|
_http = http;
|
||||||
_options = options;
|
_options = options;
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
|
|
||||||
// Timeout nur einmalig beim Erstellen setzen – spätere Änderungen an HttpClient.Timeout
|
// Timeout nur einmalig beim Erstellen setzen – spätere Änderungen an HttpClient.Timeout
|
||||||
// nach der ersten Verwendung führen zu InvalidOperationException.
|
// nach der ersten Verwendung führen zu InvalidOperationException.
|
||||||
if (_http.Timeout != options.Timeout)
|
if (_http.Timeout != options.Timeout)
|
||||||
_http.Timeout = options.Timeout;
|
_http.Timeout = options.Timeout;
|
||||||
// Standardmäßig JSON akzeptieren; doppelte Einträge vermeiden
|
// Standardmäßig JSON akzeptieren; doppelte Einträge vermeiden
|
||||||
if (!_http.DefaultRequestHeaders.Accept.Any(h => h.MediaType?.Equals("application/json", StringComparison.OrdinalIgnoreCase) == true))
|
if (!_http.DefaultRequestHeaders.Accept.Any(h => h.MediaType?.Equals("application/json", StringComparison.OrdinalIgnoreCase) == true))
|
||||||
_http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
_http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
|
||||||
// KEINE globalen Header/Properties mehr dynamisch setzen. Authorization wird pro Request gesetzt.
|
// KEINE globalen Header/Properties mehr dynamisch setzen. Authorization wird pro Request gesetzt.
|
||||||
|
|
||||||
_json = new JsonSerializerOptions {
|
_json = new JsonSerializerOptions {
|
||||||
PropertyNameCaseInsensitive = true,
|
PropertyNameCaseInsensitive = true,
|
||||||
WriteIndented = false,
|
WriteIndented = false,
|
||||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
|
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
|
||||||
};
|
};
|
||||||
|
|
||||||
// Globale Converter: erlauben numerische Felder auch als Strings (z.B. user.id)
|
// 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.JsonFlexibleInt32Converter());
|
||||||
_json.Converters.Add(new Jugenddienst_Stunden.Models.JsonFlexibleNullableInt32Converter());
|
_json.Converters.Add(new Jugenddienst_Stunden.Models.JsonFlexibleNullableInt32Converter());
|
||||||
|
|
||||||
// WICHTIG: HttpClient.BaseAddress NICHT dynamisch setzen oder ändern – das verursacht Exceptions,
|
// WICHTIG: HttpClient.BaseAddress NICHT dynamisch setzen oder ändern – das verursacht Exceptions,
|
||||||
// sobald bereits Requests gestartet wurden. Wir bauen stattdessen absolute URIs pro Request.
|
// sobald bereits Requests gestartet wurden. Wir bauen stattdessen absolute URIs pro Request.
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<T> GetAsync<T>(string path, IDictionary<string, string?>? query = null, CancellationToken ct = default)
|
public Task<T> GetAsync<T>(string path, IDictionary<string, string?>? query = null, CancellationToken ct = default)
|
||||||
=> SendAsync<T>(HttpMethod.Get, path, null, query, ct);
|
=> SendAsync<T>(HttpMethod.Get, path, null, query, ct);
|
||||||
|
|
||||||
public async Task<T> SendAsync<T>(HttpMethod method, string path, object? body = null,
|
public async Task<T> SendAsync<T>(HttpMethod method, string path, object? body = null,
|
||||||
IDictionary<string, string?>? query = null, CancellationToken ct = default) {
|
IDictionary<string, string?>? query = null, CancellationToken ct = default) {
|
||||||
// Absolute URI aus aktuellem Settings‑BaseUrl bauen, ohne HttpClient.BaseAddress zu nutzen.
|
// Absolute URI aus aktuellem Settings‑BaseUrl bauen, ohne HttpClient.BaseAddress zu nutzen.
|
||||||
var uri = BuildAbsoluteUri(_settings.ApiUrl, path, query);
|
var uri = BuildAbsoluteUri(_settings.ApiUrl, path, query);
|
||||||
using var req = new HttpRequestMessage(method, uri);
|
using var req = new HttpRequestMessage(method, uri);
|
||||||
// Authorization PRO REQUEST setzen (immer, wenn Token vorhanden ist)
|
// Authorization PRO REQUEST setzen (immer, wenn Token vorhanden ist)
|
||||||
// Hinweis: Das QR-Token kann RFC-unzulässige Zeichen (z. B. '|') enthalten.
|
// Hinweis: Das QR-Token kann RFC-unzulässige Zeichen (z. B. '|') enthalten.
|
||||||
// AuthenticationHeaderValue würde solche Werte ablehnen. Daher ohne Validierung setzen.
|
// AuthenticationHeaderValue würde solche Werte ablehnen. Daher ohne Validierung setzen.
|
||||||
var currentToken = _settings.ApiKey;
|
var currentToken = _settings.ApiKey;
|
||||||
if (!string.IsNullOrWhiteSpace(currentToken)) {
|
if (!string.IsNullOrWhiteSpace(currentToken)) {
|
||||||
// Vorherige Header (falls vorhanden) entfernen, um Duplikate zu vermeiden
|
// Vorherige Header (falls vorhanden) entfernen, um Duplikate zu vermeiden
|
||||||
req.Headers.Remove("Authorization");
|
req.Headers.Remove("Authorization");
|
||||||
req.Headers.TryAddWithoutValidation("Authorization", $"Bearer {currentToken}");
|
req.Headers.TryAddWithoutValidation("Authorization", $"Bearer {currentToken}");
|
||||||
}
|
}
|
||||||
if (body is HttpContent httpContent) {
|
if (body is HttpContent httpContent) {
|
||||||
req.Content = httpContent;
|
req.Content = httpContent;
|
||||||
} else if (body is not null) {
|
} else if (body is not null) {
|
||||||
req.Content = JsonContent.Create(body, options: _json);
|
req.Content = JsonContent.Create(body, options: _json);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sicherstellen, dass Accept: application/json auch auf Request-Ebene vorhanden ist (z. B. für LoginWithToken GET)
|
// 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)) {
|
if (!req.Headers.Accept.Any(h => h.MediaType?.Equals("application/json", StringComparison.OrdinalIgnoreCase) == true)) {
|
||||||
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
}
|
}
|
||||||
|
|
||||||
using var res = await _http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
|
using var res = await _http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
|
||||||
var text = await res.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
var text = await res.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||||
@@ -109,7 +109,7 @@ internal sealed class ApiClient : IApiClient {
|
|||||||
public Task DeleteAsync(string path, IDictionary<string, string?>? query = null, CancellationToken ct = default)
|
public Task DeleteAsync(string path, IDictionary<string, string?>? query = null, CancellationToken ct = default)
|
||||||
=> SendAsync<object>(HttpMethod.Delete, path, null, query, ct);
|
=> SendAsync<object>(HttpMethod.Delete, path, null, query, ct);
|
||||||
|
|
||||||
// Entfernt: EnsureBaseAddress – wir ändern BaseAddress nicht mehr dynamisch.
|
// Entfernt: EnsureBaseAddress – wir ändern BaseAddress nicht mehr dynamisch.
|
||||||
|
|
||||||
private static string TryGetMessage(string text) {
|
private static string TryGetMessage(string text) {
|
||||||
try {
|
try {
|
||||||
@@ -122,29 +122,29 @@ internal sealed class ApiClient : IApiClient {
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Uri BuildAbsoluteUri(string baseUrl, string path, IDictionary<string, string?>? query) {
|
private static Uri BuildAbsoluteUri(string baseUrl, string path, IDictionary<string, string?>? query) {
|
||||||
if (string.IsNullOrWhiteSpace(baseUrl))
|
if (string.IsNullOrWhiteSpace(baseUrl))
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"ApiUrl ist leer. Bitte zuerst eine gültige Server-URL setzen (Preferences key 'apiUrl').");
|
"ApiUrl ist leer. Bitte zuerst eine gültige Server-URL setzen (Preferences key 'apiUrl').");
|
||||||
|
|
||||||
// Basis muss absolut sein (z. B. https://host/appapi/)
|
// Basis muss absolut sein (z. B. https://host/appapi/)
|
||||||
var baseUri = new Uri(baseUrl, UriKind.Absolute);
|
var baseUri = new Uri(baseUrl, UriKind.Absolute);
|
||||||
|
|
||||||
// Pfad relativ zur Basis aufbauen
|
// Pfad relativ zur Basis aufbauen
|
||||||
string relativePath = path ?? string.Empty;
|
string relativePath = path ?? string.Empty;
|
||||||
if (query is not null && query.Count > 0) {
|
if (query is not null && query.Count > 0) {
|
||||||
var sb = new StringBuilder(relativePath);
|
var sb = new StringBuilder(relativePath);
|
||||||
sb.Append(relativePath.Contains('?') ? '&' : '?');
|
sb.Append(relativePath.Contains('?') ? '&' : '?');
|
||||||
sb.Append(string.Join('&', query
|
sb.Append(string.Join('&', query
|
||||||
.Where(kv => kv.Value is not null)
|
.Where(kv => kv.Value is not null)
|
||||||
.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value!)}")));
|
.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value!)}")));
|
||||||
relativePath = sb.ToString();
|
relativePath = sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wenn path bereits absolut ist, direkt verwenden
|
// Wenn path bereits absolut ist, direkt verwenden
|
||||||
if (Uri.TryCreate(relativePath, UriKind.Absolute, out var absoluteFromPath))
|
if (Uri.TryCreate(relativePath, UriKind.Absolute, out var absoluteFromPath))
|
||||||
return absoluteFromPath;
|
return absoluteFromPath;
|
||||||
|
|
||||||
return new Uri(baseUri, relativePath);
|
return new Uri(baseUri, relativePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@ internal sealed class AuthService : IAuthService {
|
|||||||
//}
|
//}
|
||||||
|
|
||||||
if (url.EndsWith("/", StringComparison.Ordinal)) {
|
if (url.EndsWith("/", StringComparison.Ordinal)) {
|
||||||
url = url.Remove(url.Length - 1, 1);
|
url = url.TrimEnd('/');
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,138 +8,142 @@ namespace Jugenddienst_Stunden.ViewModels;
|
|||||||
/// ViewModel für die Loginseite (MVVM)
|
/// ViewModel für die Loginseite (MVVM)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class LoginViewModel : ObservableObject {
|
public partial class LoginViewModel : ObservableObject {
|
||||||
private readonly IAuthService _auth;
|
private readonly IAuthService _auth;
|
||||||
private readonly IAppSettings _settings;
|
private readonly IAppSettings _settings;
|
||||||
private readonly IAlertService? _alerts;
|
private readonly IAlertService? _alerts;
|
||||||
private DateTime _lastDetectionTime = DateTime.MinValue;
|
private DateTime _lastDetectionTime = DateTime.MinValue;
|
||||||
private readonly TimeSpan _detectionInterval = TimeSpan.FromSeconds(5);
|
private readonly TimeSpan _detectionInterval = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
public event EventHandler<string>? AlertEvent;
|
public event EventHandler<string>? AlertEvent;
|
||||||
public event EventHandler<string>? InfoEvent;
|
public event EventHandler<string>? InfoEvent;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Name der Anwendung
|
/// Name der Anwendung
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string AppTitle => AppInfo.Name;
|
public string AppTitle => AppInfo.Name;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Programmversion
|
/// Programmversion
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Version => AppInfo.VersionString;
|
public string Version => AppInfo.VersionString;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string message = "Scanne den QR-Code von deinem Benutzerprofil auf der Stundenseite.";
|
private string message = "Scanne den QR-Code von deinem Benutzerprofil auf der Stundenseite.";
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string? server;
|
private string? server;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string title = Preferences.Default.Get("name", "Nicht") + " " + Preferences.Default.Get("surname", "eingeloggt");
|
private string? serverLabel;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string? username;
|
private string title = Preferences.Default.Get("name", "Nicht") + " " + Preferences.Default.Get("surname", "eingeloggt");
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string? password;
|
private string? username;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool isManualMode;
|
private string? password;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool isBusy;
|
private bool isManualMode;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool isDetecting;
|
private bool isBusy;
|
||||||
|
|
||||||
// Explizite Command-Property für den QR-Scanner-Event, damit das Binding in XAML zuverlässig greift
|
[ObservableProperty]
|
||||||
public IAsyncRelayCommand<object?> QrDetectedCommand { get; }
|
private bool isDetecting;
|
||||||
|
|
||||||
public LoginViewModel(IAuthService auth, IAppSettings settings) {
|
// Explizite Command-Property für den QR-Scanner-Event, damit das Binding in XAML zuverlässig greift
|
||||||
_auth = auth;
|
public IAsyncRelayCommand<object?> QrDetectedCommand { get; }
|
||||||
_settings = settings;
|
|
||||||
|
|
||||||
// gespeicherte Präferenz für Logintyp laden
|
public LoginViewModel(IAuthService auth, IAppSettings settings) {
|
||||||
var lt = Preferences.Default.Get("logintype", "qr");
|
_auth = auth;
|
||||||
isManualMode = string.Equals(lt, "manual", StringComparison.OrdinalIgnoreCase);
|
_settings = settings;
|
||||||
// Scanner standardmäßig nur im QR-Modus aktivieren
|
|
||||||
IsDetecting = !isManualMode;
|
|
||||||
|
|
||||||
// Serveranzeige vorbereiten
|
// gespeicherte Präferenz für Logintyp laden
|
||||||
var apiUrl = Preferences.Default.Get("apiUrl", string.Empty);
|
var lt = Preferences.Default.Get("logintype", "qr");
|
||||||
if (!string.IsNullOrWhiteSpace(apiUrl)) {
|
isManualMode = string.Equals(lt, "manual", StringComparison.OrdinalIgnoreCase);
|
||||||
Server = "Server: " + apiUrl.Replace("/appapi", "").Replace("https://", "").Replace("http://", "");
|
// Scanner standardmäßig nur im QR-Modus aktivieren
|
||||||
}
|
IsDetecting = !isManualMode;
|
||||||
|
|
||||||
// Command initialisieren
|
// Serveranzeige vorbereiten
|
||||||
QrDetectedCommand = new AsyncRelayCommand<object?>(QrDetectedAsync);
|
var apiUrl = Preferences.Default.Get("apiUrl", string.Empty);
|
||||||
}
|
if (!string.IsNullOrWhiteSpace(apiUrl)) {
|
||||||
|
Server = apiUrl.Replace("/appapi", "").Replace("https://", "").Replace("http://", "");
|
||||||
|
ServerLabel = "Server: " + Server;
|
||||||
|
}
|
||||||
|
|
||||||
// DI-Konstruktor: AlertService anbinden und Alerts an VM-Event weiterreichen (analog StundeViewModel)
|
// Command initialisieren
|
||||||
internal LoginViewModel(IAuthService auth, IAppSettings settings, IAlertService alertService) : this(auth, settings) {
|
QrDetectedCommand = new AsyncRelayCommand<object?>(QrDetectedAsync);
|
||||||
_alerts = alertService;
|
}
|
||||||
if (alertService is not null) {
|
|
||||||
alertService.AlertRaised += (s, msg) => AlertEvent?.Invoke(this, msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
partial void OnIsManualModeChanged(bool value) {
|
// DI-Konstruktor: AlertService anbinden und Alerts an VM-Event weiterreichen (analog StundeViewModel)
|
||||||
Preferences.Default.Set("logintype", value ? "manual" : "qr");
|
internal LoginViewModel(IAuthService auth, IAppSettings settings, IAlertService alertService) : this(auth, settings) {
|
||||||
// Scanner nur aktiv, wenn QR-Modus aktiv ist
|
_alerts = alertService;
|
||||||
IsDetecting = !value;
|
if (alertService is not null) {
|
||||||
}
|
alertService.AlertRaised += (s, msg) => AlertEvent?.Invoke(this, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
partial void OnIsManualModeChanged(bool value) {
|
||||||
private async Task LoginAsync() {
|
Preferences.Default.Set("logintype", value ? "manual" : "qr");
|
||||||
if (IsBusy) return;
|
// Scanner nur aktiv, wenn QR-Modus aktiv ist
|
||||||
try {
|
IsDetecting = !value;
|
||||||
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}";
|
[RelayCommand]
|
||||||
InfoEvent?.Invoke(this, "Login erfolgreich");
|
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).Trim());
|
||||||
|
|
||||||
await Shell.Current.GoToAsync("//StundenPage");
|
Title = $"{user.Name} {user.Surname}";
|
||||||
} catch (Exception ex) {
|
InfoEvent?.Invoke(this, "Login erfolgreich");
|
||||||
if (_alerts is not null) {
|
|
||||||
_alerts.Raise(ex.Message);
|
|
||||||
} else {
|
|
||||||
AlertEvent?.Invoke(this, ex.Message);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
IsBusy = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task QrDetectedAsync(object? args) {
|
await Shell.Current.GoToAsync("//StundenPage");
|
||||||
var now = DateTime.Now;
|
} catch (Exception ex) {
|
||||||
if ((now - _lastDetectionTime) <= _detectionInterval) return;
|
if (_alerts is not null) {
|
||||||
_lastDetectionTime = now;
|
_alerts.Raise(ex.Message);
|
||||||
|
} else {
|
||||||
|
AlertEvent?.Invoke(this, ex.Message);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
IsBusy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
private async Task QrDetectedAsync(object? args) {
|
||||||
var token = ExtractFirstBarcodeValue(args);
|
var now = DateTime.Now;
|
||||||
if (string.IsNullOrWhiteSpace(token)) return;
|
if ((now - _lastDetectionTime) <= _detectionInterval) return;
|
||||||
|
_lastDetectionTime = now;
|
||||||
|
|
||||||
var user = await _auth.LoginWithToken(token);
|
try {
|
||||||
Title = $"{user.Name} {user.Surname}";
|
var token = ExtractFirstBarcodeValue(args);
|
||||||
|
if (string.IsNullOrWhiteSpace(token)) return;
|
||||||
|
|
||||||
await Shell.Current.GoToAsync("//StundenPage");
|
var user = await _auth.LoginWithToken(token);
|
||||||
} catch (Exception ex) {
|
Title = $"{user.Name} {user.Surname}";
|
||||||
if (_alerts is not null) {
|
|
||||||
_alerts.Raise(ex.Message);
|
|
||||||
} else {
|
|
||||||
AlertEvent?.Invoke(this, ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? ExtractFirstBarcodeValue(object? args) {
|
await Shell.Current.GoToAsync("//StundenPage");
|
||||||
try {
|
} catch (Exception ex) {
|
||||||
if (args is ZXing.Net.Maui.BarcodeDetectionEventArgs e && e.Results is not null) {
|
if (_alerts is not null) {
|
||||||
return e.Results.FirstOrDefault()?.Value;
|
_alerts.Raise(ex.Message);
|
||||||
}
|
} else {
|
||||||
} catch { }
|
AlertEvent?.Invoke(this, ex.Message);
|
||||||
return null;
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Label x:Name="ServerLabel" Text="{Binding Server}"
|
<Label x:Name="ServerLabel" Text="{Binding ServerLabel}"
|
||||||
IsVisible="{Binding Server, Converter={StaticResource StringVisibilityConverter}}" />
|
IsVisible="{Binding Server, Converter={StaticResource StringVisibilityConverter}}" />
|
||||||
|
|
||||||
<VerticalStackLayout x:Name="LoginQR" Margin="0,20,0,0" IsVisible="{Binding IsManualMode, Converter={StaticResource InverseBoolConverter}}">
|
<VerticalStackLayout x:Name="LoginQR" Margin="0,20,0,0" IsVisible="{Binding IsManualMode, Converter={StaticResource InverseBoolConverter}}">
|
||||||
|
|||||||
Reference in New Issue
Block a user