31 Commits
1.0.7 ... main

Author SHA1 Message Date
1ee0fc61f6 HoursDto: Update 2025-12-25 19:19:17 +01:00
c6fd58a290 Reenabled deletion of Time-Entrys 2025-12-25 19:10:36 +01:00
656d39f43e Less requests to get Data faster
Load settings with `GetEntryWithSettingsAsync`, update `Hours` and `ViewModels`.
2025-12-25 11:39:44 +01:00
15856d0dd0 Refactor StundePage to MVVM with DI 2025-12-25 09:20:14 +01:00
8da8734065 Add SocketsHttpHandler 2025-12-25 09:03:01 +01:00
cd4eae34c3 Projekt and Gemeinde are required if active 2025-12-19 15:26:26 +01:00
52815d7e21 .net 10 2025-12-19 15:25:59 +01:00
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
83118103d9 1.0.9 2025-09-10 22:40:02 +02:00
5ecf6c7537 Cleanup 2025-09-10 22:37:25 +02:00
7540b6e6ad Update Nugets 2025-09-10 21:58:26 +02:00
8937332942 Typo 2025-07-12 16:23:27 +02:00
a0345cad94 Update .gitignore 2025-07-12 16:23:13 +02:00
6b4bffe5ec Format 2025-03-18 00:26:18 +01:00
093679c9bb Precision 2025-03-18 00:26:08 +01:00
7f9e3f622a 1.0.8 2025-02-23 21:02:01 +01:00
54ed791c5b Versionsabzeige korrigiert 2025-02-23 20:39:56 +01:00
c88bf148d4 Farben & Cleanup 2025-02-23 20:30:26 +01:00
1a8eeec85d Anpassungen für 1.0.8 2025-02-23 17:44:46 +01:00
1b4f73a913 Anpassungen für 1.0.8
Farbe StatusBar für Dark/Light-Mode angepasst
Loginseite optimiert
Bugfix Toast für Windows
Versionskontrolle für Windows optimiert
2025-02-23 17:25:59 +01:00
2f0be4cc5c Enable LLVM 2025-02-22 18:26:31 +01:00
21af63c02c Farbe des Themes angepasst 2025-02-22 18:26:08 +01:00
c4fdc266b5 Optimierung Stundenseite
Grid statt StackLayout
Monatssummen beachten jetzt auch das Jahr
2025-02-22 18:26:08 +01:00
6d6eaf836b Adjust spacing 2025-02-22 17:43:12 +01:00
67d662a0a7 Versionshinweis hinzugefügt 2025-02-17 10:27:37 +01:00
103 changed files with 3772 additions and 2116 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

2
.gitignore vendored
View File

@@ -396,3 +396,5 @@ FodyWeavers.xsd
# JetBrains Rider # JetBrains Rider
*.sln.iml *.sln.iml
/.idea
/var

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

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"

View File

@@ -4,14 +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,7 +11,8 @@
<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"

View File

@@ -1,4 +1,5 @@
namespace Jugenddienst_Stunden; namespace Jugenddienst_Stunden;
/// <summary> /// <summary>
/// AppShell.xaml.cs /// AppShell.xaml.cs
/// </summary> /// </summary>
@@ -13,7 +14,7 @@ public partial class AppShell : Shell {
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

@@ -9,6 +9,7 @@ namespace Jugenddienst_Stunden.Converter {
return !collection.Any(); return !collection.Any();
return collection.Any(); return collection.Any();
} }
if ((string)parameter == "Invert") if ((string)parameter == "Invert")
return true; return true;
return false; return false;

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,14 +1,22 @@
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) { public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
if (value is null) if (value is null)
return "0:0"; return "0:0";
if (value is int) { if (value is int) {
seconds = (int)value; seconds = (int)value;
}
if (value is double) {
seconds = (int)Math.Round((double)value);
} else { } else {
int.TryParse((string?)value, out seconds); int.TryParse((string?)value, out seconds);
} }

View File

@@ -1,11 +1,13 @@
using System.Globalization; using System.Globalization;
namespace Jugenddienst_Stunden.Converter; namespace Jugenddienst_Stunden.Converter;
internal class StringVisibilityConverter : IValueConverter { internal class StringVisibilityConverter : 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 string strValue) { if (value is string strValue) {
return !string.IsNullOrEmpty(strValue.Replace("Server: ","")); return !string.IsNullOrEmpty(strValue.Replace("Server: ", ""));
} }
return false; return false;
} }

View File

@@ -5,8 +5,14 @@ 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,176 @@
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.StatusCode == System.Net.HttpStatusCode.NotFound) {
var message = req.Method + ": " + req.RequestUri + " nicht gefunden";
throw ApiException.From(res.StatusCode, message);
}
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;
// Sonderfall: Wenn path ein absoluter file:// URI ist, diesen relativ zur Basis behandeln
// Weiß nicht wie file:// zustande kommt, vermutlich wäre das zu verhindern
if (Uri.TryCreate(relativePath, UriKind.Absolute, out var uri)) {
if (uri.Scheme == Uri.UriSchemeFile) {
var normalizedBase = baseUrl.Trim();
if (!normalizedBase.EndsWith('/'))
normalizedBase += "/";
if (relativePath.StartsWith('/'))
relativePath = relativePath.TrimStart('/');
var baseUriNormalized = new Uri(normalizedBase, UriKind.Absolute);
return new Uri(baseUriNormalized, relativePath);
}
return uri;
}
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,29 @@
using Jugenddienst_Stunden.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using System.Net.Http;
namespace Jugenddienst_Stunden.Infrastructure;
internal static class HttpClientRegistration {
/// <summary>
/// Registriert den ApiClient mit einem SocketsHttpHandler als prim<69>ren MessageHandler.
/// Vermeidet den Android-spezifischen Cast-Fehler in Xamarin.Android.Net.AndroidMessageHandler.
///</summary>
public static IServiceCollection AddApiHttpClient(this IServiceCollection services, ApiOptions options) {
if (services is null)
throw new ArgumentNullException(nameof(services));
if (options is null)
throw new ArgumentNullException(nameof(options));
// ApiOptions als Singleton bereitstellen (kann nach Bedarf angepasst werden)
services.AddSingleton(options);
// HttpClient f<>r ApiClient registrieren und einen SocketsHttpHandler verwenden.
// SocketsHttpHandler vermeidet das problematische Casting, das bei AndroidMessageHandler
// zur InvalidCastException (URLConnectionInvoker -> HttpURLConnection) f<>hrt.
services.AddHttpClient<IApiClient, ApiClient>()
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler());
return services;
}
}

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;
public 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,17 @@
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);
Task<(DayTime dayTime, Settings settings, List<DayTime> existingDayTimes)> GetEntryWithSettingsAsync(int id);
}

View File

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

View File

@@ -2,7 +2,7 @@
<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>net10.0-android36.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> -->
@@ -27,8 +27,8 @@
<ApplicationId>com.companyname.jugenddienststunden</ApplicationId> <ApplicationId>com.companyname.jugenddienststunden</ApplicationId>
<!-- Versions --> <!-- Versions -->
<ApplicationDisplayVersion>1.0.7</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>8</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>
@@ -39,14 +39,14 @@
<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.7</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.7</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>8</ApplicationVersion> <ApplicationVersion>10</ApplicationVersion>
<Optimize>False</Optimize> <Optimize>False</Optimize>
<Deterministic>True</Deterministic> <Deterministic>True</Deterministic>
</PropertyGroup> </PropertyGroup>
@@ -54,8 +54,8 @@
<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.7</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>8</ApplicationVersion> <ApplicationVersion>10</ApplicationVersion>
<Deterministic>True</Deterministic> <Deterministic>True</Deterministic>
</PropertyGroup> </PropertyGroup>
@@ -65,8 +65,8 @@
<DebugSymbols>True</DebugSymbols> <DebugSymbols>True</DebugSymbols>
<Optimize>False</Optimize> <Optimize>False</Optimize>
<Deterministic>True</Deterministic> <Deterministic>True</Deterministic>
<ApplicationDisplayVersion>1.0.7</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>8</ApplicationVersion> <ApplicationVersion>10</ApplicationVersion>
<AndroidKeyStore>False</AndroidKeyStore> <AndroidKeyStore>False</AndroidKeyStore>
</PropertyGroup> </PropertyGroup>
@@ -76,8 +76,8 @@
<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.7</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>8</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>
@@ -89,8 +89,8 @@
<DebugSymbols>False</DebugSymbols> <DebugSymbols>False</DebugSymbols>
<Optimize>True</Optimize> <Optimize>True</Optimize>
<Deterministic>True</Deterministic> <Deterministic>True</Deterministic>
<ApplicationDisplayVersion>1.0.7</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>8</ApplicationVersion> <ApplicationVersion>10</ApplicationVersion>
<RunAOTCompilation>False</RunAOTCompilation> <RunAOTCompilation>False</RunAOTCompilation>
<PublishTrimmed>True</PublishTrimmed> <PublishTrimmed>True</PublishTrimmed>
<AndroidKeyStore>False</AndroidKeyStore> <AndroidKeyStore>False</AndroidKeyStore>
@@ -98,8 +98,8 @@
<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.7</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>8</ApplicationVersion> <ApplicationVersion>10</ApplicationVersion>
<Optimize>True</Optimize> <Optimize>True</Optimize>
<Deterministic>True</Deterministic> <Deterministic>True</Deterministic>
</PropertyGroup> </PropertyGroup>
@@ -109,42 +109,46 @@
<SymbolPackageFormat>snupkg</SymbolPackageFormat> <SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PlatformTarget>AnyCPU</PlatformTarget> <PlatformTarget>AnyCPU</PlatformTarget>
<ProduceReferenceAssembly>False</ProduceReferenceAssembly> <ProduceReferenceAssembly>False</ProduceReferenceAssembly>
<AssemblyVersion>1.0.7</AssemblyVersion> <AssemblyVersion>1.0.9</AssemblyVersion>
<FileVersion>1.0.7</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.7</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>8</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.7</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>8</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>8</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.7</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>8</ApplicationVersion> <ApplicationVersion>10</ApplicationVersion>
<ApplicationDisplayVersion>1.0.7</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.7</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>8</ApplicationVersion> <ApplicationVersion>10</ApplicationVersion>
<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.7</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>8</ApplicationVersion> <ApplicationVersion>10</ApplicationVersion>
<EnableLLVM>True</EnableLLVM>
<DebugSymbols>False</DebugSymbols>
<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'">
@@ -159,21 +163,21 @@
<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.7</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>8</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.7</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0.9</ApplicationDisplayVersion>
<ApplicationVersion>8</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);net10.0-windows10.0.26100.0</TargetFrameworks>
<WindowsPackageType>None</WindowsPackageType> <WindowsPackageType>MSIX</WindowsPackageType>
<!-- <TargetFrameworks>;net9.0-android35.0</TargetFrameworks> --> <!-- <TargetFrameworks>;net9.0-android35.0</TargetFrameworks> -->
</PropertyGroup> </PropertyGroup>
@@ -252,18 +256,19 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommunityToolkit.Maui" Version="11.1.0" /> <PackageReference Include="CommunityToolkit.Maui" Version="12.2.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Microsoft.Maui.Controls" Version="9.0.40"> <PackageReference Include="Microsoft.Maui.Controls" Version="9.0.110">
<TreatAsUsed>true</TreatAsUsed> <TreatAsUsed>true</TreatAsUsed>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="9.0.40" /> <PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="9.0.110" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.2" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.9" />
<PackageReference Include="Microsoft.Maui.Graphics" Version="9.0.40" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Microsoft.NET.Runtime.MonoAOTCompiler.Task" Version="9.0.2" /> <PackageReference Include="Microsoft.Maui.Graphics" Version="9.0.110" />
<PackageReference Include="Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk" Version="9.0.2" /> <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="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="ZXing.Net.Maui.Controls" Version="0.4.0" /> <PackageReference Include="ZXing.Net.Maui.Controls" Version="0.5.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,7 +1,15 @@
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;
using System.Net;
namespace Jugenddienst_Stunden; namespace Jugenddienst_Stunden;
@@ -9,13 +17,12 @@ namespace Jugenddienst_Stunden;
/// Das Hauptprogramm. /// Das Hauptprogramm.
/// </summary> /// </summary>
public static class MauiProgram { public static class MauiProgram {
public static MauiApp CreateMauiApp() { public static MauiApp CreateMauiApp() {
var builder = MauiApp.CreateBuilder(); var builder = MauiApp.CreateBuilder();
builder builder
.UseMauiApp<App>() .UseMauiApp<App>()
// Initialize the .NET MAUI Community Toolkit by adding the below line of code // Initialize the .NET MAUI Community Toolkit by adding the below line of code
.UseMauiCommunityToolkit() .UseMauiCommunityToolkit(options => { options.SetShouldEnableSnackbarOnWindows(true); })
.ConfigureFonts(fonts => { .ConfigureFonts(fonts => {
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
@@ -23,19 +30,95 @@ public static class MauiProgram {
//.UseBarcodeScanning(); //.UseBarcodeScanning();
.UseBarcodeReader(); .UseBarcodeReader();
#if DEBUG //#if DEBUG
if (GlobalVar.ApiKey == null) { // if (string.IsNullOrWhiteSpace(GlobalVar.ApiKey)) {
GlobalVar.ApiKey = Preferences.Default.Get("apiKey", "MTQxfHNkdFptQkNZTXlPT3ZyMHNBZDl0UnVxNExMRXxodHRwOi8vaG91cnMuZGF1bmkubWluZS5udTo4MS9hcHBhcGk="); // GlobalVar.ApiKey = Preferences.Default.Get("apiKey",
GlobalVar.Name = Preferences.Default.Get("name", "Testserver: Isabell"); // "MTQxfHNkdFptQkNZTXlPT3ZyMHxodHRwOi8vaG91cnMuZGF1bmkubWluZS5udTo4MS9hcHBhcGk=");
GlobalVar.Surname = Preferences.Default.Get("surname", "Biasi"); // GlobalVar.Name = Preferences.Default.Get("name", "Testserver: Isabell");
GlobalVar.EmployeeId = Preferences.Default.Get("EmployeeId", 141); // GlobalVar.Surname = Preferences.Default.Get("surname", "Biasi");
GlobalVar.ApiUrl = Preferences.Default.Get("apiUrl", "https://hours.dauni.mine.nu/appapi"); // GlobalVar.EmployeeId = Preferences.Default.Get("EmployeeId", 141);
// GlobalVar.ApiUrl = Preferences.Default.Get("apiUrl", "https://hours.dauni.mine.nu/appapi");
// }
// builder.Logging.AddDebug();
//#endif
// ApiClient registrieren: SocketsHttpHandler als Primary Handler (vermeidet AndroidMessageHandler-Castfehler)
//var apiOptions = new Infrastructure.ApiOptions { BaseUrl = GlobalVar.ApiUrl, Timeout = TimeSpan.FromSeconds(15) };
//builder.Services.AddApiHttpClient(apiOptions);
// 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 SocketsHttpHandler (managed) and RequestLoggingHandler
builder.Services.AddTransient<RequestLoggingHandler>();
builder.Services.AddSingleton<HttpClient>(sp => {
var nativeHandler = new SocketsHttpHandler {
AllowAutoRedirect = false,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
ConnectTimeout = TimeSpan.FromSeconds(10)
};
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);
} }
builder.Logging.AddDebug(); });
#endif
// 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.StundeViewModel>();
builder.Services.AddTransient<Views.StundePage>();
builder.Services.AddTransient<ViewModels.LoginViewModel>();
builder.Services.AddTransient<Views.LoginPage>();
return builder.Build(); return builder.Build();
} }
} }

View File

@@ -7,10 +7,7 @@ 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) { internal static async Task<string> GetApiDataWithAuthAsync(string url, string token) {
if (Connectivity.Current.NetworkAccess == NetworkAccess.None) if (Connectivity.Current.NetworkAccess == NetworkAccess.None)
throw new Exception("Bitte überprüfen Sie Ihre Internetverbindung und versuchen Sie es erneut."); throw new Exception("Bitte überprüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.");
@@ -19,11 +16,11 @@ internal static class BaseFunc {
// Erstellen eines HttpClient-Objekts // Erstellen eines HttpClient-Objekts
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) { using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
client.DefaultRequestHeaders.Add("Accept", "application/json"); client.DefaultRequestHeaders.Add("Accept", "application/json");
// Hinzufügen des Bearer-Tokens zum Authorization-Header // Hinzufügen des Bearer-Tokens zum Authorization-Header
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
// Senden der Anfrage und Abrufen der Antwort // Senden der Anfrage und Abrufen der Antwort
using (HttpResponseMessage HttpResponseMessage = await client.GetAsync(url).ConfigureAwait(false)) { using (HttpResponseMessage HttpResponseMessage = await client.GetAsync(url).ConfigureAwait(false)) {
@@ -35,12 +32,11 @@ internal static class BaseFunc {
if (HttpResponseMessage.StatusCode == System.Net.HttpStatusCode.OK) { if (HttpResponseMessage.StatusCode == System.Net.HttpStatusCode.OK) {
return responseData; return responseData;
} else { } else {
var options = new JsonDocumentOptions { var options = new JsonDocumentOptions { AllowTrailingCommas = true };
AllowTrailingCommas = true
};
using (JsonDocument doc = JsonDocument.Parse(responseData, options)) { using (JsonDocument doc = JsonDocument.Parse(responseData, options)) {
JsonElement root = doc.RootElement; JsonElement root = doc.RootElement;
string message = root.GetProperty("message").GetString() ?? throw new Exception("Fehler: 'message' ist null."); string message = root.GetProperty("message").GetString() ??
throw new Exception("Fehler: 'message' ist null.");
throw new Exception(message); throw new Exception(message);
} }
} }
@@ -49,34 +45,26 @@ internal static class BaseFunc {
} }
internal static async Task<User> AuthUserPass(string user, string pass, string url) { internal static async Task<User> AuthUserPass(string user, string pass, string url) {
var values = new Dictionary<string, string> { { "user", user }, { "pass", pass } };
var values = new Dictionary<string, string>
{
{ "user", user },
{ "pass", pass }
};
var content = new FormUrlEncodedContent(values); var content = new FormUrlEncodedContent(values);
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) { using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
client.DefaultRequestHeaders.Add("Accept", "application/json"); client.DefaultRequestHeaders.Add("Accept", "application/json");
// Senden der Anfrage und Abrufen der Antwort // Senden der Anfrage und Abrufen der Antwort
using (HttpResponseMessage HttpResponseMessage = await client.PostAsync(url, content).ConfigureAwait(false)) { using (HttpResponseMessage HttpResponseMessage =
if (!HttpResponseMessage.IsSuccessStatusCode) await client.PostAsync(url, content).ConfigureAwait(false)) {
{ if (!HttpResponseMessage.IsSuccessStatusCode) {
//throw new Exception("Fehler beim Einloggen " + HttpResponseMessage.Content); //throw new Exception("Fehler beim Einloggen " + HttpResponseMessage.Content);
var byteArray = await HttpResponseMessage.Content.ReadAsByteArrayAsync(); var byteArray = await HttpResponseMessage.Content.ReadAsByteArrayAsync();
string responseData = Encoding.UTF8.GetString(byteArray); string responseData = Encoding.UTF8.GetString(byteArray);
var options = new JsonDocumentOptions { var options = new JsonDocumentOptions { AllowTrailingCommas = true };
AllowTrailingCommas = true
};
using (JsonDocument doc = JsonDocument.Parse(responseData, options)) { using (JsonDocument doc = JsonDocument.Parse(responseData, options)) {
JsonElement root = doc.RootElement; JsonElement root = doc.RootElement;
string message = root.GetProperty("message").GetString() ?? throw new Exception("Fehler: 'message' ist null."); string message = root.GetProperty("message").GetString() ??
throw new Exception("Fehler: 'message' ist null.");
throw new Exception(message); throw new Exception(message);
} }
} }
@@ -87,14 +75,15 @@ internal static class BaseFunc {
// Lesen und Rückgabe der Antwort als String // Lesen und Rückgabe der Antwort als String
string responseData = await HttpContent.ReadAsStringAsync(); string responseData = await HttpContent.ReadAsStringAsync();
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(responseData) ?? throw new Exception("Fehler beim Deserialisieren der Daten"); 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"); //User userData = System.Text.Json.JsonSerializer.Deserialize<User>(responseData) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
return res.user; return res.user;
} }
} }
} }
} }
return null; return null;
} }
@@ -108,16 +97,13 @@ internal static class BaseFunc {
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() { Date = File.GetLastWriteTime(filename) };
Date = File.GetLastWriteTime(filename)
};
} }
/// <summary> /// <summary>
/// Stundeneintrag speichern /// Stundeneintrag speichern
/// </summary> /// </summary>
internal static async Task SaveItemAsync(string url, string token, DayTime item, bool isNewItem = false) { 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 //Uhrzeiten sollten sinnvolle Werte haben - außer bei Freistellungen, da wäre eigentlich null
if (item.TimeSpanVon == item.TimeSpanBis && item.FreistellungAktiv == null) { if (item.TimeSpanVon == item.TimeSpanBis && item.FreistellungAktiv == null) {
throw new Exception("Beginn und Ende sind gleich"); throw new Exception("Beginn und Ende sind gleich");
@@ -138,22 +124,27 @@ internal static class BaseFunc {
if (item.GemeindeAktiv == null && GlobalVar.Settings.GemeindeAktivSet) { if (item.GemeindeAktiv == null && GlobalVar.Settings.GemeindeAktivSet) {
throw new Exception("Gemeinde nicht gewählt"); throw new Exception("Gemeinde nicht gewählt");
} }
//Projekt ist ein Pflichtfeld //Projekt ist ein Pflichtfeld
if (item.ProjektAktiv == null && GlobalVar.Settings.ProjektAktivSet) { if (item.ProjektAktiv == null && GlobalVar.Settings.ProjektAktivSet) {
throw new Exception("Projekt nicht gewählt"); throw new Exception("Projekt nicht gewählt");
} }
//Keine Beschreibung //Keine Beschreibung
if (string.IsNullOrEmpty(item.Description) && item.FreistellungAktiv == null) { if (string.IsNullOrEmpty(item.Description) && item.FreistellungAktiv == null) {
throw new Exception("Keine Beschreibung"); throw new Exception("Keine Beschreibung");
} }
//Keine Beschreibung //Keine Beschreibung
if (string.IsNullOrEmpty(item.Description)) { if (string.IsNullOrEmpty(item.Description)) {
item.Description = item.FreistellungAktiv.Name; item.Description = item.FreistellungAktiv.Name;
} }
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) { using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
//HttpClient client = new HttpClient(); //HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Accept", "application/json"); client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
//string json = JsonSerializer.Serialize<DayTime>(item); //string json = JsonSerializer.Serialize<DayTime>(item);
string json = JsonConvert.SerializeObject(item); string json = JsonConvert.SerializeObject(item);
@@ -169,7 +160,6 @@ internal static class BaseFunc {
throw new Exception("Fehler beim Speichern " + response.Content); throw new Exception("Fehler beim Speichern " + response.Content);
} }
} }
} }
/// <summary> /// <summary>
@@ -177,10 +167,10 @@ internal static class BaseFunc {
/// </summary> /// </summary>
internal static async Task DeleteItemAsync(string url, string token) { internal static async Task DeleteItemAsync(string url, string token) {
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) { using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
//HttpClient client = new HttpClient(); //HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Accept", "application/json"); client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
HttpResponseMessage response = await client.DeleteAsync(url); HttpResponseMessage response = await client.DeleteAsync(url);
@@ -188,5 +178,4 @@ internal static class BaseFunc {
throw new Exception("Fehler beim Löschen " + response.Content); 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 { public static int EmployeeId {
get => Preferences.Default.Get("EmployeeId", 0); get => Preferences.Default.Get("EmployeeId", 0);
set => Preferences.Default.Set("EmployeeId", value); set => Preferences.Default.Set("EmployeeId", value);
} }
public static string Name { public static string Name {
get => Preferences.Default.Get("name", "Nicht"); get => Preferences.Default.Get("name", "Nicht");
set => Preferences.Default.Set("name", value); set => Preferences.Default.Set("name", value);
} }
public static string Surname { public static string Surname {
get => Preferences.Default.Get("surname", "Eingeloggt"); get => Preferences.Default.Get("surname", "Eingeloggt");
set => Preferences.Default.Set("surname", value); set => Preferences.Default.Set("surname", value);
} }
public static string ApiUrl { public static string ApiUrl {
get => Preferences.Default.Get("apiUrl", ""); get => Preferences.Default.Get("apiUrl", "");
set => Preferences.Default.Set("apiUrl", value); set => Preferences.Default.Set("apiUrl", value);
} }
public static Settings Settings { get; set; } public static Settings Settings { get; set; }
} }

View File

@@ -4,14 +4,14 @@ using Newtonsoft.Json;
namespace Jugenddienst_Stunden.Models; namespace Jugenddienst_Stunden.Models;
internal static class HoursBase { internal static class HoursBase {
/// <summary> /// <summary>
/// Laden ... what can be: "settings", "hours", date="YYYY-MM-DD", id=<int/> /// Laden ... what can be: "settings", "hours", date="YYYY-MM-DD", id=<int/>
/// </summary> /// </summary>
/// <returns>Entire response</returns> /// <returns>Entire response</returns>
internal static async Task<BaseResponse> LoadBase(string what) { internal static async Task<BaseResponse> LoadBase(string what) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?"+what, GlobalVar.ApiKey); string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?" + what, GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten"); BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
throw new Exception("Fehler beim Deserialisieren der Daten");
return res; return res;
} }
@@ -21,7 +21,8 @@ internal static class HoursBase {
/// <returns>Settings only</returns> /// <returns>Settings only</returns>
internal static async Task<Settings> LoadSettings() { internal static async Task<Settings> LoadSettings() {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?settings", 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) ??
throw new Exception("Fehler beim Deserialisieren der Daten");
return res.settings; return res.settings;
} }
@@ -31,7 +32,8 @@ internal static class HoursBase {
/// <returns>Hours only</returns> /// <returns>Hours only</returns>
internal static async Task<Hours> LoadData() { internal static async Task<Hours> LoadData() {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?hours", 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) ??
throw new Exception("Fehler beim Deserialisieren der Daten");
return res.hour; return res.hour;
} }
@@ -41,7 +43,8 @@ internal static class HoursBase {
/// <returns>User-Object</returns> /// <returns>User-Object</returns>
public static async Task<User> LoadUser(string apiKey) { public static async Task<User> LoadUser(string apiKey) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl, 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) ??
throw new Exception("Fehler beim Deserialisieren der Daten");
return res.user; return res.user;
} }
@@ -49,8 +52,10 @@ internal static class HoursBase {
/// Zeiten eines Tages holen /// Zeiten eines Tages holen
/// </summary> /// </summary>
internal static async Task<List<DayTime>> LoadDay(DateTime date) { internal static async Task<List<DayTime>> LoadDay(DateTime date) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?date=" + date.ToString("yyyy-MM-dd"), GlobalVar.ApiKey); string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?date=" + date.ToString("yyyy-MM-dd"),
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten"); GlobalVar.ApiKey);
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
throw new Exception("Fehler beim Deserialisieren der Daten");
return res.daytimes; return res.daytimes;
} }
@@ -59,7 +64,8 @@ internal static class HoursBase {
/// </summary> /// </summary>
internal static async Task<DayTime> LoadEntry(int id) { internal static async Task<DayTime> LoadEntry(int id) {
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?id=" + id, 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) ??
throw new Exception("Fehler beim Deserialisieren der Daten");
res.daytime.TimeSpanVon = res.daytime.Begin.ToTimeSpan(); res.daytime.TimeSpanVon = res.daytime.Begin.ToTimeSpan();
res.daytime.TimeSpanBis = res.daytime.End.ToTimeSpan(); res.daytime.TimeSpanBis = res.daytime.End.ToTimeSpan();
return res.daytime; return res.daytime;
@@ -68,7 +74,8 @@ internal static class HoursBase {
/// <summary> /// <summary>
/// Eintrag speichern /// Eintrag speichern
/// </summary> /// </summary>
internal static async Task<DayTime> SaveEntry(DayTime stunde) { //, string begin, string end, string freistellung, string bemerkung) { internal static async Task<DayTime> SaveEntry(DayTime stunde) {
//, string begin, string end, string freistellung, string bemerkung) {
bool isNew = false; bool isNew = false;
if (stunde.Id == null) if (stunde.Id == null)
isNew = true; isNew = true;

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,12 +11,17 @@ 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) {
@@ -31,13 +36,15 @@ namespace Jugenddienst_Stunden.Models {
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}.",
reader.Path));
existingValue = existingValue ?? contract.DefaultCreator(); existingValue = existingValue ?? contract.DefaultCreator();
serializer.Populate(reader, existingValue); serializer.Populate(reader, existingValue);
} }
break; break;
} }
} }
// Should not come here. // Should not come here.
throw new JsonSerializationException(string.Format("Unclosed array at path {0}.", reader.Path)); throw new JsonSerializationException(string.Format("Unclosed array at path {0}.", reader.Path));
} }

View File

@@ -1,11 +1,12 @@
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));

View File

@@ -2,6 +2,7 @@
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;
@@ -17,5 +18,4 @@ internal class Operator {
public string? year; public string? year;
public event EventHandler<string>? AlertEvent; public event EventHandler<string>? AlertEvent;
} }

View File

@@ -13,6 +13,7 @@ internal class TokenData {
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 dat = Encoding.UTF8.GetString(Convert.FromBase64String(ak));
string[] parts = dat.Split('|'); string[] parts = dat.Split('|');
@@ -20,9 +21,12 @@ internal class TokenData {
throw new FormatException("API key format is invalid"); throw new FormatException("API key format is invalid");
} }
Token = dat.Split('|')[1]; ; Token = dat.Split('|')[1];
Url = dat.Split('|')[2]; ; ;
Operator_id = dat.Split('|')[0]; ; Url = dat.Split('|')[2];
;
Operator_id = dat.Split('|')[0];
;
ApiKey = ak; ApiKey = ak;
} }
} }

View File

@@ -7,8 +7,8 @@
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

@@ -3,6 +3,9 @@ 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 @@
<?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> -->
@@ -34,5 +34,5 @@
</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"/>
<ui-application appid="maui-application-id-placeholder" exec="Jugenddienst Stunden.dll" multiple="false"
nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
<label>maui-application-title-placeholder</label> <label>maui-application-title-placeholder</label>
<icon>maui-appicon-placeholder</icon> <icon>maui-appicon-placeholder</icon>
<metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true" /> <metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true"/>
</ui-application> </ui-application>
<shortcut-list /> <shortcut-list/>
<privileges> <privileges>
<privilege>http://tizen.org/privilege/internet</privilege> <privilege>http://tizen.org/privilege/internet</privilege>
</privileges> </privileges>
<dependencies /> <dependencies/>
<provides-appdefined-privileges /> <provides-appdefined-privileges/>
</manifest> </manifest>

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

@@ -4,11 +4,14 @@
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"
IgnorableNamespaces="uap rescap"> xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
IgnorableNamespaces="uap rescap com desktop">
<Identity Name="JugenddienstStunden" Publisher="CN=User Name" Version="0.0.0.0" /> <Identity Name="JugenddienstStunden" Publisher="CN=User Name" Version="0.0.0.0"/>
<mp:PhoneIdentity PhoneProductId="4BA4D7D7-E3C2-4BBF-92EF-0EDB5DB5CDB4" PhonePublisherId="00000000-0000-0000-0000-000000000000"/> <mp:PhoneIdentity PhoneProductId="4BA4D7D7-E3C2-4BBF-92EF-0EDB5DB5CDB4"
PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties> <Properties>
<DisplayName>$placeholder$</DisplayName> <DisplayName>$placeholder$</DisplayName>
@@ -17,12 +20,12 @@
</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>
@@ -33,14 +36,37 @@
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"
Wide310x150Logo="Resources\Windows\Wide\$placeholder$.png"
Square310x310Logo="Resources\Windows\$placeholder$.png" ShortName="Stunden"/>
<uap:SplashScreen Image="Resources\Windows\Splash\$placeholder$.png" BackgroundColor="#F7931D"/> <uap:SplashScreen Image="Resources\Windows\Splash\$placeholder$.png" BackgroundColor="#F7931D"/>
</uap:VisualElements> </uap:VisualElements>
<Extensions>
<!-- Specify which CLSID to activate when notification is clicked -->
<desktop:Extension Category="windows.toastNotificationActivation">
<desktop:ToastNotificationActivation ToastActivatorCLSID="6e919706-2634-4d97-a93c-2213b2acc334"/>
</desktop:Extension>
<!-- Register COM CLSID -->
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="Jugenddienst Stunden.exe" DisplayName="$targetnametoken$"
Arguments="----AppNotificationActivated:">
<!-- Example path to executable: CommunityToolkit.Maui.Sample\CommunityToolkit.Maui.Sample.exe -->
<com:Class Id="6e919706-2634-4d97-a93c-2213b2acc334"/>
</com:ExeServer>
</com:ComServer>
</com:Extension>
</Extensions>
</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>

View File

@@ -9,7 +9,8 @@
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
</dpiAwareness>
</windowsSettings> </windowsSettings>
</application> </application>
</assembly> </assembly>

View File

@@ -1,7 +1,7 @@
<?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>
@@ -30,5 +30,5 @@
<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

@@ -59,27 +59,28 @@
: 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"
xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType> <xsd:complexType>
<xsd:choice maxOccurs="unbounded"> <xsd:choice maxOccurs="unbounded">
<xsd:element name="data"> <xsd:element name="data">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" /> <xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="resheader"> <xsd:element name="resheader">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" /> <xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
</xsd:choice> </xsd:choice>
@@ -93,9 +94,13 @@
<value>1.3</value> <value>1.3</value>
</resheader> </resheader>
<resheader name="reader"> <resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader> </resheader>
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader> </resheader>
</root> </root>

View File

@@ -1,7 +1,7 @@
{ {
"profiles": { "profiles": {
"Windows Machine": { "Windows Machine": {
"commandName": "Project", "commandName": "MsixPackage",
"nativeDebugging": false "nativeDebugging": false
} }
} }

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,5 +1,6 @@
<?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">
@@ -7,10 +8,10 @@
<!-- Note: For Android please see also Platforms\Android\Resources\values\colors.xml --> <!-- Note: For Android please see also Platforms\Android\Resources\values\colors.xml -->
<Color x:Key="Primary">#F7931D</Color> <Color x:Key="Primary">#F7931D</Color>
<Color x:Key="PrimaryDark">#ac99ea</Color> <Color x:Key="PrimaryDark">#EC8A16</Color>
<Color x:Key="PrimaryDarkText">#242424</Color> <Color x:Key="PrimaryDarkText">#242424</Color>
<Color x:Key="Secondary">#DFD8F7</Color> <Color x:Key="Secondary">#DFD8F7</Color>
<Color x:Key="SecondaryDarkText">#9880e5</Color> <Color x:Key="SecondaryDarkText">#EC8A16</Color>
<Color x:Key="Tertiary">#2B0B98</Color> <Color x:Key="Tertiary">#2B0B98</Color>
<Color x:Key="TransparentColor">Transparent</Color> <Color x:Key="TransparentColor">Transparent</Color>
@@ -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,12 +1,14 @@
<?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>
@@ -16,39 +18,48 @@
</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,67 @@
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 dayTime, Settings settings, List<DayTime> existingDayTimes)> GetEntryWithSettingsAsync(int id) {
//var stunde = await _repo.LoadEntry(id);
//var (existingDayTimes, settings) = await GetDayWithSettingsAsync(stunde.Day);
//return (stunde, settings, existingDayTimes);
string q = $"id={id}";
var baseRes = await _repo.LoadBase(q);
return (baseRes.daytime ?? new DayTime(), baseRes.settings, baseRes.daytimes ?? new List<DayTime>());
}
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,19 +1,20 @@
using Jugenddienst_Stunden.Models; using Jugenddienst_Stunden.Models;
using Jugenddienst_Stunden.Types;
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 Types.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<Types.Hours> hours { get; set; }
/// <summary> /// <summary>
/// Liste der Stundeneinträge /// Liste der Stundeneinträge
@@ -29,6 +30,7 @@ internal class BaseResponse {
/// Auch irgendwie doppelt ... /// Auch irgendwie doppelt ...
/// </summary> /// </summary>
public Operator operatorVar { get; set; } public Operator operatorVar { get; set; }
public User user { get; set; } public User user { get; set; }
public int error { get; set; } public int error { get; set; }

View File

@@ -2,6 +2,7 @@
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>
@@ -92,5 +93,4 @@ public class DayTime {
public Freistellung? FreistellungAktiv { get; set; } public Freistellung? FreistellungAktiv { get; set; }
public int TimeTable { get; set; } public int TimeTable { get; set; }
} }

View File

@@ -1,4 +1,5 @@
namespace Jugenddienst_Stunden.Types; namespace Jugenddienst_Stunden.Types;
/// <summary> /// <summary>
/// Freistellungen: Urlaub, Zeitausgleich, Krankheit, ... /// Freistellungen: Urlaub, Zeitausgleich, Krankheit, ...
/// </summary> /// </summary>

View File

@@ -1,36 +1,66 @@
 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 class Hours : ObservableObject { public partial class Hours : ObservableObject {
public int? Zeit; /// <summary>
public int? Nominal; /// Total time in seconds for the current context.
//public Dictionary<DateOnly,NominalDay> nominal_day_api; /// "zeit" is used by the API to represent the current recorded time value.
/// </summary>
public int? zeit { get; set; }
/// <summary>
/// Nominal working time expectation (e.g. seconds per day or month depending on API semantics).
/// Represents the expected amount of time to be worked.
/// </summary>
public int? nominal { get; set; }
/// <summary>
/// List of nominal day records returned by the API.
/// May be null when the API does not provide per-day nominal data.
/// </summary>
public List<NominalDay>? Nominal_day_api; public List<NominalDay>? Nominal_day_api;
//public Dictionary<int,NominalWeek> nominal_week_api;
/// <summary>
/// List of nominal week records returned by the API.
/// May be null when the API does not provide per-week nominal data.
/// </summary>
public List<NominalWeek>? Nominal_week_api; public List<NominalWeek>? Nominal_week_api;
//public List<string> time_line;
public string? Zeit_total;
//https://stackoverflow.com/questions/29449641/deserialize-json-when-a-value-can-be-an-object-or-an-empty-array/29450279#29450279 /// <summary>
//[JsonConverter(typeof(JsonSingleOrEmptyArrayConverter<Hours>))] /// Total time in seconds reported by the API for the current period. Nullable if not provided.
//public Dictionary<int,decimal> zeit_total_daily; /// </summary>
public double? zeit_total { get; set; }
/// <summary>
/// Daily total time values returned by the API.
/// Each entry represents a day with its associated time value.
/// </summary>
public List<TimeDay> zeit_total_daily_api; public List<TimeDay> zeit_total_daily_api;
/// <summary>
/// Collection of daytime entries representing individual recorded time slots or events.
/// Nullable when the API returns no detailed daytime information.
/// </summary>
public List<DayTime>? daytime; public List<DayTime>? daytime;
//public List<string> wochensumme; //public List<string> wochensumme;
public int overtime_month;
public int overtime; [ObservableProperty] public double overtime_month;
[ObservableProperty] public double overtime;
//public List<string> overtime_day; //public List<string> overtime_day;
public int zeitausgleich;
public int zeitausgleich_month; [ObservableProperty] public double zeitausgleich;
public int holiday;
public int krankheit; public double zeitausgleich_month;
public int weiterbildung; public double? holiday { get; set; }
public int bereitschaft; public double krankheit;
public int bereitschaft_month; public double weiterbildung;
public double bereitschaft;
public double bereitschaft_month;
//public Operator operator_api; //public Operator operator_api;
public DateTime Today; public DateTime Today;
public DateTime Date; public DateTime Date;
@@ -40,5 +70,4 @@ internal 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 int 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 int Hours; public double Hours;
} }

View File

@@ -29,5 +29,10 @@ public class Settings {
/// </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>
/// Version der API
/// </summary>
public string? Version { get; set; }
} }

View File

@@ -3,5 +3,5 @@
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 float 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 decimal Hours { get; set; } public double Hours { get; set; }
} }

View File

@@ -1,5 +1,6 @@
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; }

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,9 +1,24 @@
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 {
private readonly IAuthService _auth;
private readonly IAppSettings _settings;
private readonly IAlertService? _alerts;
private DateTime _lastDetectionTime = DateTime.MinValue;
private readonly TimeSpan _detectionInterval = TimeSpan.FromSeconds(5);
public event EventHandler<string>? AlertEvent;
//public event EventHandler<string>? InfoEvent;
public event EventHandler<ConfirmationEventArgs>? InfoEvent;
/// <summary> /// <summary>
/// Name der Anwendung /// Name der Anwendung
/// </summary> /// </summary>
@@ -14,18 +29,133 @@ public class LoginViewModel {
/// </summary> /// </summary>
public string Version => AppInfo.VersionString; public string Version => AppInfo.VersionString;
/// <summary> [ObservableProperty]
/// Kurze Mitteilung für den Anwender private string message = "Scanne den QR-Code von deinem Benutzerprofil auf der Stundenseite.";
/// </summary>
public string Message { get; set; } = "Scanne den QR-Code von deinem Benutzerprofil auf der Stundenseite.";
/// <summary> [ObservableProperty]
/// Genutzer Server für die API private string? server;
/// </summary>
public string Server { get; set; } = "Server: " + Preferences.Default.Get("apiUrl", "").Replace("/appapi", "").Replace("https://", "").Replace("http://", "");
/// <summary> [ObservableProperty]
/// Titel der Seite - im Moment der aktuelle Anwender private string? serverLabel;
/// </summary>
public string Title { get; set; } = Preferences.Default.Get("name", "Nicht") + " " + Preferences.Default.Get("surname", "eingeloggt"); [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

@@ -3,8 +3,10 @@ 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 {

View File

@@ -47,7 +47,9 @@ 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,12 +5,20 @@ 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";
@@ -21,6 +29,7 @@ public partial class StundeViewModel : ObservableObject, IQueryAttributable {
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, Task<bool>> ConfirmEvent;
//public event Func<string, string, string?, string?, Task<bool>> ConfirmEvent; //public event Func<string, string, string?, string?, Task<bool>> ConfirmEvent;
//public event EventHandler<ConfirmEventArgs> ConfirmEvent; //public event EventHandler<ConfirmEventArgs> ConfirmEvent;
@@ -28,47 +37,39 @@ public partial class StundeViewModel : ObservableObject, IQueryAttributable {
/// <summary> /// <summary>
/// Gemeinden für die Auswahlliste /// Gemeinden für die Auswahlliste
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty] private List<Gemeinde> optionsGemeinde;
private List<Gemeinde> optionsGemeinde;
/// <summary> /// <summary>
/// Projekte für die Auswahlliste /// Projekte für die Auswahlliste
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty] private List<Projekt> optionsProjekt;
private List<Projekt> optionsProjekt;
/// <summary> /// <summary>
/// Freistellungen für die Auswahlliste /// Freistellungen für die Auswahlliste
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty] private List<Freistellung> optionsFreistellung;
private List<Freistellung> optionsFreistellung;
/// <summary> /// <summary>
/// Vorhandene Zeiten anzeigen, wenn neuer Eintrag erstellt wird /// Vorhandene Zeiten anzeigen, wenn neuer Eintrag erstellt wird
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty] private List<DayTime> dayTimes;
private List<DayTime> dayTimes;
/// <summary> /// <summary>
/// Aktueller Stundeneintrag /// Aktueller Stundeneintrag
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty] private DayTime dayTime;
private DayTime dayTime;
/// <summary> /// <summary>
/// Dürfen Gemeinden verwendet werden? /// Dürfen Gemeinden verwendet werden?
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty] private bool gemeindeAktivSet;
private bool gemeindeAktivSet;
/// <summary> /// <summary>
/// Dürfen Projekte verwendet werden? /// Dürfen Projekte verwendet werden?
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty] private bool projektAktivSet;
private bool projektAktivSet;
[ObservableProperty] [ObservableProperty] private bool freistellungEnabled;
private bool freistellungEnabled;
public ICommand SaveCommand { get; private set; } public ICommand SaveCommand { get; private set; }
public ICommand DeleteCommand { get; private set; } public ICommand DeleteCommand { get; private set; }
@@ -76,20 +77,21 @@ public partial class StundeViewModel : ObservableObject, IQueryAttributable {
//public ICommand LoadDataCommand { get; private set; } //public ICommand LoadDataCommand { get; private set; }
public StundeViewModel() { public StundeViewModel(IHoursService hoursService, IAlertService alertService) {
_hoursService = hoursService;
SaveCommand = new AsyncRelayCommand(Save); SaveCommand = new AsyncRelayCommand(Save);
//DeleteCommand = new AsyncRelayCommand(Delete);
DeleteConfirmCommand = new Command(async () => await DeleteConfirm()); DeleteConfirmCommand = new Command(async () => await DeleteConfirm());
if (alertService is not null) {
alertService.AlertRaised += (s, msg) => AlertEvent?.Invoke(this, msg);
} }
public StundeViewModel(DayTime stunde) { //LoadSettingsAsync();
SaveCommand = new AsyncRelayCommand(Save);
DeleteConfirmCommand = new AsyncRelayCommand(DeleteConfirm);
} }
private async void LoadSettingsAsync() { private async void LoadSettingsAsync() {
try { try {
Settings = await HoursBase.LoadSettings(); Settings = await _hoursService.GetSettingsAsync();
GlobalVar.Settings = Settings; GlobalVar.Settings = Settings;
OptionsGemeinde = Settings.Gemeinden; OptionsGemeinde = Settings.Gemeinden;
@@ -98,27 +100,57 @@ public partial class StundeViewModel : ObservableObject, IQueryAttributable {
GemeindeAktivSet = Settings.GemeindeAktivSet; GemeindeAktivSet = Settings.GemeindeAktivSet;
ProjektAktivSet = Settings.ProjektAktivSet; ProjektAktivSet = Settings.ProjektAktivSet;
} catch (Exception e) { } catch (Exception e) {
AlertEvent?.Invoke(this, e.Message); AlertEvent?.Invoke(this, e.Message);
} }
} }
private async void UpdateSettingsAsync(Settings settings) {
GlobalVar.Settings = settings;
OptionsGemeinde = settings.Gemeinden;
OptionsProjekt = settings.Projekte;
OptionsFreistellung = settings.Freistellungen;
GemeindeAktivSet = settings.GemeindeAktivSet;
ProjektAktivSet = settings.ProjektAktivSet;
}
async Task Save() { async Task Save() {
bool exceptionOccurred = false; bool exceptionOccurred = false;
bool proceed = true; bool proceed = true;
//Arbeitszeit sollte nicht null sein
if (DayTime.TimeSpanVon == DayTime.TimeSpanBis && DayTime.FreistellungAktiv.Name == null) { if (DayTime.TimeSpanVon == DayTime.TimeSpanBis && DayTime.FreistellungAktiv.Name == null) {
proceed = false; proceed = false;
AlertEvent?.Invoke(this, "Uhrzeiten sollten unterschiedlich sein"); AlertEvent?.Invoke(this, "Uhrzeiten sollten unterschiedlich sein");
} }
//Projekt ist ein Pflichtfeld
if (Settings.ProjektAktivSet) {
var projektId = DayTime.ProjektAktiv?.Id ?? 0;
if (projektId == 0) {
proceed = false;
AlertEvent?.Invoke(this, "Projekt darf nicht leer sein");
}
}
//Gemeinde ist ein Pflichtfeld
if (Settings.GemeindeAktivSet) {
var gemeindeId = DayTime.GemeindeAktiv?.Id ?? 0;
if (gemeindeId == 0) {
proceed = false;
AlertEvent?.Invoke(this, "Gemeinde darf nicht leer sein");
}
}
if (proceed) { if (proceed) {
try { try {
await HoursBase.SaveEntry(DayTime); await _hoursService.SaveEntryAsync(DayTime);
} catch (Exception e) { } catch (Exception e) {
AlertEvent?.Invoke(this, e.Message); AlertEvent?.Invoke(this, e.Message);
exceptionOccurred = true; exceptionOccurred = true;
} }
if (!exceptionOccurred) { if (!exceptionOccurred) {
if (DayTime.Id != null) { if (DayTime.Id != null) {
await Shell.Current.GoToAsync($"..?saved={DayTime.Id}"); await Shell.Current.GoToAsync($"..?saved={DayTime.Id}");
@@ -133,7 +165,7 @@ public partial class StundeViewModel : ObservableObject, IQueryAttributable {
/// Löschen ohne Bestätigung /// Löschen ohne Bestätigung
/// </summary> /// </summary>
private async Task Delete() { private async Task Delete() {
await HoursBase.DeleteEntry(DayTime); await _hoursService.DeleteEntryAsync(DayTime);
await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}"); await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}");
} }
@@ -142,11 +174,16 @@ public partial class StundeViewModel : ObservableObject, IQueryAttributable {
/// </summary> /// </summary>
private async Task DeleteConfirm() { private async Task DeleteConfirm() {
if (ConfirmEvent != null) { if (ConfirmEvent != null) {
bool answer = await ConfirmEvent.Invoke("Achtung", "Löschen kann nicht ungeschehen gemacht werden. Fortfahren?"); bool answer =
await ConfirmEvent.Invoke("Achtung", "Löschen kann nicht ungeschehen gemacht werden. Fortfahren?");
if (answer) { if (answer) {
//Löschen //Löschen
await HoursBase.DeleteEntry(DayTime); try {
await _hoursService.DeleteEntryAsync(DayTime);
await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}"); await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}");
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
}
} else { } else {
//nicht Löschen //nicht Löschen
} }
@@ -160,72 +197,63 @@ public partial class StundeViewModel : ObservableObject, IQueryAttributable {
//load beinhaltet die ID: Eintrag bearbeiten //load beinhaltet die ID: Eintrag bearbeiten
//date beinhaltet einen Tag: Neuen Eintrag erstellen //date beinhaltet einen Tag: Neuen Eintrag erstellen
if (query.ContainsKey("load")) { if (query.ContainsKey("load")) {
//DateTime heute = DateTime.Now; //DateTime heute = DateTime.Now;
try { try {
//_dayTime = await HoursBase.LoadEntry(Convert.ToInt32(query["load"])); //var entry = await _hoursService.GetEntryAsync(Convert.ToInt32(query["load"]));
BaseResponse dat = await HoursBase.LoadBase("id=" + Convert.ToInt32(query["load"])); var (entry, settings, daytimes) = await _hoursService.GetEntryWithSettingsAsync(Convert.ToInt32(query["load"]));
GlobalVar.Settings = dat.settings; UpdateSettingsAsync(settings);
GemeindeAktivSet = dat.settings.GemeindeAktivSet;
ProjektAktivSet = dat.settings.ProjektAktivSet;
DayTime = dat.daytime; DayTime = entry;
DayTime.TimeSpanVon = dat.daytime.Begin.ToTimeSpan(); DayTime.TimeSpanVon = entry.Begin.ToTimeSpan();
DayTime.TimeSpanBis = dat.daytime.End.ToTimeSpan(); DayTime.TimeSpanBis = entry.End.ToTimeSpan();
OptionsGemeinde = dat.settings.Gemeinden ?? new List<Gemeinde>(); DayTime.GemeindeAktiv = OptionsGemeinde.FirstOrDefault(Gemeinde => Gemeinde.Id == DayTime.Gemeinde) ??
OptionsProjekt = dat.settings.Projekte ?? new List<Projekt>(); new Gemeinde();
OptionsFreistellung = dat.settings.Freistellungen ?? new List<Freistellung>(); DayTime.ProjektAktiv = OptionsProjekt.FirstOrDefault(Projekt => Projekt.Id == DayTime.Projekt) ??
new Projekt();
DayTime.FreistellungAktiv =
OptionsFreistellung.FirstOrDefault(Freistellung => Freistellung.Id == DayTime.Free) ??
new Freistellung();
DayTime.GemeindeAktiv = OptionsGemeinde.FirstOrDefault(Gemeinde => Gemeinde.Id == DayTime.Gemeinde) ?? new Gemeinde();
DayTime.ProjektAktiv = OptionsProjekt.FirstOrDefault(Projekt => Projekt.Id == DayTime.Projekt) ?? new Projekt();
DayTime.FreistellungAktiv = OptionsFreistellung.FirstOrDefault(Freistellung => Freistellung.Id == DayTime.Free) ?? new Freistellung();
//Evtl. noch die anderen Zeiten des gleichen Tages holen
BaseResponse dat1 = await HoursBase.LoadBase("date=" + DayTime.Day.ToString("yyyy-MM-dd"));
DayTimes = dat1.daytimes;
OnPropertyChanged(nameof(DayTime)); 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"); SubTitle = DayTime.Day.ToString("dddd, d. MMMM yyyy");
OnPropertyChanged(nameof(SubTitle)); OnPropertyChanged(nameof(SubTitle));
FreistellungEnabled = !DayTime.Approved; FreistellungEnabled = !DayTime.Approved;
//OnPropertyChanged(nameof(DayTime));
DayTimes = daytimes;
OnPropertyChanged(nameof(DayTimes));
} catch (Exception e) {
AlertEvent?.Invoke(this, e.Message);
} finally {
//Evtl. noch die anderen Zeiten des gleichen Tages holen
//var day = await _hoursService.GetDayWithSettingsAsync(DayTime.Day);
//DayTimes = day.dayTimes;
//OnPropertyChanged(nameof(DayTimes));
}
//OnPropertyChanged(nameof(DayTime));
} else if (query.ContainsKey("date")) { } else if (query.ContainsKey("date")) {
Title = "Neuer Eintrag"; Title = "Neuer Eintrag";
OnPropertyChanged(nameof(Title)); 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",
System.Globalization.CultureInfo.InvariantCulture);
//Bei neuem Eintrag die vorhandenen des gleichen Tages anzeigen //Bei neuem Eintrag die vorhandenen des gleichen Tages anzeigen
try { try {
//DayTimes = await HoursBase.LoadDay(_date); var (list, settings) = await _hoursService.GetDayWithSettingsAsync(_date);
BaseResponse dat = await HoursBase.LoadBase("date=" + _date.ToString("yyyy-MM-dd")); UpdateSettingsAsync(settings);
GlobalVar.Settings = dat.settings; DayTimes = list;
DayTimes = dat.daytimes; OnPropertyChanged(nameof(DayTimes));
OptionsGemeinde = dat.settings.Gemeinden;
OptionsProjekt = dat.settings.Projekte;
OptionsFreistellung = dat.settings.Freistellungen;
GemeindeAktivSet = dat.settings.GemeindeAktivSet;
ProjektAktivSet = dat.settings.ProjektAktivSet;
} catch (Exception) { } catch (Exception) {
//Ein Tag ohne Einträge gibt eine Fehlermeldung, //Ein Tag ohne Einträge gibt eine Fehlermeldung,
//die soll aber ignoriert werden, weil beim Neueintrag ist das ja Wurscht //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 //In dem Fall müssen die Settings aber nochmal geholt werden, weil die dann nicht geladen wurden
LoadSettingsAsync(); // LoadSettingsAsync();
} finally { } finally {
DayTime = new DayTime(); DayTime = new DayTime();
DayTime.Day = _date; DayTime.Day = _date;
@@ -240,8 +268,6 @@ public partial class StundeViewModel : ObservableObject, IQueryAttributable {
OnPropertyChanged(nameof(SubTitle)); OnPropertyChanged(nameof(SubTitle));
//OnPropertyChanged(nameof(DayTime)); //OnPropertyChanged(nameof(DayTime));
} }
} }
} }
} }

View File

@@ -5,15 +5,16 @@ 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;
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; }
@@ -29,8 +30,7 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
/// <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();
@@ -38,27 +38,22 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
/// <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;
private Hours _hour; [ObservableProperty] private Hours hours;
public Hours Hours {
get => _hour;
}
/// <summary> /// <summary>
/// Mindest-Datum für den Datepicker /// Mindest-Datum für den Datepicker
@@ -78,14 +73,26 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
/// 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 { public DateTime DateToday {
get => dateToday; get => dateToday;
set { set {
if (dateToday != value) { if (dateToday != value) {
dateToday = value; dateToday = value;
LoadOverview = "Lade Summen für " + dateToday.ToString("MMMM"); LoadOverview = "Lade Summen für " + dateToday.ToString("MMMM yy");
//OnPropertyChanged(); // Task.Run(() => LoadDay(value));
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);
}
});
} }
} }
} }
@@ -93,43 +100,50 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
/// <summary> /// <summary>
/// Monatsübersicht: Geleistete Stunden /// Monatsübersicht: Geleistete Stunden
/// </summary> /// </summary>
public string? ZeitCalculated { public double? ZeitCalculated {
get => Hours.Zeit_total; get => Hours.zeit_total;
} }
/// <summary> /// <summary>
/// Monatsübersicht: Sollstunden /// Monatsübersicht: Sollstunden
/// </summary> /// </summary>
public int? Nominal { public double? Nominal {
get => Hours.Nominal; get => Hours.nominal;
} }
/// <summary> /// <summary>
/// Monatsübersicht: Differenz zwischen Soll und geleisteten Stunden /// Monatsübersicht: Differenz zwischen Soll und geleisteten Stunden
/// </summary> /// </summary>
public int? Overtime { public double? Overtime {
get => Hours.overtime; get => Hours.overtime;
} }
/// <summary> /// <summary>
/// Monatsübersicht: Restüberstunden insgesamt /// Monatsübersicht: Restüberstunden insgesamt
/// </summary> /// </summary>
public int OvertimeMonth { public double OvertimeMonth {
get => Hours.overtime_month; get => Hours.overtime_month;
} }
public double Zeitausgleich {
get => Hours.zeitausgleich;
}
public double ZeitausgleichMonth {
get => Hours.zeitausgleich_month;
}
/// <summary> /// <summary>
/// Monatsübersicht: Resturlaub /// Monatsübersicht: Resturlaub
/// </summary> /// </summary>
public int Holiday { public double? Holiday {
get => Hours.holiday; get => Hours.holiday;
} }
/// <summary> /// <summary>
/// Seite neu laden /// Seite neu laden
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty] private bool isRefreshing;
private bool isRefreshing;
/// <summary> /// <summary>
@@ -145,12 +159,12 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
private bool doContinue = true; private bool doContinue = true;
/// <summary> /// <summary>
/// CTOR /// CTOR (DI)
/// </summary> /// </summary>
public StundenViewModel() { public StundenViewModel(IHoursService hoursService) {
_hour = new Hours(); _hoursService = hoursService;
Hours = new Hours();
LoadOverview = "Lade Summen für " + DateToday.ToString("MMMM"); LoadOverview = "Lade Summen für " + DateToday.ToString("MMMM");
@@ -160,8 +174,22 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
RefreshListCommand = new AsyncRelayCommand(RefreshList); RefreshListCommand = new AsyncRelayCommand(RefreshList);
RefreshCommand = new Command(async () => await RefreshItemsAsync()); RefreshCommand = new Command(async () => await RefreshItemsAsync());
Task task = LoadDay(DateTime.Today); // Task task = LoadDay(DateTime.Today);
// Beim Startup NICHT direkt im CTOR laden (kann Startup/Navigation blockieren)
// Stattdessen via Dispatcher "nach" dem Aufbau starten:
MainThread.BeginInvokeOnMainThread(async () =>
{
try
{
await LoadDay(DateTime.Today);
} }
catch (Exception ex)
{
AlertEvent?.Invoke(this, ex.Message);
}
});
}
/// <summary> /// <summary>
@@ -193,8 +221,16 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
/// </summary> /// </summary>
private async Task LoadData() { private async Task LoadData() {
try { try {
BaseResponse dat = await HoursBase.LoadBase("hours&month=" + DateToday.ToString("MM")); var (hours, settings) = await _hoursService.GetMonthSummaryAsync(DateToday);
_hour = dat.hour; Hours = hours;
Settings = settings;
if (Settings.Version != AppInfo.Current.VersionString.Substring(0, 5)) {
InfoEvent?.Invoke(this,
"Version: " + Settings.Version + " verfügbar (" + AppInfo.Current.VersionString.Substring(0, 5) +
" installiert)");
}
//_hour = await HoursBase.LoadData(); //_hour = await HoursBase.LoadData();
RefreshProperties(); RefreshProperties();
} catch (Exception e) { } catch (Exception e) {
@@ -207,21 +243,25 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
/// 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) {
// kleine Initialwerte sind ok, aber UI-Thread sicher setzen:
await MainThread.InvokeOnMainThreadAsync(() =>
{
DayTotal = new TimeOnly(0); DayTotal = new TimeOnly(0);
Sollstunden = new TimeOnly(0); Sollstunden = new TimeOnly(0);
});
try { try {
//_dayTimes = await HoursBase.LoadDay(date); var (dayTimes, settings) = await _hoursService.GetDayWithSettingsAsync(date);
BaseResponse dat = await HoursBase.LoadBase("date=" + date.ToString("yyyy-MM-dd"));
DayTimes = dat.daytimes; await MainThread.InvokeOnMainThreadAsync(() =>
Settings = dat.settings; {
DayTimes = dayTimes;
Settings = settings;
GemeindeAktivSet = Settings.GemeindeAktivSet; GemeindeAktivSet = Settings.GemeindeAktivSet;
ProjektAktivSet = Settings.ProjektAktivSet; 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;
@@ -233,31 +273,55 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
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(); _soll = Settings.Nominal.Where(w => w.Timetable == dt.TimeTable && w.Wochentag == dt.Wday).ToList();
if (_soll.Count > 0) if (_soll.Count > 0)
Sollstunden = TimeOnly.FromTimeSpan(TimeSpan.FromHours(_soll[0].Zeit)); {
var soll = TimeOnly.FromTimeSpan(TimeSpan.FromHours(_soll[0].Zeit));
await MainThread.InvokeOnMainThreadAsync(() => Sollstunden = soll);
} }
DayTotal = TimeOnly.FromTimeSpan(span); }
var total = TimeOnly.FromTimeSpan(span);
await MainThread.InvokeOnMainThreadAsync(() => DayTotal = total);
//Nach der Tagessumme die anderen Tage anhängen //Nach der Tagessumme die anderen Tage anhängen
if (DayTimes != null) { if (DayTimes != null) {
BaseResponse dat1 = await HoursBase.LoadBase("date=" + date.ToString("yyyy-MM-dd") + "&tilldate=" + date.AddDays(3).ToString("yyyy-MM-dd")); var more = await _hoursService.GetDayRangeAsync(date.AddDays(1), date.AddDays(3));
if (dat1.daytimes != null) if (more != null && more.Count > 0)
DayTimes = dat.daytimes.Concat(dat1.daytimes).ToList(); {
await MainThread.InvokeOnMainThreadAsync(() =>
DayTimes = DayTimes.Concat(more).ToList()
);
}
} }
} catch (Exception e) { } catch (Exception e) {
await MainThread.InvokeOnMainThreadAsync(() =>
{
DayTimes = new List<DayTime>(); DayTimes = new List<DayTime>();
//TODO: hier könnte auch ein Fehler kommen, dann wäre InfoEvent falsch. //TODO: hier könnte auch ein Fehler kommen, dann wäre InfoEvent falsch.
if (Settings.Version != null && Settings.Version != AppInfo.Current.VersionString.Substring(0, 5)) {
InfoEvent?.Invoke(this,
"Version: " + Settings.Version + " verfügbar (" + AppInfo.Current.VersionString.Substring(0, 5) +
" installiert)");
} else {
InfoEvent?.Invoke(this, e.Message); InfoEvent?.Invoke(this, e.Message);
}
});
} finally { } finally {
await MainThread.InvokeOnMainThreadAsync(() =>
{
OnPropertyChanged(nameof(DayTotal)); OnPropertyChanged(nameof(DayTotal));
OnPropertyChanged(nameof(Sollstunden)); OnPropertyChanged(nameof(Sollstunden));
OnPropertyChanged(nameof(DateToday)); OnPropertyChanged(nameof(DateToday));
OnPropertyChanged(nameof(LoadOverview)); OnPropertyChanged(nameof(LoadOverview));
//OnPropertyChanged(nameof(DayTimes)); });
} }
} }
async void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) { async void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) {
@@ -282,13 +346,14 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
/// Refreshes all properties /// Refreshes all properties
/// </summary> /// </summary>
private void RefreshProperties() { private void RefreshProperties() {
OnPropertyChanged(nameof(Hours));
OnPropertyChanged(nameof(Title));
OnPropertyChanged(nameof(Nominal)); OnPropertyChanged(nameof(Nominal));
OnPropertyChanged(nameof(Overtime)); OnPropertyChanged(nameof(Overtime));
OnPropertyChanged(nameof(OvertimeMonth)); OnPropertyChanged(nameof(OvertimeMonth));
OnPropertyChanged(nameof(Zeitausgleich));
OnPropertyChanged(nameof(ZeitCalculated)); OnPropertyChanged(nameof(ZeitCalculated));
OnPropertyChanged(nameof(Holiday)); OnPropertyChanged(nameof(Holiday));
OnPropertyChanged(nameof(Hours));
OnPropertyChanged(nameof(Title));
OnPropertyChanged(nameof(MinimumDate)); OnPropertyChanged(nameof(MinimumDate));
OnPropertyChanged(nameof(MaximumDate)); OnPropertyChanged(nameof(MaximumDate));
OnPropertyChanged(nameof(LoadOverview)); OnPropertyChanged(nameof(LoadOverview));
@@ -302,6 +367,4 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
//Console.WriteLine($"Fehler bei OnPropertyChanged: {ex.Message}"); //Console.WriteLine($"Fehler bei OnPropertyChanged: {ex.Message}");
} }
} }
} }

View File

@@ -1,6 +1,8 @@
<?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:viewModels="clr-namespace:Jugenddienst_Stunden.ViewModels" xmlns:viewModels="clr-namespace:Jugenddienst_Stunden.ViewModels"
x:Class="Jugenddienst_Stunden.Views.AllNotesPage" x:Class="Jugenddienst_Stunden.Views.AllNotesPage"
Title="Deine Notizen" Title="Deine Notizen"
@@ -12,11 +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>
<toolkit:StatusBarBehavior
StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}"
StatusBarStyle="LightContent" />
</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"
@@ -26,7 +35,7 @@
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 are laid out --> <!-- Designate how the collection of items is laid out -->
<CollectionView.ItemsLayout> <CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical" ItemSpacing="10" /> <LinearItemsLayout Orientation="Vertical" ItemSpacing="10" />
</CollectionView.ItemsLayout> </CollectionView.ItemsLayout>
@@ -35,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,6 +1,8 @@
<?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:models="clr-namespace:Jugenddienst_Stunden.ViewModels" xmlns:models="clr-namespace:Jugenddienst_Stunden.ViewModels"
xmlns:zxing="clr-namespace:ZXing.Net.Maui.Controls;assembly=ZXing.Net.MAUI.Controls" xmlns:zxing="clr-namespace:ZXing.Net.Maui.Controls;assembly=ZXing.Net.MAUI.Controls"
xmlns:conv="clr-namespace:Jugenddienst_Stunden.Converter" xmlns:conv="clr-namespace:Jugenddienst_Stunden.Converter"
@@ -8,54 +10,68 @@
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>
<toolkit:StatusBarBehavior
StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}"
StatusBarStyle="LightContent" />
</ContentPage.Behaviors>
<ScrollView> <ScrollView>
<VerticalStackLayout Spacing="10" Margin="15,0"> <VerticalStackLayout Spacing="10" Margin="15,0">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<HorizontalStackLayout Spacing="10" HorizontalOptions="Start" Grid.Column="0"> <HorizontalStackLayout Spacing="5" HorizontalOptions="Start" Grid.Column="0">
<Label FontSize="22" FontAttributes="Bold" Text="{Binding AppTitle}" Margin="0,4,0,0" /> <Label FontSize="20" FontAttributes="Bold" Text="{Binding AppTitle}" Margin="0,7,0,0" />
<Label FontSize="22" Text="{Binding Version}" Margin="0,4,0,0" /> <Label FontSize="16" Text="{Binding Version}" Margin="0,11,0,0" />
</HorizontalStackLayout>
<HorizontalStackLayout HorizontalOptions="End" Spacing="10" Grid.Column="1">
<Label Text="Login QR/manuell" Margin="0,12,0,0"/>
<Switch x:Name="LoginSwitch" IsToggled="False" Toggled="Switch_Toggled"/>
</HorizontalStackLayout> </HorizontalStackLayout>
<Grid Grid.Column="1" ColumnDefinitions="*,50" ColumnSpacing="10">
<Label Text="Login QR/manuell" VerticalOptions="Center" Grid.Column="0" />
<Switch x:Name="LoginSwitch" IsToggled="{Binding IsManualMode}" VerticalOptions="Center"
Grid.Column="1" />
</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"> <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 x:Name="barcodeScannerView" <zxing:CameraBarcodeReaderView
BarcodesDetected="BarcodesDetected" x:Name="barcodeScannerView"
HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"/> IsDetecting="{Binding IsDetecting}">
<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="15"> <VerticalStackLayout x:Name="LoginManual" Spacing="25" IsVisible="{Binding IsManualMode}">
<Label Text="Manueller Login" FontSize="32" HorizontalOptions="Start" /> <Label Text="Manueller Login" FontSize="32" HorizontalOptions="Start" Margin="0, 20, 0, 0" />
<Entry x:Name="UsernameEntry" Placeholder="Benutzername" 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" Keyboard="Url" /> <Entry x:Name="ServerEntry" Text="{Binding Server}" Placeholder="Server (gleich wie im Browser)" Keyboard="Url" />
<Button Text="Login" Clicked="OnLoginButtonClicked" /> <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,7 +10,6 @@ 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);
@@ -20,10 +20,43 @@ public partial class LoginPage : ContentPage {
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) {
vm.AlertEvent += async (_, msg) => await DisplayAlert("Fehler:", msg, "OK");
//vm.InfoEvent += async (_, msg) => await DisplayAlert("Information:", msg, "OK");
// 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);
});
};
}
barcodeScannerView.Options =
new BarcodeReaderOptions { Formats = BarcodeFormat.QrCode, AutoRotate = true, Multiple = false };
// Fallback-Verkabelung: Falls das EventToCommandBehavior in XAML nicht greift,
// leiten wir das Kamera-Event manuell an das ViewModel-Command weiter.
barcodeScannerView.BarcodesDetected += (s, e) => {
if (BindingContext is LoginViewModel vm && vm.QrDetectedCommand is not null) {
// Sicherstellen, dass die Command-Ausführung im UI-Thread erfolgt
MainThread.BeginInvokeOnMainThread(async () => {
if (vm.QrDetectedCommand.CanExecute(e)) {
await vm.QrDetectedCommand.ExecuteAsync(e);
}
});
}
}; };
//if (BindingContext is LoginViewModel vm) { //if (BindingContext is LoginViewModel vm) {
@@ -33,19 +66,7 @@ public partial class LoginPage : ContentPage {
//} //}
//Zwischen manuellem und automatischem Login (mit QR-Code) umschalten und die Schalterstellung merken //Zwischen manuellem und automatischem Login (mit QR-Code) umschalten und die Schalterstellung merken
bool sqr = true; // MVVM übernimmt Umschalten über IsManualMode im ViewModel; keine Code-Behind-Umschaltung mehr
bool sma = false;
if (Preferences.Default.Get("logintype", "") == "manual") {
sqr = false;
sma = true;
LoginSwitch.IsToggled = true;
Message.IsVisible = false;
} else {
LoginSwitch.IsToggled = false;
Message.IsVisible = true;
}
LoginQR.IsVisible = sqr;
LoginManual.IsVisible = sma;
} }
@@ -54,7 +75,6 @@ public partial class LoginPage : ContentPage {
/// 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; var currentTime = DateTime.Now;
if ((currentTime - _lastDetectionTime) > _detectionInterval) { if ((currentTime - _lastDetectionTime) > _detectionInterval) {
_lastDetectionTime = currentTime; _lastDetectionTime = currentTime;
@@ -75,22 +95,21 @@ public partial class LoginPage : ContentPage {
Title = user.Name + " " + user.Surname; Title = user.Name + " " + user.Surname;
//Auf der Loginseite wird der Server als Info ohne Protokoll und ohne /appapi angezeigt //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://", ""); 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) { } catch (Exception e) {
await DisplayAlert("Fehler", e.Message, "OK"); await DisplayAlert("Fehler", e.Message, "OK");
} }
}); });
} else { } else {
MainThread.InvokeOnMainThreadAsync(() => { MainThread.InvokeOnMainThreadAsync(() => {
@@ -101,20 +120,19 @@ public partial class LoginPage : ContentPage {
} }
} }
} }
} }
protected override void OnDisappearing() { protected override void OnDisappearing() {
base.OnDisappearing(); base.OnDisappearing();
barcodeScannerView.CameraLocation = CameraLocation.Front; barcodeScannerView.CameraLocation = CameraLocation.Front;
barcodeScannerView.IsDetecting = false; // IsDetecting wird via Binding vom ViewModel gesteuert
} }
protected override void OnAppearing() { protected override void OnAppearing() {
base.OnAppearing(); base.OnAppearing();
barcodeScannerView.IsDetecting = true; // IsDetecting wird via Binding vom ViewModel gesteuert
barcodeScannerView.CameraLocation = CameraLocation.Rear; barcodeScannerView.CameraLocation = CameraLocation.Rear;
} }
@@ -123,6 +141,7 @@ public partial class LoginPage : ContentPage {
if (status != PermissionStatus.Granted) { if (status != PermissionStatus.Granted) {
status = Permissions.RequestAsync<Permissions.Camera>().Result; status = Permissions.RequestAsync<Permissions.Camera>().Result;
} }
return status != PermissionStatus.Granted; return status != PermissionStatus.Granted;
} }
@@ -133,13 +152,15 @@ public partial class LoginPage : ContentPage {
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 { try {
Uri uri = new Uri(InputUrlWithSchema(server)); Uri uri = new Uri(InputUrlWithSchema(server));
Types.User response = await BaseFunc.AuthUserPass(username, password, uri.Scheme + "://" + uri.Authority + "/appapi"); Types.User response =
await BaseFunc.AuthUserPass(username, password, uri.Scheme + "://" + uri.Authority + "/appapi");
GlobalVar.ApiKey = response.Token; GlobalVar.ApiKey = response.Token;
GlobalVar.Name = response.Name; GlobalVar.Name = response.Name;
@@ -169,29 +190,16 @@ public partial class LoginPage : ContentPage {
if (!url.StartsWith("http://") && !url.StartsWith("https://")) { if (!url.StartsWith("http://") && !url.StartsWith("https://")) {
url = "https://" + url; url = "https://" + url;
} }
if (url.StartsWith("http://")) { if (url.StartsWith("http://")) {
url = url.Replace("http://", "https://"); url = url.Replace("http://", "https://");
} }
return url; return url;
} }
//Zwischen manuellem und automatischem Login (mit QR-Code) umschalten und die Schalterstellung merken //Zwischen manuellem und automatischem Login (mit QR-Code) umschalten und die Schalterstellung merken
private void Switch_Toggled(object sender, ToggledEventArgs e) { // Umschalt-Logik erfolgt über Binding an IsManualMode im ViewModel
var switcher = (Switch)sender;
if (switcher.IsToggled) {
LoginQR.IsVisible = false;
LoginManual.IsVisible = true;
Message.IsVisible = false;
Preferences.Default.Set("logintype", "manual");
} else {
LoginQR.IsVisible = true;
LoginManual.IsVisible = false;
Message.IsVisible = true;
Preferences.Default.Set("logintype", "qr");
}
}
//private void Vm_AlertEvent(object? sender, string e) { //private void Vm_AlertEvent(object? sender, string e) {
// DisplayAlert("Fehler:", e, "OK"); // DisplayAlert("Fehler:", e, "OK");

View File

@@ -1,12 +1,22 @@
<?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:viewModels="clr-namespace:Jugenddienst_Stunden.ViewModels" xmlns:viewModels="clr-namespace:Jugenddienst_Stunden.ViewModels"
x:Class="Jugenddienst_Stunden.Views.NotePage" x:Class="Jugenddienst_Stunden.Views.NotePage"
Title="Notiz"> Title="Notiz">
<ContentPage.BindingContext> <ContentPage.BindingContext>
<viewModels:NoteViewModel /> <viewModels:NoteViewModel />
</ContentPage.BindingContext> </ContentPage.BindingContext>
<ContentPage.Behaviors>
<toolkit:StatusBarBehavior
StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}"
StatusBarStyle="LightContent" />
</ContentPage.Behaviors>
<VerticalStackLayout Spacing="10" Margin="5"> <VerticalStackLayout Spacing="10" Margin="5">
<Editor x:Name="TextEditor" <Editor x:Name="TextEditor"
Placeholder="Gib deine Notiz ein" Placeholder="Gib deine Notiz ein"

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,15 +1,13 @@
<?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:models="clr-namespace:Jugenddienst_Stunden.ViewModels" xmlns:models="clr-namespace:Jugenddienst_Stunden.ViewModels"
xmlns:conv="clr-namespace:Jugenddienst_Stunden.Converter" xmlns:conv="clr-namespace:Jugenddienst_Stunden.Converter"
x:Class="Jugenddienst_Stunden.Views.StundePage" x:Class="Jugenddienst_Stunden.Views.StundePage"
Title="{Binding Title}"> Title="{Binding Title}">
<ContentPage.BindingContext>
<models:StundeViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources> <ContentPage.Resources>
<ResourceDictionary> <ResourceDictionary>
<conv:IntBoolConverter x:Key="IntBoolConverter" /> <conv:IntBoolConverter x:Key="IntBoolConverter" />
@@ -17,6 +15,12 @@
</ResourceDictionary> </ResourceDictionary>
</ContentPage.Resources> </ContentPage.Resources>
<ContentPage.Behaviors>
<toolkit:StatusBarBehavior
StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}"
StatusBarStyle="LightContent" />
</ContentPage.Behaviors>
<VerticalStackLayout Spacing="10" Margin="10"> <VerticalStackLayout Spacing="10" Margin="10">
<Label Text="{Binding SubTitle}" FontSize="Medium" FontAttributes="Bold" Margin="4,0,0,0" /> <Label Text="{Binding SubTitle}" FontSize="Medium" FontAttributes="Bold" Margin="4,0,0,0" />
@@ -24,20 +28,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>
@@ -45,41 +55,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 Text="Speichern" Command="{Binding SaveCommand}" /> <Button Grid.Column="1" Text="Speichern"
<Button Grid.Column="1" Text="Löschen" Command="{Binding DeleteConfirmCommand}" IsEnabled="{Binding DayTime.Id, Converter={StaticResource IntBoolConverter}}" IsVisible="{Binding FreistellungEnabled}" /> TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}"
Command="{Binding SaveCommand}" />
<Button Grid.Column="0" Text="Löschen"
Command="{Binding DeleteConfirmCommand}"
IsEnabled="{Binding DayTime.Id, Converter={StaticResource IntBoolConverter}}"
IsVisible="{Binding FreistellungEnabled}"
BackgroundColor="{StaticResource Gray500}"
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>
@@ -115,15 +134,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,19 +10,17 @@ namespace Jugenddienst_Stunden.Views;
/// Einzelner Stundeneintrag /// Einzelner Stundeneintrag
/// </summary> /// </summary>
public partial class StundePage : ContentPage { public partial class StundePage : ContentPage {
/// <summary> /// <summary>
/// CTOR /// CTOR
/// </summary> /// </summary>
public StundePage() { public StundePage(StundeViewModel vm) {
InitializeComponent(); InitializeComponent();
if (BindingContext is StundeViewModel vm) { BindingContext = vm;
vm.AlertEvent += Vm_AlertEvent; vm.AlertEvent += Vm_AlertEvent;
vm.InfoEvent += Vm_InfoEvent; vm.InfoEvent += Vm_InfoEvent;
vm.ConfirmEvent += ShowConfirm; vm.ConfirmEvent += ShowConfirm;
} }
}
private void Vm_AlertEvent(object? sender, string e) { private void Vm_AlertEvent(object? sender, string e) {
DisplayAlert("Fehler:", e, "OK"); DisplayAlert("Fehler:", e, "OK");
@@ -50,5 +48,4 @@ public partial class StundePage : ContentPage {
// bool result = await DisplayAlert(e.Title, e.Message, e.Ok, e.NotOk); // bool result = await DisplayAlert(e.Title, e.Message, e.Ok, e.NotOk);
// e.Result = result; // e.Result = result;
//} //}
} }

View File

@@ -1,15 +1,14 @@
<?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:mmodels="clr-namespace:Jugenddienst_Stunden.Models" xmlns:mmodels="clr-namespace:Jugenddienst_Stunden.Models"
xmlns:models="clr-namespace:Jugenddienst_Stunden.ViewModels" xmlns:models="clr-namespace:Jugenddienst_Stunden.ViewModels"
xmlns:conv="clr-namespace:Jugenddienst_Stunden.Converter" xmlns:conv="clr-namespace:Jugenddienst_Stunden.Converter"
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>
@@ -17,28 +16,35 @@
<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>
<toolkit:StatusBarBehavior
StatusBarColor="{AppThemeBinding Dark={StaticResource OffBlack}, Light={StaticResource Primary}}"
StatusBarStyle="LightContent" />
</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}"
<VerticalStackLayout Spacing="10" Margin="10"> Margin="10" Padding="10">
<Grid RowDefinitions="50,*,Auto,80">
<!--<VerticalStackLayout Spacing="10" Margin="10">-->
<Grid RowDefinitions="Auto" ColumnDefinitions="Auto,*" HeightRequest="50"> <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>
@@ -46,11 +52,13 @@
<CollectionView <CollectionView
ItemsSource="{Binding DayTimes}" ItemsSource="{Binding DayTimes}"
x:Name="stundeItems" Margin="0" x:Name="stundeItems" Margin="0,0,0,20"
HeightRequest="300"
SelectionMode="Single" SelectionMode="Single"
SelectionChangedCommand="{Binding SelectEntryCommand}" SelectionChangedCommand="{Binding SelectEntryCommand}"
SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}"> SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}"
VerticalOptions="Start"
Grid.Row="1">
<CollectionView.ItemsLayout> <CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical" ItemSpacing="0" /> <LinearItemsLayout Orientation="Vertical" ItemSpacing="0" />
@@ -64,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" />
@@ -93,44 +103,60 @@
<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" /> <!--<BoxView HeightRequest="1" Grid.Row="2" Margin="0,5,0,15" />-->
<Button Text="{Binding LoadOverview}"
<Button Text="{Binding LoadOverview}" Command="{Binding LoadDataCommand}" /> Grid.Row="2"
<Border Padding="2" HeightRequest="125"> Command="{Binding LoadDataCommand}"
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,*" ColumnDefinitions="Auto,*" Margin="10,2"> TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}" />
<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">
<Label Grid.Row="0" Text="Soll:" /> <Label Grid.Row="0" Text="Soll:" />
<Label Grid.Row="1" Text="Summe:" /> <Label Grid.Row="1" Text="Summe:" />
<Label Grid.Row="2" Text="Differenz:" Margin="0,0,15,0" /> <Label Grid.Row="2" Text="Differenz:" Margin="0,0,15,0" />
<BoxView Grid.Row="3" Grid.ColumnSpan="2" HeightRequest="1" Color="LightGrey" Margin="0,5"/> <!--<BoxView Grid.Row="3" Grid.ColumnSpan="4" HeightRequest="1" Color="LightGrey" Margin="0,5"/>-->
<Label Grid.Row="4" Text="Restüberstunden:" /> <Label Grid.Row="0" Grid.Column="2" Text="Restüberstunden:" Margin="15,0,0,0" />
<Label Grid.Row="5" Text="Resturlaub:" /> <Label Grid.Row="1" Grid.Column="2" Text="Zeitausgleich:" Margin="15,0,0,0" />
<Label Grid.Row="2" Grid.Column="2" Text="Resturlaub:" Margin="15,0,0,0" />
<Label Grid.Row="0" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding Nominal, Converter={StaticResource secToTime}}" ToolTipProperties.Text="Sollstunden" /> <Label Grid.Row="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="4" Grid.Column="1" 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="5" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding Holiday, Converter={StaticResource secToTime}}" /> Text="{Binding ZeitCalculated, Converter={StaticResource secToTime}}"
ToolTipProperties.Text="Geleistete Stunden" />
<Label Grid.Row="2" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0"
Text="{Binding OvertimeMonth, Converter={StaticResource secToTime}}" />
<Label Grid.Row="0" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0"
Text="{Binding Overtime, Converter={StaticResource secToTime}}" />
<Label Grid.Row="1" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0"
Text="{Binding Zeitausgleich, Converter={StaticResource secToTime}}" />
<Label Grid.Row="2" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0"
Text="{Binding Holiday, Converter={StaticResource secToTime}}" />
</Grid> </Grid>
</Border> </Border>
</VerticalStackLayout> </Grid>
<!--</VerticalStackLayout>-->
</RefreshView> </RefreshView>
</ContentPage> </ContentPage>

View File

@@ -9,40 +9,35 @@ 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>
private int heightValue = 300; /// 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();
}
#if ANDROID
heightValue = 280;
#elif IOS
heightValue = 280;
#elif WINDOWS
heightValue = 320;
#else
heightValue = 300;
#endif
SizeChanged += OnPageSizeChanged; // 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) { //private void Vm_InfoEvent(object? sender, string e) {
// DisplayAlert("Information:", e, "OK"); // DisplayAlert("Information:", e, "OK");
//} //}
@@ -55,7 +50,7 @@ public partial class StundenPage : ContentPage {
MainThread.BeginInvokeOnMainThread(async () => { MainThread.BeginInvokeOnMainThread(async () => {
CancellationTokenSource cts = new CancellationTokenSource(); CancellationTokenSource cts = new CancellationTokenSource();
ToastDuration duration = ToastDuration.Short; ToastDuration duration = ToastDuration.Short;
double fontSize = 20; double fontSize = 16;
var toast = Toast.Make(e, duration, fontSize); var toast = Toast.Make(e, duration, fontSize);
await toast.Show(cts.Token); await toast.Show(cts.Token);
}); });
@@ -64,26 +59,30 @@ public partial class StundenPage : ContentPage {
/// <summary> /// <summary>
/// Beim Laden der Seite den Titel setzen /// Beim Laden der Seite den Titel setzen
/// </summary> /// </summary>
protected override void OnAppearing() { protected override async void OnAppearing() {
base.OnAppearing(); base.OnAppearing();
Title = Preferences.Default.Get("name", "Nicht") + " " + Preferences.Default.Get("surname", "eingeloggt"); Title = Preferences.Default.Get("name", "Nicht") + " " + Preferences.Default.Get("surname", "eingeloggt");
if (!CheckLogin()) {
try {
await NavigateToTargetPage();
} catch (Exception ex) {
await DisplayAlert("Fehler:", ex.Message, "OK");
}
}
} }
private bool CheckLogin() { private bool CheckLogin() {
return Preferences.Default.Get("apiKey", "") != ""; return Preferences.Default.Get("apiKey", "") != "";
} }
private async void NavigateToTargetPage() { // private async void NavigateToTargetPage() {
await Navigation.PushAsync(new LoginPage()); // await Navigation.PushAsync(new LoginPage());
} // }
private void OnPageSizeChanged(object sender, EventArgs e) { private Task NavigateToTargetPage() {
double windowHeight = this.Height; // Shell-Navigation statt Navigation.PushAsync
AdjustLayout(windowHeight); // Voraussetzung: LoginPage-Route ist in AppShell registriert (Routing.RegisterRoute(...))
} return Shell.Current.GoToAsync(nameof(Views.LoginPage));
private void AdjustLayout(double height) {
// Passen Sie Ihre UI-Elemente basierend auf der Fensterh<72>he an
stundeItems.HeightRequest = height - heightValue; //Datepicker Height 50, Monatssummen Height 125, Titel + Navigation Height xyz
} }
} }

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Binary file not shown.