Compare commits

7 Commits

Author SHA1 Message Date
8d512963b5 Add InfoEvent with confirmation 2025-12-19 10:29:30 +01:00
98d6d61f16 Projekt and Gemeinde are required if active 2025-12-18 16:17:17 +01:00
5fd97deada LoginFixes 2025-12-18 15:35:39 +01:00
c11b361655 Refactor LoginPage to MVVM 2025-12-17 17:25:42 +01:00
bb5aac2944 Return Statusmesssage from API 2025-12-17 12:14:15 +01:00
76eb71946f Refactor Api-Client
Add Exceptionhandler, AlertService JSON-Converter
AppSettings via DI

Reformat Code
2025-12-17 09:34:08 +01:00
544b0c9591 Architecture
Add DI, Interfaces, Repositories
2025-12-16 15:27:09 +01:00
90 changed files with 3628 additions and 2151 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" <Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Jugenddienst_Stunden" xmlns:local="clr-namespace:Jugenddienst_Stunden"
@@ -11,4 +12,4 @@
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>
</Application> </Application>

View File

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

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8"?>
<Shell <Shell
x:Class="Jugenddienst_Stunden.AppShell" x:Class="Jugenddienst_Stunden.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
@@ -10,8 +11,9 @@
<ShellContent <ShellContent
Title="Stunden" Title="Stunden"
ContentTemplate="{DataTemplate views:StundenPage}" 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 <ShellContent
Title="Notizen" Title="Notizen"
ContentTemplate="{DataTemplate views:AllNotesPage}" ContentTemplate="{DataTemplate views:AllNotesPage}"

View File

@@ -1,4 +1,5 @@
namespace Jugenddienst_Stunden; namespace Jugenddienst_Stunden;
/// <summary> /// <summary>
/// AppShell.xaml.cs /// AppShell.xaml.cs
/// </summary> /// </summary>
@@ -12,8 +13,8 @@ public partial class AppShell : Shell {
//Seiten, die nicht in der Appshell sichtbar sind, aber trotzdem aufgerufen werden können //Seiten, die nicht in der Appshell sichtbar sind, aber trotzdem aufgerufen werden können
Routing.RegisterRoute(nameof(Views.NotePage), typeof(Views.NotePage)); Routing.RegisterRoute(nameof(Views.NotePage), typeof(Views.NotePage));
Routing.RegisterRoute(nameof(Views.StundePage), typeof(Views.StundePage)); 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 /// Gib true zurück, wenn die Collection Werte enthält
namespace Jugenddienst_Stunden.Converter { namespace Jugenddienst_Stunden.Converter {
internal class CollectionVisibilityConverter : IValueConverter { internal class CollectionVisibilityConverter : IValueConverter {
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
if (value is IEnumerable<object> collection) { if (value is IEnumerable<object> collection) {
if ((string)parameter == "Invert") if ((string)parameter == "Invert")
return !collection.Any(); return !collection.Any();
return collection.Any(); return collection.Any();
} }
if ((string)parameter == "Invert")
return true;
return false;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { if ((string)parameter == "Invert")
throw new NotImplementedException(); return true;
} return false;
} }
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,8 @@
using System.Globalization;
namespace Jugenddienst_Stunden.Converter;
public sealed class EventArgsPassThroughConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => value;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => value;
}

View File

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

View File

@@ -0,0 +1,15 @@
using System.Globalization;
namespace Jugenddienst_Stunden.Converter;
public sealed class InverseBoolConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
if (value is bool b) return !b;
return true;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
if (value is bool b) return !b;
return false;
}
}

View File

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

View File

@@ -1,15 +1,17 @@
using System.Globalization; using System.Globalization;
namespace Jugenddienst_Stunden.Converter; 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) { internal class StringVisibilityConverter : IValueConverter {
throw new NotImplementedException(); 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

@@ -4,9 +4,15 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Jugenddienst_Stunden.Exceptions; namespace Jugenddienst_Stunden.Exceptions;
public class NoDataException : Exception { public class NoDataException : Exception {
public NoDataException() : base("Keine Daten gefunden") { } public NoDataException() : base("Keine Daten gefunden") {
public NoDataException(string message) : base(message) { } }
public NoDataException(string message, Exception inner) : base(message, inner) { }
} 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,150 @@
using Jugenddienst_Stunden.Interfaces;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using ZXing.Aztec.Internal;
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;
// Timeout nur einmalig beim Erstellen setzen spätere Änderungen an HttpClient.Timeout
// nach der ersten Verwendung führen zu InvalidOperationException.
if (_http.Timeout != options.Timeout)
_http.Timeout = options.Timeout;
// Standardmäßig JSON akzeptieren; doppelte Einträge vermeiden
if (!_http.DefaultRequestHeaders.Accept.Any(h => h.MediaType?.Equals("application/json", StringComparison.OrdinalIgnoreCase) == true))
_http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// KEINE globalen Header/Properties mehr dynamisch setzen. Authorization wird pro Request gesetzt.
_json = new JsonSerializerOptions {
PropertyNameCaseInsensitive = true,
WriteIndented = false,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
// 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());
// WICHTIG: HttpClient.BaseAddress NICHT dynamisch setzen oder ändern das verursacht Exceptions,
// sobald bereits Requests gestartet wurden. Wir bauen stattdessen absolute URIs pro Request.
}
public Task<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) {
// Absolute URI aus aktuellem SettingsBaseUrl bauen, ohne HttpClient.BaseAddress zu nutzen.
var uri = BuildAbsoluteUri(_settings.ApiUrl, path, query);
using var req = new HttpRequestMessage(method, uri);
// Authorization PRO REQUEST setzen (immer, wenn Token vorhanden ist)
// Hinweis: Das QR-Token kann RFC-unzulässige Zeichen (z. B. '|') enthalten.
// AuthenticationHeaderValue würde solche Werte ablehnen. Daher ohne Validierung setzen.
var currentToken = _settings.ApiKey;
if (!string.IsNullOrWhiteSpace(currentToken)) {
// Vorherige Header (falls vorhanden) entfernen, um Duplikate zu vermeiden
req.Headers.Remove("Authorization");
req.Headers.TryAddWithoutValidation("Authorization", $"Bearer {currentToken}");
}
if (body is HttpContent httpContent) {
req.Content = httpContent;
} else if (body is not null) {
req.Content = JsonContent.Create(body, options: _json);
}
// Sicherstellen, dass Accept: application/json auch auf Request-Ebene vorhanden ist (z. B. für LoginWithToken GET)
if (!req.Headers.Accept.Any(h => h.MediaType?.Equals("application/json", StringComparison.OrdinalIgnoreCase) == true)) {
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
using var res = await _http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
var text = await res.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
if (!res.IsSuccessStatusCode)
throw ApiException.From(res.StatusCode, TryGetMessage(text), text);
if (res.StatusCode != System.Net.HttpStatusCode.OK) {
// Verhalten wie in BaseFunc: bei Fehlerstatus -> "message" aus Body lesen und mit dessen Inhalt eine Exception werfen.
try {
var options = new JsonDocumentOptions { AllowTrailingCommas = true };
using var doc = JsonDocument.Parse(text, options);
var root = doc.RootElement;
// GetProperty wirft, wenn "message" fehlt — das entspricht dem bisherigen Verhalten in BaseFunc.
var messageElement = root.GetProperty("message");
if (messageElement.ValueKind != JsonValueKind.String)
throw ApiException.From(res.StatusCode, "Fehler: 'message' ist null.", text);
var message = messageElement.GetString() ?? throw ApiException.From(res.StatusCode, "Fehler: 'message' ist null.", text);
throw ApiException.From(res.StatusCode, message, text);
} catch (ApiException) {
throw;
} catch (Exception) {
// Fallback: Wenn Parsing fehlschlägt oder "message" fehlt, konsistente Fehlermeldung wie BaseFunc
throw ApiException.From(res.StatusCode, "Fehler: 'message' ist null.", 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);
// Entfernt: EnsureBaseAddress wir ändern BaseAddress nicht mehr dynamisch.
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 BuildAbsoluteUri(string baseUrl, string path, IDictionary<string, string?>? query) {
if (string.IsNullOrWhiteSpace(baseUrl))
throw new InvalidOperationException(
"ApiUrl ist leer. Bitte zuerst eine gültige Server-URL setzen (Preferences key 'apiUrl').");
// Basis muss absolut sein (z. B. https://host/appapi/)
var baseUri = new Uri(baseUrl, UriKind.Absolute);
// Pfad relativ zur Basis aufbauen
string relativePath = path ?? string.Empty;
if (query is not null && query.Count > 0) {
var sb = new StringBuilder(relativePath);
sb.Append(relativePath.Contains('?') ? '&' : '?');
sb.Append(string.Join('&', query
.Where(kv => kv.Value is not null)
.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value!)}")));
relativePath = sb.ToString();
}
// Wenn path bereits absolut ist, direkt verwenden
if (Uri.TryCreate(relativePath, UriKind.Absolute, out var absoluteFromPath))
return absoluteFromPath;
return new Uri(baseUri, relativePath);
}
}

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,33 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jugenddienst_Stunden.Infrastructure;
internal sealed class RequestLoggingHandler : DelegatingHandler {
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
// Log outgoing request URI + headers
Debug.WriteLine($"[Http] Request: {request.Method} {request.RequestUri}");
foreach (var h in request.Headers) {
Debug.WriteLine($"[Http] RequestHeader: {h.Key} = {string.Join(", ", h.Value)}");
}
if (request.Content is not null) {
foreach (var h in request.Content.Headers) {
Debug.WriteLine($"[Http] ContentHeader: {h.Key} = {string.Join(", ", h.Value)}");
}
}
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
// Log response status + Location (bei Redirects) + final request URI used by handler
Debug.WriteLine($"[Http] Response: {(int)response.StatusCode} {response.ReasonPhrase}");
if (response.Headers.Location is not null)
Debug.WriteLine($"[Http] Response Location: {response.Headers.Location}");
if (response.RequestMessage?.RequestUri is not null)
Debug.WriteLine($"[Http] Final RequestUri: {response.RequestMessage.RequestUri}");
return response;
}
}

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

@@ -0,0 +1,8 @@
namespace Jugenddienst_Stunden.Interfaces;
using Jugenddienst_Stunden.Types;
public interface IAuthService {
Task<User> LoginWithCredentials(string username, string password, string serverUrl, CancellationToken ct = default);
Task<User> LoginWithToken(string token, CancellationToken ct = default);
}

View File

@@ -0,0 +1,17 @@
using Jugenddienst_Stunden.Types;
namespace Jugenddienst_Stunden.Interfaces;
/// <summary>
/// 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);
}

View File

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

View File

@@ -1,7 +1,14 @@
using CommunityToolkit.Maui; using CommunityToolkit.Maui;
using Jugenddienst_Stunden.Models; 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 Microsoft.Extensions.Logging;
using ZXing.Net.Maui.Controls; using ZXing.Net.Maui.Controls;
using System.Net.Http;
using Jugenddienst_Stunden.ViewModels;
namespace Jugenddienst_Stunden; namespace Jugenddienst_Stunden;
@@ -9,35 +16,96 @@ namespace Jugenddienst_Stunden;
/// Das Hauptprogramm. /// Das Hauptprogramm.
/// </summary> /// </summary>
public static class MauiProgram { 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() { //#if DEBUG
var builder = MauiApp.CreateBuilder(); // if (string.IsNullOrWhiteSpace(GlobalVar.ApiKey)) {
builder // GlobalVar.ApiKey = Preferences.Default.Get("apiKey",
.UseMauiApp<App>() // "MTQxfHNkdFptQkNZTXlPT3ZyMHNBZDl0UnVxNExMRXxodHRwOi8vaG91cnMuZGF1bmkubWluZS5udTo4MS9hcHBhcGk=");
// Initialize the .NET MAUI Community Toolkit by adding the below line of code // GlobalVar.Name = Preferences.Default.Get("name", "Testserver: Isabell");
.UseMauiCommunityToolkit(options => { // GlobalVar.Surname = Preferences.Default.Get("surname", "Biasi");
options.SetShouldEnableSnackbarOnWindows(true); // GlobalVar.EmployeeId = Preferences.Default.Get("EmployeeId", 141);
}) // GlobalVar.ApiUrl = Preferences.Default.Get("apiUrl", "https://hours.dauni.mine.nu/appapi");
.ConfigureFonts(fonts => { // }
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
})
//.UseBarcodeScanning();
.UseBarcodeReader();
#if DEBUG // builder.Logging.AddDebug();
if (GlobalVar.ApiKey == null) { //#endif
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
return builder.Build(); // DI: AlertService für globale Alerts (z. B. leere ApiUrl)
} builder.Services.AddSingleton<IAlertService, AlertService>();
// 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
// Configure HttpClient with RequestLoggingHandler and disable automatic redirects for diagnosis
builder.Services.AddTransient<RequestLoggingHandler>();
builder.Services.AddSingleton<HttpClient>(sp => {
var nativeHandler = new HttpClientHandler { AllowAutoRedirect = false };
var logging = sp.GetRequiredService<RequestLoggingHandler>();
logging.InnerHandler = nativeHandler;
// HttpClient.Timeout will be adjusted by ApiClient if needed
return new HttpClient(logging, disposeHandler: true);
});
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>();
builder.Services.AddSingleton<IAuthService, AuthService>();
// DI: Views/ViewModels
builder.Services.AddTransient<ViewModels.StundenViewModel>();
builder.Services.AddTransient<Views.StundenPage>();
builder.Services.AddTransient<ViewModels.LoginViewModel>();
builder.Services.AddTransient<Views.LoginPage>();
return builder.Build();
}
}

View File

@@ -7,186 +7,175 @@ using System.Text.Json;
namespace Jugenddienst_Stunden.Models; namespace Jugenddienst_Stunden.Models;
internal static class BaseFunc { 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) var content = new FormUrlEncodedContent(values);
throw new Exception("Bitte überprüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.");
if (string.IsNullOrEmpty(token)) using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
throw new Exception("Kein APIKEY, bitte zuerst Login durchführen"); client.DefaultRequestHeaders.Add("Accept", "application/json");
// Erstellen eines HttpClient-Objekts // Senden der Anfrage und Abrufen der Antwort
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) { 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 string responseData = await HttpContent.ReadAsStringAsync();
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); 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 return null;
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);
}
}
}
}
}
/// <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> /// <summary>
{ /// Stundeneintrag speichern
{ "user", user }, /// </summary>
{ "pass", pass } 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 //Projekt ist ein Pflichtfeld
using (HttpResponseMessage HttpResponseMessage = await client.PostAsync(url, content).ConfigureAwait(false)) { if (item.ProjektAktiv == null && GlobalVar.Settings.ProjektAktivSet) {
if (!HttpResponseMessage.IsSuccessStatusCode) throw new Exception("Projekt nicht gewählt");
{ }
//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);
}
}
// Überprüfen, ob die Anfrage erfolgreich war //Keine Beschreibung
if (HttpResponseMessage.StatusCode == System.Net.HttpStatusCode.OK) { if (string.IsNullOrEmpty(item.Description) && item.FreistellungAktiv == null) {
using (HttpContent HttpContent = HttpResponseMessage.Content) { throw new Exception("Keine Beschreibung");
// Lesen und Rückgabe der Antwort als String }
string responseData = await HttpContent.ReadAsStringAsync(); //Keine Beschreibung
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(responseData) ?? throw new Exception("Fehler beim Deserialisieren der Daten"); if (string.IsNullOrEmpty(item.Description)) {
//User userData = System.Text.Json.JsonSerializer.Deserialize<User>(responseData) ?? throw new Exception("Fehler beim Deserialisieren der Daten"); item.Description = item.FreistellungAktiv.Name;
return res.user; }
}
}
}
} using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
return null; //HttpClient client = new HttpClient();
} client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
/// <summary> //string json = JsonSerializer.Serialize<DayTime>(item);
/// Notiz laden string json = JsonConvert.SerializeObject(item);
/// </summary> StringContent content = new StringContent(json, Encoding.UTF8, "application/json");
internal static Note Load(string filename) {
filename = System.IO.Path.Combine(FileSystem.AppDataDirectory, filename);
if (!File.Exists(filename)) HttpResponseMessage? response = null;
throw new FileNotFoundException("Unable to find file on local storage.", filename); if (isNewItem)
response = await client.PostAsync(url, content);
else
response = await client.PutAsync(url, content);
return if (!response.IsSuccessStatusCode) {
new() { throw new Exception("Fehler beim Speichern " + response.Content);
Date = File.GetLastWriteTime(filename) }
}; }
} }
/// <summary> /// <summary>
/// Stundeneintrag speichern /// Stundeneintrag löschen
/// </summary> /// </summary>
internal static async Task SaveItemAsync(string url, string token, DayTime item, bool isNewItem = false) { 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 HttpResponseMessage response = await client.DeleteAsync(url);
if (item.TimeSpanVon == item.TimeSpanBis && item.FreistellungAktiv == null) {
throw new Exception("Beginn und Ende sind gleich");
}
if (item.TimeSpanBis < item.TimeSpanVon) { if (!response.IsSuccessStatusCode)
throw new Exception("Ende ist vor Beginn"); throw new Exception("Fehler beim Löschen " + response.Content);
} }
}
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);
}
}
}

View File

@@ -0,0 +1,21 @@
using System.Threading.Tasks;
namespace Jugenddienst_Stunden.Models;
public sealed class ConfirmationEventArgs : System.EventArgs {
public string Title { get; }
public string Message { get; }
public string ConfirmText { get; }
private readonly TaskCompletionSource<bool> _tcs = new();
public ConfirmationEventArgs(string title, string message, string confirmText = "OK") {
Title = title;
Message = message;
ConfirmText = confirmText;
}
public Task<bool> Task => _tcs.Task;
public void SetResult(bool result) => _tcs.TrySetResult(result);
}

View File

@@ -1,26 +1,32 @@
using Jugenddienst_Stunden.Types; using Jugenddienst_Stunden.Types;
namespace Jugenddienst_Stunden.Models; namespace Jugenddienst_Stunden.Models;
internal static class GlobalVar { internal static class GlobalVar {
public static string ApiKey { public static string ApiKey {
get => Preferences.Default.Get("apiKey", ""); get => Preferences.Default.Get("apiKey", "");
set => Preferences.Default.Set("apiKey", value); set => Preferences.Default.Set("apiKey", value);
} }
public static int EmployeeId {
get => Preferences.Default.Get("EmployeeId", 0); public static int EmployeeId {
set => Preferences.Default.Set("EmployeeId", value); 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 Name {
} get => Preferences.Default.Get("name", "Nicht");
public static string Surname { set => Preferences.Default.Set("name", value);
get => Preferences.Default.Get("surname", "Eingeloggt"); }
set => Preferences.Default.Set("surname", value);
} public static string Surname {
public static string ApiUrl { get => Preferences.Default.Get("surname", "Eingeloggt");
get => Preferences.Default.Get("apiUrl", ""); set => Preferences.Default.Set("surname", value);
set => Preferences.Default.Set("apiUrl", value); }
}
public static Settings Settings { get; set; } 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; namespace Jugenddienst_Stunden.Models;
internal static class HoursBase { 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> /// <summary>
/// Laden ... what can be: "settings", "hours", date="YYYY-MM-DD", id=<int/> /// Einstellungen laden
/// </summary> /// </summary>
/// <returns>Entire response</returns> /// <returns>Settings only</returns>
internal static async Task<BaseResponse> LoadBase(string what) { internal static async Task<Settings> LoadSettings() {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?"+what, GlobalVar.ApiKey); string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?settings", GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten"); BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
return res; throw new Exception("Fehler beim Deserialisieren der Daten");
} return res.settings;
}
/// <summary> /// <summary>
/// Einstellungen laden /// Daten laden
/// </summary> /// </summary>
/// <returns>Settings only</returns> /// <returns>Hours only</returns>
internal static async Task<Settings> LoadSettings() { internal static async Task<Hours> LoadData() {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?settings", GlobalVar.ApiKey); string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?hours", GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten"); BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
return res.settings; throw new Exception("Fehler beim Deserialisieren der Daten");
} return res.hour;
}
/// <summary> /// <summary>
/// Daten laden /// Benutzerdaten laden
/// </summary> /// </summary>
/// <returns>Hours only</returns> /// <returns>User-Object</returns>
internal static async Task<Hours> LoadData() { public static async Task<User> LoadUser(string apiKey) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?hours", GlobalVar.ApiKey); string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl, apiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten"); BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
return res.hour; throw new Exception("Fehler beim Deserialisieren der Daten");
} return res.user;
}
/// <summary> /// <summary>
/// Benutzerdaten laden /// Zeiten eines Tages holen
/// </summary> /// </summary>
/// <returns>User-Object</returns> internal static async Task<List<DayTime>> LoadDay(DateTime date) {
public static async Task<User> LoadUser(string apiKey) { string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?date=" + date.ToString("yyyy-MM-dd"),
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl, apiKey); GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten"); BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
return res.user; throw new Exception("Fehler beim Deserialisieren der Daten");
} return res.daytimes;
}
/// <summary> /// <summary>
/// Zeiten eines Tages holen /// Einzelnen Stundeneintrag holen
/// </summary> /// </summary>
internal static async Task<List<DayTime>> LoadDay(DateTime date) { internal static async Task<DayTime> LoadEntry(int id) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?date=" + date.ToString("yyyy-MM-dd"), GlobalVar.ApiKey); 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"); BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
return res.daytimes; 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> /// <summary>
/// Einzelnen Stundeneintrag holen /// Eintrag speichern
/// </summary> /// </summary>
internal static async Task<DayTime> LoadEntry(int id) { internal static async Task<DayTime> SaveEntry(DayTime stunde) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?id=" + id, GlobalVar.ApiKey); //, string begin, string end, string freistellung, string bemerkung) {
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten"); bool isNew = false;
res.daytime.TimeSpanVon = res.daytime.Begin.ToTimeSpan(); if (stunde.Id == null)
res.daytime.TimeSpanBis = res.daytime.End.ToTimeSpan(); isNew = true;
return res.daytime; await BaseFunc.SaveItemAsync(GlobalVar.ApiUrl, GlobalVar.ApiKey, stunde, isNew);
}
/// <summary> return stunde;
/// 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; /// <summary>
} /// Eintrag löschen
/// </summary>
/// <summary> internal static async Task DeleteEntry(DayTime stunde) {
/// Eintrag löschen await BaseFunc.DeleteItemAsync(GlobalVar.ApiUrl + "/entry/" + stunde.Id, GlobalVar.ApiKey);
/// </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); 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); var contract = serializer.ContractResolver.ResolveContract(objectType);
if (!(contract is Newtonsoft.Json.Serialization.JsonObjectContract || contract is Newtonsoft.Json.Serialization.JsonDictionaryContract)) { if (!(contract is Newtonsoft.Json.Serialization.JsonObjectContract ||
throw new JsonSerializationException(string.Format("Unsupported objectType {0} at {1}.", objectType, reader.Path)); contract is Newtonsoft.Json.Serialization.JsonDictionaryContract)) {
throw new JsonSerializationException(string.Format("Unsupported objectType {0} at {1}.", objectType,
reader.Path));
} }
switch (reader.SkipComments().TokenType) { switch (reader.SkipComments().TokenType) {
case JsonToken.StartArray: { case JsonToken.StartArray: {
int count = 0; int count = 0;
while (reader.Read()) { while (reader.Read()) {
switch (reader.TokenType) { switch (reader.TokenType) {
case JsonToken.Comment: case JsonToken.Comment:
break; break;
case JsonToken.EndArray: case JsonToken.EndArray:
return existingValue; return existingValue;
default: { default: {
count++; count++;
if (count > 1) if (count > 1)
throw new JsonSerializationException(string.Format("Too many objects at path {0}.", reader.Path)); throw new JsonSerializationException(string.Format("Too many objects at path {0}.",
existingValue = existingValue ?? contract.DefaultCreator(); reader.Path));
serializer.Populate(reader, existingValue); existingValue = existingValue ?? contract.DefaultCreator();
} serializer.Populate(reader, existingValue);
break;
} }
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: case JsonToken.Null:
return null; return null;
@@ -67,4 +74,4 @@ namespace Jugenddienst_Stunden.Models {
return reader; return reader;
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,10 @@
using Android.Content.PM; using Android.Content.PM;
using Android.OS; using Android.OS;
namespace Jugenddienst_Stunden; 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 { public class MainActivity : MauiAppCompatActivity {
} }

View File

@@ -1,7 +1,7 @@
using Android.App; using Android.App;
using Android.Runtime; using Android.Runtime;
namespace Jugenddienst_Stunden; namespace Jugenddienst_Stunden;
#if DEBUG #if DEBUG
[Application(UsesCleartextTraffic = true)] [Application(UsesCleartextTraffic = true)]
#else #else
@@ -13,4 +13,4 @@ public class MainApplication : MauiApplication {
} }
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
} }

View File

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

View File

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

View File

@@ -5,4 +5,4 @@
xmlns:maui="using:Microsoft.Maui" xmlns:maui="using:Microsoft.Maui"
xmlns:local="using:Jugenddienst_Stunden.WinUI"> xmlns:local="using:Jugenddienst_Stunden.WinUI">
</maui:MauiWinUIApplication> </maui:MauiWinUIApplication>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,80 @@
using Jugenddienst_Stunden.Interfaces;
using Jugenddienst_Stunden.Infrastructure;
using Jugenddienst_Stunden.Models;
using Jugenddienst_Stunden.Types;
namespace Jugenddienst_Stunden.Repositories;
/// <summary>
/// Standard-Repository, das die bestehende API-/Model-Logik kapselt.
/// </summary>
internal class HoursRepository : IHoursRepository {
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"?> <?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"> <!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;"/> <rect x="0" y="0" width="456" height="456" style="fill:white;"/>
</svg> </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"?> <?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"> <!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 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)"> <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>
<g transform="matrix(0,12.0607,12.0607,0,87.6884,17.5193)"> <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>
<g transform="matrix(12.0607,0,0,12.0607,32.4324,105.748)"> <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>
<g transform="matrix(12.0607,0,0,12.0607,109.437,86.7078)"> <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> </g>
<g id="Uhr" transform="matrix(0.0220493,0,0,0.0220493,267.879,222.51)"> <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)"> <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>
<g transform="matrix(4.14211,-0.451661,0.451661,4.14211,-6813.16,744.211)"> <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>
<g transform="matrix(-4.09852,-0.750473,-0.750473,4.09852,7333.98,1287.55)"> <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>
<g transform="matrix(4.16667,0,0,4.16667,-6599.04,0)"> <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>
<g transform="matrix(4.16667,0,0,4.16667,-6599.04,0)"> <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>
<g transform="matrix(4.16667,0,0,4.16667,-6599.04,0)"> <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>
<g transform="matrix(4.16667,0,0,4.16667,-6599.04,0)"> <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>
</g> </g>
</svg> </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"?> <?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"> <!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>
<g transform="matrix(12.0607,0,0,12.0607,201.169,356)"> <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>
<g transform="matrix(0,12.0607,12.0607,0,187.688,117.519)"> <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>
<g transform="matrix(12.0607,0,0,12.0607,132.432,205.748)"> <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>
<g transform="matrix(12.0607,0,0,12.0607,132.432,205.748)"> <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>
<g transform="matrix(12.0607,0,0,12.0607,209.437,186.708)"> <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>
</g> </g>
</svg> </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"?> <?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"> <!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>
<g transform="matrix(12.0607,0,0,12.0607,201.169,356)"> <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>
<g transform="matrix(0,12.0607,12.0607,0,187.688,117.519)"> <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>
<g transform="matrix(12.0607,0,0,12.0607,132.432,205.748)"> <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>
<g transform="matrix(12.0607,0,0,12.0607,132.432,205.748)"> <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>
<g transform="matrix(12.0607,0,0,12.0607,209.437,186.708)"> <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>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

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

View File

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

View File

@@ -0,0 +1,94 @@
using System.Net.Http;
using System.Text;
using Jugenddienst_Stunden.Infrastructure;
using Jugenddienst_Stunden.Interfaces;
using Jugenddienst_Stunden.Models;
using Jugenddienst_Stunden.Types;
namespace Jugenddienst_Stunden.Services;
internal sealed class AuthService : IAuthService {
private readonly IApiClient _api;
private readonly IAppSettings _settings;
private readonly IAlertService _alerts;
public AuthService(IApiClient api, IAppSettings settings, IAlertService alerts) {
_api = api;
_settings = settings;
_alerts = alerts;
}
public async Task<User> LoginWithCredentials(string username, string password, string serverUrl, CancellationToken ct = default) {
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
throw new Exception("Benutzername und Passwort werden benötigt.");
var apiBase = NormalizeApiUrl(serverUrl);
_settings.ApiUrl = apiBase; // BaseAddress für IApiClient setzen
var content = new FormUrlEncodedContent(new[] {
new KeyValuePair<string, string>("user", username),
new KeyValuePair<string, string>("pass", password)
});
// POST ohne Pfad die API erwartet /appapi
// Wichtig: Basis-URL hat garantiert einen abschließenden Slash (…/appapi/),
// sodass ein leerer Pfad nicht zu Redirects führt (die den POST in GET verwandeln könnten).
var res = await _api.SendAsync<BaseResponse>(HttpMethod.Post, string.Empty, content, null, ct).ConfigureAwait(false);
if (res.user is null)
throw new Exception(res.message ?? "Ungültige Antwort vom Server.");
ApplyUser(res.user, apiBase);
return res.user;
}
public async Task<User> LoginWithToken(string token, CancellationToken ct = default) {
if (string.IsNullOrWhiteSpace(token)) throw new Exception("Kein Token erkannt.");
// QR-Token enthält die URL extrahiere sie
var td = new TokenData(token);
// URL aus dem Token ebenfalls normalisieren, damit sie auf "/appapi/" endet
_settings.ApiUrl = NormalizeApiUrl(td.Url);
_settings.ApiKey = token;
var res = await _api.GetAsync<BaseResponse>(string.Empty, null, ct).ConfigureAwait(false);
if (res.user is null)
throw new Exception(res.message ?? "Ungültige Antwort vom Server.");
ApplyUser(res.user, td.Url);
return res.user;
}
private void ApplyUser(User user, string apiBase) {
_settings.ApiUrl = apiBase;
// Wenn der Server keinen Token im User zurückliefert (QR-Login-Fall), bestehenden Token beibehalten
var tokenToUse = string.IsNullOrWhiteSpace(user.Token) ? _settings.ApiKey : user.Token;
_settings.ApiKey = tokenToUse;
_settings.EmployeeId = user.Id;
_settings.Name = user.Name;
_settings.Surname = user.Surname;
}
private static string NormalizeApiUrl(string input) {
if (string.IsNullOrWhiteSpace(input)) throw new Exception("Server-URL wird benötigt.");
var url = input.Trim();
if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) {
url = "https://" + url;
}
// Sicherstellen, dass der Pfad auf "/appapi" endet
if (!url.EndsWith("/appapi", StringComparison.OrdinalIgnoreCase)) {
url = url.TrimEnd('/') + "/appapi";
}
// WICHTIG: Einen abschließenden Slash erzwingen, damit relative Pfade korrekt angehängt werden
// und damit POST auf Basis-URL (leerem Pfad) nicht zu einem 301/302-Redirect führt,
// der den Body (user/pass) verlieren könnte.
//if (!url.EndsWith("/", StringComparison.Ordinal)) {
// url += "/";
//}
if (url.EndsWith("/", StringComparison.Ordinal)) {
url = url.TrimEnd('/');
}
return url;
}
}

View File

@@ -0,0 +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 IHoursValidator _validator;
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}";
var baseRes = await _repo.LoadBase(q);
// 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>> 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<DayTime> GetEntryAsync(int id) => await _repo.LoadEntry(id);
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);
}

View File

@@ -1,36 +1,37 @@
using Jugenddienst_Stunden.Models; using Jugenddienst_Stunden.Models;
namespace Jugenddienst_Stunden.Types; namespace Jugenddienst_Stunden.Types;
internal class BaseResponse { internal class BaseResponse {
public Settings settings { get; set; }
public Settings settings { get; set; }
/// <summary> /// <summary>
/// Monatsübersicht /// Monatsübersicht
/// </summary> /// </summary>
public Hours hour { get; set; } public Hours hour { get; set; }
/// <summary> /// <summary>
/// Stundenliste ... für die Katz? /// Stundenliste ... für die Katz?
/// </summary> /// </summary>
public List<Hours> hours { get; set; } public List<Hours> hours { get; set; }
/// <summary> /// <summary>
/// Liste der Stundeneinträge /// Liste der Stundeneinträge
/// </summary> /// </summary>
public List<DayTime> daytimes { get; set; } public List<DayTime> daytimes { get; set; }
/// <summary> /// <summary>
/// Einzelner Stundeneintrag /// Einzelner Stundeneintrag
/// </summary> /// </summary>
public DayTime daytime { get; set; } public DayTime daytime { get; set; }
/// <summary> /// <summary>
/// Auch irgendwie doppelt ... /// Auch irgendwie doppelt ...
/// </summary> /// </summary>
public Operator operatorVar { get; set; } public Operator operatorVar { get; set; }
public User user { get; set; }
public int error { get; set; } public User user { 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; using System.Collections.ObjectModel;
namespace Jugenddienst_Stunden.Types; namespace Jugenddienst_Stunden.Types;
/// <summary> /// <summary>
/// Represents a day time entry for an employee. /// Represents a day time entry for an employee.
/// </summary> /// </summary>
public class DayTime { public class DayTime {
/// <summary> /// <summary>
/// ID des Stundeneintrages /// ID des Stundeneintrages
/// </summary> /// </summary>
public int? Id { get; set; } public int? Id { get; set; }
/// <summary> /// <summary>
/// Mitarbeiter-ID /// Mitarbeiter-ID
/// </summary> /// </summary>
public int EmployeeId { get; set; } public int EmployeeId { get; set; }
/// <summary> /// <summary>
/// Der betreffende Tag /// Der betreffende Tag
/// </summary> /// </summary>
public DateTime Day { get; set; } public DateTime Day { get; set; }
/// <summary> /// <summary>
/// Der Wochentag /// Der Wochentag
/// </summary> /// </summary>
public int Wday { get; set; } public int Wday { get; set; }
/// <summary> /// <summary>
/// Arbeitsbeginn /// Arbeitsbeginn
/// </summary> /// </summary>
public TimeOnly Begin { get; set; } public TimeOnly Begin { get; set; }
/// <summary> /// <summary>
/// Arbeitsende /// Arbeitsende
/// </summary> /// </summary>
public TimeOnly End { get; set; } public TimeOnly End { get; set; }
/// <summary> /// <summary>
/// Beschreibung der Tätigkeit /// Beschreibung der Tätigkeit
/// </summary> /// </summary>
public string? Description { get; set; } public string? Description { get; set; }
/// <summary> /// <summary>
/// Freistellung /// Freistellung
/// </summary> /// </summary>
public string? Free { get; set; } public string? Free { get; set; }
/// <summary> /// <summary>
/// Freistellung genehmigt? /// Freistellung genehmigt?
/// </summary> /// </summary>
public bool Approved { get; set; } public bool Approved { get; set; }
/// <summary> /// <summary>
/// Das gewählte Projekt /// Das gewählte Projekt
/// </summary> /// </summary>
public int? Projekt { get; set; } public int? Projekt { get; set; }
/// <summary> /// <summary>
/// Die gewählte Gemeinde /// Die gewählte Gemeinde
/// </summary> /// </summary>
public int? Gemeinde { get; set; } public int? Gemeinde { get; set; }
/// <summary> /// <summary>
/// Nachtstunden /// Nachtstunden
/// </summary> /// </summary>
public TimeOnly Night { get; set; } public TimeOnly Night { get; set; }
/// <summary> /// <summary>
/// Summe Arbeitszeit (inklusive Nachstunden mit Faktor) /// Summe Arbeitszeit (inklusive Nachstunden mit Faktor)
/// </summary> /// </summary>
public Dictionary<string, TimeOnly> Total { get; set; } public Dictionary<string, TimeOnly> Total { get; set; }
public TimeOnly End_print { get; set; } public TimeOnly End_print { get; set; }
public TimeSpan TimeSpanVon { get; set; } public TimeSpan TimeSpanVon { get; set; }
public TimeSpan TimeSpanBis { get; set; } public TimeSpan TimeSpanBis { get; set; }
/// <summary> /// <summary>
/// Gets the active Gemeinde based on the gemeinde ID. /// Gets the active Gemeinde based on the gemeinde ID.
/// </summary> /// </summary>
public Gemeinde? GemeindeAktiv { get; set; } public Gemeinde? GemeindeAktiv { get; set; }
/// <summary> /// <summary>
/// Gets the active Projekt based on the projekt ID. /// Gets the active Projekt based on the projekt ID.
/// </summary> /// </summary>
public Projekt? ProjektAktiv { get; set; } public Projekt? ProjektAktiv { get; set; }
/// <summary> /// <summary>
/// Gets the active Freistellung based on the Freistellung ID /// Gets the active Freistellung based on the Freistellung ID
/// </summary> /// </summary>
public Freistellung? FreistellungAktiv { get; set; } public Freistellung? FreistellungAktiv { get; set; }
public int TimeTable { get; set; } public int TimeTable { get; set; }
}
}

View File

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

View File

@@ -13,4 +13,4 @@ public class Gemeinde {
/// Name der Gemeinde. /// Name der Gemeinde.
/// </summary> /// </summary>
public string? Name { 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; using System.Collections.ObjectModel;
namespace Jugenddienst_Stunden.Types; namespace Jugenddienst_Stunden.Types;
internal partial class Hours : ObservableObject { public partial class Hours : ObservableObject {
public double? Zeit; public double? Zeit;
public double? Nominal; public double? Nominal;
//public Dictionary<DateOnly,NominalDay> nominal_day_api; //public Dictionary<DateOnly,NominalDay> nominal_day_api;
public List<NominalDay>? Nominal_day_api; public List<NominalDay>? Nominal_day_api;
//public Dictionary<int,NominalWeek> nominal_week_api; //public Dictionary<int,NominalWeek> nominal_week_api;
public List<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 //public List<string> time_line;
//[JsonConverter(typeof(JsonSingleOrEmptyArrayConverter<Hours>))] public double? Zeit_total;
//public Dictionary<int,decimal> zeit_total_daily;
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<DayTime>? daytime;
//public List<string> wochensumme; //public List<string> wochensumme;
[ObservableProperty] [ObservableProperty] public double overtime_month;
public double overtime_month;
[ObservableProperty] [ObservableProperty] public double overtime;
public double overtime; //public List<string> overtime_day;
//public List<string> overtime_day;
[ObservableProperty] [ObservableProperty] public double zeitausgleich;
public double zeitausgleich;
public double zeitausgleich_month; public double zeitausgleich_month;
public double holiday; public double holiday;
public double krankheit; public double krankheit;
public double weiterbildung; public double weiterbildung;
public double bereitschaft; public double bereitschaft;
public double bereitschaft_month; public double bereitschaft_month;
//public Operator operator_api; //public Operator operator_api;
public DateTime Today; public DateTime Today;
public DateTime Date; public DateTime Date;
@@ -47,5 +49,4 @@ internal partial class Hours : ObservableObject {
public Collection<Gemeinde> Gemeinden { get; set; } public Collection<Gemeinde> Gemeinden { get; set; }
public Collection<Freistellung> Freistellungen { get; set; } public Collection<Freistellung> Freistellungen { get; set; }
public int EmployeeId { get; set; } public int EmployeeId { get; set; }
}
}

View File

@@ -1,7 +1,8 @@
namespace Jugenddienst_Stunden.Types; namespace Jugenddienst_Stunden.Types;
internal class NominalDay {
public class NominalDay {
public int day_number; public int day_number;
public int month_number; public int month_number;
public double hours; public double hours;
public DateOnly date; public DateOnly date;
} }

View File

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

View File

@@ -13,4 +13,4 @@ public class Projekt {
/// Holt oder setzt den Namen des Projekts. /// Holt oder setzt den Namen des Projekts.
/// </summary> /// </summary>
public string Name { get; set; } public string Name { get; set; }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
namespace Jugenddienst_Stunden.Types; namespace Jugenddienst_Stunden.Types;
internal class User {
public class User {
public int Id { get; set; } public int Id { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string Surname { get; set; } public string Surname { get; set; }
public string Token { get; set; } public string Token { 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

@@ -1,31 +1,161 @@
namespace Jugenddienst_Stunden.ViewModels; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Jugenddienst_Stunden.Interfaces;
using Jugenddienst_Stunden.Models;
namespace Jugenddienst_Stunden.ViewModels;
/// <summary> /// <summary>
/// Die Loginseite /// ViewModel für die Loginseite (MVVM)
/// </summary> /// </summary>
public class LoginViewModel { public partial class LoginViewModel : ObservableObject {
/// <summary> private readonly IAuthService _auth;
/// Name der Anwendung private readonly IAppSettings _settings;
/// </summary> private readonly IAlertService? _alerts;
public string AppTitle => AppInfo.Name; private DateTime _lastDetectionTime = DateTime.MinValue;
private readonly TimeSpan _detectionInterval = TimeSpan.FromSeconds(5);
/// <summary> public event EventHandler<string>? AlertEvent;
/// Programmversion //public event EventHandler<string>? InfoEvent;
/// </summary> public event EventHandler<ConfirmationEventArgs>? InfoEvent;
public string Version => AppInfo.VersionString;
/// <summary> /// <summary>
/// Kurze Mitteilung für den Anwender /// Name der Anwendung
/// </summary> /// </summary>
public string Message { get; set; } = "Scanne den QR-Code von deinem Benutzerprofil auf der Stundenseite."; public string AppTitle => AppInfo.Name;
/// <summary> /// <summary>
/// Genutzer Server für die API /// Programmversion
/// </summary> /// </summary>
public string Server { get; set; } = "Server: " + Preferences.Default.Get("apiUrl", "").Replace("/appapi", "").Replace("https://", "").Replace("http://", ""); public string Version => AppInfo.VersionString;
/// <summary> [ObservableProperty]
/// Titel der Seite - im Moment der aktuelle Anwender private string message = "Scanne den QR-Code von deinem Benutzerprofil auf der Stundenseite.";
/// </summary>
public string Title { get; set; } = Preferences.Default.Get("name", "Nicht") + " " + Preferences.Default.Get("surname", "eingeloggt"); [ObservableProperty]
} private string? server;
[ObservableProperty]
private string? serverLabel;
[ObservableProperty]
private string title = Preferences.Default.Get("name", "Nicht") + " " + Preferences.Default.Get("surname", "eingeloggt");
[ObservableProperty]
private string? username;
[ObservableProperty]
private string? password;
[ObservableProperty]
private bool isManualMode;
[ObservableProperty]
private bool isBusy;
[ObservableProperty]
private bool isDetecting;
// Explizite Command-Property für den QR-Scanner-Event, damit das Binding in XAML zuverlässig greift
public IAsyncRelayCommand<object?> QrDetectedCommand { get; }
public LoginViewModel(IAuthService auth, IAppSettings settings) {
_auth = auth;
_settings = settings;
// gespeicherte Präferenz für Logintyp laden
var lt = Preferences.Default.Get("logintype", "qr");
isManualMode = string.Equals(lt, "manual", StringComparison.OrdinalIgnoreCase);
// Scanner standardmäßig nur im QR-Modus aktivieren
IsDetecting = !isManualMode;
// Serveranzeige vorbereiten
var apiUrl = Preferences.Default.Get("apiUrl", string.Empty);
if (!string.IsNullOrWhiteSpace(apiUrl)) {
Server = apiUrl.Replace("/appapi", "").Replace("https://", "").Replace("http://", "");
ServerLabel = "Server: " + Server;
}
// Command initialisieren
QrDetectedCommand = new AsyncRelayCommand<object?>(QrDetectedAsync);
}
// DI-Konstruktor: AlertService anbinden und Alerts an VM-Event weiterreichen (analog StundeViewModel)
internal LoginViewModel(IAuthService auth, IAppSettings settings, IAlertService alertService) : this(auth, settings) {
_alerts = alertService;
if (alertService is not null) {
alertService.AlertRaised += (s, msg) => AlertEvent?.Invoke(this, msg);
}
}
partial void OnIsManualModeChanged(bool value) {
Preferences.Default.Set("logintype", value ? "manual" : "qr");
// Scanner nur aktiv, wenn QR-Modus aktiv ist
IsDetecting = !value;
}
[RelayCommand]
private async Task LoginAsync() {
if (IsBusy) return;
try {
IsBusy = true;
var user = await _auth.LoginWithCredentials(Username?.Trim() ?? string.Empty,
Password ?? string.Empty,
(Server ?? string.Empty).Trim());
Title = $"{user.Name} {user.Surname}";
// Info zeigen und auf Bestätigung warten
var args = new ConfirmationEventArgs("Information:", "Login erfolgreich");
InfoEvent?.Invoke(this, args);
bool confirmed = await args.Task;
if (confirmed) {
await Shell.Current.GoToAsync("//StundenPage");
}
} catch (Exception ex) {
if (_alerts is not null) {
_alerts.Raise(ex.Message);
} else {
AlertEvent?.Invoke(this, ex.Message);
}
} finally {
IsBusy = false;
}
}
private async Task QrDetectedAsync(object? args) {
var now = DateTime.Now;
if ((now - _lastDetectionTime) <= _detectionInterval) return;
_lastDetectionTime = now;
try {
var token = ExtractFirstBarcodeValue(args);
if (string.IsNullOrWhiteSpace(token)) return;
var user = await _auth.LoginWithToken(token);
Title = $"{user.Name} {user.Surname}";
// Info zeigen und auf Bestätigung warten
var infoArgs = new ConfirmationEventArgs("Information:", "Login erfolgreich");
InfoEvent?.Invoke(this, infoArgs);
bool confirmed = await infoArgs.Task;
if (confirmed) {
await Shell.Current.GoToAsync("//StundenPage");
}
} catch (Exception ex) {
if (_alerts is not null) {
_alerts.Raise(ex.Message);
} else {
AlertEvent?.Invoke(this, ex.Message);
}
}
}
private static string? ExtractFirstBarcodeValue(object? args) {
try {
if (args is ZXing.Net.Maui.BarcodeDetectionEventArgs e && e.Results is not null) {
return e.Results.FirstOrDefault()?.Value;
}
} catch { }
return null;
}
}

View File

@@ -2,9 +2,11 @@
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using System.Windows.Input; using System.Windows.Input;
namespace Jugenddienst_Stunden.ViewModels; namespace Jugenddienst_Stunden.ViewModels;
internal class NoteViewModel : ObservableObject, IQueryAttributable { internal class NoteViewModel : ObservableObject, IQueryAttributable {
private Models.Note _note; private Models.Note _note;
public string Text { public string Text {
get => _note.Text; get => _note.Text;
set { set {
@@ -61,4 +63,4 @@ internal class NoteViewModel : ObservableObject, IQueryAttributable {
OnPropertyChanged(nameof(Text)); OnPropertyChanged(nameof(Text));
OnPropertyChanged(nameof(Date)); OnPropertyChanged(nameof(Date));
} }
} }

View File

@@ -47,8 +47,10 @@ internal class NotesViewModel : IQueryAttributable {
} }
// If note isn't found, it's new; add it. // 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

@@ -5,243 +5,275 @@ using Jugenddienst_Stunden.Types;
using System.ComponentModel; using System.ComponentModel;
using System.Windows.Input; using System.Windows.Input;
using static System.Runtime.InteropServices.JavaScript.JSType; 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; namespace Jugenddienst_Stunden.ViewModels;
/// <summary> /// <summary>
/// Viewmodel für die einzelnen Stundeneinträge / Bearbeitung /// Viewmodel für die einzelnen Stundeneinträge / Bearbeitung
/// </summary> /// </summary>
public partial class StundeViewModel : ObservableObject, IQueryAttributable { public partial class StundeViewModel : ObservableObject, IQueryAttributable {
private readonly IHoursService _hoursService;
public int Id { get; set; } public int Id { get; set; }
public string Title { get; set; } = "Eintrag bearbeiten"; public string Title { get; set; } = "Eintrag bearbeiten";
public string SubTitle { get; set; } = DateTime.Today.ToString("dddd, d. MMMM yyyy"); public string SubTitle { get; set; } = DateTime.Today.ToString("dddd, d. MMMM yyyy");
//private HoursBase HoursBase = new HoursBase(); //private HoursBase HoursBase = new HoursBase();
internal Settings Settings = new Settings(); internal Settings Settings = new Settings();
public event EventHandler<string> AlertEvent; public event EventHandler<string> AlertEvent;
public event EventHandler<string> InfoEvent; 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;
/// <summary> public event Func<string, string, Task<bool>> ConfirmEvent;
/// Gemeinden für die Auswahlliste //public event Func<string, string, string?, string?, Task<bool>> ConfirmEvent;
/// </summary> //public event EventHandler<ConfirmEventArgs> ConfirmEvent;
[ObservableProperty]
private List<Gemeinde> optionsGemeinde;
/// <summary> /// <summary>
/// Projekte für die Auswahlliste /// Gemeinden für die Auswahlliste
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty] private List<Gemeinde> optionsGemeinde;
private List<Projekt> optionsProjekt;
/// <summary> /// <summary>
/// Freistellungen für die Auswahlliste /// Projekte für die Auswahlliste
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty] private List<Projekt> optionsProjekt;
private List<Freistellung> optionsFreistellung;
/// <summary> /// <summary>
/// Vorhandene Zeiten anzeigen, wenn neuer Eintrag erstellt wird /// Freistellungen für die Auswahlliste
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty] private List<Freistellung> optionsFreistellung;
private List<DayTime> dayTimes;
/// <summary> /// <summary>
/// Aktueller Stundeneintrag /// Vorhandene Zeiten anzeigen, wenn neuer Eintrag erstellt wird
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty] private List<DayTime> dayTimes;
private DayTime dayTime;
/// <summary> /// <summary>
/// Dürfen Gemeinden verwendet werden? /// Aktueller Stundeneintrag
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty] private DayTime dayTime;
private bool gemeindeAktivSet;
/// <summary> /// <summary>
/// Dürfen Projekte verwendet werden? /// Dürfen Gemeinden verwendet werden?
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty] private bool gemeindeAktivSet;
private bool projektAktivSet;
[ObservableProperty] /// <summary>
private bool freistellungEnabled; /// Dürfen Projekte verwendet werden?
/// </summary>
[ObservableProperty] private bool projektAktivSet;
public ICommand SaveCommand { get; private set; } [ObservableProperty] private bool freistellungEnabled;
public ICommand DeleteCommand { get; private set; }
public ICommand DeleteConfirmCommand { get; private set; } public ICommand SaveCommand { get; private set; }
//public ICommand LoadDataCommand { get; private set; } public ICommand DeleteCommand { get; private set; }
public ICommand DeleteConfirmCommand { get; private set; }
//public ICommand LoadDataCommand { get; private set; }
public StundeViewModel() { public StundeViewModel() : this(GetServiceOrCreate()) {
SaveCommand = new AsyncRelayCommand(Save); LoadSettingsAsync();
//DeleteCommand = new AsyncRelayCommand(Delete); }
DeleteConfirmCommand = new Command(async () => await DeleteConfirm());
}
public StundeViewModel(DayTime stunde) { private static IHoursService GetServiceOrCreate() {
SaveCommand = new AsyncRelayCommand(Save); // Fallback-Konstruktion, falls DI nicht injiziert wurde (z. B. im Designer)
DeleteConfirmCommand = new AsyncRelayCommand(DeleteConfirm); 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);
}
private async void LoadSettingsAsync() { internal StundeViewModel(IHoursService hoursService) {
try { _hoursService = hoursService;
Settings = await HoursBase.LoadSettings(); SaveCommand = new AsyncRelayCommand(Save);
GlobalVar.Settings = Settings; //DeleteCommand = new AsyncRelayCommand(Delete);
DeleteConfirmCommand = new Command(async () => await DeleteConfirm());
}
OptionsGemeinde = Settings.Gemeinden; // DI-Konstruktor, der den globalen Alert-Service abonniert und Alerts an das ViewModel weiterreicht.
OptionsProjekt = Settings.Projekte; internal StundeViewModel(IHoursService hoursService, IAlertService alertService) : this(hoursService) {
OptionsFreistellung = Settings.Freistellungen; if (alertService is not null) {
alertService.AlertRaised += (s, msg) => AlertEvent?.Invoke(this, msg);
}
}
GemeindeAktivSet = Settings.GemeindeAktivSet; private async void LoadSettingsAsync() {
ProjektAktivSet = Settings.ProjektAktivSet; try {
Settings = await _hoursService.GetSettingsAsync();
GlobalVar.Settings = Settings;
} catch (Exception e) { OptionsGemeinde = Settings.Gemeinden;
AlertEvent?.Invoke(this, e.Message); OptionsProjekt = Settings.Projekte;
} OptionsFreistellung = Settings.Freistellungen;
}
async Task Save() { GemeindeAktivSet = Settings.GemeindeAktivSet;
bool exceptionOccurred = false; ProjektAktivSet = Settings.ProjektAktivSet;
bool proceed = true; } catch (Exception e) {
if (DayTime.TimeSpanVon == DayTime.TimeSpanBis && DayTime.FreistellungAktiv.Name == null) { AlertEvent?.Invoke(this, e.Message);
proceed = false; }
AlertEvent?.Invoke(this, "Uhrzeiten sollten unterschiedlich sein"); }
}
if (proceed) {
try {
await HoursBase.SaveEntry(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")}");
}
}
}
}
/// <summary> async Task Save() {
/// Löschen ohne Bestätigung bool exceptionOccurred = false;
/// </summary> bool proceed = true;
private async Task Delete() {
await HoursBase.DeleteEntry(DayTime); //Arbeitszeit sollte nicht null sein
await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}"); if (DayTime.TimeSpanVon == DayTime.TimeSpanBis && DayTime.FreistellungAktiv.Name == null) {
} proceed = false;
AlertEvent?.Invoke(this, "Uhrzeiten sollten unterschiedlich sein");
}
//Projekt ist ein Pflichtfeld
if (Settings.ProjektAktivSet && DayTime.ProjektAktiv.Id == 0) {
proceed = false;
AlertEvent?.Invoke(this, "Projekt darf nicht leer sein");
}
//Gemeinde ist ein Pflichtfeld
if (Settings.GemeindeAktivSet && DayTime.GemeindeAktiv.Id == 0) {
proceed = false;
AlertEvent?.Invoke(this, "Gemeinde darf nicht leer sein");
}
/// <summary> if (proceed) {
/// Löschen mit Bestätigung try {
/// </summary> await _hoursService.SaveEntryAsync(DayTime);
private async Task DeleteConfirm() { } catch (Exception e) {
if (ConfirmEvent != null) { AlertEvent?.Invoke(this, e.Message);
bool answer = await ConfirmEvent.Invoke("Achtung", "Löschen kann nicht ungeschehen gemacht werden. Fortfahren?"); exceptionOccurred = true;
if (answer) { }
//Löschen
await HoursBase.DeleteEntry(DayTime);
await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}");
} else {
//nicht Löschen
}
}
}
/// <summary> if (!exceptionOccurred) {
/// Anwenden der Query-Parameter if (DayTime.Id != null) {
/// </summary> await Shell.Current.GoToAsync($"..?saved={DayTime.Id}");
async void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) { } else {
//load beinhaltet die ID: Eintrag bearbeiten await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}");
//date beinhaltet einen Tag: Neuen Eintrag erstellen }
if (query.ContainsKey("load")) { }
}
}
//DateTime heute = DateTime.Now; /// <summary>
try { /// Löschen ohne Bestätigung
//_dayTime = await HoursBase.LoadEntry(Convert.ToInt32(query["load"])); /// </summary>
BaseResponse dat = await HoursBase.LoadBase("id=" + Convert.ToInt32(query["load"])); private async Task Delete() {
GlobalVar.Settings = dat.settings; await _hoursService.DeleteEntryAsync(DayTime);
GemeindeAktivSet = dat.settings.GemeindeAktivSet; await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}");
ProjektAktivSet = dat.settings.ProjektAktivSet; }
DayTime = dat.daytime; /// <summary>
DayTime.TimeSpanVon = dat.daytime.Begin.ToTimeSpan(); /// Löschen mit Bestätigung
DayTime.TimeSpanBis = dat.daytime.End.ToTimeSpan(); /// </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
}
}
}
OptionsGemeinde = dat.settings.Gemeinden ?? new List<Gemeinde>(); /// <summary>
OptionsProjekt = dat.settings.Projekte ?? new List<Projekt>(); /// Anwenden der Query-Parameter
OptionsFreistellung = dat.settings.Freistellungen ?? new List<Freistellung>(); /// </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.GemeindeAktiv = OptionsGemeinde.FirstOrDefault(Gemeinde => Gemeinde.Id == DayTime.Gemeinde) ?? new Gemeinde(); DayTime = entry;
DayTime.ProjektAktiv = OptionsProjekt.FirstOrDefault(Projekt => Projekt.Id == DayTime.Projekt) ?? new Projekt(); DayTime.TimeSpanVon = entry.Begin.ToTimeSpan();
DayTime.FreistellungAktiv = OptionsFreistellung.FirstOrDefault(Freistellung => Freistellung.Id == DayTime.Free) ?? new Freistellung(); DayTime.TimeSpanBis = entry.End.ToTimeSpan();
//Evtl. noch die anderen Zeiten des gleichen Tages holen // OptionsGemeinde = settings.Gemeinden ?? new List<Gemeinde>();
BaseResponse dat1 = await HoursBase.LoadBase("date=" + DayTime.Day.ToString("yyyy-MM-dd")); // OptionsProjekt = settings.Projekte ?? new List<Projekt>();
DayTimes = dat1.daytimes; // OptionsFreistellung = settings.Freistellungen ?? new List<Freistellung>();
OnPropertyChanged(nameof(DayTime));
OnPropertyChanged(nameof(DayTimes));
} catch (Exception e) { DayTime.GemeindeAktiv = OptionsGemeinde.FirstOrDefault(Gemeinde => Gemeinde.Id == DayTime.Gemeinde) ??
AlertEvent?.Invoke(this, e.Message); new Gemeinde();
} finally { 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));
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
} finally {
}
if (System.String.IsNullOrEmpty(DayTime.Description)) { if (System.String.IsNullOrEmpty(DayTime.Description)) {
InfoEvent?.Invoke(this, "Eintrag hat keinen Beschreibungstext"); InfoEvent?.Invoke(this, "Eintrag hat keinen Beschreibungstext");
} }
SubTitle = DayTime.Day.ToString("dddd, d. MMMM yyyy");
OnPropertyChanged(nameof(SubTitle));
FreistellungEnabled = !DayTime.Approved; SubTitle = DayTime.Day.ToString("dddd, d. MMMM yyyy");
//OnPropertyChanged(nameof(DayTime)); OnPropertyChanged(nameof(SubTitle));
} else if (query.ContainsKey("date")) { FreistellungEnabled = !DayTime.Approved;
Title = "Neuer Eintrag"; //OnPropertyChanged(nameof(DayTime));
OnPropertyChanged(nameof(Title)); } else if (query.ContainsKey("date")) {
Title = "Neuer Eintrag";
OnPropertyChanged(nameof(Title));
DateTime _date = DateTime.ParseExact((string)query["date"], "yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture); DateTime _date = DateTime.ParseExact((string)query["date"], "yyyy-MM-dd",
//Bei neuem Eintrag die vorhandenen des gleichen Tages anzeigen System.Globalization.CultureInfo.InvariantCulture);
try { //Bei neuem Eintrag die vorhandenen des gleichen Tages anzeigen
//DayTimes = await HoursBase.LoadDay(_date); try {
BaseResponse dat = await HoursBase.LoadBase("date=" + _date.ToString("yyyy-MM-dd")); var (list, settings) = await _hoursService.GetDayWithSettingsAsync(_date);
GlobalVar.Settings = dat.settings; GlobalVar.Settings = settings;
DayTimes = dat.daytimes; DayTimes = list;
OptionsGemeinde = dat.settings.Gemeinden; OptionsGemeinde = settings.Gemeinden;
OptionsProjekt = dat.settings.Projekte; OptionsProjekt = settings.Projekte;
OptionsFreistellung = dat.settings.Freistellungen; OptionsFreistellung = settings.Freistellungen;
GemeindeAktivSet = dat.settings.GemeindeAktivSet; GemeindeAktivSet = settings.GemeindeAktivSet;
ProjektAktivSet = dat.settings.ProjektAktivSet; 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();
} catch (Exception) { SubTitle = _date.ToString("dddd, d. MMMM yyyy");
//Ein Tag ohne Einträge gibt eine Fehlermeldung, FreistellungEnabled = true;
//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"); OnPropertyChanged(nameof(SubTitle));
FreistellungEnabled = true; //OnPropertyChanged(nameof(DayTime));
}
OnPropertyChanged(nameof(SubTitle)); }
//OnPropertyChanged(nameof(DayTime)); }
} }
}
}
}

View File

@@ -5,316 +5,366 @@ using Jugenddienst_Stunden.Types;
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Windows.Input; using System.Windows.Input;
using CommunityToolkit.Maui.Alerts; using Jugenddienst_Stunden.Interfaces;
using CommunityToolkit.Maui.Core;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace Jugenddienst_Stunden.ViewModels; namespace Jugenddienst_Stunden.ViewModels;
/// <summary> /// <summary>
/// ViewModel für die Stundenliste /// ViewModel für die Stundenliste
/// </summary> /// </summary>
internal partial class StundenViewModel : ObservableObject, IQueryAttributable, INotifyPropertyChanged { public partial class StundenViewModel : ObservableObject, IQueryAttributable, INotifyPropertyChanged {
private readonly IHoursService _hoursService;
public ICommand NewEntryCommand { get; } public ICommand NewEntryCommand { get; }
public ICommand SelectEntryCommand { get; } public ICommand SelectEntryCommand { get; }
public ICommand LoadDataCommand { get; private set; } public ICommand LoadDataCommand { get; private set; }
public ICommand LoadDayCommand { get; private set; } public ICommand LoadDayCommand { get; private set; }
public ICommand RefreshListCommand { get; } public ICommand RefreshListCommand { get; }
public ICommand RefreshCommand { get; } public ICommand RefreshCommand { get; }
public event EventHandler<string> AlertEvent; public event EventHandler<string> AlertEvent;
public event EventHandler<string> InfoEvent; public event EventHandler<string> InfoEvent;
/// <summary> /// <summary>
/// Beschriftung Button Monatsübersicht /// Beschriftung Button Monatsübersicht
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty] private string loadOverview;
private string loadOverview;
//private HoursBase HoursBase = new HoursBase(); //private HoursBase HoursBase = new HoursBase();
internal Settings Settings = new Settings(); internal Settings Settings = new Settings();
/// <summary> /// <summary>
/// Zu leistende Stunden /// Zu leistende Stunden
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty] private TimeOnly sollstunden;
private TimeOnly sollstunden;
/// <summary> /// <summary>
/// Geleistete Stunden an einem Tag /// Geleistete Stunden an einem Tag
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty] private TimeOnly dayTotal;
private TimeOnly dayTotal;
/// <summary> /// <summary>
/// Liste der Tageszeiten /// Liste der Tageszeiten
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty] private List<DayTime> dayTimes = new List<DayTime>();
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] [ObservableProperty] private Hours hours;
private Hours hours;
/// <summary> /// <summary>
/// Mindest-Datum für den Datepicker /// Mindest-Datum für den Datepicker
/// </summary> /// </summary>
public DateTime MinimumDate { public DateTime MinimumDate {
get => DateTime.Today.AddDays(-365); get => DateTime.Today.AddDays(-365);
} }
/// <summary> /// <summary>
/// Höchst-Datum für den Datepicker /// Höchst-Datum für den Datepicker
/// </summary> /// </summary>
public DateTime MaximumDate { public DateTime MaximumDate {
get => DateTime.Today.AddDays(60); get => DateTime.Today.AddDays(60);
} }
/// <summary> /// <summary>
/// Heutiges Datum, wenn das Datum geändert wird, wird auch der Tag geladen /// Heutiges Datum, wenn das Datum geändert wird, wird auch der Tag geladen
/// </summary> /// </summary>
private DateTime dateToday = DateTime.Today; 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> public DateTime DateToday {
/// Monatsübersicht: Geleistete Stunden get => dateToday;
/// </summary> set {
public double? ZeitCalculated { if (dateToday != value) {
get => Hours.Zeit_total; 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> /// <summary>
/// Monatsübersicht: Sollstunden /// Monatsübersicht: Geleistete Stunden
/// </summary> /// </summary>
public double? Nominal { public double? ZeitCalculated {
get => Hours.Nominal; get => Hours.Zeit_total;
} }
/// <summary> /// <summary>
/// Monatsübersicht: Differenz zwischen Soll und geleisteten Stunden /// Monatsübersicht: Sollstunden
/// </summary> /// </summary>
public double? Overtime { public double? Nominal {
get => Hours.overtime; get => Hours.Nominal;
} }
/// <summary> /// <summary>
/// Monatsübersicht: Restüberstunden insgesamt /// Monatsübersicht: Differenz zwischen Soll und geleisteten Stunden
/// </summary> /// </summary>
public double OvertimeMonth { public double? Overtime {
get => Hours.overtime_month; get => Hours.overtime;
} }
public double Zeitausgleich { /// <summary>
get => Hours.zeitausgleich; /// Monatsübersicht: Restüberstunden insgesamt
} /// </summary>
public double ZeitausgleichMonth { public double OvertimeMonth {
get => Hours.zeitausgleich_month; get => Hours.overtime_month;
} }
/// <summary> public double Zeitausgleich {
/// Monatsübersicht: Resturlaub get => Hours.zeitausgleich;
/// </summary> }
public double Holiday {
get => Hours.holiday;
}
/// <summary> public double ZeitausgleichMonth {
/// Seite neu laden get => Hours.zeitausgleich_month;
/// </summary> }
[ObservableProperty]
private bool isRefreshing; /// <summary>
/// Monatsübersicht: Resturlaub
/// </summary>
public double Holiday {
get => Hours.holiday;
}
/// <summary>
/// Seite neu laden
/// </summary>
[ObservableProperty] private bool isRefreshing;
/// <summary> /// <summary>
/// Dürfen Gemeinden verwendet werden? /// Dürfen Gemeinden verwendet werden?
/// </summary> /// </summary>
public bool GemeindeAktivSet { get; set; } public bool GemeindeAktivSet { get; set; }
/// <summary> /// <summary>
/// Dürfen Projekte verwendet werden? /// Dürfen Projekte verwendet werden?
/// </summary> /// </summary>
public bool ProjektAktivSet { get; set; } 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();
/// <summary> LoadOverview = "Lade Summen für " + DateToday.ToString("MMMM");
/// CTOR
/// </summary>
public StundenViewModel() {
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());
LoadDataCommand = new AsyncRelayCommand(LoadData); // Task task = LoadDay(DateTime.Today);
NewEntryCommand = new AsyncRelayCommand(NewEntryAsync); // Beim Startup NICHT direkt im CTOR laden (kann Startup/Navigation blockieren)
SelectEntryCommand = new AsyncRelayCommand<DayTime>(SelectEntryAsync); // Stattdessen via Dispatcher "nach" dem Aufbau starten:
RefreshListCommand = new AsyncRelayCommand(RefreshList); MainThread.BeginInvokeOnMainThread(async () =>
RefreshCommand = new Command(async () => await RefreshItemsAsync()); {
try
Task task = LoadDay(DateTime.Today); {
} await LoadDay(DateTime.Today);
}
catch (Exception ex)
{
AlertEvent?.Invoke(this, ex.Message);
}
});
}
/// <summary> /// <summary>
/// Öffnet eine neue Stundeneingabe /// Öffnet eine neue Stundeneingabe
/// </summary> /// </summary>
private async Task NewEntryAsync() { private async Task NewEntryAsync() {
//Hier muss das Datum übergeben werden //Hier muss das Datum übergeben werden
//await Shell.Current.GoToAsync(nameof(Views.StundePage)); //await Shell.Current.GoToAsync(nameof(Views.StundePage));
await Shell.Current.GoToAsync($"{nameof(Views.StundePage)}?date={dateToday:yyyy-MM-dd}"); await Shell.Current.GoToAsync($"{nameof(Views.StundePage)}?date={dateToday:yyyy-MM-dd}");
} }
/// <summary> /// <summary>
/// Öffnet eine bestehende Stundeneingabe /// Öffnet eine bestehende Stundeneingabe
/// </summary> /// </summary>
private async Task SelectEntryAsync(DayTime entry) { private async Task SelectEntryAsync(DayTime entry) {
if (entry != null && entry.Id != null) { if (entry != null && entry.Id != null) {
//var navigationParameters = new Dictionary<string, object> { { "load", entry.id } }; //var navigationParameters = new Dictionary<string, object> { { "load", entry.id } };
//await Shell.Current.GoToAsync($"{nameof(Views.StundePage)}", navigationParameters); //await Shell.Current.GoToAsync($"{nameof(Views.StundePage)}", navigationParameters);
await Shell.Current.GoToAsync($"{nameof(Views.StundePage)}?load={entry.Id}"); await Shell.Current.GoToAsync($"{nameof(Views.StundePage)}?load={entry.Id}");
} else AlertEvent?.Invoke(this, "Auswahl enthält keine Daten"); } else AlertEvent?.Invoke(this, "Auswahl enthält keine Daten");
} }
private async Task RefreshList() { private async Task RefreshList() {
OnPropertyChanged(nameof(DayTimes)); OnPropertyChanged(nameof(DayTimes));
} }
/// <summary> /// <summary>
/// Lädt die Monatssummen für die Übersicht /// Lädt die Monatssummen für die Übersicht
/// </summary> /// </summary>
private async Task LoadData() { private async Task LoadData() {
try { try {
BaseResponse dat = await HoursBase.LoadBase("hours&year=" + DateToday.ToString("yyyy") + "&month=" + DateToday.ToString("MM")); var (hours, settings) = await _hoursService.GetMonthSummaryAsync(DateToday);
Hours = dat.hour; Hours = hours;
Settings = dat.settings; Settings = settings;
if (Settings.Version != AppInfo.Current.VersionString.Substring(0, 5)) { if (Settings.Version != AppInfo.Current.VersionString.Substring(0, 5)) {
InfoEvent?.Invoke(this, "Version: " + Settings.Version + " verfügbar (" + AppInfo.Current.VersionString.Substring(0, 5) + " installiert)"); InfoEvent?.Invoke(this,
} "Version: " + Settings.Version + " verfügbar (" + AppInfo.Current.VersionString.Substring(0, 5) +
//_hour = await HoursBase.LoadData(); " installiert)");
RefreshProperties(); }
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message); //_hour = await HoursBase.LoadData();
} RefreshProperties();
} } catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
}
}
/// <summary> /// <summary>
/// Lädt die Arbeitszeiten für einen Tag /// Lädt die Arbeitszeiten für einen Tag
/// </summary> /// </summary>
public async Task LoadDay(DateTime date) { public async Task LoadDay(DateTime date) {
DayTotal = new TimeOnly(0); // kleine Initialwerte sind ok, aber UI-Thread sicher setzen:
Sollstunden = new TimeOnly(0); await MainThread.InvokeOnMainThreadAsync(() =>
try { {
//_dayTimes = await HoursBase.LoadDay(date); DayTotal = new TimeOnly(0);
BaseResponse dat = await HoursBase.LoadBase("date=" + date.ToString("yyyy-MM-dd")); Sollstunden = new TimeOnly(0);
});
try {
var (dayTimes, settings) = await _hoursService.GetDayWithSettingsAsync(date);
DayTimes = dat.daytimes; await MainThread.InvokeOnMainThreadAsync(() =>
Settings = dat.settings; {
GemeindeAktivSet = Settings.GemeindeAktivSet; DayTimes = dayTimes;
ProjektAktivSet = Settings.ProjektAktivSet; Settings = settings;
GemeindeAktivSet = Settings.GemeindeAktivSet;
ProjektAktivSet = Settings.ProjektAktivSet;
OnPropertyChanged(nameof(GemeindeAktivSet)); OnPropertyChanged(nameof(GemeindeAktivSet));
OnPropertyChanged(nameof(ProjektAktivSet)); OnPropertyChanged(nameof(ProjektAktivSet));
});
List<Sollstunden> _soll; List<Sollstunden> _soll;
TimeSpan span = TimeSpan.Zero; TimeSpan span = TimeSpan.Zero;
bool merker = false; bool merker = false;
foreach (DayTime dt in DayTimes) { foreach (DayTime dt in DayTimes) {
span += dt.End - dt.Begin; span += dt.End - dt.Begin;
//Nachtstunden dazurechnen //Nachtstunden dazurechnen
if (dt.Night.Ticks > 0 && !merker) { if (dt.Night.Ticks > 0 && !merker) {
span += dt.Night.ToTimeSpan() * .5; span += dt.Night.ToTimeSpan() * .5;
merker = true; 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);
//Nach der Tagessumme die anderen Tage anhängen _soll = Settings.Nominal.Where(w => w.Timetable == dt.TimeTable && w.Wochentag == dt.Wday).ToList();
if (DayTimes != null) { if (_soll.Count > 0)
BaseResponse dat1 = await HoursBase.LoadBase("date=" + date.ToString("yyyy-MM-dd") + "&tilldate=" + date.AddDays(3).ToString("yyyy-MM-dd")); {
if (dat1.daytimes != null) var soll = TimeOnly.FromTimeSpan(TimeSpan.FromHours(_soll[0].Zeit));
DayTimes = dat.daytimes.Concat(dat1.daytimes).ToList(); await MainThread.InvokeOnMainThreadAsync(() => Sollstunden = soll);
} }
}
} catch (Exception e) { var total = TimeOnly.FromTimeSpan(span);
DayTimes = new List<DayTime>(); await MainThread.InvokeOnMainThreadAsync(() => DayTotal = total);
//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)) { //Nach der Tagessumme die anderen Tage anhängen
InfoEvent?.Invoke(this, "Version: " + Settings.Version + " verfügbar (" + AppInfo.Current.VersionString.Substring(0, 5) + " installiert)"); if (DayTimes != null) {
} else { InfoEvent?.Invoke(this, e.Message); } var more = await _hoursService.GetDayRangeAsync(date, date.AddDays(3));
} finally { if (more != null && more.Count > 0)
OnPropertyChanged(nameof(DayTotal)); {
OnPropertyChanged(nameof(Sollstunden)); await MainThread.InvokeOnMainThreadAsync(() =>
OnPropertyChanged(nameof(DateToday)); DayTimes = DayTimes.Concat(more).ToList()
OnPropertyChanged(nameof(LoadOverview)); );
//OnPropertyChanged(nameof(DayTimes)); }
} }
} } catch (Exception e) {
await MainThread.InvokeOnMainThreadAsync(() =>
{
DayTimes = new List<DayTime>();
//TODO: hier könnte auch ein Fehler kommen, dann wäre InfoEvent falsch.
async void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) { if (Settings.Version != null && Settings.Version != AppInfo.Current.VersionString.Substring(0, 5)) {
if (query.ContainsKey("date")) { InfoEvent?.Invoke(this,
await LoadDay(Convert.ToDateTime(query["date"])); "Version: " + Settings.Version + " verfügbar (" + AppInfo.Current.VersionString.Substring(0, 5) +
} " installiert)");
} } else {
InfoEvent?.Invoke(this, e.Message);
}
});
/// <summary>
/// Seite aktualisieren } finally {
/// </summary> await MainThread.InvokeOnMainThreadAsync(() =>
private async Task RefreshItemsAsync() { {
IsRefreshing = true; OnPropertyChanged(nameof(DayTotal));
OnPropertyChanged(nameof(Sollstunden));
OnPropertyChanged(nameof(DateToday));
OnPropertyChanged(nameof(LoadOverview));
});
}
}
//await Task.Delay(2000); // Simuliert eine Datenaktualisierung async void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) {
await LoadDay(DateToday); if (query.ContainsKey("date")) {
await LoadDay(Convert.ToDateTime(query["date"]));
}
}
IsRefreshing = false; /// <summary>
} /// Seite aktualisieren
/// </summary>
private async Task RefreshItemsAsync() {
IsRefreshing = true;
/// <summary> //await Task.Delay(2000); // Simuliert eine Datenaktualisierung
/// Refreshes all properties await LoadDay(DateToday);
/// </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) { IsRefreshing = false;
try { }
base.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
} catch (Exception ex) {
AlertEvent?.Invoke(this, ex.Message);
//Console.WriteLine($"Fehler bei OnPropertyChanged: {ex.Message}");
}
}
/// <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" <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit" xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
@@ -13,17 +14,18 @@
<!-- Add an item to the toolbar --> <!-- Add an item to the toolbar -->
<ContentPage.ToolbarItems> <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.ToolbarItems>
<ContentPage.Behaviors> <ContentPage.Behaviors>
<toolkit:StatusBarBehavior <toolkit:StatusBarBehavior
StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}" StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}"
StatusBarStyle="LightContent" /> StatusBarStyle="LightContent" />
</ContentPage.Behaviors> </ContentPage.Behaviors>
<VerticalStackLayout Margin="20,0,0,0"> <VerticalStackLayout Margin="20,0,0,0">
<Label Text="Werden nur lokal gespeichert"/> <Label Text="Werden nur lokal gespeichert" />
<!-- Display notes in a list --> <!-- Display notes in a list -->
<CollectionView x:Name="notesCollection" <CollectionView x:Name="notesCollection"
@@ -31,7 +33,7 @@
Margin="0,20,0,0" Margin="0,20,0,0"
SelectionMode="Single" SelectionMode="Single"
SelectionChangedCommand="{Binding SelectNoteCommand}" 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 --> <!-- Designate how the collection of items is laid out -->
<CollectionView.ItemsLayout> <CollectionView.ItemsLayout>
@@ -42,8 +44,8 @@
<CollectionView.ItemTemplate> <CollectionView.ItemTemplate>
<DataTemplate> <DataTemplate>
<StackLayout> <StackLayout>
<Label Text="{Binding Text}" FontSize="22"/> <Label Text="{Binding Text}" FontSize="22" />
<Label Text="{Binding Date}" FontSize="14" TextColor="Silver"/> <Label Text="{Binding Date}" FontSize="14" TextColor="Silver" />
</StackLayout> </StackLayout>
</DataTemplate> </DataTemplate>
</CollectionView.ItemTemplate> </CollectionView.ItemTemplate>

View File

@@ -1,11 +1,8 @@
namespace Jugenddienst_Stunden.Views; namespace Jugenddienst_Stunden.Views;
public partial class AllNotesPage : ContentPage public partial class AllNotesPage : ContentPage {
{ public AllNotesPage() {
public AllNotesPage()
{
InitializeComponent(); InitializeComponent();
} }
private void ContentPage_NavigatedTo(object sender, NavigatedToEventArgs e) { 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" <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit" xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
@@ -9,19 +10,19 @@
Title="{Binding Title}"> Title="{Binding Title}">
<ContentPage.BindingContext> <!-- BindingContext wird via DI im Code-Behind gesetzt -->
<models:LoginViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources> <ContentPage.Resources>
<ResourceDictionary> <ResourceDictionary>
<conv:StringVisibilityConverter x:Key="StringVisibilityConverter" /> <conv:StringVisibilityConverter x:Key="StringVisibilityConverter" />
<conv:InverseBoolConverter x:Key="InverseBoolConverter" />
<conv:EventArgsPassThroughConverter x:Key="EventArgsPassThroughConverter" />
</ResourceDictionary> </ResourceDictionary>
</ContentPage.Resources> </ContentPage.Resources>
<ContentPage.Behaviors> <ContentPage.Behaviors>
<toolkit:StatusBarBehavior <toolkit:StatusBarBehavior
StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}" StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}"
StatusBarStyle="LightContent" /> StatusBarStyle="LightContent" />
</ContentPage.Behaviors> </ContentPage.Behaviors>
@@ -29,40 +30,48 @@
<VerticalStackLayout Spacing="10" Margin="15,0"> <VerticalStackLayout Spacing="10" Margin="15,0">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<HorizontalStackLayout Spacing="5" HorizontalOptions="Start" Grid.Column="0"> <HorizontalStackLayout Spacing="5" HorizontalOptions="Start" Grid.Column="0">
<Label FontSize="20" FontAttributes="Bold" Text="{Binding AppTitle}" Margin="0,7,0,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" /> <Label FontSize="16" Text="{Binding Version}" Margin="0,11,0,0" />
</HorizontalStackLayout> </HorizontalStackLayout>
<Grid Grid.Column="1" ColumnDefinitions="*,50" ColumnSpacing="10"> <Grid Grid.Column="1" ColumnDefinitions="*,50" ColumnSpacing="10">
<Label Text="Login QR/manuell" VerticalOptions="Center" Grid.Column="0"/> <Label Text="Login QR/manuell" VerticalOptions="Center" Grid.Column="0" />
<Switch x:Name="LoginSwitch" IsToggled="False" Toggled="Switch_Toggled" VerticalOptions="Center" Grid.Column="1"/> <Switch x:Name="LoginSwitch" IsToggled="{Binding IsManualMode}" VerticalOptions="Center"
Grid.Column="1" />
</Grid> </Grid>
</Grid> </Grid>
<Label x:Name="ServerLabel" Text="{Binding Server}" IsVisible="{Binding Server, Converter={StaticResource StringVisibilityConverter}}" /> <Label x:Name="ServerLabel" Text="{Binding ServerLabel}"
IsVisible="{Binding Server, Converter={StaticResource StringVisibilityConverter}}" />
<VerticalStackLayout x:Name="LoginQR" Margin="0,20,0,0"> <VerticalStackLayout x:Name="LoginQR" Margin="0,20,0,0" IsVisible="{Binding IsManualMode, Converter={StaticResource InverseBoolConverter}}">
<Label Text="Login mit QR-Code" FontSize="32" HorizontalOptions="Start" /> <Label Text="Login mit QR-Code" FontSize="32" HorizontalOptions="Start" />
<Label x:Name="Message" Text="{Binding Message}" Margin="0,15" /> <Label x:Name="Message" Text="{Binding Message}" Margin="0,15" />
<Border HeightRequest="300" Padding="0"> <Border HeightRequest="300" Padding="0">
<zxing:CameraBarcodeReaderView <zxing:CameraBarcodeReaderView
x:Name="barcodeScannerView" x:Name="barcodeScannerView"
BarcodesDetected="BarcodesDetected" VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand" IsDetecting="{Binding IsDetecting}">
VerticalOptions="FillAndExpand"/> <zxing:CameraBarcodeReaderView.Behaviors>
<toolkit:EventToCommandBehavior EventName="BarcodesDetected"
Command="{Binding QrDetectedCommand}"
EventArgsConverter="{StaticResource EventArgsPassThroughConverter}" />
</zxing:CameraBarcodeReaderView.Behaviors>
</zxing:CameraBarcodeReaderView>
</Border> </Border>
</VerticalStackLayout> </VerticalStackLayout>
<VerticalStackLayout x:Name="LoginManual" Spacing="25"> <VerticalStackLayout x:Name="LoginManual" Spacing="25" IsVisible="{Binding IsManualMode}">
<Label Text="Manueller Login" FontSize="32" HorizontalOptions="Start" Margin="0, 20, 0, 0" /> <Label Text="Manueller Login" FontSize="32" HorizontalOptions="Start" Margin="0, 20, 0, 0" />
<Entry x:Name="UsernameEntry" Placeholder="Benutzername (Mailadresse)" Keyboard="Email" /> <Entry x:Name="UsernameEntry" Text="{Binding Username}" Placeholder="Benutzername (Mailadresse)" Keyboard="Email" />
<Entry x:Name="PasswordEntry" Placeholder="Passwort" IsPassword="True" /> <Entry x:Name="PasswordEntry" Text="{Binding Password}" Placeholder="Passwort" IsPassword="True" />
<Entry x:Name="ServerEntry" Placeholder="Server (gleich wie im Browser)" Keyboard="Url" /> <Entry x:Name="ServerEntry" Text="{Binding Server}" Placeholder="Server (gleich wie im Browser)" Keyboard="Url" />
<Button Text="Login" Clicked="OnLoginButtonClicked" TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}" /> <Button Text="Login" Command="{Binding LoginCommand}"
TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}" />
</VerticalStackLayout> </VerticalStackLayout>
</VerticalStackLayout> </VerticalStackLayout>

View File

@@ -1,5 +1,6 @@
using Jugenddienst_Stunden.Models; using Jugenddienst_Stunden.Models;
using Jugenddienst_Stunden.Types; using Jugenddienst_Stunden.Types;
using Jugenddienst_Stunden.ViewModels;
using ZXing.Net.Maui; using ZXing.Net.Maui;
@@ -9,197 +10,204 @@ namespace Jugenddienst_Stunden.Views;
/// Die Loginseite mit dem Barcodescanner /// Die Loginseite mit dem Barcodescanner
/// </summary> /// </summary>
public partial class LoginPage : ContentPage { public partial class LoginPage : ContentPage {
private DateTime _lastDetectionTime;
private DateTime _lastDetectionTime; private readonly TimeSpan _detectionInterval = TimeSpan.FromSeconds(5);
private readonly TimeSpan _detectionInterval = TimeSpan.FromSeconds(5);
/// <summary> /// <summary>
/// CTOR /// CTOR
/// </summary> /// </summary>
public LoginPage() { public LoginPage() {
InitializeComponent(); InitializeComponent();
barcodeScannerView.Options = new BarcodeReaderOptions { // BindingContext via DI beziehen, falls nicht bereits gesetzt
Formats = BarcodeFormat.QrCode, try {
AutoRotate = true, if (BindingContext is null) {
Multiple = false var sp = Application.Current?.Handler?.MauiContext?.Services
}; ?? throw new InvalidOperationException("DI container ist nicht verfügbar.");
BindingContext = sp.GetRequiredService<LoginViewModel>();
}
} catch (Exception) {
// Ignorieren: Fallback bleibt leerer BindingContext
}
//if (BindingContext is LoginViewModel vm) { if (BindingContext is LoginViewModel vm) {
// vm.AlertEvent += Vm_AlertEvent; vm.AlertEvent += async (_, msg) => await DisplayAlert("Fehler:", msg, "OK");
// vm.InfoEvent += Vm_InfoEvent; //vm.InfoEvent += async (_, msg) => await DisplayAlert("Information:", msg, "OK");
// vm.MsgEvent += Vm_MsgEvent; // Neues InfoEvent: Dialog anzeigen und nach Bestätigung das Result setzen
//} vm.InfoEvent += async (_, infoArgs) => {
await MainThread.InvokeOnMainThreadAsync(async () => {
await DisplayAlert(infoArgs.Title, infoArgs.Message, infoArgs.ConfirmText);
infoArgs.SetResult(true);
});
};
}
//Zwischen manuellem und automatischem Login (mit QR-Code) umschalten und die Schalterstellung merken barcodeScannerView.Options =
bool sqr = true; new BarcodeReaderOptions { Formats = BarcodeFormat.QrCode, AutoRotate = true, Multiple = false };
bool sma = false;
if (Preferences.Default.Get("logintype", "") == "manual") { // Fallback-Verkabelung: Falls das EventToCommandBehavior in XAML nicht greift,
sqr = false; // leiten wir das Kamera-Event manuell an das ViewModel-Command weiter.
sma = true; barcodeScannerView.BarcodesDetected += (s, e) => {
LoginSwitch.IsToggled = true; if (BindingContext is LoginViewModel vm && vm.QrDetectedCommand is not null) {
Message.IsVisible = false; // Sicherstellen, dass die Command-Ausführung im UI-Thread erfolgt
} else { MainThread.BeginInvokeOnMainThread(async () => {
LoginSwitch.IsToggled = false; if (vm.QrDetectedCommand.CanExecute(e)) {
Message.IsVisible = true; await vm.QrDetectedCommand.ExecuteAsync(e);
} }
LoginQR.IsVisible = sqr; });
LoginManual.IsVisible = sma; }
} };
//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
// MVVM übernimmt Umschalten über IsManualMode im ViewModel; keine Code-Behind-Umschaltung mehr
}
/// <summary> /// <summary>
/// Nach der Erkennung des Barcodes wird der Benutzer eingeloggt /// Nach der Erkennung des Barcodes wird der Benutzer eingeloggt
/// ZXing.Net.Maui.Controls 0.4.4 /// ZXing.Net.Maui.Controls 0.4.4
/// </summary> /// </summary>
private void BarcodesDetected(object sender, BarcodeDetectionEventArgs e) { 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; try {
if ((currentTime - _lastDetectionTime) > _detectionInterval) { var tokendata = new TokenData(barcode.Value);
_lastDetectionTime = currentTime; GlobalVar.ApiUrl = tokendata.Url;
foreach (var barcode in e.Results) { User user = await HoursBase.LoadUser(barcode.Value);
if (GlobalVar.ApiKey != barcode.Value) {
_ = MainThread.InvokeOnMainThreadAsync(async () => {
//await DisplayAlert("Barcode erkannt", $"Barcode: {barcode.Format} - {barcode.Value}", "OK");
try { GlobalVar.ApiKey = barcode.Value;
var tokendata = new TokenData(barcode.Value); GlobalVar.Name = user.Name;
GlobalVar.ApiUrl = tokendata.Url; GlobalVar.Surname = user.Surname;
User user = await HoursBase.LoadUser(barcode.Value); GlobalVar.EmployeeId = user.Id;
GlobalVar.ApiKey = barcode.Value; Title = user.Name + " " + user.Surname;
GlobalVar.Name = user.Name; //Auf der Loginseite wird der Server als Info ohne Protokoll und ohne /appapi angezeigt
GlobalVar.Surname = user.Surname; ServerLabel.Text = "Server: " + tokendata.Url.Replace("/appapi", "").Replace("https://", "")
GlobalVar.EmployeeId = user.Id; .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"); await DisplayAlert("Login erfolgreich", user.Name + " " + user.Surname, "OK");
if (Navigation.NavigationStack.Count > 1) { if (Navigation.NavigationStack.Count > 1) {
//Beim ersten Start ohne Login, wird man automatisch auf die Loginseite geleitet. Danach in der History zur<75>ck //Beim ersten Start ohne Login, wird man automatisch auf die Loginseite geleitet. Danach in der History zur<75>ck
await Navigation.PopAsync(); await Navigation.PopAsync();
} else { } else {
//Beim manuellen Wechsel auf die Loginseite leiten wir nach erfolgreichem Login auf die Stunden<65>bersicht //Beim manuellen Wechsel auf die Loginseite leiten wir nach erfolgreichem Login auf die Stunden<65>bersicht
await Shell.Current.GoToAsync($"//StundenPage"); 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) { protected override void OnDisappearing() {
await DisplayAlert("Fehler", e.Message, "OK"); base.OnDisappearing();
}
}); barcodeScannerView.CameraLocation = CameraLocation.Front;
} else { // IsDetecting wird via Binding vom ViewModel gesteuert
MainThread.InvokeOnMainThreadAsync(() => { }
DisplayAlert("Bereits eingeloggt",
Preferences.Default.Get("name", "") + " " + Preferences.Default.Get("surname", ""),
"OK");
});
}
}
}
} protected override void OnAppearing() {
base.OnAppearing();
protected override void OnDisappearing() { // IsDetecting wird via Binding vom ViewModel gesteuert
base.OnDisappearing(); barcodeScannerView.CameraLocation = CameraLocation.Rear;
}
barcodeScannerView.CameraLocation = CameraLocation.Front; public bool IsCameraAvailable() {
barcodeScannerView.IsDetecting = false; var status = Permissions.CheckStatusAsync<Permissions.Camera>().Result;
} if (status != PermissionStatus.Granted) {
status = Permissions.RequestAsync<Permissions.Camera>().Result;
}
protected override void OnAppearing() { return status != PermissionStatus.Granted;
base.OnAppearing(); }
barcodeScannerView.IsDetecting = true; private async void OnLoginButtonClicked(object sender, EventArgs e) {
barcodeScannerView.CameraLocation = CameraLocation.Rear; var username = UsernameEntry.Text;
} var password = PasswordEntry.Text;
var server = ServerEntry.Text;
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;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(server)) { if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(server)) {
await DisplayAlert("Fehler", "Bitte alle Felder ausf<73>llen", "OK"); await DisplayAlert("Fehler", "Bitte alle Felder ausf<73>llen", "OK");
return; return;
} }
try {
Uri uri = new Uri(InputUrlWithSchema(server));
Types.User response = await BaseFunc.AuthUserPass(username, password, uri.Scheme + "://" + uri.Authority + "/appapi"); try {
Uri uri = new Uri(InputUrlWithSchema(server));
GlobalVar.ApiKey = response.Token; Types.User response =
GlobalVar.Name = response.Name; await BaseFunc.AuthUserPass(username, password, uri.Scheme + "://" + uri.Authority + "/appapi");
GlobalVar.Surname = response.Surname;
GlobalVar.EmployeeId = response.Id;
GlobalVar.ApiUrl = uri.Scheme + "://" + uri.Authority + "/appapi";
Title = response.Name + " " + response.Surname; GlobalVar.ApiKey = response.Token;
//ServerLabel.Text = "Server: " + server.Replace("/appapi", "").Replace("https://", "").Replace("http://", ""); GlobalVar.Name = response.Name;
ServerLabel.Text = "Server: " + uri.Authority; GlobalVar.Surname = response.Surname;
GlobalVar.EmployeeId = response.Id;
GlobalVar.ApiUrl = uri.Scheme + "://" + uri.Authority + "/appapi";
await DisplayAlert("Login erfolgreich", response.Name + " " + response.Surname, "OK"); Title = response.Name + " " + response.Surname;
if (Navigation.NavigationStack.Count > 1) //ServerLabel.Text = "Server: " + server.Replace("/appapi", "").Replace("https://", "").Replace("http://", "");
await Navigation.PopAsync(); ServerLabel.Text = "Server: " + uri.Authority;
else {
await Shell.Current.GoToAsync($"//StundenPage");
}
} catch (Exception ex) {
await DisplayAlert("Fehler", ex.Message, "OK");
}
}
/// <summary> await DisplayAlert("Login erfolgreich", response.Name + " " + response.Surname, "OK");
/// Aus einer URL ohne Schema eine URL mit Schema machen if (Navigation.NavigationStack.Count > 1)
/// </summary> await Navigation.PopAsync();
private static string InputUrlWithSchema(string url) { else {
if (!url.StartsWith("http://") && !url.StartsWith("https://")) { await Shell.Current.GoToAsync($"//StundenPage");
url = "https://" + url; }
} } catch (Exception ex) {
if (url.StartsWith("http://")) { await DisplayAlert("Fehler", ex.Message, "OK");
url = url.Replace("http://", "https://"); }
} }
return url;
}
//Zwischen manuellem und automatischem Login (mit QR-Code) umschalten und die Schalterstellung merken /// <summary>
private void Switch_Toggled(object sender, ToggledEventArgs e) { /// Aus einer URL ohne Schema eine URL mit Schema machen
var switcher = (Switch)sender; /// </summary>
private static string InputUrlWithSchema(string url) {
if (!url.StartsWith("http://") && !url.StartsWith("https://")) {
url = "https://" + url;
}
if (switcher.IsToggled) { if (url.StartsWith("http://")) {
LoginQR.IsVisible = false; url = url.Replace("http://", "https://");
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");
}
} return url;
}
//private void Vm_AlertEvent(object? sender, string e) { //Zwischen manuellem und automatischem Login (mit QR-Code) umschalten und die Schalterstellung merken
// DisplayAlert("Fehler:", e, "OK"); // Umschalt-Logik erfolgt über Binding an IsManualMode im ViewModel
//}
//private void Vm_InfoEvent(object? sender, string e) { //private void Vm_AlertEvent(object? sender, string e) {
// DisplayAlert("Information:", e, "OK"); // DisplayAlert("Fehler:", e, "OK");
//} //}
//private async Task Vm_MsgEvent(string title, string message) { //private void Vm_InfoEvent(object? sender, string e) {
// await DisplayAlert(title, message, "OK"); // 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" <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit" xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
@@ -11,8 +12,8 @@
</ContentPage.BindingContext> </ContentPage.BindingContext>
<ContentPage.Behaviors> <ContentPage.Behaviors>
<toolkit:StatusBarBehavior <toolkit:StatusBarBehavior
StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}" StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}"
StatusBarStyle="LightContent" /> StatusBarStyle="LightContent" />
</ContentPage.Behaviors> </ContentPage.Behaviors>

View File

@@ -4,7 +4,6 @@ namespace Jugenddienst_Stunden.Views;
/// Einzelne Notiz /// Einzelne Notiz
/// </summary> /// </summary>
public partial class NotePage : ContentPage { public partial class NotePage : ContentPage {
/// <summary> /// <summary>
/// CTOR /// CTOR
/// </summary> /// </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" <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit" xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
@@ -19,8 +20,8 @@
</ContentPage.Resources> </ContentPage.Resources>
<ContentPage.Behaviors> <ContentPage.Behaviors>
<toolkit:StatusBarBehavior <toolkit:StatusBarBehavior
StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}" StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}"
StatusBarStyle="LightContent" /> StatusBarStyle="LightContent" />
</ContentPage.Behaviors> </ContentPage.Behaviors>
@@ -31,20 +32,26 @@
<Border> <Border>
<Border.Padding> <Border.Padding>
<OnPlatform x:TypeArguments="Thickness" Default="0,15,10,0"> <OnPlatform x:TypeArguments="Thickness" Default="0,15,10,0">
<On Platform="Android" Value="0,4,10,8"/> <On Platform="Android" Value="0,4,10,8" />
<On Platform="WPF" Value="0,15,10,0"/> <On Platform="WPF" Value="0,15,10,0" />
</OnPlatform> </OnPlatform>
</Border.Padding> </Border.Padding>
<FlexLayout Direction="Row" AlignItems="Start" Wrap="Wrap" JustifyContent="SpaceBetween"> <FlexLayout Direction="Row" AlignItems="Start" Wrap="Wrap" JustifyContent="SpaceBetween">
<HorizontalStackLayout Spacing="10"> <HorizontalStackLayout Spacing="10">
<Label Text="Beginn" VerticalTextAlignment="Center" HorizontalTextAlignment="End" MinimumWidthRequest="60"></Label> <Label Text="Beginn" VerticalTextAlignment="Center" HorizontalTextAlignment="End"
<TimePicker x:Name="TimeBegin" HorizontalOptions="Center" Format="HH:mm" MinimumWidthRequest="80" Time="{Binding DayTime.TimeSpanVon}" /> MinimumWidthRequest="60">
</Label>
<TimePicker x:Name="TimeBegin" HorizontalOptions="Center" Format="HH:mm" MinimumWidthRequest="80"
Time="{Binding DayTime.TimeSpanVon}" />
</HorizontalStackLayout> </HorizontalStackLayout>
<HorizontalStackLayout Spacing="10"> <HorizontalStackLayout Spacing="10">
<Label Text="Ende" VerticalTextAlignment="Center" HorizontalTextAlignment="End" MinimumWidthRequest="60"></Label> <Label Text="Ende" VerticalTextAlignment="Center" HorizontalTextAlignment="End"
<TimePicker x:Name="TimeEnd" Format="HH:mm" MinimumWidthRequest="80" Time="{Binding DayTime.TimeSpanBis}" /> MinimumWidthRequest="60">
</Label>
<TimePicker x:Name="TimeEnd" Format="HH:mm" MinimumWidthRequest="80"
Time="{Binding DayTime.TimeSpanBis}" />
</HorizontalStackLayout> </HorizontalStackLayout>
</FlexLayout> </FlexLayout>
</Border> </Border>
@@ -52,48 +59,50 @@
<Border> <Border>
<Border.Padding> <Border.Padding>
<OnPlatform x:TypeArguments="Thickness" Default="5"> <OnPlatform x:TypeArguments="Thickness" Default="5">
<On Platform="Android" Value="5,4,5,8"/> <On Platform="Android" Value="5,4,5,8" />
<On Platform="WPF" Value="5"/> <On Platform="WPF" Value="5" />
</OnPlatform> </OnPlatform>
</Border.Padding> </Border.Padding>
<!--<Grid ColumnDefinitions="*,*,*">
<Picker x:Name="pick_gemeinde" Title="Gemeinde" ItemsSource="{Binding OptionsGemeinde}" SelectedItem="{Binding DayTime.GemeindeAktiv, Mode=TwoWay}" ItemDisplayBinding="{Binding Name}" Grid.Column="0" IsVisible="{Binding GemeindeAktivSet}">
</Picker>
<Picker x:Name="pick_projekt" Title="Projekt" ItemsSource="{Binding OptionsProjekt}" SelectedItem="{Binding DayTime.ProjektAktiv, Mode=TwoWay}" ItemDisplayBinding="{Binding Name}" Grid.Column="1" IsVisible="{Binding ProjektAktivSet}">
</Picker>
<Picker x:Name="pick_freistellung" Title="Freistellung" ItemsSource="{Binding OptionsFreistellung}" SelectedItem="{Binding DayTime.FreistellungAktiv, Mode=TwoWay}" ItemDisplayBinding="{Binding Name}" Grid.Column="2" IsEnabled="{Binding FreistellungEnabled}">
</Picker>
</Grid>-->
<HorizontalStackLayout> <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>
<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>
<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> </Picker>
</HorizontalStackLayout> </HorizontalStackLayout>
</Border> </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"> <Grid ColumnDefinitions="*,*" ColumnSpacing="4">
<Button Grid.Column="1" Text="Speichern" <Button Grid.Column="1" Text="Speichern"
TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}" TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}"
Command="{Binding SaveCommand}" /> Command="{Binding SaveCommand}" />
<Button Grid.Column="0" Text="Löschen" <Button Grid.Column="0" Text="Löschen"
Command="{Binding DeleteConfirmCommand}" Command="{Binding DeleteConfirmCommand}"
IsEnabled="{Binding DayTime.Id, Converter={StaticResource IntBoolConverter}}" IsEnabled="{Binding DayTime.Id, Converter={StaticResource IntBoolConverter}}"
IsVisible="{Binding FreistellungEnabled}" IsVisible="{Binding FreistellungEnabled}"
BackgroundColor="{StaticResource Gray500}" BackgroundColor="{StaticResource Gray500}"
TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}"/> TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}" />
</Grid> </Grid>
<BoxView HeightRequest="1" Margin="3,10" /> <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>
<Label.FormattedText> <Label.FormattedText>
<FormattedString> <FormattedString>
@@ -105,13 +114,13 @@
</StackLayout> </StackLayout>
<ScrollView IsVisible="{Binding DayTimes, Converter={StaticResource CollectionVisibilityConverter}}"> <ScrollView IsVisible="{Binding DayTimes, Converter={StaticResource CollectionVisibilityConverter}}">
<CollectionView <CollectionView
ItemsSource="{Binding DayTimes}" ItemsSource="{Binding DayTimes}"
x:Name="stundeItems" Margin="0" x:Name="stundeItems" Margin="0"
HeightRequest="350" HeightRequest="350"
SelectionMode="Single" SelectionMode="Single"
SelectionChangedCommand="{Binding SelectEntryCommand}" SelectionChangedCommand="{Binding SelectEntryCommand}"
SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}"> SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}">
<CollectionView.ItemsLayout> <CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical" ItemSpacing="0" /> <LinearItemsLayout Orientation="Vertical" ItemSpacing="0" />
@@ -129,15 +138,18 @@
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<HorizontalStackLayout Grid.Row="0" Grid.Column="0"> <HorizontalStackLayout Grid.Row="0" Grid.Column="0">
<Label Grid.Column="0" Text="{Binding Begin}"/> <Label Grid.Column="0" Text="{Binding Begin}" />
<Label Text="bis" Padding="5,0,5,0"/> <Label Text="bis" Padding="5,0,5,0" />
<Label Text="{Binding End}"/> <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 GemeindeAktiv.Name}" Margin="10,0,0,0"
<Label Text="{Binding ProjektAktiv.Name}" Margin="10,0,0,0" IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.ProjektAktivSet}"/> IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.GemeindeAktivSet}" />
<Label Text="{Binding FreistellungAktiv.Name}" Margin="10,0,0,0"/> <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> </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> </Grid>
</DataTemplate> </DataTemplate>
</CollectionView.ItemTemplate> </CollectionView.ItemTemplate>

View File

@@ -10,45 +10,43 @@ namespace Jugenddienst_Stunden.Views;
/// Einzelner Stundeneintrag /// Einzelner Stundeneintrag
/// </summary> /// </summary>
public partial class StundePage : ContentPage { public partial class StundePage : ContentPage {
/// <summary>
/// CTOR
/// </summary>
public StundePage() {
InitializeComponent();
/// <summary> if (BindingContext is StundeViewModel vm) {
/// CTOR vm.AlertEvent += Vm_AlertEvent;
/// </summary> vm.InfoEvent += Vm_InfoEvent;
public StundePage() { vm.ConfirmEvent += ShowConfirm;
InitializeComponent(); }
}
if (BindingContext is StundeViewModel vm) { private void Vm_AlertEvent(object? sender, string e) {
vm.AlertEvent += Vm_AlertEvent; DisplayAlert("Fehler:", e, "OK");
vm.InfoEvent += Vm_InfoEvent; }
vm.ConfirmEvent += ShowConfirm;
}
}
private void Vm_AlertEvent(object? sender, string e) { private async Task<bool> ShowConfirm(string title, string message) {
DisplayAlert("Fehler:", e, "OK"); return await DisplayAlert(title, message, "Passt!", "Na, nor decht nit.");
} }
private async Task<bool> ShowConfirm(string title, string message) { private void Vm_InfoEvent(object? sender, string e) {
return await DisplayAlert(title, message, "Passt!", "Na, nor decht nit."); 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) { //private async Task<bool> ShowConfirm(string title, string message, string ok, string not_ok) {
MainThread.BeginInvokeOnMainThread(async () => { // return await DisplayAlert(title, message, ok, not_ok);
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 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" <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit" xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
@@ -8,52 +9,51 @@
x:Class="Jugenddienst_Stunden.Views.StundenPage" x:Class="Jugenddienst_Stunden.Views.StundenPage"
Title="{Binding Title}"> Title="{Binding Title}">
<ContentPage.BindingContext>
<models:StundenViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources> <ContentPage.Resources>
<ResourceDictionary> <ResourceDictionary>
<conv:SecondsTimeConverter x:Key="secToTime" /> <conv:SecondsTimeConverter x:Key="secToTime" />
<FontImageSource x:Key="ToolbarIcon" <FontImageSource x:Key="ToolbarIcon"
Glyph="+" Glyph="+"
Size="22" Size="22"
Color="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}"/> Color="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
</ResourceDictionary> </ResourceDictionary>
</ContentPage.Resources> </ContentPage.Resources>
<ContentPage.Behaviors> <ContentPage.Behaviors>
<toolkit:StatusBarBehavior <toolkit:StatusBarBehavior
StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}" StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}"
StatusBarStyle="LightContent" /> StatusBarStyle="LightContent" />
</ContentPage.Behaviors> </ContentPage.Behaviors>
<ContentPage.ToolbarItems> <ContentPage.ToolbarItems>
<!--<ToolbarItem Text="Lade Liste" Command="{Binding RefreshListCommand}"/>--> <!--<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> </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"> <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"> <Grid RowDefinitions="Auto" ColumnDefinitions="Auto,*" HeightRequest="50" Grid.Row="0">
<DatePicker Grid.Column="0" MinimumDate="{Binding MinimumDate}" <DatePicker Grid.Column="0" MinimumDate="{Binding MinimumDate}"
MaximumDate="{Binding MaximumDate}" MaximumDate="{Binding MaximumDate}"
Date="{Binding DateToday}" Format="dddd, d. MMMM yyyy" /> 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"> <Border Grid.Column="1" Margin="15,0,0,0" Padding="15,0,0,0" ToolTipProperties.Text="Tagessumme">
<HorizontalStackLayout> <HorizontalStackLayout>
<Label Text="{Binding DayTotal,StringFormat='{}{0:HH:mm}'}" VerticalOptions="Center"></Label> <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> <Label Text="{Binding Sollstunden,StringFormat='{}{0:HH:mm}'}" VerticalOptions="Center"></Label>
</HorizontalStackLayout> </HorizontalStackLayout>
</Border> </Border>
</Grid> </Grid>
<CollectionView <CollectionView
ItemsSource="{Binding DayTimes}" ItemsSource="{Binding DayTimes}"
x:Name="stundeItems" Margin="0,0,0,20" x:Name="stundeItems" Margin="0,0,0,20"
SelectionMode="Single" SelectionMode="Single"
SelectionChangedCommand="{Binding SelectEntryCommand}" SelectionChangedCommand="{Binding SelectEntryCommand}"
SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}" SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}"
@@ -72,27 +72,29 @@
<VisualStateGroup Name="CommonStates"> <VisualStateGroup Name="CommonStates">
<VisualState Name="Normal"> <VisualState Name="Normal">
<VisualState.Setters> <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.Setters>
</VisualState> </VisualState>
<VisualState Name="Selected"> <VisualState Name="Selected">
<VisualState.Setters> <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.Setters>
</VisualState> </VisualState>
</VisualStateGroup> </VisualStateGroup>
</VisualStateManager.VisualStateGroups> </VisualStateManager.VisualStateGroups>
<HorizontalStackLayout> <HorizontalStackLayout>
<HorizontalStackLayout.Triggers> <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="BackgroundColor" Value="LightCoral" />
<Setter Property="Padding" Value="4"/> <Setter Property="Padding" Value="4" />
</DataTrigger> </DataTrigger>
</HorizontalStackLayout.Triggers> </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="von" Padding="5,0,5,0" />
<Label Text="{Binding Begin}" /> <Label Text="{Binding Begin}" />
<Label Text="bis" Padding="5,0,5,0" /> <Label Text="bis" Padding="5,0,5,0" />
@@ -101,27 +103,30 @@
<HorizontalStackLayout HorizontalOptions="FillAndExpand"> <HorizontalStackLayout HorizontalOptions="FillAndExpand">
<HorizontalStackLayout.Triggers> <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="BackgroundColor" Value="LightCoral" />
<Setter Property="Padding" Value="4"/> <Setter Property="Padding" Value="4" />
</DataTrigger> </DataTrigger>
</HorizontalStackLayout.Triggers> </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 GemeindeAktiv.Name}" Margin="0,0,10,0"
<Label Text="{Binding ProjektAktiv.Name}" Margin="0,0,10,0" IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.ProjektAktivSet}" /> 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}" /> <Label Text="{Binding FreistellungAktiv.Name}" IsVisible="{Binding Approved}" />
</HorizontalStackLayout> </HorizontalStackLayout>
<Label Text="{Binding Description}" Padding="0,0,0,15"/> <Label Text="{Binding Description}" Padding="0,0,0,15" />
</VerticalStackLayout> </VerticalStackLayout>
</DataTemplate> </DataTemplate>
</CollectionView.ItemTemplate> </CollectionView.ItemTemplate>
</CollectionView> </CollectionView>
<!--<BoxView HeightRequest="1" Grid.Row="2" Margin="0,5,0,15" />--> <!--<BoxView HeightRequest="1" Grid.Row="2" Margin="0,5,0,15" />-->
<Button Text="{Binding LoadOverview}" <Button Text="{Binding LoadOverview}"
Grid.Row="2" Grid.Row="2"
Command="{Binding LoadDataCommand}" Command="{Binding LoadDataCommand}"
TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}" /> TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}" />
<Border Padding="2" Grid.Row="3" Margin="0,10,0,0"> <Border Padding="2" Grid.Row="3" Margin="0,10,0,0">
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,*" ColumnDefinitions="Auto,Auto,*,Auto" Margin="10,2"> <Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,*" ColumnDefinitions="Auto,Auto,*,Auto" Margin="10,2">
@@ -133,15 +138,23 @@
<Label Grid.Row="1" Grid.Column="2" Text="Zeitausgleich:" Margin="15,0,0,0" /> <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="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="0" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0"
<Label Grid.Row="1" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding ZeitCalculated, Converter={StaticResource secToTime}}" ToolTipProperties.Text="Geleistete Stunden" /> Text="{Binding Nominal, Converter={StaticResource secToTime}}"
<Label Grid.Row="2" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding OvertimeMonth, Converter={StaticResource secToTime}}" /> ToolTipProperties.Text="Sollstunden" />
<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="1" HorizontalTextAlignment="End" Padding="0,0,5,0"
<Label Grid.Row="1" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding Zeitausgleich, Converter={StaticResource secToTime}}" /> Text="{Binding ZeitCalculated, Converter={StaticResource secToTime}}"
<Label Grid.Row="2" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding Holiday, 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> </Grid>
</Border> </Border>
</Grid> </Grid>
<!--</VerticalStackLayout>--> <!--</VerticalStackLayout>-->

View File

@@ -9,60 +9,80 @@ namespace Jugenddienst_Stunden.Views;
/// Code-Behind f<>r die Stunden-<2D>bersicht /// Code-Behind f<>r die Stunden-<2D>bersicht
/// </summary> /// </summary>
public partial class StundenPage : ContentPage { 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> /// <summary>
/// CTOR /// CTOR (DI)
/// </summary> /// </summary>
public StundenPage() { public StundenPage(StundenViewModel vm) {
InitializeComponent(); InitializeComponent();
BindingContext = vm;
if (BindingContext is StundenViewModel vm) { vm.AlertEvent += Vm_AlertEvent;
vm.AlertEvent += Vm_AlertEvent; vm.InfoEvent += Vm_InfoEvent;
vm.InfoEvent += Vm_InfoEvent;
}
if (!CheckLogin()) {
NavigateToTargetPage();
}
} // 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) { private void Vm_AlertEvent(object? sender, string e) {
MainThread.BeginInvokeOnMainThread(async () => { MainThread.BeginInvokeOnMainThread(async () => { await DisplayAlert("Fehler:", e, "OK"); });
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);
});
}
/// <summary> //private void Vm_InfoEvent(object? sender, string e) {
/// Beim Laden der Seite den Titel setzen // DisplayAlert("Information:", e, "OK");
/// </summary> //}
protected override void OnAppearing() { //private void Vm_InfoEvent(object? sender, string e) {
base.OnAppearing(); // MainThread.BeginInvokeOnMainThread(async () => {
Title = Preferences.Default.Get("name", "Nicht") + " " + Preferences.Default.Get("surname", "eingeloggt"); // 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() { /// <summary>
return Preferences.Default.Get("apiKey", "") != ""; /// 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() { if (!CheckLogin()) {
await Navigation.PushAsync(new LoginPage()); 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));
}
} }