Refactor Api-Client

Add Exceptionhandler, AlertService JSON-Converter
AppSettings via DI

Reformat Code
This commit is contained in:
2025-12-17 09:34:08 +01:00
parent 544b0c9591
commit 76eb71946f
84 changed files with 3178 additions and 2191 deletions

247
.editorconfig Normal file
View File

@@ -0,0 +1,247 @@
# Entfernen Sie die folgende Zeile, wenn Sie EDITORCONFIG-Einstellungen von höheren Verzeichnissen vererben möchten.
root = true
# C#-Dateien
[*.cs]
#### Wichtige EditorConfig-Optionen ####
# Einzüge und Abstände
indent_size = 4
indent_style = tab
tab_width = 4
# Einstellungen für neue Zeilen
end_of_line = crlf
insert_final_newline = false
#### .NET Codeaktionen ####
# Typmitglied
dotnet_hide_advanced_members = false
dotnet_member_insertion_location = with_other_members_of_the_same_kind
dotnet_property_generation_behavior = prefer_throwing_properties
# Symbolsuche
dotnet_search_reference_assemblies = true
#### .NET-Codierungskonventionen ####
# Using-Direktiven organisieren
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = false
file_header_template = unset
# this.- und Me.-Einstellungen
dotnet_style_qualification_for_event = false
dotnet_style_qualification_for_field = false
dotnet_style_qualification_for_method = false
dotnet_style_qualification_for_property = false
# Einstellungen für Sprachschlüsselwörter und BCL-Typen
dotnet_style_predefined_type_for_locals_parameters_members = true
dotnet_style_predefined_type_for_member_access = true
# Einstellungen für Klammern
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
# Einstellungen für Modifizierer
dotnet_style_require_accessibility_modifiers = for_non_interface_members
# Einstellungen für Ausdrucksebene
dotnet_prefer_system_hash_code = true
dotnet_style_coalesce_expression = true
dotnet_style_collection_initializer = true
dotnet_style_explicit_tuple_names = true
dotnet_style_namespace_match_folder = true
dotnet_style_null_propagation = true
dotnet_style_object_initializer = true
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true
dotnet_style_prefer_collection_expression = when_types_loosely_match
dotnet_style_prefer_compound_assignment = true
dotnet_style_prefer_conditional_expression_over_assignment = true
dotnet_style_prefer_conditional_expression_over_return = true
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
dotnet_style_prefer_inferred_anonymous_type_member_names = true
dotnet_style_prefer_inferred_tuple_names = true
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
dotnet_style_prefer_simplified_boolean_expressions = true
dotnet_style_prefer_simplified_interpolation = true
# Einstellungen für Felder
dotnet_style_readonly_field = true
# Einstellungen für Parameter
dotnet_code_quality_unused_parameters = all
# Unterdrückungseinstellungen
dotnet_remove_unnecessary_suppression_exclusions = none
# Einstellungen für neue Zeilen
dotnet_style_allow_multiple_blank_lines_experimental = true
dotnet_style_allow_statement_immediately_after_block_experimental = true
#### C#-Codierungskonventionen ####
# Var-Einstellungen
csharp_style_var_elsewhere = false
csharp_style_var_for_built_in_types = false
csharp_style_var_when_type_is_apparent = false
# Ausdruckskörpermember
csharp_style_expression_bodied_accessors = true
csharp_style_expression_bodied_constructors = false
csharp_style_expression_bodied_indexers = true
csharp_style_expression_bodied_lambdas = true
csharp_style_expression_bodied_local_functions = false
csharp_style_expression_bodied_methods = false
csharp_style_expression_bodied_operators = false
csharp_style_expression_bodied_properties = true
# Einstellungen für den Musterabgleich
csharp_style_pattern_matching_over_as_with_null_check = true
csharp_style_pattern_matching_over_is_with_cast_check = true
csharp_style_prefer_extended_property_pattern = true
csharp_style_prefer_not_pattern = true
csharp_style_prefer_pattern_matching = true
csharp_style_prefer_switch_expression = true
# Einstellungen für NULL-Überprüfung
csharp_style_conditional_delegate_call = true
# Einstellungen für Modifizierer
csharp_prefer_static_anonymous_function = true
csharp_prefer_static_local_function = true
csharp_preferred_modifier_order = public, private, protected, internal, file, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, required, volatile, async
csharp_style_prefer_readonly_struct = true
csharp_style_prefer_readonly_struct_member = true
# Einstellungen für Codeblöcke
csharp_prefer_braces = true
csharp_prefer_simple_using_statement = true
csharp_prefer_system_threading_lock = true
csharp_style_namespace_declarations = block_scoped
csharp_style_prefer_method_group_conversion = true
csharp_style_prefer_primary_constructors = true
csharp_style_prefer_simple_property_accessors = true
csharp_style_prefer_top_level_statements = true
# Einstellungen für Ausdrucksebene
csharp_prefer_simple_default_expression = true
csharp_style_deconstructed_variable_declaration = true
csharp_style_implicit_object_creation_when_type_is_apparent = true
csharp_style_inlined_variable_declaration = true
csharp_style_prefer_implicitly_typed_lambda_expression = true
csharp_style_prefer_index_operator = true
csharp_style_prefer_local_over_anonymous_function = true
csharp_style_prefer_null_check_over_type_check = true
csharp_style_prefer_range_operator = true
csharp_style_prefer_tuple_swap = true
csharp_style_prefer_unbound_generic_type_in_nameof = true
csharp_style_prefer_utf8_string_literals = true
csharp_style_throw_expression = true
csharp_style_unused_value_assignment_preference = discard_variable
csharp_style_unused_value_expression_statement_preference = discard_variable
# Einstellungen für using-Anweisungen
csharp_using_directive_placement = outside_namespace
# Einstellungen für neue Zeilen
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true
csharp_style_allow_embedded_statements_on_same_line_experimental = true
#### C#-Formatierungsregeln ####
# Einstellungen für neue Zeilen
csharp_new_line_before_catch = false
csharp_new_line_before_else = false
csharp_new_line_before_finally = false
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = none
csharp_new_line_between_query_expression_clauses = true
# Einstellungen für Einrückung
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
# Einstellungen für Abstände
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Umbrucheinstellungen
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### Benennungsstile ####
# Benennungsregeln
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbolspezifikationen
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Benennungsstile
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case

View File

@@ -1,4 +1,5 @@
<?xml version = "1.0" encoding = "UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Jugenddienst_Stunden"

View File

@@ -4,12 +4,11 @@
/// Die Hauptanwendungsklasse.
/// </summary>
public partial class App : Application {
/// <summary>
/// Initialisiert eine neue Instanz der <see cref="App"/>-Klasse.
/// </summary>
public App() {
InitializeComponent();
MainPage = new AppShell();
}
/// <summary>
/// Initialisiert eine neue Instanz der <see cref="App"/>-Klasse.
/// </summary>
public App() {
InitializeComponent();
MainPage = new AppShell();
}
}

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<Shell
x:Class="Jugenddienst_Stunden.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
@@ -10,7 +11,8 @@
<ShellContent
Title="Stunden"
ContentTemplate="{DataTemplate views:StundenPage}"
Icon="{OnPlatform 'icon_watch.png', iOS='icon_watch_ios.png', MacCatalyst='icon_watch_ios.png'}" Route="StundenPage" />
Icon="{OnPlatform 'icon_watch.png', iOS='icon_watch_ios.png', MacCatalyst='icon_watch_ios.png'}"
Route="StundenPage" />
<ShellContent
Title="Notizen"

View File

@@ -1,4 +1,5 @@
namespace Jugenddienst_Stunden;
/// <summary>
/// AppShell.xaml.cs
/// </summary>
@@ -13,7 +14,7 @@ public partial class AppShell : Shell {
Routing.RegisterRoute(nameof(Views.NotePage), typeof(Views.NotePage));
Routing.RegisterRoute(nameof(Views.StundePage), typeof(Views.StundePage));
//Muss ich die registrieren?
Routing.RegisterRoute(nameof(Views.LoginPage), typeof(Views.LoginPage));
}
}

View File

@@ -2,20 +2,21 @@
/// Gib true zurück, wenn die Collection Werte enthält
namespace Jugenddienst_Stunden.Converter {
internal class CollectionVisibilityConverter : IValueConverter {
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
if (value is IEnumerable<object> collection) {
if ((string)parameter == "Invert")
return !collection.Any();
return collection.Any();
}
if ((string)parameter == "Invert")
return true;
return false;
}
internal class CollectionVisibilityConverter : IValueConverter {
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
if (value is IEnumerable<object> collection) {
if ((string)parameter == "Invert")
return !collection.Any();
return collection.Any();
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
if ((string)parameter == "Invert")
return true;
return false;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View File

@@ -1,11 +1,11 @@
using System.Globalization;
namespace Jugenddienst_Stunden.Converter;
/// <summary>
/// Falls ein int als bool dargestellt werden soll
/// </summary>
public class IntBoolConverter : IValueConverter {
/// <summary>
/// Konvertiert einen int in einen bool
/// </summary>
@@ -18,6 +18,7 @@ public class IntBoolConverter : IValueConverter {
if (value is int) {
return (int)value != 0;
}
return false;
}
@@ -33,6 +34,7 @@ public class IntBoolConverter : IValueConverter {
if (value is bool) {
return (bool)value ? 1 : 0;
}
return 0;
}
}

View File

@@ -1,36 +1,37 @@
using System.Globalization;
namespace Jugenddienst_Stunden.Converter;
internal class SecondsTimeConverter : IValueConverter {
private int seconds;
private int seconds;
/// <summary>
/// Konvertiert eine Sekundenangabe nach Stunden:Minuten, auch bei mehr als 24 Stunden
/// </summary>
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
if (value is null)
return "0:0";
if (value is int) {
seconds = (int)value;
}
/// <summary>
/// Konvertiert eine Sekundenangabe nach Stunden:Minuten, auch bei mehr als 24 Stunden
/// </summary>
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
if (value is null)
return "0:0";
if (value is int) {
seconds = (int)value;
}
if (value is double) {
seconds = (int)Math.Round((double)value);
} else {
int.TryParse((string?)value, out seconds);
}
if (value is double) {
seconds = (int)Math.Round((double)value);
} else {
int.TryParse((string?)value, out seconds);
}
TimeSpan time = TimeSpan.FromSeconds(seconds);
TimeSpan time = TimeSpan.FromSeconds(seconds);
return (int)time.TotalHours + ":" + Math.Abs(time.Minutes);
return (int)time.TotalHours + ":" + Math.Abs(time.Minutes);
//return time.ToString(@"hh\:mm");
//return time.ToString(@"hh\:mm\:ss");
//return time.ToString(@"hh\:mm");
//return time.ToString(@"hh\:mm\:ss");
//return "00:00";
}
//return "00:00";
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) {
throw new NotImplementedException();
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}

View File

@@ -1,15 +1,17 @@
using System.Globalization;
namespace Jugenddienst_Stunden.Converter;
internal class StringVisibilityConverter : IValueConverter {
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
if (value is string strValue) {
return !string.IsNullOrEmpty(strValue.Replace("Server: ",""));
}
return false;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) {
throw new NotImplementedException();
}
internal class StringVisibilityConverter : IValueConverter {
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
if (value is string strValue) {
return !string.IsNullOrEmpty(strValue.Replace("Server: ", ""));
}
return false;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}

View File

@@ -5,8 +5,14 @@ using System.Text;
using System.Threading.Tasks;
namespace Jugenddienst_Stunden.Exceptions;
public class NoDataException : Exception {
public NoDataException() : base("Keine Daten gefunden") { }
public NoDataException(string message) : base(message) { }
public NoDataException(string message, Exception inner) : base(message, inner) { }
public NoDataException() : base("Keine Daten gefunden") {
}
public NoDataException(string message) : base(message) {
}
public NoDataException(string message, Exception inner) : base(message, inner) {
}
}

View File

@@ -0,0 +1,15 @@
using Jugenddienst_Stunden.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jugenddienst_Stunden.Infrastructure;
internal sealed class AlertService : IAlertService {
public event EventHandler<string>? AlertRaised;
public void Raise(string message) {
AlertRaised?.Invoke(this, message);
}
}

View File

@@ -0,0 +1,111 @@
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Net.Http.Json;
using Jugenddienst_Stunden.Interfaces;
namespace Jugenddienst_Stunden.Infrastructure;
internal sealed class ApiClient : IApiClient {
private readonly HttpClient _http;
private readonly JsonSerializerOptions _json;
private readonly ApiOptions _options;
private readonly IAppSettings _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"));
var token = tokenProvider.GetToken();
if (!string.IsNullOrWhiteSpace(token))
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
_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();
}
public Task<T> GetAsync<T>(string path, IDictionary<string, string?>? query = null, CancellationToken ct = default)
=> SendAsync<T>(HttpMethod.Get, path, null, query, ct);
public async Task<T> SendAsync<T>(HttpMethod method, string path, object? body = null,
IDictionary<string, string?>? 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)
};
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 (typeof(T) == typeof(void) || typeof(T) == typeof(object) || string.IsNullOrWhiteSpace(text))
return default!;
var obj = System.Text.Json.JsonSerializer.Deserialize<T>(text, _json);
if (obj is null)
throw new ApiException("Fehler beim Deserialisieren der Daten.");
return obj;
}
public Task DeleteAsync(string path, IDictionary<string, string?>? query = null, CancellationToken ct = default)
=> SendAsync<object>(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;
}
}
private static string TryGetMessage(string text) {
try {
using var doc = JsonDocument.Parse(text);
if (doc.RootElement.TryGetProperty("message", out var m) && m.ValueKind == JsonValueKind.String)
return m.GetString() ?? text;
} catch {
}
return text;
}
private static Uri BuildUri(string path, IDictionary<string, string?>? query) {
if (query is null || query.Count == 0)
return new Uri(path, UriKind.Relative);
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);
}
}

View File

@@ -0,0 +1,6 @@
namespace Jugenddienst_Stunden.Infrastructure;
internal sealed class ApiOptions {
public required string BaseUrl { get; init; }
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(15);
}

View File

@@ -0,0 +1,23 @@
using System.Net;
namespace Jugenddienst_Stunden.Infrastructure;
internal class ApiException : Exception {
public HttpStatusCode StatusCode { get; }
public string? ResponseBody { get; }
public ApiException(string message, HttpStatusCode statusCode = 0, string? responseBody = null,
Exception? inner = null)
: base(message, inner) {
StatusCode = statusCode;
ResponseBody = responseBody;
}
public static ApiException From(HttpStatusCode statusCode, string message, string? responseBody = null)
=> new ApiException(message, statusCode, responseBody);
}
internal class ValidationException : Exception {
public ValidationException(string message) : base(message) {
}
}

View File

@@ -0,0 +1,23 @@
using Jugenddienst_Stunden.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jugenddienst_Stunden.Infrastructure;
internal sealed class NullApiClient : IApiClient {
private readonly string _message;
public NullApiClient(string message) => _message = message ?? "API nicht konfiguriert.";
public Task<T> GetAsync<T>(string path, IDictionary<string, string?>? query = null, CancellationToken ct = default)
=> Task.FromException<T>(new InvalidOperationException(_message));
public Task<T> SendAsync<T>(HttpMethod method, string path, object? body = null,
IDictionary<string, string?>? query = null, CancellationToken ct = default)
=> Task.FromException<T>(new InvalidOperationException(_message));
public Task DeleteAsync(string path, IDictionary<string, string?>? query = null, CancellationToken ct = default)
=> Task.FromException(new InvalidOperationException(_message));
}

View File

@@ -0,0 +1,30 @@
using Jugenddienst_Stunden.Interfaces;
namespace Jugenddienst_Stunden.Infrastructure;
internal sealed class PreferencesAppSettings : IAppSettings {
public string ApiUrl {
get => Preferences.Default.Get("apiUrl", "");
set => Preferences.Default.Set("apiUrl", value);
}
public string ApiKey {
get => Preferences.Default.Get("apiKey", "");
set => Preferences.Default.Set("apiKey", value);
}
public int EmployeeId {
get => Preferences.Default.Get("EmployeeId", 0);
set => Preferences.Default.Set("EmployeeId", value);
}
public string Name {
get => Preferences.Default.Get("name", "Nicht");
set => Preferences.Default.Set("name", value);
}
public string Surname {
get => Preferences.Default.Get("surname", "Eingeloggt");
set => Preferences.Default.Set("surname", value);
}
}

View File

@@ -0,0 +1,10 @@
using Jugenddienst_Stunden.Interfaces;
namespace Jugenddienst_Stunden.Infrastructure;
internal sealed class SettingsTokenProvider : ITokenProvider {
private readonly IAppSettings _settings;
public SettingsTokenProvider(IAppSettings settings) => _settings = settings;
public string GetToken() => _settings.ApiKey;
}

View File

@@ -0,0 +1,7 @@
using Jugenddienst_Stunden.Interfaces;
namespace Jugenddienst_Stunden.Infrastructure;
internal sealed class GlobalVarTokenProvider : ITokenProvider {
public string? GetToken() => Models.GlobalVar.ApiKey;
}

View File

@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jugenddienst_Stunden.Interfaces;
internal interface IAlertService {
event EventHandler<string> AlertRaised;
void Raise(string message);
}

View File

@@ -0,0 +1,10 @@
namespace Jugenddienst_Stunden.Interfaces;
internal interface IApiClient {
Task<T> GetAsync<T>(string path, IDictionary<string, string?>? query = null, CancellationToken ct = default);
Task<T> SendAsync<T>(HttpMethod method, string path, object? body = null,
IDictionary<string, string?>? query = null, CancellationToken ct = default);
Task DeleteAsync(string path, IDictionary<string, string?>? query = null, CancellationToken ct = default);
}

View File

@@ -0,0 +1,10 @@
namespace Jugenddienst_Stunden.Interfaces;
public interface IAppSettings {
string ApiUrl { get; set; }
string ApiKey { get; set; }
int EmployeeId { get; set; }
string Name { get; set; }
string Surname { get; set; }
}

View File

@@ -6,12 +6,12 @@ namespace Jugenddienst_Stunden.Interfaces;
/// RepositorySchnittstelle für Datenzugriff (API/Storage) rund um Stunden.
/// </summary>
internal interface IHoursRepository {
Task<BaseResponse> LoadBase(string query);
Task<Settings> LoadSettings();
Task<Hours> LoadData();
Task<User> LoadUser(string apiKey);
Task<List<DayTime>> LoadDay(DateTime date);
Task<DayTime> LoadEntry(int id);
Task<DayTime> SaveEntry(DayTime stunde);
Task DeleteEntry(DayTime stunde);
Task<BaseResponse> LoadBase(string query);
Task<Settings> LoadSettings();
Task<Hours> LoadData();
Task<User> LoadUser(string apiKey);
Task<List<DayTime>> LoadDay(DateTime date);
Task<DayTime> LoadEntry(int id);
Task<DayTime> SaveEntry(DayTime stunde);
Task DeleteEntry(DayTime stunde);
}

View File

@@ -5,12 +5,12 @@ namespace Jugenddienst_Stunden.Interfaces;
/// <summary>
/// Fachlicher Service für Stunden konsumiert Repository und stellt VMfreundliche Methoden bereit.
/// </summary>
internal interface IHoursService {
Task<(Hours hours, Settings settings)> GetMonthSummaryAsync(DateTime monthDate);
Task<(List<DayTime> dayTimes, Settings settings)> GetDayWithSettingsAsync(DateTime date);
Task<List<DayTime>> GetDayRangeAsync(DateTime from, DateTime to);
Task<Settings> GetSettingsAsync();
Task<DayTime> GetEntryAsync(int id);
Task<DayTime> SaveEntryAsync(DayTime stunde);
Task DeleteEntryAsync(DayTime stunde);
public interface IHoursService {
Task<(Hours hours, Settings settings)> GetMonthSummaryAsync(DateTime monthDate);
Task<(List<DayTime> dayTimes, Settings settings)> GetDayWithSettingsAsync(DateTime date);
Task<List<DayTime>> GetDayRangeAsync(DateTime from, DateTime to);
Task<Settings> GetSettingsAsync();
Task<DayTime> GetEntryAsync(int id);
Task<DayTime> SaveEntryAsync(DayTime stunde);
Task DeleteEntryAsync(DayTime stunde);
}

View File

@@ -0,0 +1,5 @@
namespace Jugenddienst_Stunden.Interfaces;
internal interface ITokenProvider {
string? GetToken();
}

View File

@@ -1,312 +1,312 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- <TargetFrameworks>net8.0-maccatalyst;net9.0-android35.0</TargetFrameworks> -->
<TargetFrameworks>net9.0-android35.0</TargetFrameworks>
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
<!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> -->
<PropertyGroup>
<!-- <TargetFrameworks>net8.0-maccatalyst;net9.0-android35.0</TargetFrameworks> -->
<TargetFrameworks>net9.0-android35.0</TargetFrameworks>
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
<!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> -->
<!-- Note for MacCatalyst:
The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.
When specifying both architectures, use the plural <RuntimeIdentifiers> instead of the singular <RuntimeIdentifier>.
The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated;
either BOTH runtimes must be indicated or ONLY macatalyst-x64. -->
<!-- For example: <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> -->
<!-- Note for MacCatalyst:
The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.
When specifying both architectures, use the plural <RuntimeIdentifiers> instead of the singular <RuntimeIdentifier>.
The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated;
either BOTH runtimes must be indicated or ONLY macatalyst-x64. -->
<!-- For example: <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> -->
<OutputType>Exe</OutputType>
<RootNamespace>Jugenddienst_Stunden</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
<RootNamespace>Jugenddienst_Stunden</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Display name -->
<ApplicationTitle>Jugenddienst Stunden</ApplicationTitle>
<!-- Display name -->
<ApplicationTitle>Jugenddienst Stunden</ApplicationTitle>
<!-- App Identifier -->
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<!-- App Identifier -->
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<!-- Versions -->
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">11.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">13.1</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">29.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<PackageIcon>paket_icon.png</PackageIcon>
<NeutralLanguage>de</NeutralLanguage>
<PackageVersion>1.0.9</PackageVersion>
</PropertyGroup>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">11.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">13.1</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">29.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<PackageIcon>paket_icon.png</PackageIcon>
<NeutralLanguage>de</NeutralLanguage>
<PackageVersion>1.0.9</PackageVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-maccatalyst|AnyCPU'">
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<Optimize>False</Optimize>
<Deterministic>True</Deterministic>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-maccatalyst|AnyCPU'">
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<Optimize>False</Optimize>
<Deterministic>True</Deterministic>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-maccatalyst|AnyCPU'">
<Optimize>True</Optimize>
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<Deterministic>True</Deterministic>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-maccatalyst|AnyCPU'">
<Optimize>True</Optimize>
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<Deterministic>True</Deterministic>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-android34.0|AnyCPU'">
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<Debugger>Xamarin</Debugger>
<DebugSymbols>True</DebugSymbols>
<Optimize>False</Optimize>
<Deterministic>True</Deterministic>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<AndroidKeyStore>False</AndroidKeyStore>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-android34.0|AnyCPU'">
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<Debugger>Xamarin</Debugger>
<DebugSymbols>True</DebugSymbols>
<Optimize>False</Optimize>
<Deterministic>True</Deterministic>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<AndroidKeyStore>False</AndroidKeyStore>
</PropertyGroup>
<PropertyGroup>
<DefaultLanguage>de-de</DefaultLanguage>
</PropertyGroup>
<PropertyGroup>
<DefaultLanguage>de-de</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-windows10.0.26100.0|AnyCPU'">
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION</DefineConstants>
<Deterministic>True</Deterministic>
<Optimize>False</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-windows10.0.26100.0|AnyCPU'">
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION</DefineConstants>
<Deterministic>True</Deterministic>
<Optimize>False</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-android34.0|AnyCPU'">
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<Debugger>Xamarin</Debugger>
<DebugSymbols>False</DebugSymbols>
<Optimize>True</Optimize>
<Deterministic>True</Deterministic>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<RunAOTCompilation>False</RunAOTCompilation>
<PublishTrimmed>True</PublishTrimmed>
<AndroidKeyStore>False</AndroidKeyStore>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-android34.0|AnyCPU'">
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<Debugger>Xamarin</Debugger>
<DebugSymbols>False</DebugSymbols>
<Optimize>True</Optimize>
<Deterministic>True</Deterministic>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<RunAOTCompilation>False</RunAOTCompilation>
<PublishTrimmed>True</PublishTrimmed>
<AndroidKeyStore>False</AndroidKeyStore>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-windows10.0.26100.0|AnyCPU'">
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<Optimize>True</Optimize>
<Deterministic>True</Deterministic>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-windows10.0.26100.0|AnyCPU'">
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<Optimize>True</Optimize>
<Deterministic>True</Deterministic>
</PropertyGroup>
<PropertyGroup>
<IncludeSymbols>True</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PlatformTarget>AnyCPU</PlatformTarget>
<ProduceReferenceAssembly>False</ProduceReferenceAssembly>
<AssemblyVersion>1.0.9</AssemblyVersion>
<FileVersion>1.0.9</FileVersion>
</PropertyGroup>
<PropertyGroup>
<IncludeSymbols>True</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PlatformTarget>AnyCPU</PlatformTarget>
<ProduceReferenceAssembly>False</ProduceReferenceAssembly>
<AssemblyVersion>1.0.9</AssemblyVersion>
<FileVersion>1.0.9</FileVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-android|AnyCPU'">
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-android|AnyCPU'">
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-android|AnyCPU'">
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-android|AnyCPU'">
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-windows10.0.19041.0|AnyCPU'">
<ApplicationVersion>10</ApplicationVersion>
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION</DefineConstants>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-windows10.0.19041.0|AnyCPU'">
<ApplicationVersion>10</ApplicationVersion>
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION</DefineConstants>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-windows10.0.19041.0|AnyCPU'">
<ApplicationVersion>10</ApplicationVersion>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-windows10.0.19041.0|AnyCPU'">
<ApplicationVersion>10</ApplicationVersion>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net9.0-android35.0|AnyCPU'">
<WarningLevel>8</WarningLevel>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<EnableLLVM>True</EnableLLVM>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net9.0-android35.0|AnyCPU'">
<WarningLevel>8</WarningLevel>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<EnableLLVM>True</EnableLLVM>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net9.0-android35.0|AnyCPU'">
<WarningLevel>8</WarningLevel>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<EnableLLVM>True</EnableLLVM>
<DebugSymbols>False</DebugSymbols>
<AndroidEnableProfiledAot>False</AndroidEnableProfiledAot>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net9.0-android35.0|AnyCPU'">
<WarningLevel>8</WarningLevel>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
<EnableLLVM>True</EnableLLVM>
<DebugSymbols>False</DebugSymbols>
<AndroidEnableProfiledAot>False</AndroidEnableProfiledAot>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net9.0-ios|AnyCPU'">
<WarningLevel>8</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net9.0-ios|AnyCPU'">
<WarningLevel>8</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net9.0-ios|AnyCPU'">
<WarningLevel>8</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net9.0-ios|AnyCPU'">
<WarningLevel>8</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net9.0-windows10.0.26100.0|AnyCPU'">
<WarningLevel>8</WarningLevel>
<NoWarn>1701;1702</NoWarn>
<WarningsAsErrors>$(WarningsAsErrors);NU1605</WarningsAsErrors>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net9.0-windows10.0.26100.0|AnyCPU'">
<WarningLevel>8</WarningLevel>
<NoWarn>1701;1702</NoWarn>
<WarningsAsErrors>$(WarningsAsErrors);NU1605</WarningsAsErrors>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net9.0-windows10.0.26100.0|AnyCPU'">
<WarningLevel>8</WarningLevel>
<NoWarn>1701;1702</NoWarn>
<WarningsAsErrors>$(WarningsAsErrors);NU1605</WarningsAsErrors>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net9.0-windows10.0.26100.0|AnyCPU'">
<WarningLevel>8</WarningLevel>
<NoWarn>1701;1702</NoWarn>
<WarningsAsErrors>$(WarningsAsErrors);NU1605</WarningsAsErrors>
<ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>10</ApplicationVersion>
</PropertyGroup>
<PropertyGroup>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.26100.0</TargetFrameworks>
<WindowsPackageType>None</WindowsPackageType>
<!-- <TargetFrameworks>;net9.0-android35.0</TargetFrameworks> -->
</PropertyGroup>
<PropertyGroup>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.26100.0</TargetFrameworks>
<WindowsPackageType>None</WindowsPackageType>
<!-- <TargetFrameworks>;net9.0-android35.0</TargetFrameworks> -->
</PropertyGroup>
<ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
<ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4"/>
<!-- Splash Screen -->
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#F7931D" BaseSize="128,128" />
<!-- Splash Screen -->
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#F7931D" BaseSize="128,128"/>
<!-- Splash Screen (Windows fix) -->
<!--<MauiImage Include="Resources\Images\logo_splash_win.svg" Color="#F7931D" BaseSize="208,208" />-->
<!-- Splash Screen (Windows fix) -->
<!--<MauiImage Include="Resources\Images\logo_splash_win.svg" Color="#F7931D" BaseSize="208,208" />-->
<!-- Images -->
<MauiImage Include="Resources\Images\*" />
<MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="300,185" />
<!-- Images -->
<MauiImage Include="Resources\Images\*"/>
<MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="300,185"/>
<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*" />
<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*"/>
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)"/>
</ItemGroup>
<ItemGroup>
<None Remove="Resources\Windows\%24placeholder%24.scale-100.png" />
<None Remove="Resources\Windows\%24placeholder%24.scale-125.png" />
<None Remove="Resources\Windows\%24placeholder%24.scale-150.png" />
<None Remove="Resources\Windows\%24placeholder%24.scale-200.png" />
<None Remove="Resources\Windows\%24placeholder%24.scale-400.png" />
<None Remove="Resources\Windows\Small\%24placeholder%24.scale-100.png" />
<None Remove="Resources\Windows\Small\%24placeholder%24.scale-125.png" />
<None Remove="Resources\Windows\Small\%24placeholder%24.scale-150.png" />
<None Remove="Resources\Windows\Small\%24placeholder%24.scale-200.png" />
<None Remove="Resources\Windows\Small\%24placeholder%24.scale-400.png" />
<None Remove="Resources\Windows\Splash\%24placeholder%24.scale-100.png" />
<None Remove="Resources\Windows\Splash\%24placeholder%24.scale-125.png" />
<None Remove="Resources\Windows\Splash\%24placeholder%24.scale-150.png" />
<None Remove="Resources\Windows\Splash\%24placeholder%24.scale-200.png" />
<None Remove="Resources\Windows\Splash\%24placeholder%24.scale-400.png" />
<None Remove="Resources\Windows\Wide\%24placeholder%24.scale-100.png" />
<None Remove="Resources\Windows\Wide\%24placeholder%24.scale-125.png" />
<None Remove="Resources\Windows\Wide\%24placeholder%24.scale-150.png" />
<None Remove="Resources\Windows\Wide\%24placeholder%24.scale-200.png" />
<None Remove="Resources\Windows\Wide\%24placeholder%24.scale-400.png" />
</ItemGroup>
<ItemGroup>
<None Remove="Resources\Windows\%24placeholder%24.scale-100.png"/>
<None Remove="Resources\Windows\%24placeholder%24.scale-125.png"/>
<None Remove="Resources\Windows\%24placeholder%24.scale-150.png"/>
<None Remove="Resources\Windows\%24placeholder%24.scale-200.png"/>
<None Remove="Resources\Windows\%24placeholder%24.scale-400.png"/>
<None Remove="Resources\Windows\Small\%24placeholder%24.scale-100.png"/>
<None Remove="Resources\Windows\Small\%24placeholder%24.scale-125.png"/>
<None Remove="Resources\Windows\Small\%24placeholder%24.scale-150.png"/>
<None Remove="Resources\Windows\Small\%24placeholder%24.scale-200.png"/>
<None Remove="Resources\Windows\Small\%24placeholder%24.scale-400.png"/>
<None Remove="Resources\Windows\Splash\%24placeholder%24.scale-100.png"/>
<None Remove="Resources\Windows\Splash\%24placeholder%24.scale-125.png"/>
<None Remove="Resources\Windows\Splash\%24placeholder%24.scale-150.png"/>
<None Remove="Resources\Windows\Splash\%24placeholder%24.scale-200.png"/>
<None Remove="Resources\Windows\Splash\%24placeholder%24.scale-400.png"/>
<None Remove="Resources\Windows\Wide\%24placeholder%24.scale-100.png"/>
<None Remove="Resources\Windows\Wide\%24placeholder%24.scale-125.png"/>
<None Remove="Resources\Windows\Wide\%24placeholder%24.scale-150.png"/>
<None Remove="Resources\Windows\Wide\%24placeholder%24.scale-200.png"/>
<None Remove="Resources\Windows\Wide\%24placeholder%24.scale-400.png"/>
</ItemGroup>
<ItemGroup>
<Content Include="Resources\Windows\$placeholder$.scale-100.png" />
<Content Include="Resources\Windows\$placeholder$.scale-125.png" />
<Content Include="Resources\Windows\$placeholder$.scale-150.png" />
<Content Include="Resources\Windows\$placeholder$.scale-200.png" />
<Content Include="Resources\Windows\$placeholder$.scale-400.png" />
<Content Include="Resources\Windows\Small\$placeholder$.scale-100.png" />
<Content Include="Resources\Windows\Small\$placeholder$.scale-125.png" />
<Content Include="Resources\Windows\Small\$placeholder$.scale-150.png" />
<Content Include="Resources\Windows\Small\$placeholder$.scale-200.png" />
<Content Include="Resources\Windows\Small\$placeholder$.scale-400.png" />
<Content Include="Resources\Windows\Splash\$placeholder$.scale-100.png" />
<Content Include="Resources\Windows\Splash\$placeholder$.scale-125.png" />
<Content Include="Resources\Windows\Splash\$placeholder$.scale-150.png" />
<Content Include="Resources\Windows\Splash\$placeholder$.scale-200.png" />
<Content Include="Resources\Windows\Splash\$placeholder$.scale-400.png" />
<Content Include="Resources\Windows\Wide\$placeholder$.scale-100.png" />
<Content Include="Resources\Windows\Wide\$placeholder$.scale-125.png" />
<Content Include="Resources\Windows\Wide\$placeholder$.scale-150.png" />
<Content Include="Resources\Windows\Wide\$placeholder$.scale-200.png" />
<Content Include="Resources\Windows\Wide\$placeholder$.scale-400.png" />
</ItemGroup>
<ItemGroup>
<Content Include="Resources\Windows\$placeholder$.scale-100.png"/>
<Content Include="Resources\Windows\$placeholder$.scale-125.png"/>
<Content Include="Resources\Windows\$placeholder$.scale-150.png"/>
<Content Include="Resources\Windows\$placeholder$.scale-200.png"/>
<Content Include="Resources\Windows\$placeholder$.scale-400.png"/>
<Content Include="Resources\Windows\Small\$placeholder$.scale-100.png"/>
<Content Include="Resources\Windows\Small\$placeholder$.scale-125.png"/>
<Content Include="Resources\Windows\Small\$placeholder$.scale-150.png"/>
<Content Include="Resources\Windows\Small\$placeholder$.scale-200.png"/>
<Content Include="Resources\Windows\Small\$placeholder$.scale-400.png"/>
<Content Include="Resources\Windows\Splash\$placeholder$.scale-100.png"/>
<Content Include="Resources\Windows\Splash\$placeholder$.scale-125.png"/>
<Content Include="Resources\Windows\Splash\$placeholder$.scale-150.png"/>
<Content Include="Resources\Windows\Splash\$placeholder$.scale-200.png"/>
<Content Include="Resources\Windows\Splash\$placeholder$.scale-400.png"/>
<Content Include="Resources\Windows\Wide\$placeholder$.scale-100.png"/>
<Content Include="Resources\Windows\Wide\$placeholder$.scale-125.png"/>
<Content Include="Resources\Windows\Wide\$placeholder$.scale-150.png"/>
<Content Include="Resources\Windows\Wide\$placeholder$.scale-200.png"/>
<Content Include="Resources\Windows\Wide\$placeholder$.scale-400.png"/>
</ItemGroup>
<ItemGroup>
<None Include="..\paket_icon.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<None Include="..\paket_icon.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Maui" Version="12.2.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Microsoft.Maui.Controls" Version="9.0.110">
<TreatAsUsed>true</TreatAsUsed>
</PackageReference>
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="9.0.110" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.9" />
<PackageReference Include="Microsoft.Maui.Graphics" Version="9.0.110" />
<PackageReference Include="Microsoft.NET.Runtime.MonoAOTCompiler.Task" Version="9.0.9" />
<PackageReference Include="Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk" Version="9.0.9" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="ZXing.Net.Maui.Controls" Version="0.5.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Maui" Version="12.2.0"/>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0"/>
<PackageReference Include="Microsoft.Maui.Controls" Version="9.0.110">
<TreatAsUsed>true</TreatAsUsed>
</PackageReference>
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="9.0.110"/>
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.9"/>
<PackageReference Include="Microsoft.Maui.Graphics" Version="9.0.110"/>
<PackageReference Include="Microsoft.NET.Runtime.MonoAOTCompiler.Task" Version="9.0.9"/>
<PackageReference Include="Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk" Version="9.0.9"/>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
<PackageReference Include="ZXing.Net.Maui.Controls" Version="0.5.0"/>
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Update="Views\LoginPage.xaml.cs">
<DependentUpon>LoginPage.xaml</DependentUpon>
</Compile>
<Compile Update="Views\StundePage.xaml.cs">
<DependentUpon>StundePage.xaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Update="Views\LoginPage.xaml.cs">
<DependentUpon>LoginPage.xaml</DependentUpon>
</Compile>
<Compile Update="Views\StundePage.xaml.cs">
<DependentUpon>StundePage.xaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<MauiXaml Update="Views\LoginPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\AllNotesPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\StundePage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\NotePage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\StundenPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
</ItemGroup>
<ItemGroup>
<MauiXaml Update="Views\LoginPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\AllNotesPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\StundePage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\NotePage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\StundenPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
</ItemGroup>
</Project>

View File

@@ -3,8 +3,11 @@ using Jugenddienst_Stunden.Models;
using Jugenddienst_Stunden.Interfaces;
using Jugenddienst_Stunden.Repositories;
using Jugenddienst_Stunden.Services;
using Jugenddienst_Stunden.Infrastructure;
using Jugenddienst_Stunden.Validators;
using Microsoft.Extensions.Logging;
using ZXing.Net.Maui.Controls;
using System.Net.Http;
namespace Jugenddienst_Stunden;
@@ -12,39 +15,84 @@ namespace Jugenddienst_Stunden;
/// Das Hauptprogramm.
/// </summary>
public static class MauiProgram {
public static MauiApp CreateMauiApp() {
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
// Initialize the .NET MAUI Community Toolkit by adding the below line of code
.UseMauiCommunityToolkit(options => { options.SetShouldEnableSnackbarOnWindows(true); })
.ConfigureFonts(fonts => {
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
})
//.UseBarcodeScanning();
.UseBarcodeReader();
public static MauiApp CreateMauiApp() {
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
// Initialize the .NET MAUI Community Toolkit by adding the below line of code
.UseMauiCommunityToolkit(options => {
options.SetShouldEnableSnackbarOnWindows(true);
})
.ConfigureFonts(fonts => {
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
})
//.UseBarcodeScanning();
.UseBarcodeReader();
//#if DEBUG
// if (string.IsNullOrWhiteSpace(GlobalVar.ApiKey)) {
// GlobalVar.ApiKey = Preferences.Default.Get("apiKey",
// "MTQxfHNkdFptQkNZTXlPT3ZyMHNBZDl0UnVxNExMRXxodHRwOi8vaG91cnMuZGF1bmkubWluZS5udTo4MS9hcHBhcGk=");
// GlobalVar.Name = Preferences.Default.Get("name", "Testserver: Isabell");
// GlobalVar.Surname = Preferences.Default.Get("surname", "Biasi");
// GlobalVar.EmployeeId = Preferences.Default.Get("EmployeeId", 141);
// GlobalVar.ApiUrl = Preferences.Default.Get("apiUrl", "https://hours.dauni.mine.nu/appapi");
// }
#if DEBUG
if (GlobalVar.ApiKey == null) {
GlobalVar.ApiKey = Preferences.Default.Get("apiKey", "MTQxfHNkdFptQkNZTXlPT3ZyMHNBZDl0UnVxNExMRXxodHRwOi8vaG91cnMuZGF1bmkubWluZS5udTo4MS9hcHBhcGk=");
GlobalVar.Name = Preferences.Default.Get("name", "Testserver: Isabell");
GlobalVar.Surname = Preferences.Default.Get("surname", "Biasi");
GlobalVar.EmployeeId = Preferences.Default.Get("EmployeeId", 141);
GlobalVar.ApiUrl = Preferences.Default.Get("apiUrl", "https://hours.dauni.mine.nu/appapi");
}
builder.Logging.AddDebug();
#endif
// builder.Logging.AddDebug();
//#endif
// DI: Services & Repositories
builder.Services.AddSingleton<IHoursRepository, HoursRepository>();
builder.Services.AddSingleton<IHoursService, HoursService>();
// DI: AlertService für globale Alerts (z. B. leere ApiUrl)
builder.Services.AddSingleton<IAlertService, AlertService>();
return builder.Build();
}
// DI: Settings aus Preferences (Single Source of Truth bleibt Preferences)
builder.Services.AddSingleton<IAppSettings, PreferencesAppSettings>();
// DI: ApiOptions IMMER aus aktuellen Settings erzeugen (nicht beim Start einfrieren)
builder.Services.AddTransient(sp => new ApiOptions {
BaseUrl = sp.GetRequiredService<IAppSettings>().ApiUrl, Timeout = TimeSpan.FromSeconds(15)
});
// Token Provider soll ebenfalls aus Settings/Preferences lesen
builder.Services.AddSingleton<ITokenProvider, SettingsTokenProvider>();
// HttpClient + ApiClient
builder.Services.AddSingleton<HttpClient>(_ => new HttpClient());
builder.Services.AddSingleton<IApiClient>(sp => {
var alert = sp.GetRequiredService<IAlertService>();
try {
return new ApiClient(
sp.GetRequiredService<HttpClient>(),
sp.GetRequiredService<ApiOptions>(),
sp.GetRequiredService<ITokenProvider>(),
sp.GetRequiredService<IAppSettings>());
} catch (Exception e) {
// Alert an UI/VM weiterreichen
alert.Raise(e.Message);
// Fallback: NullApiClient liefert beim Aufruf aussagekräftige Exception
return new NullApiClient(e.Message);
}
});
// DI: Infrastruktur
//builder.Services.AddSingleton(new ApiOptions { BaseUrl = GlobalVar.ApiUrl, Timeout = TimeSpan.FromSeconds(15) });
//builder.Services.AddSingleton<ITokenProvider, GlobalVarTokenProvider>();
//builder.Services.AddSingleton<HttpClient>(_ => new HttpClient());
//builder.Services.AddSingleton<IApiClient>(sp => new ApiClient(
// sp.GetRequiredService<HttpClient>(),
// sp.GetRequiredService<ApiOptions>(),
// sp.GetRequiredService<ITokenProvider>()));
// DI: Validatoren
builder.Services.AddSingleton<IHoursValidator, HoursValidator>();
// DI: Services & Repositories
builder.Services.AddSingleton<IHoursRepository, HoursRepository>();
builder.Services.AddSingleton<IHoursService, HoursService>();
// DI: Views/ViewModels
builder.Services.AddTransient<ViewModels.StundenViewModel>();
builder.Services.AddTransient<Views.StundenPage>();
return builder.Build();
}
}

View File

@@ -7,186 +7,175 @@ using System.Text.Json;
namespace Jugenddienst_Stunden.Models;
internal static class BaseFunc {
internal static async Task<string> GetApiDataWithAuthAsync(string url, string token) {
if (Connectivity.Current.NetworkAccess == NetworkAccess.None)
throw new Exception("Bitte überprüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.");
if (string.IsNullOrEmpty(token))
throw new Exception("Kein APIKEY, bitte zuerst Login durchführen");
// Erstellen eines HttpClient-Objekts
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
client.DefaultRequestHeaders.Add("Accept", "application/json");
// Hinzufügen des Bearer-Tokens zum Authorization-Header
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
// Senden der Anfrage und Abrufen der Antwort
using (HttpResponseMessage HttpResponseMessage = await client.GetAsync(url).ConfigureAwait(false)) {
var byteArray = await HttpResponseMessage.Content.ReadAsByteArrayAsync();
string responseData = Encoding.UTF8.GetString(byteArray);
//using (HttpContent HttpContent = HttpResponseMessage.Content) {
// //responseData = await HttpContent.ReadAsStringAsync();
//}
if (HttpResponseMessage.StatusCode == System.Net.HttpStatusCode.OK) {
return responseData;
} else {
var options = new JsonDocumentOptions { AllowTrailingCommas = true };
using (JsonDocument doc = JsonDocument.Parse(responseData, options)) {
JsonElement root = doc.RootElement;
string message = root.GetProperty("message").GetString() ??
throw new Exception("Fehler: 'message' ist null.");
throw new Exception(message);
}
}
}
}
}
internal static async Task<string> GetApiDataWithAuthAsync(string url, string token) {
internal static async Task<User> AuthUserPass(string user, string pass, string url) {
var values = new Dictionary<string, string> { { "user", user }, { "pass", pass } };
if (Connectivity.Current.NetworkAccess == NetworkAccess.None)
throw new Exception("Bitte überprüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.");
var content = new FormUrlEncodedContent(values);
if (string.IsNullOrEmpty(token))
throw new Exception("Kein APIKEY, bitte zuerst Login durchführen");
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
client.DefaultRequestHeaders.Add("Accept", "application/json");
// Erstellen eines HttpClient-Objekts
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
// Senden der Anfrage und Abrufen der Antwort
using (HttpResponseMessage HttpResponseMessage =
await client.PostAsync(url, content).ConfigureAwait(false)) {
if (!HttpResponseMessage.IsSuccessStatusCode) {
//throw new Exception("Fehler beim Einloggen " + HttpResponseMessage.Content);
var byteArray = await HttpResponseMessage.Content.ReadAsByteArrayAsync();
string responseData = Encoding.UTF8.GetString(byteArray);
var options = new JsonDocumentOptions { AllowTrailingCommas = true };
using (JsonDocument doc = JsonDocument.Parse(responseData, options)) {
JsonElement root = doc.RootElement;
string message = root.GetProperty("message").GetString() ??
throw new Exception("Fehler: 'message' ist null.");
throw new Exception(message);
}
}
client.DefaultRequestHeaders.Add("Accept", "application/json");
// Überprüfen, ob die Anfrage erfolgreich war
if (HttpResponseMessage.StatusCode == System.Net.HttpStatusCode.OK) {
using (HttpContent HttpContent = HttpResponseMessage.Content) {
// Lesen und Rückgabe der Antwort als String
// Hinzufügen des Bearer-Tokens zum Authorization-Header
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
string responseData = await HttpContent.ReadAsStringAsync();
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(responseData) ??
throw new Exception("Fehler beim Deserialisieren der Daten");
//User userData = System.Text.Json.JsonSerializer.Deserialize<User>(responseData) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
return res.user;
}
}
}
}
// Senden der Anfrage und Abrufen der Antwort
using (HttpResponseMessage HttpResponseMessage = await client.GetAsync(url).ConfigureAwait(false)) {
var byteArray = await HttpResponseMessage.Content.ReadAsByteArrayAsync();
string responseData = Encoding.UTF8.GetString(byteArray);
//using (HttpContent HttpContent = HttpResponseMessage.Content) {
// //responseData = await HttpContent.ReadAsStringAsync();
//}
if (HttpResponseMessage.StatusCode == System.Net.HttpStatusCode.OK) {
return responseData;
} else {
var options = new JsonDocumentOptions {
AllowTrailingCommas = true
};
using (JsonDocument doc = JsonDocument.Parse(responseData, options)) {
JsonElement root = doc.RootElement;
string message = root.GetProperty("message").GetString() ?? throw new Exception("Fehler: 'message' ist null.");
throw new Exception(message);
}
}
}
}
}
return null;
}
/// <summary>
/// Notiz laden
/// </summary>
internal static Note Load(string filename) {
filename = System.IO.Path.Combine(FileSystem.AppDataDirectory, filename);
if (!File.Exists(filename))
throw new FileNotFoundException("Unable to find file on local storage.", filename);
internal static async Task<User> AuthUserPass(string user, string pass, string url) {
return
new() { Date = File.GetLastWriteTime(filename) };
}
var values = new Dictionary<string, string>
{
{ "user", user },
{ "pass", pass }
};
/// <summary>
/// Stundeneintrag speichern
/// </summary>
internal static async Task SaveItemAsync(string url, string token, DayTime item, bool isNewItem = false) {
//Uhrzeiten sollten sinnvolle Werte haben - außer bei Freistellungen, da wäre eigentlich null
if (item.TimeSpanVon == item.TimeSpanBis && item.FreistellungAktiv == null) {
throw new Exception("Beginn und Ende sind gleich");
}
var content = new FormUrlEncodedContent(values);
if (item.TimeSpanBis < item.TimeSpanVon) {
throw new Exception("Ende ist vor Beginn");
}
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
TimeSpan span = TimeSpan.Zero;
span += item.TimeSpanBis - item.TimeSpanVon;
if (span.Hours > 10) {
//Hier vielleicht eine Abfrage, ob mehr als 10 Stunden gesund sind?
//Das müsste aber das ViewModel machen
}
client.DefaultRequestHeaders.Add("Accept", "application/json");
//Gemeinde ist ein Pflichtfeld
if (item.GemeindeAktiv == null && GlobalVar.Settings.GemeindeAktivSet) {
throw new Exception("Gemeinde nicht gewählt");
}
// Senden der Anfrage und Abrufen der Antwort
using (HttpResponseMessage HttpResponseMessage = await client.PostAsync(url, content).ConfigureAwait(false)) {
if (!HttpResponseMessage.IsSuccessStatusCode)
{
//throw new Exception("Fehler beim Einloggen " + HttpResponseMessage.Content);
var byteArray = await HttpResponseMessage.Content.ReadAsByteArrayAsync();
string responseData = Encoding.UTF8.GetString(byteArray);
var options = new JsonDocumentOptions {
AllowTrailingCommas = true
};
using (JsonDocument doc = JsonDocument.Parse(responseData, options)) {
JsonElement root = doc.RootElement;
string message = root.GetProperty("message").GetString() ?? throw new Exception("Fehler: 'message' ist null.");
throw new Exception(message);
}
}
//Projekt ist ein Pflichtfeld
if (item.ProjektAktiv == null && GlobalVar.Settings.ProjektAktivSet) {
throw new Exception("Projekt nicht gewählt");
}
// Überprüfen, ob die Anfrage erfolgreich war
if (HttpResponseMessage.StatusCode == System.Net.HttpStatusCode.OK) {
using (HttpContent HttpContent = HttpResponseMessage.Content) {
// Lesen und Rückgabe der Antwort als String
//Keine Beschreibung
if (string.IsNullOrEmpty(item.Description) && item.FreistellungAktiv == null) {
throw new Exception("Keine Beschreibung");
}
string responseData = await HttpContent.ReadAsStringAsync();
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(responseData) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
//User userData = System.Text.Json.JsonSerializer.Deserialize<User>(responseData) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
return res.user;
}
}
}
//Keine Beschreibung
if (string.IsNullOrEmpty(item.Description)) {
item.Description = item.FreistellungAktiv.Name;
}
}
return null;
}
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
//HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
/// <summary>
/// Notiz laden
/// </summary>
internal static Note Load(string filename) {
filename = System.IO.Path.Combine(FileSystem.AppDataDirectory, filename);
//string json = JsonSerializer.Serialize<DayTime>(item);
string json = JsonConvert.SerializeObject(item);
StringContent content = new StringContent(json, Encoding.UTF8, "application/json");
if (!File.Exists(filename))
throw new FileNotFoundException("Unable to find file on local storage.", filename);
HttpResponseMessage? response = null;
if (isNewItem)
response = await client.PostAsync(url, content);
else
response = await client.PutAsync(url, content);
return
new() {
Date = File.GetLastWriteTime(filename)
};
}
if (!response.IsSuccessStatusCode) {
throw new Exception("Fehler beim Speichern " + response.Content);
}
}
}
/// <summary>
/// Stundeneintrag speichern
/// </summary>
internal static async Task SaveItemAsync(string url, string token, DayTime item, bool isNewItem = false) {
/// <summary>
/// Stundeneintrag löschen
/// </summary>
internal static async Task DeleteItemAsync(string url, string token) {
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
//HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
//Uhrzeiten sollten sinnvolle Werte haben - außer bei Freistellungen, da wäre eigentlich null
if (item.TimeSpanVon == item.TimeSpanBis && item.FreistellungAktiv == null) {
throw new Exception("Beginn und Ende sind gleich");
}
if (item.TimeSpanBis < item.TimeSpanVon) {
throw new Exception("Ende ist vor Beginn");
}
TimeSpan span = TimeSpan.Zero;
span += item.TimeSpanBis - item.TimeSpanVon;
if (span.Hours > 10) {
//Hier vielleicht eine Abfrage, ob mehr als 10 Stunden gesund sind?
//Das müsste aber das ViewModel machen
}
//Gemeinde ist ein Pflichtfeld
if (item.GemeindeAktiv == null && GlobalVar.Settings.GemeindeAktivSet) {
throw new Exception("Gemeinde nicht gewählt");
}
//Projekt ist ein Pflichtfeld
if (item.ProjektAktiv == null && GlobalVar.Settings.ProjektAktivSet) {
throw new Exception("Projekt nicht gewählt");
}
//Keine Beschreibung
if (string.IsNullOrEmpty(item.Description) && item.FreistellungAktiv == null) {
throw new Exception("Keine Beschreibung");
}
//Keine Beschreibung
if (string.IsNullOrEmpty(item.Description)) {
item.Description = item.FreistellungAktiv.Name;
}
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
//HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
//string json = JsonSerializer.Serialize<DayTime>(item);
string json = JsonConvert.SerializeObject(item);
StringContent content = new StringContent(json, Encoding.UTF8, "application/json");
HttpResponseMessage? response = null;
if (isNewItem)
response = await client.PostAsync(url, content);
else
response = await client.PutAsync(url, content);
if (!response.IsSuccessStatusCode) {
throw new Exception("Fehler beim Speichern " + response.Content);
}
}
}
/// <summary>
/// Stundeneintrag löschen
/// </summary>
internal static async Task DeleteItemAsync(string url, string token) {
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
//HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
HttpResponseMessage response = await client.DeleteAsync(url);
if (!response.IsSuccessStatusCode)
throw new Exception("Fehler beim Löschen " + response.Content);
}
}
HttpResponseMessage response = await client.DeleteAsync(url);
if (!response.IsSuccessStatusCode)
throw new Exception("Fehler beim Löschen " + response.Content);
}
}
}

View File

@@ -1,26 +1,32 @@
using Jugenddienst_Stunden.Types;
namespace Jugenddienst_Stunden.Models;
internal static class GlobalVar {
public static string ApiKey {
get => Preferences.Default.Get("apiKey", "");
set => Preferences.Default.Set("apiKey", value);
}
public static int EmployeeId {
get => Preferences.Default.Get("EmployeeId", 0);
set => Preferences.Default.Set("EmployeeId", value);
}
public static string Name {
get => Preferences.Default.Get("name", "Nicht");
set => Preferences.Default.Set("name", value);
}
public static string Surname {
get => Preferences.Default.Get("surname", "Eingeloggt");
set => Preferences.Default.Set("surname", value);
}
public static string ApiUrl {
get => Preferences.Default.Get("apiUrl", "");
set => Preferences.Default.Set("apiUrl", value);
}
public static Settings Settings { get; set; }
public static string ApiKey {
get => Preferences.Default.Get("apiKey", "");
set => Preferences.Default.Set("apiKey", value);
}
public static int EmployeeId {
get => Preferences.Default.Get("EmployeeId", 0);
set => Preferences.Default.Set("EmployeeId", value);
}
public static string Name {
get => Preferences.Default.Get("name", "Nicht");
set => Preferences.Default.Set("name", value);
}
public static string Surname {
get => Preferences.Default.Get("surname", "Eingeloggt");
set => Preferences.Default.Set("surname", value);
}
public static string ApiUrl {
get => Preferences.Default.Get("apiUrl", "");
set => Preferences.Default.Set("apiUrl", value);
}
public static Settings Settings { get; set; }
}

View File

@@ -4,83 +4,90 @@ using Newtonsoft.Json;
namespace Jugenddienst_Stunden.Models;
internal static class HoursBase {
/// <summary>
/// Laden ... what can be: "settings", "hours", date="YYYY-MM-DD", id=<int/>
/// </summary>
/// <returns>Entire response</returns>
internal static async Task<BaseResponse> LoadBase(string what) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?" + what, GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
throw new Exception("Fehler beim Deserialisieren der Daten");
return res;
}
/// <summary>
/// Laden ... what can be: "settings", "hours", date="YYYY-MM-DD", id=<int/>
/// </summary>
/// <returns>Entire response</returns>
internal static async Task<BaseResponse> LoadBase(string what) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?"+what, GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
return res;
}
/// <summary>
/// Einstellungen laden
/// </summary>
/// <returns>Settings only</returns>
internal static async Task<Settings> LoadSettings() {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?settings", GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
throw new Exception("Fehler beim Deserialisieren der Daten");
return res.settings;
}
/// <summary>
/// Einstellungen laden
/// </summary>
/// <returns>Settings only</returns>
internal static async Task<Settings> LoadSettings() {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?settings", GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
return res.settings;
}
/// <summary>
/// Daten laden
/// </summary>
/// <returns>Hours only</returns>
internal static async Task<Hours> LoadData() {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?hours", GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
throw new Exception("Fehler beim Deserialisieren der Daten");
return res.hour;
}
/// <summary>
/// Daten laden
/// </summary>
/// <returns>Hours only</returns>
internal static async Task<Hours> LoadData() {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?hours", GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
return res.hour;
}
/// <summary>
/// Benutzerdaten laden
/// </summary>
/// <returns>User-Object</returns>
public static async Task<User> LoadUser(string apiKey) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl, apiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
throw new Exception("Fehler beim Deserialisieren der Daten");
return res.user;
}
/// <summary>
/// Benutzerdaten laden
/// </summary>
/// <returns>User-Object</returns>
public static async Task<User> LoadUser(string apiKey) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl, apiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
return res.user;
}
/// <summary>
/// Zeiten eines Tages holen
/// </summary>
internal static async Task<List<DayTime>> LoadDay(DateTime date) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?date=" + date.ToString("yyyy-MM-dd"),
GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
throw new Exception("Fehler beim Deserialisieren der Daten");
return res.daytimes;
}
/// <summary>
/// Zeiten eines Tages holen
/// </summary>
internal static async Task<List<DayTime>> LoadDay(DateTime date) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?date=" + date.ToString("yyyy-MM-dd"), GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
return res.daytimes;
}
/// <summary>
/// Einzelnen Stundeneintrag holen
/// </summary>
internal static async Task<DayTime> LoadEntry(int id) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?id=" + id, GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
throw new Exception("Fehler beim Deserialisieren der Daten");
res.daytime.TimeSpanVon = res.daytime.Begin.ToTimeSpan();
res.daytime.TimeSpanBis = res.daytime.End.ToTimeSpan();
return res.daytime;
}
/// <summary>
/// Einzelnen Stundeneintrag holen
/// </summary>
internal static async Task<DayTime> LoadEntry(int id) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?id=" + id, GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
res.daytime.TimeSpanVon = res.daytime.Begin.ToTimeSpan();
res.daytime.TimeSpanBis = res.daytime.End.ToTimeSpan();
return res.daytime;
}
/// <summary>
/// Eintrag speichern
/// </summary>
internal static async Task<DayTime> SaveEntry(DayTime stunde) {
//, string begin, string end, string freistellung, string bemerkung) {
bool isNew = false;
if (stunde.Id == null)
isNew = true;
await BaseFunc.SaveItemAsync(GlobalVar.ApiUrl, GlobalVar.ApiKey, stunde, isNew);
/// <summary>
/// Eintrag speichern
/// </summary>
internal static async Task<DayTime> SaveEntry(DayTime stunde) { //, string begin, string end, string freistellung, string bemerkung) {
bool isNew = false;
if (stunde.Id == null)
isNew = true;
await BaseFunc.SaveItemAsync(GlobalVar.ApiUrl, GlobalVar.ApiKey, stunde, isNew);
return stunde;
}
return stunde;
}
/// <summary>
/// Eintrag löschen
/// </summary>
internal static async Task DeleteEntry(DayTime stunde) {
await BaseFunc.DeleteItemAsync(GlobalVar.ApiUrl + "/entry/" + stunde.Id, GlobalVar.ApiKey);
}
/// <summary>
/// Eintrag löschen
/// </summary>
internal static async Task DeleteEntry(DayTime stunde) {
await BaseFunc.DeleteItemAsync(GlobalVar.ApiUrl + "/entry/" + stunde.Id, GlobalVar.ApiKey);
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jugenddienst_Stunden.Models;
internal sealed class JsonFlexibleInt32Converter : JsonConverter<int> {
public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
switch (reader.TokenType) {
case JsonTokenType.Number:
if (reader.TryGetInt32(out var n)) return n;
// Fallback via double to cover edge cases
var d = reader.GetDouble();
return (int)d;
case JsonTokenType.String:
var s = reader.GetString();
if (string.IsNullOrWhiteSpace(s)) return 0;
s = s.Trim();
// Some APIs embed id like "141|..." -> take leading numeric part
int i = 0;
int sign = 1;
int idx = 0;
if (s.StartsWith("-")) { sign = -1; idx = 1; }
for (; idx < s.Length; idx++) {
char c = s[idx];
if (c < '0' || c > '9') break;
i = i * 10 + (c - '0');
}
if (idx > 0 && (idx > 1 || sign == 1)) return i * sign;
if (int.TryParse(s, out var parsed)) return parsed;
if (long.TryParse(s, out var l)) return (int)l;
throw new JsonException($"Cannot convert string '{s}' to Int32.");
case JsonTokenType.Null:
return 0;
default:
throw new JsonException($"Token {reader.TokenType} is not valid for Int32.");
}
}
public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
=> writer.WriteNumberValue(value);
}
internal sealed class JsonFlexibleNullableInt32Converter : JsonConverter<int?> {
public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
if (reader.TokenType == JsonTokenType.Null) return null;
// Reuse non-nullable converter
var conv = new JsonFlexibleInt32Converter();
return conv.Read(ref reader, typeof(int), options);
}
public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options) {
if (value.HasValue) writer.WriteNumberValue(value.Value);
else writer.WriteNullValue();
}
}

View File

@@ -11,37 +11,44 @@ namespace Jugenddienst_Stunden.Models {
return typeof(T).IsAssignableFrom(objectType);
}
public override bool CanWrite { get { return false; } }
public override bool CanWrite {
get { return false; }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
JsonSerializer serializer) {
var contract = serializer.ContractResolver.ResolveContract(objectType);
if (!(contract is Newtonsoft.Json.Serialization.JsonObjectContract || contract is Newtonsoft.Json.Serialization.JsonDictionaryContract)) {
throw new JsonSerializationException(string.Format("Unsupported objectType {0} at {1}.", objectType, reader.Path));
if (!(contract is Newtonsoft.Json.Serialization.JsonObjectContract ||
contract is Newtonsoft.Json.Serialization.JsonDictionaryContract)) {
throw new JsonSerializationException(string.Format("Unsupported objectType {0} at {1}.", objectType,
reader.Path));
}
switch (reader.SkipComments().TokenType) {
case JsonToken.StartArray: {
int count = 0;
while (reader.Read()) {
switch (reader.TokenType) {
case JsonToken.Comment:
break;
case JsonToken.EndArray:
return existingValue;
default: {
count++;
if (count > 1)
throw new JsonSerializationException(string.Format("Too many objects at path {0}.", reader.Path));
existingValue = existingValue ?? contract.DefaultCreator();
serializer.Populate(reader, existingValue);
}
break;
int count = 0;
while (reader.Read()) {
switch (reader.TokenType) {
case JsonToken.Comment:
break;
case JsonToken.EndArray:
return existingValue;
default: {
count++;
if (count > 1)
throw new JsonSerializationException(string.Format("Too many objects at path {0}.",
reader.Path));
existingValue = existingValue ?? contract.DefaultCreator();
serializer.Populate(reader, existingValue);
}
break;
}
// Should not come here.
throw new JsonSerializationException(string.Format("Unclosed array at path {0}.", reader.Path));
}
// Should not come here.
throw new JsonSerializationException(string.Format("Unclosed array at path {0}.", reader.Path));
}
case JsonToken.Null:
return null;

View File

@@ -1,49 +1,50 @@
namespace Jugenddienst_Stunden.Models;
internal class Note {
public string Filename { get; set; }
public string Text { get; set; }
public DateTime Date { get; set; }
public string Filename { get; set; }
public string Text { get; set; }
public DateTime Date { get; set; }
public void Save() =>
File.WriteAllText(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename), Text);
public void Save() =>
File.WriteAllText(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename), Text);
public void Delete() =>
File.Delete(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename));
public void Delete() =>
File.Delete(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename));
public static Note Load(string filename) {
filename = System.IO.Path.Combine(FileSystem.AppDataDirectory, filename);
public static Note Load(string filename) {
filename = System.IO.Path.Combine(FileSystem.AppDataDirectory, filename);
if (!File.Exists(filename))
throw new FileNotFoundException("Unable to find file on local storage.", filename);
if (!File.Exists(filename))
throw new FileNotFoundException("Unable to find file on local storage.", filename);
return
new() {
Filename = Path.GetFileName(filename),
Text = File.ReadAllText(filename),
Date = File.GetLastWriteTime(filename)
};
}
return
new() {
Filename = Path.GetFileName(filename),
Text = File.ReadAllText(filename),
Date = File.GetLastWriteTime(filename)
};
}
public static IEnumerable<Note> LoadAll() {
// Get the folder where the notes are stored.
string appDataPath = FileSystem.AppDataDirectory;
public static IEnumerable<Note> LoadAll() {
// Get the folder where the notes are stored.
string appDataPath = FileSystem.AppDataDirectory;
// Use Linq extensions to load the *.notes.txt files.
return Directory
// Use Linq extensions to load the *.notes.txt files.
return Directory
// Select the file names from the directory
.EnumerateFiles(appDataPath, "*.notes.txt")
// Select the file names from the directory
.EnumerateFiles(appDataPath, "*.notes.txt")
// Each file name is used to load a note
.Select(filename => Note.Load(Path.GetFileName(filename)))
// Each file name is used to load a note
.Select(filename => Note.Load(Path.GetFileName(filename)))
// With the final collection of notes, order them by date
.OrderByDescending(note => note.Date);
}
// With the final collection of notes, order them by date
.OrderByDescending(note => note.Date);
}
public Note() {
Filename = $"{Path.GetRandomFileName()}.notes.txt";
Date = DateTime.Now;
Text = "";
}
public Note() {
Filename = $"{Path.GetRandomFileName()}.notes.txt";
Date = DateTime.Now;
Text = "";
}
}

View File

@@ -2,20 +2,20 @@
using Newtonsoft.Json;
namespace Jugenddienst_Stunden.Models;
internal class Operator {
public string? id;
public string? name;
public string? surname;
public string? email;
public string? password;
public string? lang;
public string? admin;
public string? aktiv;
public string? department;
public string? department_name;
public string? num;
public string? year;
public event EventHandler<string>? AlertEvent;
public string? id;
public string? name;
public string? surname;
public string? email;
public string? password;
public string? lang;
public string? admin;
public string? aktiv;
public string? department;
public string? department_name;
public string? num;
public string? year;
public event EventHandler<string>? AlertEvent;
}

View File

@@ -10,19 +10,23 @@ internal class TokenData {
public string Operator_id { get; set; }
public TokenData(string ak) {
if (string.IsNullOrEmpty(ak)) {
throw new ArgumentException("API key cannot be null or empty", nameof(ak));
}
string dat = Encoding.UTF8.GetString(Convert.FromBase64String(ak));
if (string.IsNullOrEmpty(ak)) {
throw new ArgumentException("API key cannot be null or empty", nameof(ak));
}
string[] parts = dat.Split('|');
if (parts.Length < 3) {
throw new FormatException("API key format is invalid");
}
string dat = Encoding.UTF8.GetString(Convert.FromBase64String(ak));
Token = dat.Split('|')[1]; ;
Url = dat.Split('|')[2]; ;
Operator_id = dat.Split('|')[0]; ;
string[] parts = dat.Split('|');
if (parts.Length < 3) {
throw new FormatException("API key format is invalid");
}
Token = dat.Split('|')[1];
;
Url = dat.Split('|')[2];
;
Operator_id = dat.Split('|')[0];
;
ApiKey = ak;
}
}

View File

@@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/appicon"
android:supportsRtl="true"
android:label="Stunden"
/>
<!-- android:usesCleartextTraffic="true" -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
android:allowBackup="true"
android:icon="@mipmap/appicon"
android:supportsRtl="true"
android:label="Stunden"
/>
<!-- android:usesCleartextTraffic="true" -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
</manifest>

View File

@@ -3,6 +3,9 @@ using Android.Content.PM;
using Android.OS;
namespace Jugenddienst_Stunden;
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop,
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode |
ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity {
}

View File

@@ -1,38 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- The Mac App Store requires you specify if the app uses encryption. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption -->
<!-- <key>ITSAppUsesNonExemptEncryption</key> -->
<!-- Please indicate <true/> or <false/> here. -->
<dict>
<!-- The Mac App Store requires you specify if the app uses encryption. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption -->
<!-- <key>ITSAppUsesNonExemptEncryption</key> -->
<!-- Please indicate <true/> or <false/> here. -->
<!-- Specify the category for your app here. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/lsapplicationcategorytype -->
<!-- <key>LSApplicationCategoryType</key> -->
<!-- <string>public.app-category.YOUR-CATEGORY-HERE</string> -->
<key>UIDeviceFamily</key>
<array>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
<!-- Specify the category for your app here. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/lsapplicationcategorytype -->
<!-- <key>LSApplicationCategoryType</key> -->
<!-- <string>public.app-category.YOUR-CATEGORY-HERE</string> -->
<key>UIDeviceFamily</key>
<array>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
</plist>

View File

@@ -1,15 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="maui-application-id-placeholder" version="0.0.0" api-version="8" xmlns="http://tizen.org/ns/packages">
<profile name="common" />
<ui-application appid="maui-application-id-placeholder" exec="Jugenddienst Stunden.dll" multiple="false" nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
<label>maui-application-title-placeholder</label>
<icon>maui-appicon-placeholder</icon>
<metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true" />
</ui-application>
<shortcut-list />
<privileges>
<privilege>http://tizen.org/privilege/internet</privilege>
</privileges>
<dependencies />
<provides-appdefined-privileges />
<manifest package="maui-application-id-placeholder" version="0.0.0" api-version="8"
xmlns="http://tizen.org/ns/packages">
<profile name="common"/>
<ui-application appid="maui-application-id-placeholder" exec="Jugenddienst Stunden.dll" multiple="false"
nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
<label>maui-application-title-placeholder</label>
<icon>maui-appicon-placeholder</icon>
<metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true"/>
</ui-application>
<shortcut-list/>
<privileges>
<privilege>http://tizen.org/privilege/internet</privilege>
</privileges>
<dependencies/>
<provides-appdefined-privileges/>
</manifest>

View File

@@ -19,9 +19,5 @@ namespace Jugenddienst_Stunden.WinUI {
/// </summary>
/// <returns></returns>
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
}

View File

@@ -1,70 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
IgnorableNamespaces="uap rescap com desktop">
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
IgnorableNamespaces="uap rescap com desktop">
<Identity Name="JugenddienstStunden" Publisher="CN=User Name" Version="0.0.0.0" />
<Identity Name="JugenddienstStunden" Publisher="CN=User Name" Version="0.0.0.0"/>
<mp:PhoneIdentity PhoneProductId="4BA4D7D7-E3C2-4BBF-92EF-0EDB5DB5CDB4" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<mp:PhoneIdentity PhoneProductId="4BA4D7D7-E3C2-4BBF-92EF-0EDB5DB5CDB4"
PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>$placeholder$</DisplayName>
<PublisherDisplayName>Daniel Pichler</PublisherDisplayName>
<Logo>Resources\Windows\$placeholder$.png</Logo>
</Properties>
<Properties>
<DisplayName>$placeholder$</DisplayName>
<PublisherDisplayName>Daniel Pichler</PublisherDisplayName>
<Logo>Resources\Windows\$placeholder$.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0"/>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0"/>
</Dependencies>
<Resources>
<Resource Language="x-generate" />
</Resources>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="$placeholder$"
Description="$placeholder$"
Square150x150Logo="Resources\Windows\$placeholder$.png"
Square44x44Logo="Resources\Windows\$placeholder$.png"
BackgroundColor="transparent">
<uap:DefaultTile Square71x71Logo="Resources\Windows\Small\$placeholder$.png" Wide310x150Logo="Resources\Windows\Wide\$placeholder$.png" Square310x310Logo="Resources\Windows\$placeholder$.png" ShortName="Stunden"/>
<uap:SplashScreen Image="Resources\Windows\Splash\$placeholder$.png" BackgroundColor="#F7931D"/>
</uap:VisualElements>
<Applications>
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="$placeholder$"
Description="$placeholder$"
Square150x150Logo="Resources\Windows\$placeholder$.png"
Square44x44Logo="Resources\Windows\$placeholder$.png"
BackgroundColor="transparent">
<uap:DefaultTile Square71x71Logo="Resources\Windows\Small\$placeholder$.png"
Wide310x150Logo="Resources\Windows\Wide\$placeholder$.png"
Square310x310Logo="Resources\Windows\$placeholder$.png" ShortName="Stunden"/>
<uap:SplashScreen Image="Resources\Windows\Splash\$placeholder$.png" BackgroundColor="#F7931D"/>
</uap:VisualElements>
<Extensions>
<Extensions>
<!-- Specify which CLSID to activate when notification is clicked -->
<desktop:Extension Category="windows.toastNotificationActivation">
<desktop:ToastNotificationActivation ToastActivatorCLSID="6e919706-2634-4d97-a93c-2213b2acc334" />
</desktop:Extension>
<!-- Specify which CLSID to activate when notification is clicked -->
<desktop:Extension Category="windows.toastNotificationActivation">
<desktop:ToastNotificationActivation ToastActivatorCLSID="6e919706-2634-4d97-a93c-2213b2acc334"/>
</desktop:Extension>
<!-- Register COM CLSID -->
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="Jugenddienst Stunden.exe" DisplayName="$targetnametoken$" Arguments="----AppNotificationActivated:">
<!-- Example path to executable: CommunityToolkit.Maui.Sample\CommunityToolkit.Maui.Sample.exe -->
<com:Class Id="6e919706-2634-4d97-a93c-2213b2acc334" />
</com:ExeServer>
</com:ComServer>
</com:Extension>
<!-- Register COM CLSID -->
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="Jugenddienst Stunden.exe" DisplayName="$targetnametoken$"
Arguments="----AppNotificationActivated:">
<!-- Example path to executable: CommunityToolkit.Maui.Sample\CommunityToolkit.Maui.Sample.exe -->
<com:Class Id="6e919706-2634-4d97-a93c-2213b2acc334"/>
</com:ExeServer>
</com:ComServer>
</com:Extension>
</Extensions>
</Extensions>
</Application>
</Applications>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
<Capability Name="internetClient"/>
<DeviceCapability Name="webcam"/>
</Capabilities>
<Capabilities>
<rescap:Capability Name="runFullTrust"/>
<Capability Name="internetClient"/>
<DeviceCapability Name="webcam"/>
</Capabilities>
</Package>

View File

@@ -1,15 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="Jugenddienst Stunden.WinUI.app"/>
<assemblyIdentity version="1.0.0.0" name="Jugenddienst Stunden.WinUI.app"/>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- The combination of below two tags have the following effect:
1) Per-Monitor for >= Windows 10 Anniversary Update
2) System < Windows 10 Anniversary Update
-->
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
</windowsSettings>
</application>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- The combination of below two tags have the following effect:
1) Per-Monitor for >= Windows 10 Anniversary Update
2) System < Windows 10 Anniversary Update
-->
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor
</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View File

@@ -1,34 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSCameraUsageDescription</key>
<string>This app uses barcode scanning to...</string>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
<key>CFBundleIdentifier</key>
<string></string>
</dict>
<dict>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSCameraUsageDescription</key>
<string>This app uses barcode scanning to...</string>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
<key>CFBundleIdentifier</key>
<string></string>
</dict>
</plist>

View File

@@ -1,10 +1,9 @@
//------------------------------------------------------------------------------
// <auto-generated>
// Dieser Code wurde von einem Tool generiert.
// Laufzeitversion:4.0.30319.42000
// This code was generated by a tool.
//
// Änderungen an dieser Datei können falsches Verhalten verursachen und gehen verloren, wenn
// der Code erneut generiert wird.
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
@@ -13,12 +12,12 @@ namespace Jugenddienst_Stunden.Properties {
/// <summary>
/// Eine stark typisierte Ressourcenklasse zum Suchen von lokalisierten Zeichenfolgen usw.
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// Diese Klasse wurde von der StronglyTypedResourceBuilder automatisch generiert
// -Klasse über ein Tool wie ResGen oder Visual Studio automatisch generiert.
// Um einen Member hinzuzufügen oder zu entfernen, bearbeiten Sie die .ResX-Datei und führen dann ResGen
// mit der /str-Option erneut aus, oder Sie erstellen Ihr VS-Projekt neu.
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
@@ -33,7 +32,7 @@ namespace Jugenddienst_Stunden.Properties {
}
/// <summary>
/// Gibt die zwischengespeicherte ResourceManager-Instanz zurück, die von dieser Klasse verwendet wird.
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
@@ -47,8 +46,8 @@ namespace Jugenddienst_Stunden.Properties {
}
/// <summary>
/// Überschreibt die CurrentUICulture-Eigenschaft des aktuellen Threads für alle
/// Ressourcenzuordnungen, die diese stark typisierte Ressourcenklasse verwenden.
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {

View File

@@ -1,101 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 1.3
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
</root>

View File

@@ -1,4 +1,5 @@
using Jugenddienst_Stunden.Interfaces;
using Jugenddienst_Stunden.Infrastructure;
using Jugenddienst_Stunden.Models;
using Jugenddienst_Stunden.Types;
@@ -8,12 +9,72 @@ namespace Jugenddienst_Stunden.Repositories;
/// Standard-Repository, das die bestehende API-/Model-Logik kapselt.
/// </summary>
internal class HoursRepository : IHoursRepository {
public async Task<BaseResponse> LoadBase(string query) => await HoursBase.LoadBase(query);
public async Task<Settings> LoadSettings() => await HoursBase.LoadSettings();
public async Task<Hours> LoadData() => await HoursBase.LoadData();
public async Task<User> LoadUser(string apiKey) => await HoursBase.LoadUser(apiKey);
public async Task<List<DayTime>> LoadDay(DateTime date) => await HoursBase.LoadDay(date);
public async Task<DayTime> LoadEntry(int id) => await HoursBase.LoadEntry(id);
public async Task<DayTime> SaveEntry(DayTime stunde) => await HoursBase.SaveEntry(stunde);
public async Task DeleteEntry(DayTime stunde) => await HoursBase.DeleteEntry(stunde);
private readonly IApiClient _api;
public HoursRepository(IApiClient api) {
_api = api;
}
public async Task<BaseResponse> LoadBase(string query) {
// Der bestehende Code übergab eine Query ohne führendes '?'
var dict = QueryToDictionary(query);
var res= await _api.GetAsync<BaseResponse>("", dict).ConfigureAwait(false);
return res;
}
public async Task<Settings> LoadSettings() {
var res = await _api.GetAsync<BaseResponse>("", new Dictionary<string, string?> { ["settings"] = "1" })
.ConfigureAwait(false);
return res.settings;
}
public async Task<Hours> LoadData() {
var res = await _api.GetAsync<BaseResponse>("", new Dictionary<string, string?> { ["hours"] = "1" })
.ConfigureAwait(false);
return res.hour;
}
public Task<User> LoadUser(string apiKey) {
// Für die erste Iteration bleibt das Token global; der Endpoint ohne Query liefert user
return _api.GetAsync<BaseResponse>("", null).ContinueWith(t => t.Result.user);
}
public async Task<List<DayTime>> LoadDay(DateTime date) {
var res = await _api
.GetAsync<BaseResponse>("", new Dictionary<string, string?> { ["date"] = date.ToString("yyyy-MM-dd") })
.ConfigureAwait(false);
return res.daytimes ?? new List<DayTime>();
}
public async Task<DayTime> LoadEntry(int id) {
var res = await _api.GetAsync<BaseResponse>("", new Dictionary<string, string?> { ["id"] = id.ToString() })
.ConfigureAwait(false);
res.daytime.TimeSpanVon = res.daytime.Begin.ToTimeSpan();
res.daytime.TimeSpanBis = res.daytime.End.ToTimeSpan();
return res.daytime;
}
public async Task<DayTime> SaveEntry(DayTime stunde) {
bool isNew = stunde.Id is null;
var method = isNew ? HttpMethod.Post : HttpMethod.Put;
await _api.SendAsync<object>(method, "", stunde).ConfigureAwait(false);
return stunde;
}
public Task DeleteEntry(DayTime stunde)
=> _api.DeleteAsync($"/entry/{stunde.Id}");
private static Dictionary<string, string?> QueryToDictionary(string query) {
var dict = new Dictionary<string, string?>();
if (string.IsNullOrWhiteSpace(query)) return dict;
var q = query.TrimStart('?');
foreach (var part in q.Split('&', StringSplitOptions.RemoveEmptyEntries)) {
var kv = part.Split('=', 2);
var key = Uri.UnescapeDataString(kv[0]);
var val = kv.Length > 1 ? Uri.UnescapeDataString(kv[1]) : null;
dict[key] = val;
}
return dict;
}
}

View File

@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<svg width="100%" height="100%" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<rect x="0" y="0" width="456" height="456" style="fill:white;"/>
</svg>

Before

Width:  |  Height:  |  Size: 521 B

After

Width:  |  Height:  |  Size: 531 B

View File

@@ -1,41 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<svg width="100%" height="100%" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g id="JD" transform="matrix(0.96485,0,0,0.96485,104.499,104.499)">
<g transform="matrix(12.0607,0,0,12.0607,101.169,256)">
<path d="M0,-11.938L0,-4.95C0,-3.918 -0.071,-3.151 -0.214,-2.644C-0.359,-2.069 -0.604,-1.583 -0.95,-1.184C-1.624,-0.397 -2.515,0 -3.626,0C-4.117,0 -4.588,-0.075 -5.04,-0.226L-5.04,-2.305L-4.999,-2.375C-4.509,-2.048 -4.052,-1.884 -3.626,-1.884C-3.026,-1.884 -2.612,-2.112 -2.382,-2.565C-2.141,-3.019 -2.02,-3.817 -2.02,-4.95L-2.02,-11.938L0,-11.938" style="fill-rule:nonzero;"/>
<path d="M0,-11.938L0,-4.95C0,-3.918 -0.071,-3.151 -0.214,-2.644C-0.359,-2.069 -0.604,-1.583 -0.95,-1.184C-1.624,-0.397 -2.515,0 -3.626,0C-4.117,0 -4.588,-0.075 -5.04,-0.226L-5.04,-2.305L-4.999,-2.375C-4.509,-2.048 -4.052,-1.884 -3.626,-1.884C-3.026,-1.884 -2.612,-2.112 -2.382,-2.565C-2.141,-3.019 -2.02,-3.817 -2.02,-4.95L-2.02,-11.938L0,-11.938"
style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(0,12.0607,12.0607,0,87.6884,17.5193)">
<path d="M1.249,-1.246C0.559,-1.246 -0.002,-0.687 -0.002,0.002C-0.002,0.692 0.559,1.249 1.249,1.249C1.936,1.249 2.497,0.692 2.497,0.002C2.497,-0.687 1.936,-1.246 1.249,-1.246" style="fill-rule:nonzero;"/>
<path d="M1.249,-1.246C0.559,-1.246 -0.002,-0.687 -0.002,0.002C-0.002,0.692 0.559,1.249 1.249,1.249C1.936,1.249 2.497,0.692 2.497,0.002C2.497,-0.687 1.936,-1.246 1.249,-1.246"
style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(12.0607,0,0,12.0607,32.4324,105.748)">
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018" style="fill:rgb(247,147,29);fill-rule:nonzero;"/>
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018"
style="fill:rgb(247,147,29);fill-rule:nonzero;"/>
</g>
<g transform="matrix(12.0607,0,0,12.0607,109.437,86.7078)">
<path d="M0,7.464C0,4.526 2.384,2.142 5.325,2.142C6.633,2.142 7.832,2.61 8.758,3.395L8.758,-5.324L10.746,-5.324L10.746,12.598L8.758,12.598L8.758,11.534C7.832,12.318 6.633,12.788 5.325,12.788C2.384,12.788 0,10.404 0,7.464M2.113,7.401C2.113,9.234 3.598,10.719 5.432,10.719C7.263,10.719 8.747,9.234 8.747,7.401C8.747,5.569 7.263,4.083 5.432,4.083C3.598,4.083 2.113,5.569 2.113,7.401" style="fill-rule:nonzero;"/>
<path d="M0,7.464C0,4.526 2.384,2.142 5.325,2.142C6.633,2.142 7.832,2.61 8.758,3.395L8.758,-5.324L10.746,-5.324L10.746,12.598L8.758,12.598L8.758,11.534C7.832,12.318 6.633,12.788 5.325,12.788C2.384,12.788 0,10.404 0,7.464M2.113,7.401C2.113,9.234 3.598,10.719 5.432,10.719C7.263,10.719 8.747,9.234 8.747,7.401C8.747,5.569 7.263,4.083 5.432,4.083C3.598,4.083 2.113,5.569 2.113,7.401"
style="fill-rule:nonzero;"/>
</g>
</g>
<g id="Uhr" transform="matrix(0.0220493,0,0,0.0220493,267.879,222.51)">
<g transform="matrix(4.16667,0,0,4.16667,-6599.04,0)">
<path d="M1638.49,981.86C1405.19,981.86 1216.06,792.734 1216.06,559.433C1216.06,326.132 1405.19,137.006 1638.49,137.006C1871.79,137.006 2060.92,326.132 2060.92,559.433C2060.92,792.734 1871.79,981.86 1638.49,981.86ZM1638.49,70.646C1368.54,70.646 1149.7,289.481 1149.7,559.433C1149.7,829.385 1368.54,1048.22 1638.49,1048.22C1908.44,1048.22 2127.28,829.385 2127.28,559.433C2127.28,289.481 1908.44,70.646 1638.49,70.646Z" style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
<path d="M1638.49,981.86C1405.19,981.86 1216.06,792.734 1216.06,559.433C1216.06,326.132 1405.19,137.006 1638.49,137.006C1871.79,137.006 2060.92,326.132 2060.92,559.433C2060.92,792.734 1871.79,981.86 1638.49,981.86ZM1638.49,70.646C1368.54,70.646 1149.7,289.481 1149.7,559.433C1149.7,829.385 1368.54,1048.22 1638.49,1048.22C1908.44,1048.22 2127.28,829.385 2127.28,559.433C2127.28,289.481 1908.44,70.646 1638.49,70.646Z"
style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
</g>
<g transform="matrix(4.14211,-0.451661,0.451661,4.14211,-6813.16,744.211)">
<path d="M1677.2,559.433C1677.2,580.825 1659.85,598.166 1638.45,598.145C1617.12,598.124 1599.75,580.168 1599.78,558.84C1599.88,474.921 1621.18,308.102 1632.14,254.433C1633.54,247.532 1643.41,247.537 1644.81,254.44C1655.78,308.457 1677.2,476.326 1677.2,559.433Z" style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
<path d="M1677.2,559.433C1677.2,580.825 1659.85,598.166 1638.45,598.145C1617.12,598.124 1599.75,580.168 1599.78,558.84C1599.88,474.921 1621.18,308.102 1632.14,254.433C1633.54,247.532 1643.41,247.537 1644.81,254.44C1655.78,308.457 1677.2,476.326 1677.2,559.433Z"
style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
</g>
<g transform="matrix(-4.09852,-0.750473,-0.750473,4.09852,7333.98,1287.55)">
<path d="M1595.39,590.238C1532.86,586.449 1465.21,572.938 1428.29,565.401C1422.75,564.267 1422.76,556.353 1428.29,555.229C1463.02,548.172 1526.22,535.741 1585.66,531.077C1585.22,540.595 1584.97,549.88 1584.96,558.834C1584.95,570.561 1587.38,581.586 1595.39,590.238Z" style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
<path d="M1595.39,590.238C1532.86,586.449 1465.21,572.938 1428.29,565.401C1422.75,564.267 1422.76,556.353 1428.29,555.229C1463.02,548.172 1526.22,535.741 1585.66,531.077C1585.22,540.595 1584.97,549.88 1584.96,558.834C1584.95,570.561 1587.38,581.586 1595.39,590.238Z"
style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
</g>
<g transform="matrix(4.16667,0,0,4.16667,-6599.04,0)">
<path d="M1656.19,176.808C1656.19,186.583 1648.27,194.507 1638.49,194.507C1628.72,194.507 1620.79,186.583 1620.79,176.808C1620.79,167.033 1628.72,159.108 1638.49,159.108C1648.27,159.108 1656.19,167.033 1656.19,176.808Z" style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
<path d="M1656.19,176.808C1656.19,186.583 1648.27,194.507 1638.49,194.507C1628.72,194.507 1620.79,186.583 1620.79,176.808C1620.79,167.033 1628.72,159.108 1638.49,159.108C1648.27,159.108 1656.19,167.033 1656.19,176.808Z"
style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
</g>
<g transform="matrix(4.16667,0,0,4.16667,-6599.04,0)">
<path d="M1255.87,541.734C1265.64,541.734 1273.57,549.658 1273.57,559.433C1273.57,569.208 1265.64,577.132 1255.87,577.132C1246.09,577.132 1238.17,569.208 1238.17,559.433C1238.17,549.658 1246.09,541.734 1255.87,541.734Z" style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
<path d="M1255.87,541.734C1265.64,541.734 1273.57,549.658 1273.57,559.433C1273.57,569.208 1265.64,577.132 1255.87,577.132C1246.09,577.132 1238.17,569.208 1238.17,559.433C1238.17,549.658 1246.09,541.734 1255.87,541.734Z"
style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
</g>
<g transform="matrix(4.16667,0,0,4.16667,-6599.04,0)">
<path d="M1620.79,942.059C1620.79,932.283 1628.72,924.359 1638.49,924.359C1648.27,924.359 1656.19,932.283 1656.19,942.059C1656.19,951.834 1648.27,959.758 1638.49,959.758C1628.72,959.758 1620.79,951.834 1620.79,942.059Z" style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
<path d="M1620.79,942.059C1620.79,932.283 1628.72,924.359 1638.49,924.359C1648.27,924.359 1656.19,932.283 1656.19,942.059C1656.19,951.834 1648.27,959.758 1638.49,959.758C1628.72,959.758 1620.79,951.834 1620.79,942.059Z"
style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
</g>
<g transform="matrix(4.16667,0,0,4.16667,-6599.04,0)">
<path d="M2021.12,577.132C2011.34,577.132 2003.42,569.208 2003.42,559.433C2003.42,549.658 2011.34,541.734 2021.12,541.734C2030.89,541.734 2038.82,549.658 2038.82,559.433C2038.82,569.208 2030.89,577.132 2021.12,577.132Z" style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
<path d="M2021.12,577.132C2011.34,577.132 2003.42,569.208 2003.42,559.433C2003.42,549.658 2011.34,541.734 2021.12,541.734C2030.89,541.734 2038.82,549.658 2038.82,559.433C2038.82,569.208 2030.89,577.132 2021.12,577.132Z"
style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -1,21 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<svg width="100%" height="100%" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g>
<g transform="matrix(12.0607,0,0,12.0607,201.169,356)">
<path d="M0,-11.938L0,-4.95C0,-3.918 -0.071,-3.151 -0.214,-2.644C-0.359,-2.069 -0.604,-1.583 -0.95,-1.184C-1.624,-0.397 -2.515,0 -3.626,0C-4.117,0 -4.588,-0.075 -5.04,-0.226L-5.04,-2.305L-4.999,-2.375C-4.509,-2.048 -4.052,-1.884 -3.626,-1.884C-3.026,-1.884 -2.612,-2.112 -2.382,-2.565C-2.141,-3.019 -2.02,-3.817 -2.02,-4.95L-2.02,-11.938L0,-11.938" style="fill-rule:nonzero;"/>
<path d="M0,-11.938L0,-4.95C0,-3.918 -0.071,-3.151 -0.214,-2.644C-0.359,-2.069 -0.604,-1.583 -0.95,-1.184C-1.624,-0.397 -2.515,0 -3.626,0C-4.117,0 -4.588,-0.075 -5.04,-0.226L-5.04,-2.305L-4.999,-2.375C-4.509,-2.048 -4.052,-1.884 -3.626,-1.884C-3.026,-1.884 -2.612,-2.112 -2.382,-2.565C-2.141,-3.019 -2.02,-3.817 -2.02,-4.95L-2.02,-11.938L0,-11.938"
style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(0,12.0607,12.0607,0,187.688,117.519)">
<path d="M1.249,-1.246C0.559,-1.246 -0.002,-0.687 -0.002,0.002C-0.002,0.692 0.559,1.249 1.249,1.249C1.936,1.249 2.497,0.692 2.497,0.002C2.497,-0.687 1.936,-1.246 1.249,-1.246" style="fill-rule:nonzero;"/>
<path d="M1.249,-1.246C0.559,-1.246 -0.002,-0.687 -0.002,0.002C-0.002,0.692 0.559,1.249 1.249,1.249C1.936,1.249 2.497,0.692 2.497,0.002C2.497,-0.687 1.936,-1.246 1.249,-1.246"
style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(12.0607,0,0,12.0607,132.432,205.748)">
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018" style="fill:rgb(247,147,29);fill-rule:nonzero;"/>
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018"
style="fill:rgb(247,147,29);fill-rule:nonzero;"/>
</g>
<g transform="matrix(12.0607,0,0,12.0607,132.432,205.748)">
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018" style="fill:white;fill-rule:nonzero;"/>
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018"
style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(12.0607,0,0,12.0607,209.437,186.708)">
<path d="M0,7.464C0,4.526 2.384,2.142 5.325,2.142C6.633,2.142 7.832,2.61 8.758,3.395L8.758,-5.324L10.746,-5.324L10.746,12.598L8.758,12.598L8.758,11.534C7.832,12.318 6.633,12.788 5.325,12.788C2.384,12.788 0,10.404 0,7.464M2.113,7.401C2.113,9.234 3.598,10.719 5.432,10.719C7.263,10.719 8.747,9.234 8.747,7.401C8.747,5.569 7.263,4.083 5.432,4.083C3.598,4.083 2.113,5.569 2.113,7.401" style="fill-rule:nonzero;"/>
<path d="M0,7.464C0,4.526 2.384,2.142 5.325,2.142C6.633,2.142 7.832,2.61 8.758,3.395L8.758,-5.324L10.746,-5.324L10.746,12.598L8.758,12.598L8.758,11.534C7.832,12.318 6.633,12.788 5.325,12.788C2.384,12.788 0,10.404 0,7.464M2.113,7.401C2.113,9.234 3.598,10.719 5.432,10.719C7.263,10.719 8.747,9.234 8.747,7.401C8.747,5.569 7.263,4.083 5.432,4.083C3.598,4.083 2.113,5.569 2.113,7.401"
style="fill-rule:nonzero;"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,21 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<svg width="100%" height="100%" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g>
<g transform="matrix(12.0607,0,0,12.0607,201.169,356)">
<path d="M0,-11.938L0,-4.95C0,-3.918 -0.071,-3.151 -0.214,-2.644C-0.359,-2.069 -0.604,-1.583 -0.95,-1.184C-1.624,-0.397 -2.515,0 -3.626,0C-4.117,0 -4.588,-0.075 -5.04,-0.226L-5.04,-2.305L-4.999,-2.375C-4.509,-2.048 -4.052,-1.884 -3.626,-1.884C-3.026,-1.884 -2.612,-2.112 -2.382,-2.565C-2.141,-3.019 -2.02,-3.817 -2.02,-4.95L-2.02,-11.938L0,-11.938" style="fill-rule:nonzero;"/>
<path d="M0,-11.938L0,-4.95C0,-3.918 -0.071,-3.151 -0.214,-2.644C-0.359,-2.069 -0.604,-1.583 -0.95,-1.184C-1.624,-0.397 -2.515,0 -3.626,0C-4.117,0 -4.588,-0.075 -5.04,-0.226L-5.04,-2.305L-4.999,-2.375C-4.509,-2.048 -4.052,-1.884 -3.626,-1.884C-3.026,-1.884 -2.612,-2.112 -2.382,-2.565C-2.141,-3.019 -2.02,-3.817 -2.02,-4.95L-2.02,-11.938L0,-11.938"
style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(0,12.0607,12.0607,0,187.688,117.519)">
<path d="M1.249,-1.246C0.559,-1.246 -0.002,-0.687 -0.002,0.002C-0.002,0.692 0.559,1.249 1.249,1.249C1.936,1.249 2.497,0.692 2.497,0.002C2.497,-0.687 1.936,-1.246 1.249,-1.246" style="fill-rule:nonzero;"/>
<path d="M1.249,-1.246C0.559,-1.246 -0.002,-0.687 -0.002,0.002C-0.002,0.692 0.559,1.249 1.249,1.249C1.936,1.249 2.497,0.692 2.497,0.002C2.497,-0.687 1.936,-1.246 1.249,-1.246"
style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(12.0607,0,0,12.0607,132.432,205.748)">
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018" style="fill:rgb(247,147,29);fill-rule:nonzero;"/>
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018"
style="fill:rgb(247,147,29);fill-rule:nonzero;"/>
</g>
<g transform="matrix(12.0607,0,0,12.0607,132.432,205.748)">
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018" style="fill:white;fill-rule:nonzero;"/>
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018"
style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(12.0607,0,0,12.0607,209.437,186.708)">
<path d="M0,7.464C0,4.526 2.384,2.142 5.325,2.142C6.633,2.142 7.832,2.61 8.758,3.395L8.758,-5.324L10.746,-5.324L10.746,12.598L8.758,12.598L8.758,11.534C7.832,12.318 6.633,12.788 5.325,12.788C2.384,12.788 0,10.404 0,7.464M2.113,7.401C2.113,9.234 3.598,10.719 5.432,10.719C7.263,10.719 8.747,9.234 8.747,7.401C8.747,5.569 7.263,4.083 5.432,4.083C3.598,4.083 2.113,5.569 2.113,7.401" style="fill-rule:nonzero;"/>
<path d="M0,7.464C0,4.526 2.384,2.142 5.325,2.142C6.633,2.142 7.832,2.61 8.758,3.395L8.758,-5.324L10.746,-5.324L10.746,12.598L8.758,12.598L8.758,11.534C7.832,12.318 6.633,12.788 5.325,12.788C2.384,12.788 0,10.404 0,7.464M2.113,7.401C2.113,9.234 3.598,10.719 5.432,10.719C7.263,10.719 8.747,9.234 8.747,7.401C8.747,5.569 7.263,4.083 5.432,4.083C3.598,4.083 2.113,5.569 2.113,7.401"
style="fill-rule:nonzero;"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<?xaml-comp compile="true" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
@@ -30,18 +31,18 @@
<Color x:Key="Gray900">#212121</Color>
<Color x:Key="Gray950">#141414</Color>
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}"/>
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}"/>
<SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}"/>
<SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}"/>
<SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}"/>
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}" />
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}" />
<SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}" />
<SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}" />
<SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}" />
<SolidColorBrush x:Key="Gray100Brush" Color="{StaticResource Gray100}"/>
<SolidColorBrush x:Key="Gray200Brush" Color="{StaticResource Gray200}"/>
<SolidColorBrush x:Key="Gray300Brush" Color="{StaticResource Gray300}"/>
<SolidColorBrush x:Key="Gray400Brush" Color="{StaticResource Gray400}"/>
<SolidColorBrush x:Key="Gray500Brush" Color="{StaticResource Gray500}"/>
<SolidColorBrush x:Key="Gray600Brush" Color="{StaticResource Gray600}"/>
<SolidColorBrush x:Key="Gray900Brush" Color="{StaticResource Gray900}"/>
<SolidColorBrush x:Key="Gray950Brush" Color="{StaticResource Gray950}"/>
<SolidColorBrush x:Key="Gray100Brush" Color="{StaticResource Gray100}" />
<SolidColorBrush x:Key="Gray200Brush" Color="{StaticResource Gray200}" />
<SolidColorBrush x:Key="Gray300Brush" Color="{StaticResource Gray300}" />
<SolidColorBrush x:Key="Gray400Brush" Color="{StaticResource Gray400}" />
<SolidColorBrush x:Key="Gray500Brush" Color="{StaticResource Gray500}" />
<SolidColorBrush x:Key="Gray600Brush" Color="{StaticResource Gray600}" />
<SolidColorBrush x:Key="Gray900Brush" Color="{StaticResource Gray900}" />
<SolidColorBrush x:Key="Gray950Brush" Color="{StaticResource Gray950}" />
</ResourceDictionary>

View File

@@ -1,12 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<?xaml-comp compile="true" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<Style TargetType="ToolbarItem">
<Style.Triggers>
<Trigger TargetType="ToolbarItem" Property="VisualElement.BackgroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}">
<Trigger TargetType="ToolbarItem" Property="VisualElement.BackgroundColor"
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}">
</Trigger>
</Style.Triggers>
</Style>
@@ -16,39 +18,48 @@
</Style>
<Style TargetType="IndicatorView">
<Setter Property="IndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}"/>
<Setter Property="SelectedIndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray100}}"/>
<Setter Property="IndicatorColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="SelectedIndicatorColor"
Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray100}}" />
</Style>
<Style TargetType="Border">
<Setter Property="Stroke" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="StrokeShape" Value="Rectangle"/>
<Setter Property="StrokeThickness" Value="1"/>
<Setter Property="Stroke"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="StrokeShape" Value="Rectangle" />
<Setter Property="StrokeThickness" Value="1" />
</Style>
<Style TargetType="BoxView">
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
<Setter Property="Color"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
</Style>
<Style TargetType="Button">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource PrimaryDarkText}}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="BorderWidth" Value="0"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="14,10"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource PrimaryDarkText}}" />
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="BorderWidth" Value="0" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="14,10" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver" />
@@ -59,15 +70,16 @@
<Style TargetType="CheckBox">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="Color"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -76,19 +88,21 @@
</Style>
<Style TargetType="DatePicker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -97,20 +111,23 @@
</Style>
<Style TargetType="Editor">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="PlaceholderColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -119,20 +136,23 @@
</Style>
<Style TargetType="Entry">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="PlaceholderColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -142,18 +162,20 @@
<Style TargetType="Frame">
<Setter Property="HasShadow" Value="False" />
<Setter Property="BorderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="BorderColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
</Style>
<Style TargetType="ImageButton">
<Setter Property="Opacity" Value="1" />
<Setter Property="BorderColor" Value="Transparent"/>
<Setter Property="BorderWidth" Value="0"/>
<Setter Property="CornerRadius" Value="0"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="BorderColor" Value="Transparent" />
<Setter Property="BorderWidth" Value="0" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
@@ -170,7 +192,8 @@
</Style>
<Style TargetType="Label">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
@@ -180,7 +203,8 @@
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -189,44 +213,53 @@
</Style>
<Style TargetType="Span">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="Label" x:Key="Headline">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
<Setter Property="FontSize" Value="32" />
<Setter Property="HorizontalOptions" Value="Center" />
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>
<Style TargetType="Label" x:Key="SubHeadline">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
<Setter Property="FontSize" Value="24" />
<Setter Property="HorizontalOptions" Value="Center" />
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>
<Style TargetType="ListView">
<Setter Property="SeparatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="RefreshControlColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
<Setter Property="SeparatorColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="RefreshControlColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="Picker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="TitleColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TitleColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -235,14 +268,16 @@
</Style>
<Style TargetType="ProgressBar">
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="ProgressColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="ProgressColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -251,19 +286,21 @@
</Style>
<Style TargetType="RadioButton">
<Setter Property="BackgroundColor" Value="Transparent"/>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -272,26 +309,30 @@
</Style>
<Style TargetType="RefreshView">
<Setter Property="RefreshColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
<Setter Property="RefreshColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="SearchBar">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
<Setter Property="CancelButtonColor" Value="{StaticResource Gray500}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -300,7 +341,8 @@
</Style>
<Style TargetType="SearchHandler">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
@@ -311,8 +353,10 @@
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -328,18 +372,24 @@
</Style>
<Style TargetType="Slider">
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MinimumTrackColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MaximumTrackColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
<Setter Property="MinimumTrackColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="MaximumTrackColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -348,11 +398,13 @@
</Style>
<Style TargetType="SwipeItem">
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
</Style>
<Style TargetType="Switch">
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="OnColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="ThumbColor" Value="{StaticResource White}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
@@ -360,19 +412,24 @@
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="OnColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="On">
<VisualState.Setters>
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Secondary}, Dark={StaticResource Gray200}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="OnColor"
Value="{AppThemeBinding Light={StaticResource Secondary}, Dark={StaticResource Gray200}}" />
<Setter Property="ThumbColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Off">
<VisualState.Setters>
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
<Setter Property="ThumbColor"
Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -381,19 +438,21 @@
</Style>
<Style TargetType="TimePicker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent"/>
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
@@ -402,34 +461,51 @@
</Style>
<Style TargetType="Page" ApplyToDerivedTypes="True">
<Setter Property="Padding" Value="0"/>
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
<Setter Property="Padding" Value="0" />
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
</Style>
<Style TargetType="Shell" ApplyToDerivedTypes="True">
<Setter Property="Shell.BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
<Setter Property="Shell.ForegroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
<Setter Property="Shell.TitleColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
<Setter Property="Shell.DisabledColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="Shell.UnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray200}}" />
<Setter Property="Shell.BackgroundColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
<Setter Property="Shell.ForegroundColor"
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
<Setter Property="Shell.TitleColor"
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
<Setter Property="Shell.DisabledColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="Shell.UnselectedColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray200}}" />
<Setter Property="Shell.NavBarHasShadow" Value="False" />
<Setter Property="Shell.TabBarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
<Setter Property="Shell.TabBarForegroundColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarTitleColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarUnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
<Setter Property="Shell.TabBarBackgroundColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
<Setter Property="Shell.TabBarForegroundColor"
Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarTitleColor"
Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarUnselectedColor"
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="NavigationPage">
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
<Setter Property="IconColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
<Setter Property="BarBackgroundColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
<Setter Property="BarTextColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
<Setter Property="IconColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="TabbedPage">
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Gray950}}" />
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="UnselectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="SelectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
<Setter Property="BarBackgroundColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Gray950}}" />
<Setter Property="BarTextColor"
Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="UnselectedTabColor"
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="SelectedTabColor"
Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
</Style>
</ResourceDictionary>

View File

@@ -1,38 +1,58 @@
using Jugenddienst_Stunden.Interfaces;
using Jugenddienst_Stunden.Types;
using Jugenddienst_Stunden.Validators;
namespace Jugenddienst_Stunden.Services;
internal class HoursService : IHoursService {
private readonly IHoursRepository _repo;
private readonly IHoursRepository _repo;
private readonly IHoursValidator _validator;
public HoursService(IHoursRepository repo) {
_repo = repo;
}
public HoursService(IHoursRepository repo, IHoursValidator validator) {
_repo = repo;
_validator = validator;
}
public async Task<(Hours hours, Settings settings)> GetMonthSummaryAsync(DateTime monthDate) {
string q = $"hours&year={monthDate:yyyy}&month={monthDate:MM}";
public async Task<(Hours hours, Settings settings)> GetMonthSummaryAsync(DateTime monthDate) {
string q = $"hours=&year={monthDate:yyyy}&month={monthDate:MM}";
var baseRes = await _repo.LoadBase(q);
return (baseRes.hour, baseRes.settings);
// Fallbacks, da einige Endpoints 'settings' nicht mitsenden bzw. 'hour' auslassen können
var settings = baseRes.settings ?? await _repo.LoadSettings();
var hours = baseRes.hour ?? new Hours {
daytime = new List<DayTime>(),
Nominal_day_api = new List<NominalDay>(),
Nominal_week_api = new List<NominalWeek>(),
zeit_total_daily_api = new List<TimeDay>(),
Projekte = new System.Collections.ObjectModel.Collection<Projekt>(),
Gemeinden = new System.Collections.ObjectModel.Collection<Gemeinde>(),
Freistellungen = new System.Collections.ObjectModel.Collection<Freistellung>()
};
return (hours, settings);
}
public async Task<(List<DayTime> dayTimes, Settings settings)> GetDayWithSettingsAsync(DateTime date) {
string q = $"date={date:yyyy-MM-dd}";
var baseRes = await _repo.LoadBase(q);
return (baseRes.daytimes ?? new List<DayTime>(), baseRes.settings);
}
public async Task<(List<DayTime> dayTimes, Settings settings)> GetDayWithSettingsAsync(DateTime date) {
string q = $"date={date:yyyy-MM-dd}";
var baseRes = await _repo.LoadBase(q);
return (baseRes.daytimes ?? new List<DayTime>(), baseRes.settings);
}
public async Task<List<DayTime>> GetDayRangeAsync(DateTime from, DateTime to) {
string q = $"date={from:yyyy-MM-dd}&tilldate={to:yyyy-MM-dd}";
var baseRes = await _repo.LoadBase(q);
return baseRes.daytimes ?? new List<DayTime>();
}
public async Task<List<DayTime>> GetDayRangeAsync(DateTime from, DateTime to) {
string q = $"date={from:yyyy-MM-dd}&tilldate={to:yyyy-MM-dd}";
var baseRes = await _repo.LoadBase(q);
return baseRes.daytimes ?? new List<DayTime>();
}
public async Task<Settings> GetSettingsAsync() => await _repo.LoadSettings();
public async Task<Settings> GetSettingsAsync() => await _repo.LoadSettings();
public async Task<DayTime> GetEntryAsync(int id) => await _repo.LoadEntry(id);
public async Task<DayTime> GetEntryAsync(int id) => await _repo.LoadEntry(id);
public async Task<DayTime> SaveEntryAsync(DayTime stunde) => await _repo.SaveEntry(stunde);
public async Task<DayTime> SaveEntryAsync(DayTime stunde) {
var settings = await _repo.LoadSettings();
_validator.Validate(stunde, settings);
return await _repo.SaveEntry(stunde);
}
public async Task DeleteEntryAsync(DayTime stunde) => await _repo.DeleteEntry(stunde);
public async Task DeleteEntryAsync(DayTime stunde) => await _repo.DeleteEntry(stunde);
}

View File

@@ -1,36 +1,37 @@
using Jugenddienst_Stunden.Models;
namespace Jugenddienst_Stunden.Types;
internal class BaseResponse {
public Settings settings { get; set; }
public Settings settings { get; set; }
/// <summary>
/// Monatsübersicht
/// </summary>
public Hours hour { get; set; }
/// <summary>
/// Monatsübersicht
/// </summary>
public Hours hour { get; set; }
/// <summary>
/// Stundenliste ... für die Katz?
/// </summary>
public List<Hours> hours { get; set; }
/// <summary>
/// Stundenliste ... für die Katz?
/// </summary>
public List<Hours> hours { get; set; }
/// <summary>
/// Liste der Stundeneinträge
/// </summary>
public List<DayTime> daytimes { get; set; }
/// <summary>
/// Liste der Stundeneinträge
/// </summary>
public List<DayTime> daytimes { get; set; }
/// <summary>
/// Einzelner Stundeneintrag
/// </summary>
public DayTime daytime { get; set; }
/// <summary>
/// Einzelner Stundeneintrag
/// </summary>
public DayTime daytime { get; set; }
/// <summary>
/// Auch irgendwie doppelt ...
/// </summary>
public Operator operatorVar { get; set; }
/// <summary>
/// Auch irgendwie doppelt ...
/// </summary>
public Operator operatorVar { get; set; }
public User user { get; set; }
public User user { get; set; }
public int error { get; set; }
public string message { get; set; }
public int error { get; set; }
public string message { get; set; }
}

View File

@@ -2,95 +2,95 @@
using System.Collections.ObjectModel;
namespace Jugenddienst_Stunden.Types;
/// <summary>
/// Represents a day time entry for an employee.
/// </summary>
public class DayTime {
/// <summary>
/// ID des Stundeneintrages
/// </summary>
public int? Id { get; set; }
/// <summary>
/// ID des Stundeneintrages
/// </summary>
public int? Id { get; set; }
/// <summary>
/// Mitarbeiter-ID
/// </summary>
public int EmployeeId { get; set; }
/// <summary>
/// Mitarbeiter-ID
/// </summary>
public int EmployeeId { get; set; }
/// <summary>
/// Der betreffende Tag
/// </summary>
public DateTime Day { get; set; }
/// <summary>
/// Der betreffende Tag
/// </summary>
public DateTime Day { get; set; }
/// <summary>
/// Der Wochentag
/// </summary>
public int Wday { get; set; }
/// <summary>
/// Der Wochentag
/// </summary>
public int Wday { get; set; }
/// <summary>
/// Arbeitsbeginn
/// </summary>
public TimeOnly Begin { get; set; }
/// <summary>
/// Arbeitsbeginn
/// </summary>
public TimeOnly Begin { get; set; }
/// <summary>
/// Arbeitsende
/// </summary>
public TimeOnly End { get; set; }
/// <summary>
/// Arbeitsende
/// </summary>
public TimeOnly End { get; set; }
/// <summary>
/// Beschreibung der Tätigkeit
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Beschreibung der Tätigkeit
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Freistellung
/// </summary>
public string? Free { get; set; }
/// <summary>
/// Freistellung
/// </summary>
public string? Free { get; set; }
/// <summary>
/// Freistellung genehmigt?
/// </summary>
public bool Approved { get; set; }
/// <summary>
/// Freistellung genehmigt?
/// </summary>
public bool Approved { get; set; }
/// <summary>
/// Das gewählte Projekt
/// </summary>
public int? Projekt { get; set; }
/// <summary>
/// Das gewählte Projekt
/// </summary>
public int? Projekt { get; set; }
/// <summary>
/// Die gewählte Gemeinde
/// </summary>
public int? Gemeinde { get; set; }
/// <summary>
/// Die gewählte Gemeinde
/// </summary>
public int? Gemeinde { get; set; }
/// <summary>
/// Nachtstunden
/// </summary>
public TimeOnly Night { get; set; }
/// <summary>
/// Nachtstunden
/// </summary>
public TimeOnly Night { get; set; }
/// <summary>
/// Summe Arbeitszeit (inklusive Nachstunden mit Faktor)
/// </summary>
public Dictionary<string, TimeOnly> Total { get; set; }
/// <summary>
/// Summe Arbeitszeit (inklusive Nachstunden mit Faktor)
/// </summary>
public Dictionary<string, TimeOnly> Total { get; set; }
public TimeOnly End_print { get; set; }
public TimeSpan TimeSpanVon { get; set; }
public TimeSpan TimeSpanBis { get; set; }
public TimeOnly End_print { get; set; }
public TimeSpan TimeSpanVon { get; set; }
public TimeSpan TimeSpanBis { get; set; }
/// <summary>
/// Gets the active Gemeinde based on the gemeinde ID.
/// </summary>
public Gemeinde? GemeindeAktiv { get; set; }
/// <summary>
/// Gets the active Gemeinde based on the gemeinde ID.
/// </summary>
public Gemeinde? GemeindeAktiv { get; set; }
/// <summary>
/// Gets the active Projekt based on the projekt ID.
/// </summary>
public Projekt? ProjektAktiv { get; set; }
/// <summary>
/// Gets the active Projekt based on the projekt ID.
/// </summary>
public Projekt? ProjektAktiv { get; set; }
/// <summary>
/// Gets the active Freistellung based on the Freistellung ID
/// </summary>
public Freistellung? FreistellungAktiv { get; set; }
public int TimeTable { get; set; }
/// <summary>
/// Gets the active Freistellung based on the Freistellung ID
/// </summary>
public Freistellung? FreistellungAktiv { get; set; }
public int TimeTable { get; set; }
}

View File

@@ -1,8 +1,9 @@
namespace Jugenddienst_Stunden.Types;
/// <summary>
/// Freistellungen: Urlaub, Zeitausgleich, Krankheit, ...
/// </summary>
public class Freistellung {
public string? Id { get; set; }
public string? Id { get; set; }
public string? Name { get; set; }
}

View File

@@ -1,43 +1,45 @@

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.ObjectModel;
namespace Jugenddienst_Stunden.Types;
internal partial class Hours : ObservableObject {
public partial class Hours : ObservableObject {
public double? Zeit;
public double? Nominal;
//public Dictionary<DateOnly,NominalDay> nominal_day_api;
public List<NominalDay>? Nominal_day_api;
//public Dictionary<int,NominalWeek> nominal_week_api;
public List<NominalWeek>? Nominal_week_api;
//public List<string> time_line;
public double? Zeit_total;
//https://stackoverflow.com/questions/29449641/deserialize-json-when-a-value-can-be-an-object-or-an-empty-array/29450279#29450279
//[JsonConverter(typeof(JsonSingleOrEmptyArrayConverter<Hours>))]
//public Dictionary<int,decimal> zeit_total_daily;
//public List<string> time_line;
public double? Zeit_total;
public List<TimeDay> zeit_total_daily_api;
//https://stackoverflow.com/questions/29449641/deserialize-json-when-a-value-can-be-an-object-or-an-empty-array/29450279#29450279
//[JsonConverter(typeof(JsonSingleOrEmptyArrayConverter<Hours>))]
//public Dictionary<int,decimal> zeit_total_daily;
public List<TimeDay> zeit_total_daily_api;
public List<DayTime>? daytime;
//public List<string> wochensumme;
//public List<string> wochensumme;
[ObservableProperty]
public double overtime_month;
[ObservableProperty] public double overtime_month;
[ObservableProperty]
public double overtime;
//public List<string> overtime_day;
[ObservableProperty] public double overtime;
//public List<string> overtime_day;
[ObservableProperty]
public double zeitausgleich;
[ObservableProperty] public double zeitausgleich;
public double zeitausgleich_month;
public double holiday;
public double krankheit;
public double weiterbildung;
public double bereitschaft;
public double bereitschaft_month;
//public Operator operator_api;
public DateTime Today;
public DateTime Date;
@@ -47,5 +49,4 @@ internal partial class Hours : ObservableObject {
public Collection<Gemeinde> Gemeinden { get; set; }
public Collection<Freistellung> Freistellungen { get; set; }
public int EmployeeId { get; set; }
}

View File

@@ -1,5 +1,6 @@
namespace Jugenddienst_Stunden.Types;
internal class NominalDay {
public class NominalDay {
public int day_number;
public int month_number;
public double hours;

View File

@@ -1,6 +1,6 @@
namespace Jugenddienst_Stunden.Types;
internal class NominalWeek {
public int Week_number;
public double Hours;
public class NominalWeek {
public int Week_number;
public double Hours;
}

View File

@@ -4,35 +4,35 @@
/// Einstellungen
/// </summary>
public class Settings {
/// <summary>
/// Sind Projekte aktiv?
/// </summary>
public bool ProjektAktivSet { get; set; }
/// <summary>
/// Sind Projekte aktiv?
/// </summary>
public bool ProjektAktivSet { get; set; }
/// <summary>
/// Sind Gemeinden aktiv?
/// </summary>
public bool GemeindeAktivSet { get; set; }
/// <summary>
/// Sind Gemeinden aktiv?
/// </summary>
public bool GemeindeAktivSet { get; set; }
/// <summary>
/// Liste der Projekte
/// </summary>
public List<Projekt>? Projekte { get; set; }
/// <summary>
/// Liste der Projekte
/// </summary>
public List<Projekt>? Projekte { get; set; }
/// <summary>
/// Liste der Gemeinden
/// </summary>
public List<Gemeinde>? Gemeinden { get; set; }
/// <summary>
/// Liste der Gemeinden
/// </summary>
public List<Gemeinde>? Gemeinden { get; set; }
/// <summary>
/// Liste der Freistellungen
/// </summary>
public List<Freistellung>? Freistellungen { get; set; }
/// <summary>
/// Liste der Freistellungen
/// </summary>
public List<Freistellung>? Freistellungen { get; set; }
public List<Sollstunden> Nominal { get; set; }
public List<Sollstunden>? Nominal { get; set; }
/// <summary>
/// Version der API
/// </summary>
public string Version { get; set; }
/// <summary>
/// Version der API
/// </summary>
public string? Version { get; set; }
}

View File

@@ -1,7 +1,7 @@
namespace Jugenddienst_Stunden.Types;
public class Sollstunden {
public int Timetable { get; set; }
public int Wochentag { get; set; }
public double Zeit { get; set; }
public int Timetable { get; set; }
public int Wochentag { get; set; }
public double Zeit { get; set; }
}

View File

@@ -3,7 +3,7 @@
/// <summary>
/// Summe der geleisteten Stunden.
/// </summary>
internal struct TimeDay {
public int Day { get; set; }
public double Hours { get; set; }
public struct TimeDay {
public int Day { get; set; }
public double Hours { get; set; }
}

View File

@@ -1,6 +1,6 @@
namespace Jugenddienst_Stunden.Types;
internal class Timetable {
public List<TimetableEntry> timetable;
public decimal wochensumme;
public List<TimetableEntry> timetable;
public decimal wochensumme;
}

View File

@@ -1,7 +1,7 @@
namespace Jugenddienst_Stunden.Types;
internal class TimetableEntry {
public List<TimeOnly>? Von;
public List<TimeOnly>? Bis;
public decimal Summe { get; set; }
public List<TimeOnly>? Von;
public List<TimeOnly>? Bis;
public decimal Summe { get; set; }
}

View File

@@ -1,4 +1,5 @@
namespace Jugenddienst_Stunden.Types;
internal class User {
public int Id { get; set; }
public string Name { get; set; }

View File

@@ -0,0 +1,31 @@
using Jugenddienst_Stunden.Infrastructure;
using Jugenddienst_Stunden.Types;
namespace Jugenddienst_Stunden.Validators;
internal interface IHoursValidator {
void Validate(DayTime item, Settings settings);
}
internal sealed class HoursValidator : IHoursValidator {
public void Validate(DayTime item, Settings settings) {
if (item.FreistellungAktiv is null && item.TimeSpanVon == item.TimeSpanBis)
throw new ValidationException("Beginn und Ende sind gleich");
if (item.TimeSpanBis < item.TimeSpanVon)
throw new ValidationException("Ende ist vor Beginn");
if (settings.GemeindeAktivSet && item.GemeindeAktiv is null)
throw new ValidationException("Gemeinde nicht gewählt");
if (settings.ProjektAktivSet && item.ProjektAktiv is null)
throw new ValidationException("Projekt nicht gewählt");
if (string.IsNullOrWhiteSpace(item.Description)) {
if (item.FreistellungAktiv?.Name is string name && !string.IsNullOrWhiteSpace(name))
item.Description = name;
else
throw new ValidationException("Keine Beschreibung");
}
}
}

View File

@@ -4,28 +4,30 @@
/// Die Loginseite
/// </summary>
public class LoginViewModel {
/// <summary>
/// Name der Anwendung
/// </summary>
public string AppTitle => AppInfo.Name;
/// <summary>
/// Name der Anwendung
/// </summary>
public string AppTitle => AppInfo.Name;
/// <summary>
/// Programmversion
/// </summary>
public string Version => AppInfo.VersionString;
/// <summary>
/// Programmversion
/// </summary>
public string Version => AppInfo.VersionString;
/// <summary>
/// Kurze Mitteilung für den Anwender
/// </summary>
public string Message { get; set; } = "Scanne den QR-Code von deinem Benutzerprofil auf der Stundenseite.";
/// <summary>
/// Kurze Mitteilung für den Anwender
/// </summary>
public string Message { get; set; } = "Scanne den QR-Code von deinem Benutzerprofil auf der Stundenseite.";
/// <summary>
/// Genutzer Server für die API
/// </summary>
public string Server { get; set; } = "Server: " + Preferences.Default.Get("apiUrl", "").Replace("/appapi", "").Replace("https://", "").Replace("http://", "");
/// <summary>
/// Genutzer Server für die API
/// </summary>
public string Server { get; set; } = "Server: " + Preferences.Default.Get("apiUrl", "").Replace("/appapi", "")
.Replace("https://", "").Replace("http://", "");
/// <summary>
/// Titel der Seite - im Moment der aktuelle Anwender
/// </summary>
public string Title { get; set; } = Preferences.Default.Get("name", "Nicht") + " " + Preferences.Default.Get("surname", "eingeloggt");
/// <summary>
/// Titel der Seite - im Moment der aktuelle Anwender
/// </summary>
public string Title { get; set; } = Preferences.Default.Get("name", "Nicht") + " " +
Preferences.Default.Get("surname", "eingeloggt");
}

View File

@@ -3,8 +3,10 @@ using CommunityToolkit.Mvvm.ComponentModel;
using System.Windows.Input;
namespace Jugenddienst_Stunden.ViewModels;
internal class NoteViewModel : ObservableObject, IQueryAttributable {
private Models.Note _note;
public string Text {
get => _note.Text;
set {

View File

@@ -47,7 +47,9 @@ internal class NotesViewModel : IQueryAttributable {
}
// If note isn't found, it's new; add it.
else { AllNotes.Insert(0, new NoteViewModel(Note.Load(noteId))); }
else {
AllNotes.Insert(0, new NoteViewModel(Note.Load(noteId)));
}
}
}
}

View File

@@ -8,243 +8,257 @@ using static System.Runtime.InteropServices.JavaScript.JSType;
using Jugenddienst_Stunden.Interfaces;
using Jugenddienst_Stunden.Repositories;
using Jugenddienst_Stunden.Services;
using Jugenddienst_Stunden.Infrastructure;
using Jugenddienst_Stunden.Validators;
using System.Net.Http;
namespace Jugenddienst_Stunden.ViewModels;
/// <summary>
/// Viewmodel für die einzelnen Stundeneinträge / Bearbeitung
/// </summary>
public partial class StundeViewModel : ObservableObject, IQueryAttributable {
private readonly IHoursService _hoursService;
private readonly IHoursService _hoursService;
public int Id { get; set; }
public string Title { get; set; } = "Eintrag bearbeiten";
public string SubTitle { get; set; } = DateTime.Today.ToString("dddd, d. MMMM yyyy");
public int Id { get; set; }
public string Title { get; set; } = "Eintrag bearbeiten";
public string SubTitle { get; set; } = DateTime.Today.ToString("dddd, d. MMMM yyyy");
//private HoursBase HoursBase = new HoursBase();
internal Settings Settings = new Settings();
//private HoursBase HoursBase = new HoursBase();
internal Settings Settings = new Settings();
public event EventHandler<string> AlertEvent;
public event EventHandler<string> InfoEvent;
public event Func<string, string, Task<bool>> ConfirmEvent;
//public event Func<string, string, string?, string?, Task<bool>> ConfirmEvent;
//public event EventHandler<ConfirmEventArgs> ConfirmEvent;
public event EventHandler<string> AlertEvent;
public event EventHandler<string> InfoEvent;
/// <summary>
/// Gemeinden für die Auswahlliste
/// </summary>
[ObservableProperty]
private List<Gemeinde> optionsGemeinde;
public event Func<string, string, Task<bool>> ConfirmEvent;
//public event Func<string, string, string?, string?, Task<bool>> ConfirmEvent;
//public event EventHandler<ConfirmEventArgs> ConfirmEvent;
/// <summary>
/// Projekte für die Auswahlliste
/// </summary>
[ObservableProperty]
private List<Projekt> optionsProjekt;
/// <summary>
/// Gemeinden für die Auswahlliste
/// </summary>
[ObservableProperty] private List<Gemeinde> optionsGemeinde;
/// <summary>
/// Freistellungen für die Auswahlliste
/// </summary>
[ObservableProperty]
private List<Freistellung> optionsFreistellung;
/// <summary>
/// Projekte für die Auswahlliste
/// </summary>
[ObservableProperty] private List<Projekt> optionsProjekt;
/// <summary>
/// Vorhandene Zeiten anzeigen, wenn neuer Eintrag erstellt wird
/// </summary>
[ObservableProperty]
private List<DayTime> dayTimes;
/// <summary>
/// Freistellungen für die Auswahlliste
/// </summary>
[ObservableProperty] private List<Freistellung> optionsFreistellung;
/// <summary>
/// Aktueller Stundeneintrag
/// </summary>
[ObservableProperty]
private DayTime dayTime;
/// <summary>
/// Vorhandene Zeiten anzeigen, wenn neuer Eintrag erstellt wird
/// </summary>
[ObservableProperty] private List<DayTime> dayTimes;
/// <summary>
/// Dürfen Gemeinden verwendet werden?
/// </summary>
[ObservableProperty]
private bool gemeindeAktivSet;
/// <summary>
/// Aktueller Stundeneintrag
/// </summary>
[ObservableProperty] private DayTime dayTime;
/// <summary>
/// Dürfen Projekte verwendet werden?
/// </summary>
[ObservableProperty]
private bool projektAktivSet;
/// <summary>
/// Dürfen Gemeinden verwendet werden?
/// </summary>
[ObservableProperty] private bool gemeindeAktivSet;
[ObservableProperty]
private bool freistellungEnabled;
/// <summary>
/// Dürfen Projekte verwendet werden?
/// </summary>
[ObservableProperty] private bool projektAktivSet;
public ICommand SaveCommand { get; private set; }
public ICommand DeleteCommand { get; private set; }
public ICommand DeleteConfirmCommand { get; private set; }
//public ICommand LoadDataCommand { get; private set; }
[ObservableProperty] private bool freistellungEnabled;
public ICommand SaveCommand { get; private set; }
public ICommand DeleteCommand { get; private set; }
public ICommand DeleteConfirmCommand { get; private set; }
//public ICommand LoadDataCommand { get; private set; }
public StundeViewModel() : this(GetServiceOrCreate()) { }
public StundeViewModel() : this(GetServiceOrCreate()) {
}
private static IHoursService GetServiceOrCreate() => new HoursService(new HoursRepository());
private static IHoursService GetServiceOrCreate() {
// Fallback-Konstruktion, falls DI nicht injiziert wurde (z. B. im Designer)
var http = new HttpClient();
var options = new Infrastructure.ApiOptions { BaseUrl = GlobalVar.ApiUrl, Timeout = TimeSpan.FromSeconds(15) };
var tokenProvider = new GlobalVarTokenProvider();
var api = new ApiClient(http, options, tokenProvider, new PreferencesAppSettings());
var repo = new HoursRepository(api);
var validator = new HoursValidator();
return new HoursService(repo, validator);
}
internal StundeViewModel(IHoursService hoursService) {
_hoursService = hoursService;
SaveCommand = new AsyncRelayCommand(Save);
//DeleteCommand = new AsyncRelayCommand(Delete);
DeleteConfirmCommand = new Command(async () => await DeleteConfirm());
}
internal StundeViewModel(IHoursService hoursService) {
_hoursService = hoursService;
SaveCommand = new AsyncRelayCommand(Save);
//DeleteCommand = new AsyncRelayCommand(Delete);
DeleteConfirmCommand = new Command(async () => await DeleteConfirm());
}
private async void LoadSettingsAsync() {
try {
Settings = await _hoursService.GetSettingsAsync();
GlobalVar.Settings = Settings;
// DI-Konstruktor, der den globalen Alert-Service abonniert und Alerts an das ViewModel weiterreicht.
internal StundeViewModel(IHoursService hoursService, IAlertService alertService) : this(hoursService) {
if (alertService is not null) {
alertService.AlertRaised += (s, msg) => AlertEvent?.Invoke(this, msg);
}
}
OptionsGemeinde = Settings.Gemeinden;
OptionsProjekt = Settings.Projekte;
OptionsFreistellung = Settings.Freistellungen;
private async void LoadSettingsAsync() {
try {
Settings = await _hoursService.GetSettingsAsync();
GlobalVar.Settings = Settings;
GemeindeAktivSet = Settings.GemeindeAktivSet;
ProjektAktivSet = Settings.ProjektAktivSet;
OptionsGemeinde = Settings.Gemeinden;
OptionsProjekt = Settings.Projekte;
OptionsFreistellung = Settings.Freistellungen;
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
}
}
GemeindeAktivSet = Settings.GemeindeAktivSet;
ProjektAktivSet = Settings.ProjektAktivSet;
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
}
}
async Task Save() {
bool exceptionOccurred = false;
bool proceed = true;
if (DayTime.TimeSpanVon == DayTime.TimeSpanBis && DayTime.FreistellungAktiv.Name == null) {
proceed = false;
AlertEvent?.Invoke(this, "Uhrzeiten sollten unterschiedlich sein");
}
async Task Save() {
bool exceptionOccurred = false;
bool proceed = true;
if (DayTime.TimeSpanVon == DayTime.TimeSpanBis && DayTime.FreistellungAktiv.Name == null) {
proceed = false;
AlertEvent?.Invoke(this, "Uhrzeiten sollten unterschiedlich sein");
}
if (proceed) {
try {
await _hoursService.SaveEntryAsync(DayTime);
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
exceptionOccurred = true;
}
if (!exceptionOccurred) {
if (DayTime.Id != null) {
await Shell.Current.GoToAsync($"..?saved={DayTime.Id}");
} else {
await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}");
}
}
}
}
if (proceed) {
try {
await _hoursService.SaveEntryAsync(DayTime);
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
exceptionOccurred = true;
}
/// <summary>
/// Löschen ohne Bestätigung
/// </summary>
private async Task Delete() {
await _hoursService.DeleteEntryAsync(DayTime);
await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}");
}
if (!exceptionOccurred) {
if (DayTime.Id != null) {
await Shell.Current.GoToAsync($"..?saved={DayTime.Id}");
} else {
await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}");
}
}
}
}
/// <summary>
/// Löschen mit Bestätigung
/// </summary>
private async Task DeleteConfirm() {
if (ConfirmEvent != null) {
bool answer = await ConfirmEvent.Invoke("Achtung", "Löschen kann nicht ungeschehen gemacht werden. Fortfahren?");
if (answer) {
//Löschen
await _hoursService.DeleteEntryAsync(DayTime);
await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}");
} else {
//nicht Löschen
}
}
}
/// <summary>
/// Löschen ohne Bestätigung
/// </summary>
private async Task Delete() {
await _hoursService.DeleteEntryAsync(DayTime);
await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}");
}
/// <summary>
/// Anwenden der Query-Parameter
/// </summary>
async void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) {
//load beinhaltet die ID: Eintrag bearbeiten
//date beinhaltet einen Tag: Neuen Eintrag erstellen
if (query.ContainsKey("load")) {
/// <summary>
/// Löschen mit Bestätigung
/// </summary>
private async Task DeleteConfirm() {
if (ConfirmEvent != null) {
bool answer =
await ConfirmEvent.Invoke("Achtung", "Löschen kann nicht ungeschehen gemacht werden. Fortfahren?");
if (answer) {
//Löschen
await _hoursService.DeleteEntryAsync(DayTime);
await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}");
} else {
//nicht Löschen
}
}
}
//DateTime heute = DateTime.Now;
try {
var entry = await _hoursService.GetEntryAsync(Convert.ToInt32(query["load"]));
var settings = await _hoursService.GetSettingsAsync();
GlobalVar.Settings = settings;
GemeindeAktivSet = settings.GemeindeAktivSet;
ProjektAktivSet = settings.ProjektAktivSet;
/// <summary>
/// Anwenden der Query-Parameter
/// </summary>
async void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) {
//load beinhaltet die ID: Eintrag bearbeiten
//date beinhaltet einen Tag: Neuen Eintrag erstellen
if (query.ContainsKey("load")) {
//DateTime heute = DateTime.Now;
try {
var entry = await _hoursService.GetEntryAsync(Convert.ToInt32(query["load"]));
var settings = await _hoursService.GetSettingsAsync();
GlobalVar.Settings = settings;
GemeindeAktivSet = settings.GemeindeAktivSet;
ProjektAktivSet = settings.ProjektAktivSet;
DayTime = entry;
DayTime.TimeSpanVon = entry.Begin.ToTimeSpan();
DayTime.TimeSpanBis = entry.End.ToTimeSpan();
DayTime = entry;
DayTime.TimeSpanVon = entry.Begin.ToTimeSpan();
DayTime.TimeSpanBis = entry.End.ToTimeSpan();
OptionsGemeinde = settings.Gemeinden ?? new List<Gemeinde>();
OptionsProjekt = settings.Projekte ?? new List<Projekt>();
OptionsFreistellung = settings.Freistellungen ?? new List<Freistellung>();
OptionsGemeinde = settings.Gemeinden ?? new List<Gemeinde>();
OptionsProjekt = settings.Projekte ?? new List<Projekt>();
OptionsFreistellung = settings.Freistellungen ?? new List<Freistellung>();
DayTime.GemeindeAktiv = OptionsGemeinde.FirstOrDefault(Gemeinde => Gemeinde.Id == DayTime.Gemeinde) ?? new Gemeinde();
DayTime.ProjektAktiv = OptionsProjekt.FirstOrDefault(Projekt => Projekt.Id == DayTime.Projekt) ?? new Projekt();
DayTime.FreistellungAktiv = OptionsFreistellung.FirstOrDefault(Freistellung => Freistellung.Id == DayTime.Free) ?? new Freistellung();
DayTime.GemeindeAktiv = OptionsGemeinde.FirstOrDefault(Gemeinde => Gemeinde.Id == DayTime.Gemeinde) ??
new Gemeinde();
DayTime.ProjektAktiv = OptionsProjekt.FirstOrDefault(Projekt => Projekt.Id == DayTime.Projekt) ??
new Projekt();
DayTime.FreistellungAktiv =
OptionsFreistellung.FirstOrDefault(Freistellung => Freistellung.Id == DayTime.Free) ??
new Freistellung();
//Evtl. noch die anderen Zeiten des gleichen Tages holen
var day = await _hoursService.GetDayWithSettingsAsync(DayTime.Day);
DayTimes = day.dayTimes;
OnPropertyChanged(nameof(DayTime));
OnPropertyChanged(nameof(DayTimes));
//Evtl. noch die anderen Zeiten des gleichen Tages holen
var day = await _hoursService.GetDayWithSettingsAsync(DayTime.Day);
DayTimes = day.dayTimes;
OnPropertyChanged(nameof(DayTime));
OnPropertyChanged(nameof(DayTimes));
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
} finally {
}
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
} finally {
if (System.String.IsNullOrEmpty(DayTime.Description)) {
InfoEvent?.Invoke(this, "Eintrag hat keinen Beschreibungstext");
}
}
SubTitle = DayTime.Day.ToString("dddd, d. MMMM yyyy");
OnPropertyChanged(nameof(SubTitle));
if (System.String.IsNullOrEmpty(DayTime.Description)) {
InfoEvent?.Invoke(this, "Eintrag hat keinen Beschreibungstext");
}
SubTitle = DayTime.Day.ToString("dddd, d. MMMM yyyy");
OnPropertyChanged(nameof(SubTitle));
FreistellungEnabled = !DayTime.Approved;
//OnPropertyChanged(nameof(DayTime));
} else if (query.ContainsKey("date")) {
Title = "Neuer Eintrag";
OnPropertyChanged(nameof(Title));
FreistellungEnabled = !DayTime.Approved;
//OnPropertyChanged(nameof(DayTime));
DateTime _date = DateTime.ParseExact((string)query["date"], "yyyy-MM-dd",
System.Globalization.CultureInfo.InvariantCulture);
//Bei neuem Eintrag die vorhandenen des gleichen Tages anzeigen
try {
var (list, settings) = await _hoursService.GetDayWithSettingsAsync(_date);
GlobalVar.Settings = settings;
DayTimes = list;
} else if (query.ContainsKey("date")) {
Title = "Neuer Eintrag";
OnPropertyChanged(nameof(Title));
OptionsGemeinde = settings.Gemeinden;
OptionsProjekt = settings.Projekte;
OptionsFreistellung = settings.Freistellungen;
DateTime _date = DateTime.ParseExact((string)query["date"], "yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture);
//Bei neuem Eintrag die vorhandenen des gleichen Tages anzeigen
try {
var (list, settings) = await _hoursService.GetDayWithSettingsAsync(_date);
GlobalVar.Settings = settings;
DayTimes = list;
GemeindeAktivSet = settings.GemeindeAktivSet;
ProjektAktivSet = settings.ProjektAktivSet;
} catch (Exception) {
//Ein Tag ohne Einträge gibt eine Fehlermeldung,
//die soll aber ignoriert werden, weil beim Neueintrag ist das ja Wurscht
//In dem Fall müssen die Settings aber nochmal geholt werden, weil die dann nicht geladen wurden
LoadSettingsAsync();
} finally {
DayTime = new DayTime();
DayTime.Day = _date;
DayTime.EmployeeId = GlobalVar.EmployeeId;
DayTime.GemeindeAktiv = new Gemeinde();
DayTime.ProjektAktiv = new Projekt();
DayTime.FreistellungAktiv = new Freistellung();
OptionsGemeinde = settings.Gemeinden;
OptionsProjekt = settings.Projekte;
OptionsFreistellung = settings.Freistellungen;
GemeindeAktivSet = settings.GemeindeAktivSet;
ProjektAktivSet = settings.ProjektAktivSet;
} catch (Exception) {
//Ein Tag ohne Einträge gibt eine Fehlermeldung,
//die soll aber ignoriert werden, weil beim Neueintrag ist das ja Wurscht
//In dem Fall müssen die Settings aber nochmal geholt werden, weil die dann nicht geladen wurden
LoadSettingsAsync();
} finally {
DayTime = new DayTime();
DayTime.Day = _date;
DayTime.EmployeeId = GlobalVar.EmployeeId;
DayTime.GemeindeAktiv = new Gemeinde();
DayTime.ProjektAktiv = new Projekt();
DayTime.FreistellungAktiv = new Freistellung();
SubTitle = _date.ToString("dddd, d. MMMM yyyy");
FreistellungEnabled = true;
OnPropertyChanged(nameof(SubTitle));
//OnPropertyChanged(nameof(DayTime));
}
}
}
SubTitle = _date.ToString("dddd, d. MMMM yyyy");
FreistellungEnabled = true;
OnPropertyChanged(nameof(SubTitle));
//OnPropertyChanged(nameof(DayTime));
}
}
}
}

View File

@@ -5,325 +5,366 @@ using Jugenddienst_Stunden.Types;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using CommunityToolkit.Maui.Alerts;
using CommunityToolkit.Maui.Core;
using static System.Runtime.InteropServices.JavaScript.JSType;
using Jugenddienst_Stunden.Interfaces;
using Jugenddienst_Stunden.Repositories;
using Jugenddienst_Stunden.Services;
namespace Jugenddienst_Stunden.ViewModels;
/// <summary>
/// ViewModel für die Stundenliste
/// </summary>
internal partial class StundenViewModel : ObservableObject, IQueryAttributable, INotifyPropertyChanged {
private readonly IHoursService _hoursService;
public partial class StundenViewModel : ObservableObject, IQueryAttributable, INotifyPropertyChanged {
private readonly IHoursService _hoursService;
public ICommand NewEntryCommand { get; }
public ICommand SelectEntryCommand { get; }
public ICommand LoadDataCommand { get; private set; }
public ICommand LoadDayCommand { get; private set; }
public ICommand RefreshListCommand { get; }
public ICommand RefreshCommand { get; }
public ICommand NewEntryCommand { get; }
public ICommand SelectEntryCommand { get; }
public ICommand LoadDataCommand { get; private set; }
public ICommand LoadDayCommand { get; private set; }
public ICommand RefreshListCommand { get; }
public ICommand RefreshCommand { get; }
public event EventHandler<string> AlertEvent;
public event EventHandler<string> InfoEvent;
public event EventHandler<string> AlertEvent;
public event EventHandler<string> InfoEvent;
/// <summary>
/// Beschriftung Button Monatsübersicht
/// </summary>
[ObservableProperty]
private string loadOverview;
/// <summary>
/// Beschriftung Button Monatsübersicht
/// </summary>
[ObservableProperty] private string loadOverview;
//private HoursBase HoursBase = new HoursBase();
internal Settings Settings = new Settings();
//private HoursBase HoursBase = new HoursBase();
internal Settings Settings = new Settings();
/// <summary>
/// Zu leistende Stunden
/// </summary>
[ObservableProperty]
private TimeOnly sollstunden;
/// <summary>
/// Zu leistende Stunden
/// </summary>
[ObservableProperty] private TimeOnly sollstunden;
/// <summary>
/// Geleistete Stunden an einem Tag
/// </summary>
[ObservableProperty]
private TimeOnly dayTotal;
/// <summary>
/// Geleistete Stunden an einem Tag
/// </summary>
[ObservableProperty] private TimeOnly dayTotal;
/// <summary>
/// Liste der Tageszeiten
/// </summary>
[ObservableProperty]
private List<DayTime> dayTimes = new List<DayTime>();
/// <summary>
/// Liste der Tageszeiten
/// </summary>
[ObservableProperty] private List<DayTime> dayTimes = new List<DayTime>();
public string Title { get; set; } = GlobalVar.Name + " " + GlobalVar.Surname;
public string Title { get; set; } = GlobalVar.Name + " " + GlobalVar.Surname;
[ObservableProperty]
private Hours hours;
[ObservableProperty] private Hours hours;
/// <summary>
/// Mindest-Datum für den Datepicker
/// </summary>
public DateTime MinimumDate {
get => DateTime.Today.AddDays(-365);
}
/// <summary>
/// Mindest-Datum für den Datepicker
/// </summary>
public DateTime MinimumDate {
get => DateTime.Today.AddDays(-365);
}
/// <summary>
/// Höchst-Datum für den Datepicker
/// </summary>
public DateTime MaximumDate {
get => DateTime.Today.AddDays(60);
}
/// <summary>
/// Höchst-Datum für den Datepicker
/// </summary>
public DateTime MaximumDate {
get => DateTime.Today.AddDays(60);
}
/// <summary>
/// Heutiges Datum, wenn das Datum geändert wird, wird auch der Tag geladen
/// </summary>
private DateTime dateToday = DateTime.Today;
public DateTime DateToday {
get => dateToday;
set {
if (dateToday != value) {
dateToday = value;
LoadOverview = "Lade Summen für " + dateToday.ToString("MMMM yy");
//OnPropertyChanged();
Task.Run(() => LoadDay(value));
}
}
}
/// <summary>
/// Heutiges Datum, wenn das Datum geändert wird, wird auch der Tag geladen
/// </summary>
private DateTime dateToday = DateTime.Today;
/// <summary>
/// Monatsübersicht: Geleistete Stunden
/// </summary>
public double? ZeitCalculated {
get => Hours.Zeit_total;
}
public DateTime DateToday {
get => dateToday;
set {
if (dateToday != value) {
dateToday = value;
LoadOverview = "Lade Summen für " + dateToday.ToString("MMMM yy");
// Task.Run(() => LoadDay(value));
// NICHT Task.Run: LoadDay aktualisiert UI-gebundene Properties
MainThread.BeginInvokeOnMainThread(async () =>
{
try
{
await LoadDay(dateToday);
}
catch (Exception ex)
{
AlertEvent?.Invoke(this, ex.Message);
}
});
}
}
}
/// <summary>
/// Monatsübersicht: Sollstunden
/// </summary>
public double? Nominal {
get => Hours.Nominal;
}
/// <summary>
/// Monatsübersicht: Geleistete Stunden
/// </summary>
public double? ZeitCalculated {
get => Hours.Zeit_total;
}
/// <summary>
/// Monatsübersicht: Differenz zwischen Soll und geleisteten Stunden
/// </summary>
public double? Overtime {
get => Hours.overtime;
}
/// <summary>
/// Monatsübersicht: Sollstunden
/// </summary>
public double? Nominal {
get => Hours.Nominal;
}
/// <summary>
/// Monatsübersicht: Restüberstunden insgesamt
/// </summary>
public double OvertimeMonth {
get => Hours.overtime_month;
}
/// <summary>
/// Monatsübersicht: Differenz zwischen Soll und geleisteten Stunden
/// </summary>
public double? Overtime {
get => Hours.overtime;
}
public double Zeitausgleich {
get => Hours.zeitausgleich;
}
public double ZeitausgleichMonth {
get => Hours.zeitausgleich_month;
}
/// <summary>
/// Monatsübersicht: Restüberstunden insgesamt
/// </summary>
public double OvertimeMonth {
get => Hours.overtime_month;
}
/// <summary>
/// Monatsübersicht: Resturlaub
/// </summary>
public double Holiday {
get => Hours.holiday;
}
public double Zeitausgleich {
get => Hours.zeitausgleich;
}
/// <summary>
/// Seite neu laden
/// </summary>
[ObservableProperty]
private bool isRefreshing;
public double ZeitausgleichMonth {
get => Hours.zeitausgleich_month;
}
/// <summary>
/// Monatsübersicht: Resturlaub
/// </summary>
public double Holiday {
get => Hours.holiday;
}
/// <summary>
/// Seite neu laden
/// </summary>
[ObservableProperty] private bool isRefreshing;
/// <summary>
/// Dürfen Gemeinden verwendet werden?
/// </summary>
public bool GemeindeAktivSet { get; set; }
/// <summary>
/// Dürfen Gemeinden verwendet werden?
/// </summary>
public bool GemeindeAktivSet { get; set; }
/// <summary>
/// Dürfen Projekte verwendet werden?
/// </summary>
public bool ProjektAktivSet { get; set; }
/// <summary>
/// Dürfen Projekte verwendet werden?
/// </summary>
public bool ProjektAktivSet { get; set; }
private bool doContinue = true;
private bool doContinue = true;
/// <summary>
/// CTOR (DI)
/// </summary>
public StundenViewModel(IHoursService hoursService) {
_hoursService = hoursService;
Hours = new Hours();
LoadOverview = "Lade Summen für " + DateToday.ToString("MMMM");
LoadDataCommand = new AsyncRelayCommand(LoadData);
NewEntryCommand = new AsyncRelayCommand(NewEntryAsync);
SelectEntryCommand = new AsyncRelayCommand<DayTime>(SelectEntryAsync);
RefreshListCommand = new AsyncRelayCommand(RefreshList);
RefreshCommand = new Command(async () => await RefreshItemsAsync());
// Task task = LoadDay(DateTime.Today);
// Beim Startup NICHT direkt im CTOR laden (kann Startup/Navigation blockieren)
// Stattdessen via Dispatcher "nach" dem Aufbau starten:
MainThread.BeginInvokeOnMainThread(async () =>
{
try
{
await LoadDay(DateTime.Today);
}
catch (Exception ex)
{
AlertEvent?.Invoke(this, ex.Message);
}
});
}
/// <summary>
/// CTOR
/// </summary>
public StundenViewModel() : this(GetServiceOrCreate()) {
}
/// <summary>
/// Öffnet eine neue Stundeneingabe
/// </summary>
private async Task NewEntryAsync() {
//Hier muss das Datum übergeben werden
//await Shell.Current.GoToAsync(nameof(Views.StundePage));
await Shell.Current.GoToAsync($"{nameof(Views.StundePage)}?date={dateToday:yyyy-MM-dd}");
}
private static IHoursService GetServiceOrCreate() => new HoursService(new HoursRepository());
/// <summary>
/// Öffnet eine bestehende Stundeneingabe
/// </summary>
private async Task SelectEntryAsync(DayTime entry) {
if (entry != null && entry.Id != null) {
//var navigationParameters = new Dictionary<string, object> { { "load", entry.id } };
//await Shell.Current.GoToAsync($"{nameof(Views.StundePage)}", navigationParameters);
await Shell.Current.GoToAsync($"{nameof(Views.StundePage)}?load={entry.Id}");
} else AlertEvent?.Invoke(this, "Auswahl enthält keine Daten");
}
internal StundenViewModel(IHoursService hoursService) {
_hoursService = hoursService;
Hours = new Hours();
private async Task RefreshList() {
OnPropertyChanged(nameof(DayTimes));
}
LoadOverview = "Lade Summen für " + DateToday.ToString("MMMM");
/// <summary>
/// Lädt die Monatssummen für die Übersicht
/// </summary>
private async Task LoadData() {
try {
var (hours, settings) = await _hoursService.GetMonthSummaryAsync(DateToday);
Hours = hours;
Settings = settings;
LoadDataCommand = new AsyncRelayCommand(LoadData);
NewEntryCommand = new AsyncRelayCommand(NewEntryAsync);
SelectEntryCommand = new AsyncRelayCommand<DayTime>(SelectEntryAsync);
RefreshListCommand = new AsyncRelayCommand(RefreshList);
RefreshCommand = new Command(async () => await RefreshItemsAsync());
if (Settings.Version != AppInfo.Current.VersionString.Substring(0, 5)) {
InfoEvent?.Invoke(this,
"Version: " + Settings.Version + " verfügbar (" + AppInfo.Current.VersionString.Substring(0, 5) +
" installiert)");
}
Task task = LoadDay(DateTime.Today);
}
//_hour = await HoursBase.LoadData();
RefreshProperties();
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
}
}
/// <summary>
/// Öffnet eine neue Stundeneingabe
/// </summary>
private async Task NewEntryAsync() {
//Hier muss das Datum übergeben werden
//await Shell.Current.GoToAsync(nameof(Views.StundePage));
await Shell.Current.GoToAsync($"{nameof(Views.StundePage)}?date={dateToday:yyyy-MM-dd}");
}
/// <summary>
/// Lädt die Arbeitszeiten für einen Tag
/// </summary>
public async Task LoadDay(DateTime date) {
// kleine Initialwerte sind ok, aber UI-Thread sicher setzen:
await MainThread.InvokeOnMainThreadAsync(() =>
{
DayTotal = new TimeOnly(0);
Sollstunden = new TimeOnly(0);
});
try {
var (dayTimes, settings) = await _hoursService.GetDayWithSettingsAsync(date);
/// <summary>
/// Öffnet eine bestehende Stundeneingabe
/// </summary>
private async Task SelectEntryAsync(DayTime entry) {
if (entry != null && entry.Id != null) {
//var navigationParameters = new Dictionary<string, object> { { "load", entry.id } };
//await Shell.Current.GoToAsync($"{nameof(Views.StundePage)}", navigationParameters);
await Shell.Current.GoToAsync($"{nameof(Views.StundePage)}?load={entry.Id}");
} else AlertEvent?.Invoke(this, "Auswahl enthält keine Daten");
}
await MainThread.InvokeOnMainThreadAsync(() =>
{
DayTimes = dayTimes;
Settings = settings;
GemeindeAktivSet = Settings.GemeindeAktivSet;
ProjektAktivSet = Settings.ProjektAktivSet;
private async Task RefreshList() {
OnPropertyChanged(nameof(DayTimes));
}
OnPropertyChanged(nameof(GemeindeAktivSet));
OnPropertyChanged(nameof(ProjektAktivSet));
});
/// <summary>
/// Lädt die Monatssummen für die Übersicht
/// </summary>
private async Task LoadData() {
try {
var (hours, settings) = await _hoursService.GetMonthSummaryAsync(DateToday);
Hours = hours;
Settings = settings;
List<Sollstunden> _soll;
TimeSpan span = TimeSpan.Zero;
bool merker = false;
foreach (DayTime dt in DayTimes) {
span += dt.End - dt.Begin;
//Nachtstunden dazurechnen
if (dt.Night.Ticks > 0 && !merker) {
span += dt.Night.ToTimeSpan() * .5;
merker = true;
}
if (Settings.Version != AppInfo.Current.VersionString.Substring(0, 5)) {
InfoEvent?.Invoke(this, "Version: " + Settings.Version + " verfügbar (" + AppInfo.Current.VersionString.Substring(0, 5) + " installiert)");
}
//_hour = await HoursBase.LoadData();
RefreshProperties();
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
}
}
_soll = Settings.Nominal.Where(w => w.Timetable == dt.TimeTable && w.Wochentag == dt.Wday).ToList();
if (_soll.Count > 0)
{
var soll = TimeOnly.FromTimeSpan(TimeSpan.FromHours(_soll[0].Zeit));
await MainThread.InvokeOnMainThreadAsync(() => Sollstunden = soll);
}
}
var total = TimeOnly.FromTimeSpan(span);
await MainThread.InvokeOnMainThreadAsync(() => DayTotal = total);
//Nach der Tagessumme die anderen Tage anhängen
if (DayTimes != null) {
var more = await _hoursService.GetDayRangeAsync(date, date.AddDays(3));
if (more != null && more.Count > 0)
{
await MainThread.InvokeOnMainThreadAsync(() =>
DayTimes = DayTimes.Concat(more).ToList()
);
}
}
} catch (Exception e) {
await MainThread.InvokeOnMainThreadAsync(() =>
{
DayTimes = new List<DayTime>();
//TODO: hier könnte auch ein Fehler kommen, dann wäre InfoEvent falsch.
if (Settings.Version != null && Settings.Version != AppInfo.Current.VersionString.Substring(0, 5)) {
InfoEvent?.Invoke(this,
"Version: " + Settings.Version + " verfügbar (" + AppInfo.Current.VersionString.Substring(0, 5) +
" installiert)");
} else {
InfoEvent?.Invoke(this, e.Message);
}
});
/// <summary>
/// Lädt die Arbeitszeiten für einen Tag
/// </summary>
public async Task LoadDay(DateTime date) {
DayTotal = new TimeOnly(0);
Sollstunden = new TimeOnly(0);
try {
var (dayTimes, settings) = await _hoursService.GetDayWithSettingsAsync(date);
DayTimes = dayTimes;
Settings = settings;
GemeindeAktivSet = Settings.GemeindeAktivSet;
ProjektAktivSet = Settings.ProjektAktivSet;
} finally {
await MainThread.InvokeOnMainThreadAsync(() =>
{
OnPropertyChanged(nameof(DayTotal));
OnPropertyChanged(nameof(Sollstunden));
OnPropertyChanged(nameof(DateToday));
OnPropertyChanged(nameof(LoadOverview));
});
}
}
OnPropertyChanged(nameof(GemeindeAktivSet));
OnPropertyChanged(nameof(ProjektAktivSet));
async void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) {
if (query.ContainsKey("date")) {
await LoadDay(Convert.ToDateTime(query["date"]));
}
}
List<Sollstunden> _soll;
TimeSpan span = TimeSpan.Zero;
bool merker = false;
foreach (DayTime dt in DayTimes) {
span += dt.End - dt.Begin;
//Nachtstunden dazurechnen
if (dt.Night.Ticks > 0 && !merker) {
span += dt.Night.ToTimeSpan() * .5;
merker = true;
}
_soll = Settings.Nominal.Where(w => w.Timetable == dt.TimeTable && w.Wochentag == dt.Wday).ToList();
if (_soll.Count > 0)
Sollstunden = TimeOnly.FromTimeSpan(TimeSpan.FromHours(_soll[0].Zeit));
}
DayTotal = TimeOnly.FromTimeSpan(span);
/// <summary>
/// Seite aktualisieren
/// </summary>
private async Task RefreshItemsAsync() {
IsRefreshing = true;
//Nach der Tagessumme die anderen Tage anhängen
if (DayTimes != null) {
var more = await _hoursService.GetDayRangeAsync(date, date.AddDays(3));
if (more != null && more.Count > 0)
DayTimes = DayTimes.Concat(more).ToList();
}
//await Task.Delay(2000); // Simuliert eine Datenaktualisierung
await LoadDay(DateToday);
} catch (Exception e) {
DayTimes = new List<DayTime>();
//TODO: hier könnte auch ein Fehler kommen, dann wäre InfoEvent falsch.
if (Settings.Version != null && Settings.Version != AppInfo.Current.VersionString.Substring(0, 5)) {
InfoEvent?.Invoke(this, "Version: " + Settings.Version + " verfügbar (" + AppInfo.Current.VersionString.Substring(0, 5) + " installiert)");
} else { InfoEvent?.Invoke(this, e.Message); }
} finally {
OnPropertyChanged(nameof(DayTotal));
OnPropertyChanged(nameof(Sollstunden));
OnPropertyChanged(nameof(DateToday));
OnPropertyChanged(nameof(LoadOverview));
//OnPropertyChanged(nameof(DayTimes));
}
}
async void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) {
if (query.ContainsKey("date")) {
await LoadDay(Convert.ToDateTime(query["date"]));
}
}
/// <summary>
/// Seite aktualisieren
/// </summary>
private async Task RefreshItemsAsync() {
IsRefreshing = true;
//await Task.Delay(2000); // Simuliert eine Datenaktualisierung
await LoadDay(DateToday);
IsRefreshing = false;
}
/// <summary>
/// Refreshes all properties
/// </summary>
private void RefreshProperties() {
OnPropertyChanged(nameof(Hours));
OnPropertyChanged(nameof(Title));
OnPropertyChanged(nameof(Nominal));
OnPropertyChanged(nameof(Overtime));
OnPropertyChanged(nameof(OvertimeMonth));
OnPropertyChanged(nameof(Zeitausgleich));
OnPropertyChanged(nameof(ZeitCalculated));
OnPropertyChanged(nameof(Holiday));
OnPropertyChanged(nameof(MinimumDate));
OnPropertyChanged(nameof(MaximumDate));
OnPropertyChanged(nameof(LoadOverview));
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null) {
try {
base.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
} catch (Exception ex) {
AlertEvent?.Invoke(this, ex.Message);
//Console.WriteLine($"Fehler bei OnPropertyChanged: {ex.Message}");
}
}
IsRefreshing = false;
}
/// <summary>
/// Refreshes all properties
/// </summary>
private void RefreshProperties() {
OnPropertyChanged(nameof(Hours));
OnPropertyChanged(nameof(Title));
OnPropertyChanged(nameof(Nominal));
OnPropertyChanged(nameof(Overtime));
OnPropertyChanged(nameof(OvertimeMonth));
OnPropertyChanged(nameof(Zeitausgleich));
OnPropertyChanged(nameof(ZeitCalculated));
OnPropertyChanged(nameof(Holiday));
OnPropertyChanged(nameof(MinimumDate));
OnPropertyChanged(nameof(MaximumDate));
OnPropertyChanged(nameof(LoadOverview));
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null) {
try {
base.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
} catch (Exception ex) {
AlertEvent?.Invoke(this, ex.Message);
//Console.WriteLine($"Fehler bei OnPropertyChanged: {ex.Message}");
}
}
}

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<?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"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
@@ -13,7 +14,8 @@
<!-- Add an item to the toolbar -->
<ContentPage.ToolbarItems>
<ToolbarItem Text="Neue Notiz" Command="{Binding NewCommand}" IconImageSource="{FontImage Glyph='+', Color=Black, Size=22}" />
<ToolbarItem Text="Neue Notiz" Command="{Binding NewCommand}"
IconImageSource="{FontImage Glyph='+', Color=Black, Size=22}" />
</ContentPage.ToolbarItems>
<ContentPage.Behaviors>
@@ -23,7 +25,7 @@
</ContentPage.Behaviors>
<VerticalStackLayout Margin="20,0,0,0">
<Label Text="Werden nur lokal gespeichert"/>
<Label Text="Werden nur lokal gespeichert" />
<!-- Display notes in a list -->
<CollectionView x:Name="notesCollection"
@@ -31,7 +33,7 @@
Margin="0,20,0,0"
SelectionMode="Single"
SelectionChangedCommand="{Binding SelectNoteCommand}"
SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}">
SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}">
<!-- Designate how the collection of items is laid out -->
<CollectionView.ItemsLayout>
@@ -42,8 +44,8 @@
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout>
<Label Text="{Binding Text}" FontSize="22"/>
<Label Text="{Binding Date}" FontSize="14" TextColor="Silver"/>
<Label Text="{Binding Text}" FontSize="22" />
<Label Text="{Binding Date}" FontSize="14" TextColor="Silver" />
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>

View File

@@ -1,11 +1,8 @@
namespace Jugenddienst_Stunden.Views;
public partial class AllNotesPage : ContentPage
{
public AllNotesPage()
{
public partial class AllNotesPage : ContentPage {
public AllNotesPage() {
InitializeComponent();
}
private void ContentPage_NavigatedTo(object sender, NavigatedToEventArgs e) {

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<?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"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
@@ -29,20 +30,22 @@
<VerticalStackLayout Spacing="10" Margin="15,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<HorizontalStackLayout Spacing="5" HorizontalOptions="Start" Grid.Column="0">
<Label FontSize="20" FontAttributes="Bold" Text="{Binding AppTitle}" Margin="0,7,0,0" />
<Label FontSize="16" Text="{Binding Version}" Margin="0,11,0,0" />
</HorizontalStackLayout>
<Grid Grid.Column="1" ColumnDefinitions="*,50" ColumnSpacing="10">
<Label Text="Login QR/manuell" VerticalOptions="Center" Grid.Column="0"/>
<Switch x:Name="LoginSwitch" IsToggled="False" Toggled="Switch_Toggled" VerticalOptions="Center" Grid.Column="1"/>
<Label Text="Login QR/manuell" VerticalOptions="Center" Grid.Column="0" />
<Switch x:Name="LoginSwitch" IsToggled="False" Toggled="Switch_Toggled" VerticalOptions="Center"
Grid.Column="1" />
</Grid>
</Grid>
<Label x:Name="ServerLabel" Text="{Binding Server}" IsVisible="{Binding Server, Converter={StaticResource StringVisibilityConverter}}" />
<Label x:Name="ServerLabel" Text="{Binding Server}"
IsVisible="{Binding Server, Converter={StaticResource StringVisibilityConverter}}" />
<VerticalStackLayout x:Name="LoginQR" Margin="0,20,0,0">
<Label Text="Login mit QR-Code" FontSize="32" HorizontalOptions="Start" />
@@ -50,10 +53,10 @@
<Border HeightRequest="300" Padding="0">
<zxing:CameraBarcodeReaderView
x:Name="barcodeScannerView"
BarcodesDetected="BarcodesDetected"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"/>
x:Name="barcodeScannerView"
BarcodesDetected="BarcodesDetected"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand" />
</Border>
</VerticalStackLayout>
@@ -62,7 +65,8 @@
<Entry x:Name="UsernameEntry" Placeholder="Benutzername (Mailadresse)" Keyboard="Email" />
<Entry x:Name="PasswordEntry" Placeholder="Passwort" IsPassword="True" />
<Entry x:Name="ServerEntry" Placeholder="Server (gleich wie im Browser)" Keyboard="Url" />
<Button Text="Login" Clicked="OnLoginButtonClicked" TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}" />
<Button Text="Login" Clicked="OnLoginButtonClicked"
TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}" />
</VerticalStackLayout>
</VerticalStackLayout>

View File

@@ -9,197 +9,195 @@ namespace Jugenddienst_Stunden.Views;
/// Die Loginseite mit dem Barcodescanner
/// </summary>
public partial class LoginPage : ContentPage {
private DateTime _lastDetectionTime;
private readonly TimeSpan _detectionInterval = TimeSpan.FromSeconds(5);
private DateTime _lastDetectionTime;
private readonly TimeSpan _detectionInterval = TimeSpan.FromSeconds(5);
/// <summary>
/// CTOR
/// </summary>
public LoginPage() {
InitializeComponent();
/// <summary>
/// CTOR
/// </summary>
public LoginPage() {
InitializeComponent();
barcodeScannerView.Options = new BarcodeReaderOptions {
Formats = BarcodeFormat.QrCode,
AutoRotate = true,
Multiple = false
};
barcodeScannerView.Options =
new BarcodeReaderOptions { Formats = BarcodeFormat.QrCode, AutoRotate = true, Multiple = false };
//if (BindingContext is LoginViewModel vm) {
// vm.AlertEvent += Vm_AlertEvent;
// vm.InfoEvent += Vm_InfoEvent;
// vm.MsgEvent += Vm_MsgEvent;
//}
//if (BindingContext is LoginViewModel vm) {
// vm.AlertEvent += Vm_AlertEvent;
// vm.InfoEvent += Vm_InfoEvent;
// vm.MsgEvent += Vm_MsgEvent;
//}
//Zwischen manuellem und automatischem Login (mit QR-Code) umschalten und die Schalterstellung merken
bool sqr = true;
bool sma = false;
if (Preferences.Default.Get("logintype", "") == "manual") {
sqr = false;
sma = true;
LoginSwitch.IsToggled = true;
Message.IsVisible = false;
} else {
LoginSwitch.IsToggled = false;
Message.IsVisible = true;
}
LoginQR.IsVisible = sqr;
LoginManual.IsVisible = sma;
}
//Zwischen manuellem und automatischem Login (mit QR-Code) umschalten und die Schalterstellung merken
bool sqr = true;
bool sma = false;
if (Preferences.Default.Get("logintype", "") == "manual") {
sqr = false;
sma = true;
LoginSwitch.IsToggled = true;
Message.IsVisible = false;
} else {
LoginSwitch.IsToggled = false;
Message.IsVisible = true;
}
LoginQR.IsVisible = sqr;
LoginManual.IsVisible = sma;
}
/// <summary>
/// Nach der Erkennung des Barcodes wird der Benutzer eingeloggt
/// ZXing.Net.Maui.Controls 0.4.4
/// </summary>
private void BarcodesDetected(object sender, BarcodeDetectionEventArgs e) {
/// <summary>
/// Nach der Erkennung des Barcodes wird der Benutzer eingeloggt
/// ZXing.Net.Maui.Controls 0.4.4
/// </summary>
private void BarcodesDetected(object sender, BarcodeDetectionEventArgs e) {
var currentTime = DateTime.Now;
if ((currentTime - _lastDetectionTime) > _detectionInterval) {
_lastDetectionTime = currentTime;
foreach (var barcode in e.Results) {
if (GlobalVar.ApiKey != barcode.Value) {
_ = MainThread.InvokeOnMainThreadAsync(async () => {
//await DisplayAlert("Barcode erkannt", $"Barcode: {barcode.Format} - {barcode.Value}", "OK");
var currentTime = DateTime.Now;
if ((currentTime - _lastDetectionTime) > _detectionInterval) {
_lastDetectionTime = currentTime;
foreach (var barcode in e.Results) {
if (GlobalVar.ApiKey != barcode.Value) {
_ = MainThread.InvokeOnMainThreadAsync(async () => {
//await DisplayAlert("Barcode erkannt", $"Barcode: {barcode.Format} - {barcode.Value}", "OK");
try {
var tokendata = new TokenData(barcode.Value);
GlobalVar.ApiUrl = tokendata.Url;
User user = await HoursBase.LoadUser(barcode.Value);
try {
var tokendata = new TokenData(barcode.Value);
GlobalVar.ApiUrl = tokendata.Url;
User user = await HoursBase.LoadUser(barcode.Value);
GlobalVar.ApiKey = barcode.Value;
GlobalVar.Name = user.Name;
GlobalVar.Surname = user.Surname;
GlobalVar.EmployeeId = user.Id;
GlobalVar.ApiKey = barcode.Value;
GlobalVar.Name = user.Name;
GlobalVar.Surname = user.Surname;
GlobalVar.EmployeeId = user.Id;
Title = user.Name + " " + user.Surname;
//Auf der Loginseite wird der Server als Info ohne Protokoll und ohne /appapi angezeigt
ServerLabel.Text = "Server: " + tokendata.Url.Replace("/appapi", "").Replace("https://", "").Replace("http://", "");
Title = user.Name + " " + user.Surname;
//Auf der Loginseite wird der Server als Info ohne Protokoll und ohne /appapi angezeigt
ServerLabel.Text = "Server: " + tokendata.Url.Replace("/appapi", "").Replace("https://", "")
.Replace("http://", "");
await DisplayAlert("Login erfolgreich", user.Name + " " + user.Surname, "OK");
if (Navigation.NavigationStack.Count > 1) {
//Beim ersten Start ohne Login, wird man automatisch auf die Loginseite geleitet. Danach in der History zur<75>ck
await Navigation.PopAsync();
} else {
//Beim manuellen Wechsel auf die Loginseite leiten wir nach erfolgreichem Login auf die Stunden<65>bersicht
await Shell.Current.GoToAsync($"//StundenPage");
}
await DisplayAlert("Login erfolgreich", user.Name + " " + user.Surname, "OK");
if (Navigation.NavigationStack.Count > 1) {
//Beim ersten Start ohne Login, wird man automatisch auf die Loginseite geleitet. Danach in der History zur<75>ck
await Navigation.PopAsync();
} else {
//Beim manuellen Wechsel auf die Loginseite leiten wir nach erfolgreichem Login auf die Stunden<65>bersicht
await Shell.Current.GoToAsync($"//StundenPage");
}
} catch (Exception e) {
await DisplayAlert("Fehler", e.Message, "OK");
}
});
} else {
MainThread.InvokeOnMainThreadAsync(() => {
DisplayAlert("Bereits eingeloggt",
Preferences.Default.Get("name", "") + " " + Preferences.Default.Get("surname", ""),
"OK");
});
}
}
}
}
} catch (Exception e) {
await DisplayAlert("Fehler", e.Message, "OK");
}
protected override void OnDisappearing() {
base.OnDisappearing();
});
} else {
MainThread.InvokeOnMainThreadAsync(() => {
DisplayAlert("Bereits eingeloggt",
Preferences.Default.Get("name", "") + " " + Preferences.Default.Get("surname", ""),
"OK");
});
}
}
}
barcodeScannerView.CameraLocation = CameraLocation.Front;
barcodeScannerView.IsDetecting = false;
}
}
protected override void OnAppearing() {
base.OnAppearing();
protected override void OnDisappearing() {
base.OnDisappearing();
barcodeScannerView.IsDetecting = true;
barcodeScannerView.CameraLocation = CameraLocation.Rear;
}
barcodeScannerView.CameraLocation = CameraLocation.Front;
barcodeScannerView.IsDetecting = false;
}
public bool IsCameraAvailable() {
var status = Permissions.CheckStatusAsync<Permissions.Camera>().Result;
if (status != PermissionStatus.Granted) {
status = Permissions.RequestAsync<Permissions.Camera>().Result;
}
protected override void OnAppearing() {
base.OnAppearing();
return status != PermissionStatus.Granted;
}
barcodeScannerView.IsDetecting = true;
barcodeScannerView.CameraLocation = CameraLocation.Rear;
}
public bool IsCameraAvailable() {
var status = Permissions.CheckStatusAsync<Permissions.Camera>().Result;
if (status != PermissionStatus.Granted) {
status = Permissions.RequestAsync<Permissions.Camera>().Result;
}
return status != PermissionStatus.Granted;
}
private async void OnLoginButtonClicked(object sender, EventArgs e) {
var username = UsernameEntry.Text;
var password = PasswordEntry.Text;
var server = ServerEntry.Text;
private async void OnLoginButtonClicked(object sender, EventArgs e) {
var username = UsernameEntry.Text;
var password = PasswordEntry.Text;
var server = ServerEntry.Text;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(server)) {
await DisplayAlert("Fehler", "Bitte alle Felder ausf<73>llen", "OK");
return;
}
try {
Uri uri = new Uri(InputUrlWithSchema(server));
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(server)) {
await DisplayAlert("Fehler", "Bitte alle Felder ausf<73>llen", "OK");
return;
}
Types.User response = await BaseFunc.AuthUserPass(username, password, uri.Scheme + "://" + uri.Authority + "/appapi");
try {
Uri uri = new Uri(InputUrlWithSchema(server));
GlobalVar.ApiKey = response.Token;
GlobalVar.Name = response.Name;
GlobalVar.Surname = response.Surname;
GlobalVar.EmployeeId = response.Id;
GlobalVar.ApiUrl = uri.Scheme + "://" + uri.Authority + "/appapi";
Types.User response =
await BaseFunc.AuthUserPass(username, password, uri.Scheme + "://" + uri.Authority + "/appapi");
Title = response.Name + " " + response.Surname;
//ServerLabel.Text = "Server: " + server.Replace("/appapi", "").Replace("https://", "").Replace("http://", "");
ServerLabel.Text = "Server: " + uri.Authority;
GlobalVar.ApiKey = response.Token;
GlobalVar.Name = response.Name;
GlobalVar.Surname = response.Surname;
GlobalVar.EmployeeId = response.Id;
GlobalVar.ApiUrl = uri.Scheme + "://" + uri.Authority + "/appapi";
await DisplayAlert("Login erfolgreich", response.Name + " " + response.Surname, "OK");
if (Navigation.NavigationStack.Count > 1)
await Navigation.PopAsync();
else {
await Shell.Current.GoToAsync($"//StundenPage");
}
} catch (Exception ex) {
await DisplayAlert("Fehler", ex.Message, "OK");
}
}
Title = response.Name + " " + response.Surname;
//ServerLabel.Text = "Server: " + server.Replace("/appapi", "").Replace("https://", "").Replace("http://", "");
ServerLabel.Text = "Server: " + uri.Authority;
/// <summary>
/// Aus einer URL ohne Schema eine URL mit Schema machen
/// </summary>
private static string InputUrlWithSchema(string url) {
if (!url.StartsWith("http://") && !url.StartsWith("https://")) {
url = "https://" + url;
}
if (url.StartsWith("http://")) {
url = url.Replace("http://", "https://");
}
return url;
}
await DisplayAlert("Login erfolgreich", response.Name + " " + response.Surname, "OK");
if (Navigation.NavigationStack.Count > 1)
await Navigation.PopAsync();
else {
await Shell.Current.GoToAsync($"//StundenPage");
}
} catch (Exception ex) {
await DisplayAlert("Fehler", ex.Message, "OK");
}
}
//Zwischen manuellem und automatischem Login (mit QR-Code) umschalten und die Schalterstellung merken
private void Switch_Toggled(object sender, ToggledEventArgs e) {
var switcher = (Switch)sender;
/// <summary>
/// Aus einer URL ohne Schema eine URL mit Schema machen
/// </summary>
private static string InputUrlWithSchema(string url) {
if (!url.StartsWith("http://") && !url.StartsWith("https://")) {
url = "https://" + url;
}
if (switcher.IsToggled) {
LoginQR.IsVisible = false;
LoginManual.IsVisible = true;
Message.IsVisible = false;
Preferences.Default.Set("logintype", "manual");
} else {
LoginQR.IsVisible = true;
LoginManual.IsVisible = false;
Message.IsVisible = true;
Preferences.Default.Set("logintype", "qr");
}
if (url.StartsWith("http://")) {
url = url.Replace("http://", "https://");
}
}
return url;
}
//private void Vm_AlertEvent(object? sender, string e) {
// DisplayAlert("Fehler:", e, "OK");
//}
//private void Vm_InfoEvent(object? sender, string e) {
// DisplayAlert("Information:", e, "OK");
//}
//private async Task Vm_MsgEvent(string title, string message) {
// await DisplayAlert(title, message, "OK");
//}
//Zwischen manuellem und automatischem Login (mit QR-Code) umschalten und die Schalterstellung merken
private void Switch_Toggled(object sender, ToggledEventArgs e) {
var switcher = (Switch)sender;
if (switcher.IsToggled) {
LoginQR.IsVisible = false;
LoginManual.IsVisible = true;
Message.IsVisible = false;
Preferences.Default.Set("logintype", "manual");
} else {
LoginQR.IsVisible = true;
LoginManual.IsVisible = false;
Message.IsVisible = true;
Preferences.Default.Set("logintype", "qr");
}
}
//private void Vm_AlertEvent(object? sender, string e) {
// DisplayAlert("Fehler:", e, "OK");
//}
//private void Vm_InfoEvent(object? sender, string e) {
// DisplayAlert("Information:", e, "OK");
//}
//private async Task Vm_MsgEvent(string title, string message) {
// await DisplayAlert(title, message, "OK");
//}
}

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<?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"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"

View File

@@ -4,7 +4,6 @@ namespace Jugenddienst_Stunden.Views;
/// Einzelne Notiz
/// </summary>
public partial class NotePage : ContentPage {
/// <summary>
/// CTOR
/// </summary>

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<?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"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
@@ -31,20 +32,26 @@
<Border>
<Border.Padding>
<OnPlatform x:TypeArguments="Thickness" Default="0,15,10,0">
<On Platform="Android" Value="0,4,10,8"/>
<On Platform="WPF" Value="0,15,10,0"/>
<On Platform="Android" Value="0,4,10,8" />
<On Platform="WPF" Value="0,15,10,0" />
</OnPlatform>
</Border.Padding>
<FlexLayout Direction="Row" AlignItems="Start" Wrap="Wrap" JustifyContent="SpaceBetween">
<HorizontalStackLayout Spacing="10">
<Label Text="Beginn" VerticalTextAlignment="Center" HorizontalTextAlignment="End" MinimumWidthRequest="60"></Label>
<TimePicker x:Name="TimeBegin" HorizontalOptions="Center" Format="HH:mm" MinimumWidthRequest="80" Time="{Binding DayTime.TimeSpanVon}" />
<Label Text="Beginn" VerticalTextAlignment="Center" HorizontalTextAlignment="End"
MinimumWidthRequest="60">
</Label>
<TimePicker x:Name="TimeBegin" HorizontalOptions="Center" Format="HH:mm" MinimumWidthRequest="80"
Time="{Binding DayTime.TimeSpanVon}" />
</HorizontalStackLayout>
<HorizontalStackLayout Spacing="10">
<Label Text="Ende" VerticalTextAlignment="Center" HorizontalTextAlignment="End" MinimumWidthRequest="60"></Label>
<TimePicker x:Name="TimeEnd" Format="HH:mm" MinimumWidthRequest="80" Time="{Binding DayTime.TimeSpanBis}" />
<Label Text="Ende" VerticalTextAlignment="Center" HorizontalTextAlignment="End"
MinimumWidthRequest="60">
</Label>
<TimePicker x:Name="TimeEnd" Format="HH:mm" MinimumWidthRequest="80"
Time="{Binding DayTime.TimeSpanBis}" />
</HorizontalStackLayout>
</FlexLayout>
</Border>
@@ -52,8 +59,8 @@
<Border>
<Border.Padding>
<OnPlatform x:TypeArguments="Thickness" Default="5">
<On Platform="Android" Value="5,4,5,8"/>
<On Platform="WPF" Value="5"/>
<On Platform="Android" Value="5,4,5,8" />
<On Platform="WPF" Value="5" />
</OnPlatform>
</Border.Padding>
<!--<Grid ColumnDefinitions="*,*,*">
@@ -65,16 +72,23 @@
</Picker>
</Grid>-->
<HorizontalStackLayout>
<Picker x:Name="pick_gemeinde" Title="Gemeinde" ItemsSource="{Binding OptionsGemeinde}" SelectedItem="{Binding DayTime.GemeindeAktiv, Mode=TwoWay}" ItemDisplayBinding="{Binding Name}" IsVisible="{Binding GemeindeAktivSet}">
<Picker x:Name="pick_gemeinde" Title="Gemeinde" ItemsSource="{Binding OptionsGemeinde}"
SelectedItem="{Binding DayTime.GemeindeAktiv, Mode=TwoWay}" ItemDisplayBinding="{Binding Name}"
IsVisible="{Binding GemeindeAktivSet}">
</Picker>
<Picker x:Name="pick_projekt" Title="Projekt" ItemsSource="{Binding OptionsProjekt}" SelectedItem="{Binding DayTime.ProjektAktiv, Mode=TwoWay}" ItemDisplayBinding="{Binding Name}" IsVisible="{Binding ProjektAktivSet}">
<Picker x:Name="pick_projekt" Title="Projekt" ItemsSource="{Binding OptionsProjekt}"
SelectedItem="{Binding DayTime.ProjektAktiv, Mode=TwoWay}" ItemDisplayBinding="{Binding Name}"
IsVisible="{Binding ProjektAktivSet}">
</Picker>
<Picker x:Name="pick_freistellung" Title="Freistellung" ItemsSource="{Binding OptionsFreistellung}" SelectedItem="{Binding DayTime.FreistellungAktiv, Mode=TwoWay}" ItemDisplayBinding="{Binding Name}" IsEnabled="{Binding FreistellungEnabled}">
<Picker x:Name="pick_freistellung" Title="Freistellung" ItemsSource="{Binding OptionsFreistellung}"
SelectedItem="{Binding DayTime.FreistellungAktiv, Mode=TwoWay}"
ItemDisplayBinding="{Binding Name}" IsEnabled="{Binding FreistellungEnabled}">
</Picker>
</HorizontalStackLayout>
</Border>
<Editor Placeholder="Beschreibung" Text="{Binding DayTime.Description}" MinimumHeightRequest="40" AutoSize="TextChanges" FontSize="18" />
<Editor Placeholder="Beschreibung" Text="{Binding DayTime.Description}" MinimumHeightRequest="40"
AutoSize="TextChanges" FontSize="18" />
<Grid ColumnDefinitions="*,*" ColumnSpacing="4">
<Button Grid.Column="1" Text="Speichern"
@@ -85,15 +99,18 @@
IsEnabled="{Binding DayTime.Id, Converter={StaticResource IntBoolConverter}}"
IsVisible="{Binding FreistellungEnabled}"
BackgroundColor="{StaticResource Gray500}"
TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}"/>
TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}" />
</Grid>
<BoxView HeightRequest="1" Margin="3,10" />
<Label Text="Noch keine Einträge vorhanden" IsVisible="{Binding DayTimes, Converter={StaticResource CollectionVisibilityConverter}, ConverterParameter=Invert}" Margin="6,0,0,0"/>
<Label Text="Noch keine Einträge vorhanden"
IsVisible="{Binding DayTimes, Converter={StaticResource CollectionVisibilityConverter}, ConverterParameter=Invert}"
Margin="6,0,0,0" />
<StackLayout Margin="6,0,0,0" IsVisible="{Binding DayTimes, Converter={StaticResource CollectionVisibilityConverter}}">
<StackLayout Margin="6,0,0,0"
IsVisible="{Binding DayTimes, Converter={StaticResource CollectionVisibilityConverter}}">
<Label>
<Label.FormattedText>
<FormattedString>
@@ -106,12 +123,12 @@
<ScrollView IsVisible="{Binding DayTimes, Converter={StaticResource CollectionVisibilityConverter}}">
<CollectionView
ItemsSource="{Binding DayTimes}"
x:Name="stundeItems" Margin="0"
HeightRequest="350"
SelectionMode="Single"
SelectionChangedCommand="{Binding SelectEntryCommand}"
SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}">
ItemsSource="{Binding DayTimes}"
x:Name="stundeItems" Margin="0"
HeightRequest="350"
SelectionMode="Single"
SelectionChangedCommand="{Binding SelectEntryCommand}"
SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}">
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical" ItemSpacing="0" />
@@ -129,15 +146,18 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<HorizontalStackLayout Grid.Row="0" Grid.Column="0">
<Label Grid.Column="0" Text="{Binding Begin}"/>
<Label Text="bis" Padding="5,0,5,0"/>
<Label Text="{Binding End}"/>
<Label Text="{Binding GemeindeAktiv.Name}" Margin="10,0,0,0" IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.GemeindeAktivSet}"/>
<Label Text="{Binding ProjektAktiv.Name}" Margin="10,0,0,0" IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.ProjektAktivSet}"/>
<Label Text="{Binding FreistellungAktiv.Name}" Margin="10,0,0,0"/>
<Label Grid.Column="0" Text="{Binding Begin}" />
<Label Text="bis" Padding="5,0,5,0" />
<Label Text="{Binding End}" />
<Label Text="{Binding GemeindeAktiv.Name}" Margin="10,0,0,0"
IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.GemeindeAktivSet}" />
<Label Text="{Binding ProjektAktiv.Name}" Margin="10,0,0,0"
IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.ProjektAktivSet}" />
<Label Text="{Binding FreistellungAktiv.Name}" Margin="10,0,0,0" />
</HorizontalStackLayout>
<Label Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Text="{Binding Description}" Padding="0,0,0,15"/>
<Label Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Text="{Binding Description}"
Padding="0,0,0,15" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>

View File

@@ -10,45 +10,43 @@ namespace Jugenddienst_Stunden.Views;
/// Einzelner Stundeneintrag
/// </summary>
public partial class StundePage : ContentPage {
/// <summary>
/// CTOR
/// </summary>
public StundePage() {
InitializeComponent();
/// <summary>
/// CTOR
/// </summary>
public StundePage() {
InitializeComponent();
if (BindingContext is StundeViewModel vm) {
vm.AlertEvent += Vm_AlertEvent;
vm.InfoEvent += Vm_InfoEvent;
vm.ConfirmEvent += ShowConfirm;
}
}
if (BindingContext is StundeViewModel vm) {
vm.AlertEvent += Vm_AlertEvent;
vm.InfoEvent += Vm_InfoEvent;
vm.ConfirmEvent += ShowConfirm;
}
}
private void Vm_AlertEvent(object? sender, string e) {
DisplayAlert("Fehler:", e, "OK");
}
private void Vm_AlertEvent(object? sender, string e) {
DisplayAlert("Fehler:", e, "OK");
}
private async Task<bool> ShowConfirm(string title, string message) {
return await DisplayAlert(title, message, "Passt!", "Na, nor decht nit.");
}
private async Task<bool> ShowConfirm(string title, string message) {
return await DisplayAlert(title, message, "Passt!", "Na, nor decht nit.");
}
private void Vm_InfoEvent(object? sender, string e) {
MainThread.BeginInvokeOnMainThread(async () => {
CancellationTokenSource cts = new CancellationTokenSource();
ToastDuration duration = ToastDuration.Short;
double fontSize = 20;
var toast = Toast.Make(e, duration, fontSize);
await toast.Show(cts.Token);
});
}
private void Vm_InfoEvent(object? sender, string e) {
MainThread.BeginInvokeOnMainThread(async () => {
CancellationTokenSource cts = new CancellationTokenSource();
ToastDuration duration = ToastDuration.Short;
double fontSize = 20;
var toast = Toast.Make(e, duration, fontSize);
await toast.Show(cts.Token);
});
}
//private async Task<bool> ShowConfirm(string title, string message, string ok, string not_ok) {
// return await DisplayAlert(title, message, ok, not_ok);
//}
//private async void ShowConfirm(object? sender, ConfirmEventArgs e) {
// bool result = await DisplayAlert(e.Title, e.Message, e.Ok, e.NotOk);
// e.Result = result;
//}
//private async Task<bool> ShowConfirm(string title, string message, string ok, string not_ok) {
// return await DisplayAlert(title, message, ok, not_ok);
//}
//private async void ShowConfirm(object? sender, ConfirmEventArgs e) {
// bool result = await DisplayAlert(e.Title, e.Message, e.Ok, e.NotOk);
// e.Result = result;
//}
}

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<?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"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
@@ -8,17 +9,14 @@
x:Class="Jugenddienst_Stunden.Views.StundenPage"
Title="{Binding Title}">
<ContentPage.BindingContext>
<models:StundenViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<ResourceDictionary>
<conv:SecondsTimeConverter x:Key="secToTime" />
<FontImageSource x:Key="ToolbarIcon"
Glyph="+"
Size="22"
Color="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}"/>
Glyph="+"
Size="22"
Color="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
</ResourceDictionary>
</ContentPage.Resources>
@@ -30,21 +28,23 @@
<ContentPage.ToolbarItems>
<!--<ToolbarItem Text="Lade Liste" Command="{Binding RefreshListCommand}"/>-->
<ToolbarItem Text="Neuer Eintrag" IconImageSource="{StaticResource ToolbarIcon}" Command="{Binding NewEntryCommand}" />
<ToolbarItem Text="Neuer Eintrag" IconImageSource="{StaticResource ToolbarIcon}"
Command="{Binding NewEntryCommand}" />
</ContentPage.ToolbarItems>
<RefreshView x:Name="MyRefreshView" Command="{Binding RefreshCommand}" IsRefreshing="{Binding IsRefreshing}" Margin="10" Padding="10">
<RefreshView x:Name="MyRefreshView" Command="{Binding RefreshCommand}" IsRefreshing="{Binding IsRefreshing}"
Margin="10" Padding="10">
<Grid RowDefinitions="50,*,Auto,80">
<!--<VerticalStackLayout Spacing="10" Margin="10">-->
<!--<VerticalStackLayout Spacing="10" Margin="10">-->
<Grid RowDefinitions="Auto" ColumnDefinitions="Auto,*" HeightRequest="50" Grid.Row="0">
<DatePicker Grid.Column="0" MinimumDate="{Binding MinimumDate}"
MaximumDate="{Binding MaximumDate}"
Date="{Binding DateToday}" Format="dddd, d. MMMM yyyy" />
MaximumDate="{Binding MaximumDate}"
Date="{Binding DateToday}" Format="dddd, d. MMMM yyyy" />
<Border Grid.Column="1" Margin="15,0,0,0" Padding="15,0,0,0" ToolTipProperties.Text="Tagessumme">
<HorizontalStackLayout>
<Label Text="{Binding DayTotal,StringFormat='{}{0:HH:mm}'}" VerticalOptions="Center"></Label>
<Label Text="/" VerticalOptions="Center" Margin="3,0"/>
<Label Text="/" VerticalOptions="Center" Margin="3,0" />
<Label Text="{Binding Sollstunden,StringFormat='{}{0:HH:mm}'}" VerticalOptions="Center"></Label>
</HorizontalStackLayout>
</Border>
@@ -72,27 +72,29 @@
<VisualStateGroup Name="CommonStates">
<VisualState Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource TransparentColor}}" />
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource TransparentColor}}" />
</VisualState.Setters>
</VisualState>
<VisualState Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<HorizontalStackLayout>
<HorizontalStackLayout.Triggers>
<DataTrigger TargetType="HorizontalStackLayout" Binding="{Binding Approved}" Value="True">
<DataTrigger TargetType="HorizontalStackLayout" Binding="{Binding Approved}"
Value="True">
<Setter Property="BackgroundColor" Value="LightCoral" />
<Setter Property="Padding" Value="4"/>
<Setter Property="Padding" Value="4" />
</DataTrigger>
</HorizontalStackLayout.Triggers>
<Label Text="{Binding Day, StringFormat='{0:dddd, dd. MMMM}'}"/>
<Label Text="{Binding Day, StringFormat='{0:dddd, dd. MMMM}'}" />
<Label Text="von" Padding="5,0,5,0" />
<Label Text="{Binding Begin}" />
<Label Text="bis" Padding="5,0,5,0" />
@@ -101,17 +103,20 @@
<HorizontalStackLayout HorizontalOptions="FillAndExpand">
<HorizontalStackLayout.Triggers>
<DataTrigger TargetType="HorizontalStackLayout" Binding="{Binding Approved}" Value="True">
<DataTrigger TargetType="HorizontalStackLayout" Binding="{Binding Approved}"
Value="True">
<Setter Property="BackgroundColor" Value="LightCoral" />
<Setter Property="Padding" Value="4"/>
<Setter Property="Padding" Value="4" />
</DataTrigger>
</HorizontalStackLayout.Triggers>
<Label Text="{Binding GemeindeAktiv.Name}" Margin="0,0,10,0" IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.GemeindeAktivSet}" />
<Label Text="{Binding ProjektAktiv.Name}" Margin="0,0,10,0" IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.ProjektAktivSet}" />
<Label Text="{Binding GemeindeAktiv.Name}" Margin="0,0,10,0"
IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.GemeindeAktivSet}" />
<Label Text="{Binding ProjektAktiv.Name}" Margin="0,0,10,0"
IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.ProjektAktivSet}" />
<Label Text="{Binding FreistellungAktiv.Name}" IsVisible="{Binding Approved}" />
</HorizontalStackLayout>
<Label Text="{Binding Description}" Padding="0,0,0,15"/>
<Label Text="{Binding Description}" Padding="0,0,0,15" />
</VerticalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
@@ -133,12 +138,20 @@
<Label Grid.Row="1" Grid.Column="2" Text="Zeitausgleich:" Margin="15,0,0,0" />
<Label Grid.Row="2" Grid.Column="2" Text="Resturlaub:" Margin="15,0,0,0" />
<Label Grid.Row="0" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding Nominal, Converter={StaticResource secToTime}}" ToolTipProperties.Text="Sollstunden" />
<Label Grid.Row="1" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding ZeitCalculated, Converter={StaticResource secToTime}}" ToolTipProperties.Text="Geleistete Stunden" />
<Label Grid.Row="2" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding OvertimeMonth, Converter={StaticResource secToTime}}" />
<Label Grid.Row="0" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding Overtime, Converter={StaticResource secToTime}}" />
<Label Grid.Row="1" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding Zeitausgleich, Converter={StaticResource secToTime}}" />
<Label Grid.Row="2" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding Holiday, Converter={StaticResource secToTime}}" />
<Label Grid.Row="0" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0"
Text="{Binding Nominal, Converter={StaticResource secToTime}}"
ToolTipProperties.Text="Sollstunden" />
<Label Grid.Row="1" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0"
Text="{Binding ZeitCalculated, Converter={StaticResource secToTime}}"
ToolTipProperties.Text="Geleistete Stunden" />
<Label Grid.Row="2" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0"
Text="{Binding OvertimeMonth, Converter={StaticResource secToTime}}" />
<Label Grid.Row="0" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0"
Text="{Binding Overtime, Converter={StaticResource secToTime}}" />
<Label Grid.Row="1" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0"
Text="{Binding Zeitausgleich, Converter={StaticResource secToTime}}" />
<Label Grid.Row="2" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0"
Text="{Binding Holiday, Converter={StaticResource secToTime}}" />
</Grid>
</Border>

View File

@@ -9,60 +9,80 @@ namespace Jugenddienst_Stunden.Views;
/// Code-Behind f<>r die Stunden-<2D>bersicht
/// </summary>
public partial class StundenPage : ContentPage {
/// <summary>
/// CTOR (f<>r Shell/XAML DataTemplate erforderlich)
/// </summary>
public StundenPage() : this(
(Application.Current?.Handler?.MauiContext?.Services
?? throw new InvalidOperationException("DI container ist nicht verf<72>gbar."))
.GetRequiredService<StundenViewModel>()) {
}
/// <summary>
/// CTOR
/// </summary>
public StundenPage() {
InitializeComponent();
/// <summary>
/// CTOR (DI)
/// </summary>
public StundenPage(StundenViewModel vm) {
InitializeComponent();
BindingContext = vm;
if (BindingContext is StundenViewModel vm) {
vm.AlertEvent += Vm_AlertEvent;
vm.InfoEvent += Vm_InfoEvent;
}
if (!CheckLogin()) {
NavigateToTargetPage();
}
vm.AlertEvent += Vm_AlertEvent;
vm.InfoEvent += Vm_InfoEvent;
}
// Navigation NICHT im CTOR ausf<73>hren (Shell/Navigation-Stack ist hier oft noch nicht ?ready?)
// if (!CheckLogin()) {
// NavigateToTargetPage();
// }
}
private void Vm_AlertEvent(object? sender, string e) {
MainThread.BeginInvokeOnMainThread(async () => {
await DisplayAlert("Fehler:", e, "OK");
});
}
//private void Vm_InfoEvent(object? sender, string e) {
// DisplayAlert("Information:", e, "OK");
//}
//private void Vm_InfoEvent(object? sender, string e) {
// MainThread.BeginInvokeOnMainThread(async () => {
// await DisplayAlert("Information:", e, "OK");
// });
//}
private void Vm_InfoEvent(object? sender, string e) {
MainThread.BeginInvokeOnMainThread(async () => {
CancellationTokenSource cts = new CancellationTokenSource();
ToastDuration duration = ToastDuration.Short;
double fontSize = 16;
var toast = Toast.Make(e, duration, fontSize);
await toast.Show(cts.Token);
});
}
private void Vm_AlertEvent(object? sender, string e) {
MainThread.BeginInvokeOnMainThread(async () => { await DisplayAlert("Fehler:", e, "OK"); });
}
/// <summary>
/// Beim Laden der Seite den Titel setzen
/// </summary>
protected override void OnAppearing() {
base.OnAppearing();
Title = Preferences.Default.Get("name", "Nicht") + " " + Preferences.Default.Get("surname", "eingeloggt");
}
//private void Vm_InfoEvent(object? sender, string e) {
// DisplayAlert("Information:", e, "OK");
//}
//private void Vm_InfoEvent(object? sender, string e) {
// MainThread.BeginInvokeOnMainThread(async () => {
// await DisplayAlert("Information:", e, "OK");
// });
//}
private void Vm_InfoEvent(object? sender, string e) {
MainThread.BeginInvokeOnMainThread(async () => {
CancellationTokenSource cts = new CancellationTokenSource();
ToastDuration duration = ToastDuration.Short;
double fontSize = 16;
var toast = Toast.Make(e, duration, fontSize);
await toast.Show(cts.Token);
});
}
private bool CheckLogin() {
return Preferences.Default.Get("apiKey", "") != "";
}
/// <summary>
/// Beim Laden der Seite den Titel setzen
/// </summary>
protected override async void OnAppearing() {
base.OnAppearing();
Title = Preferences.Default.Get("name", "Nicht") + " " + Preferences.Default.Get("surname", "eingeloggt");
private async void NavigateToTargetPage() {
await Navigation.PushAsync(new LoginPage());
}
if (!CheckLogin()) {
try {
await NavigateToTargetPage();
} catch (Exception ex) {
await DisplayAlert("Fehler:", ex.Message, "OK");
}
}
}
private bool CheckLogin() {
return Preferences.Default.Get("apiKey", "") != "";
}
// private async void NavigateToTargetPage() {
// await Navigation.PushAsync(new LoginPage());
// }
private Task NavigateToTargetPage() {
// Shell-Navigation statt Navigation.PushAsync
// Voraussetzung: LoginPage-Route ist in AppShell registriert (Routing.RegisterRoute(...))
return Shell.Current.GoToAsync(nameof(Views.LoginPage));
}
}