Compare commits
14 Commits
83118103d9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ee0fc61f6 | |||
| c6fd58a290 | |||
| 656d39f43e | |||
| 15856d0dd0 | |||
| 8da8734065 | |||
| cd4eae34c3 | |||
| 52815d7e21 | |||
| 8d512963b5 | |||
| 98d6d61f16 | |||
| 5fd97deada | |||
| c11b361655 | |||
| bb5aac2944 | |||
| 76eb71946f | |||
| 544b0c9591 |
247
.editorconfig
Normal 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
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version = "1.0" encoding = "UTF-8" ?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:local="clr-namespace:Jugenddienst_Stunden"
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
/// Die Hauptanwendungsklasse.
|
||||
/// </summary>
|
||||
public partial class App : Application {
|
||||
|
||||
/// <summary>
|
||||
/// Initialisiert eine neue Instanz der <see cref="App"/>-Klasse.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<Shell
|
||||
x:Class="Jugenddienst_Stunden.AppShell"
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
@@ -10,7 +11,8 @@
|
||||
<ShellContent
|
||||
Title="Stunden"
|
||||
ContentTemplate="{DataTemplate views:StundenPage}"
|
||||
Icon="{OnPlatform 'icon_watch.png', iOS='icon_watch_ios.png', MacCatalyst='icon_watch_ios.png'}" Route="StundenPage" />
|
||||
Icon="{OnPlatform 'icon_watch.png', iOS='icon_watch_ios.png', MacCatalyst='icon_watch_ios.png'}"
|
||||
Route="StundenPage" />
|
||||
|
||||
<ShellContent
|
||||
Title="Notizen"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
namespace Jugenddienst_Stunden;
|
||||
|
||||
/// <summary>
|
||||
/// AppShell.xaml.cs
|
||||
/// </summary>
|
||||
@@ -13,7 +14,7 @@ public partial class AppShell : Shell {
|
||||
Routing.RegisterRoute(nameof(Views.NotePage), typeof(Views.NotePage));
|
||||
Routing.RegisterRoute(nameof(Views.StundePage), typeof(Views.StundePage));
|
||||
|
||||
//Muss ich die registrieren?
|
||||
Routing.RegisterRoute(nameof(Views.LoginPage), typeof(Views.LoginPage));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -9,6 +9,7 @@ namespace Jugenddienst_Stunden.Converter {
|
||||
return !collection.Any();
|
||||
return collection.Any();
|
||||
}
|
||||
|
||||
if ((string)parameter == "Invert")
|
||||
return true;
|
||||
return false;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace Jugenddienst_Stunden.Converter;
|
||||
|
||||
/// <summary>
|
||||
/// Falls ein int als bool dargestellt werden soll
|
||||
/// </summary>
|
||||
public class IntBoolConverter : IValueConverter {
|
||||
|
||||
/// <summary>
|
||||
/// Konvertiert einen int in einen bool
|
||||
/// </summary>
|
||||
@@ -18,6 +18,7 @@ public class IntBoolConverter : IValueConverter {
|
||||
if (value is int) {
|
||||
return (int)value != 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -33,6 +34,7 @@ public class IntBoolConverter : IValueConverter {
|
||||
if (value is bool) {
|
||||
return (bool)value ? 1 : 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
15
Jugenddienst Stunden/Converter/InverseBoolConverter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace Jugenddienst_Stunden.Converter;
|
||||
internal class SecondsTimeConverter : IValueConverter {
|
||||
|
||||
internal class SecondsTimeConverter : IValueConverter {
|
||||
private int seconds;
|
||||
|
||||
/// <summary>
|
||||
@@ -14,6 +14,7 @@ internal class SecondsTimeConverter : IValueConverter {
|
||||
if (value is int) {
|
||||
seconds = (int)value;
|
||||
}
|
||||
|
||||
if (value is double) {
|
||||
seconds = (int)Math.Round((double)value);
|
||||
} else {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace Jugenddienst_Stunden.Converter;
|
||||
|
||||
internal class StringVisibilityConverter : IValueConverter {
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
|
||||
if (value is string strValue) {
|
||||
return !string.IsNullOrEmpty(strValue.Replace("Server: ",""));
|
||||
return !string.IsNullOrEmpty(strValue.Replace("Server: ", ""));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,14 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jugenddienst_Stunden.Exceptions;
|
||||
|
||||
public class NoDataException : Exception {
|
||||
public NoDataException() : base("Keine Daten gefunden") { }
|
||||
public NoDataException(string message) : base(message) { }
|
||||
public NoDataException(string message, Exception inner) : base(message, inner) { }
|
||||
public NoDataException() : base("Keine Daten gefunden") {
|
||||
}
|
||||
|
||||
public NoDataException(string message) : base(message) {
|
||||
}
|
||||
|
||||
public NoDataException(string message, Exception inner) : base(message, inner) {
|
||||
}
|
||||
}
|
||||
15
Jugenddienst Stunden/Infrastructure/AlertService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
176
Jugenddienst Stunden/Infrastructure/ApiClient.cs
Normal 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 Settings‑BaseUrl bauen, ohne HttpClient.BaseAddress zu nutzen.
|
||||
var uri = BuildAbsoluteUri(_settings.ApiUrl, path, query);
|
||||
using var req = new HttpRequestMessage(method, uri);
|
||||
// Authorization PRO REQUEST setzen (immer, wenn Token vorhanden ist)
|
||||
// Hinweis: Das QR-Token kann RFC-unzulässige Zeichen (z. B. '|') enthalten.
|
||||
// AuthenticationHeaderValue würde solche Werte ablehnen. Daher ohne Validierung setzen.
|
||||
var currentToken = _settings.ApiKey;
|
||||
if (!string.IsNullOrWhiteSpace(currentToken)) {
|
||||
// Vorherige Header (falls vorhanden) entfernen, um Duplikate zu vermeiden
|
||||
req.Headers.Remove("Authorization");
|
||||
req.Headers.TryAddWithoutValidation("Authorization", $"Bearer {currentToken}");
|
||||
}
|
||||
if (body is HttpContent httpContent) {
|
||||
req.Content = httpContent;
|
||||
} else if (body is not null) {
|
||||
req.Content = JsonContent.Create(body, options: _json);
|
||||
}
|
||||
|
||||
// Sicherstellen, dass Accept: application/json auch auf Request-Ebene vorhanden ist (z. B. für LoginWithToken GET)
|
||||
if (!req.Headers.Accept.Any(h => h.MediaType?.Equals("application/json", StringComparison.OrdinalIgnoreCase) == true)) {
|
||||
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
}
|
||||
|
||||
using var res = await _http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
|
||||
var text = await res.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
|
||||
if (res.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);
|
||||
}
|
||||
}
|
||||
6
Jugenddienst Stunden/Infrastructure/ApiOptions.cs
Normal 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);
|
||||
}
|
||||
23
Jugenddienst Stunden/Infrastructure/Exceptions.cs
Normal 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) {
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
23
Jugenddienst Stunden/Infrastructure/NullApiClient.cs
Normal 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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
33
Jugenddienst Stunden/Infrastructure/RequestLoggingHandler.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
10
Jugenddienst Stunden/Infrastructure/SettingsTokenProvider.cs
Normal 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;
|
||||
}
|
||||
7
Jugenddienst Stunden/Infrastructure/TokenProvider.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using Jugenddienst_Stunden.Interfaces;
|
||||
|
||||
namespace Jugenddienst_Stunden.Infrastructure;
|
||||
|
||||
internal sealed class GlobalVarTokenProvider : ITokenProvider {
|
||||
public string? GetToken() => Models.GlobalVar.ApiKey;
|
||||
}
|
||||
11
Jugenddienst Stunden/Interfaces/IAlertService.cs
Normal 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);
|
||||
}
|
||||
10
Jugenddienst Stunden/Interfaces/IApiClient.cs
Normal 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);
|
||||
}
|
||||
10
Jugenddienst Stunden/Interfaces/IAppSettings.cs
Normal 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; }
|
||||
}
|
||||
8
Jugenddienst Stunden/Interfaces/IAuthService.cs
Normal 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);
|
||||
}
|
||||
17
Jugenddienst Stunden/Interfaces/IHoursRepository.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Jugenddienst_Stunden.Types;
|
||||
|
||||
namespace Jugenddienst_Stunden.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Repository‑Schnittstelle 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);
|
||||
}
|
||||
17
Jugenddienst Stunden/Interfaces/IHoursService.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Jugenddienst_Stunden.Types;
|
||||
|
||||
namespace Jugenddienst_Stunden.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Fachlicher Service für Stunden – konsumiert Repository und stellt VM‑freundliche 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);
|
||||
}
|
||||
5
Jugenddienst Stunden/Interfaces/ITokenProvider.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace Jugenddienst_Stunden.Interfaces;
|
||||
|
||||
internal interface ITokenProvider {
|
||||
string? GetToken();
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- <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 -->
|
||||
<!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> -->
|
||||
|
||||
@@ -176,8 +176,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.26100.0</TargetFrameworks>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net10.0-windows10.0.26100.0</TargetFrameworks>
|
||||
<WindowsPackageType>MSIX</WindowsPackageType>
|
||||
<!-- <TargetFrameworks>;net9.0-android35.0</TargetFrameworks> -->
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -263,6 +263,7 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="9.0.110" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Maui.Graphics" Version="9.0.110" />
|
||||
<PackageReference Include="Microsoft.NET.Runtime.MonoAOTCompiler.Task" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.NET.Runtime.WebAssembly.Wasi.Sdk" Version="9.0.9" />
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
using CommunityToolkit.Maui;
|
||||
using Jugenddienst_Stunden.Models;
|
||||
using Jugenddienst_Stunden.Interfaces;
|
||||
using Jugenddienst_Stunden.Repositories;
|
||||
using Jugenddienst_Stunden.Services;
|
||||
using Jugenddienst_Stunden.Infrastructure;
|
||||
using Jugenddienst_Stunden.Validators;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZXing.Net.Maui.Controls;
|
||||
using System.Net.Http;
|
||||
using Jugenddienst_Stunden.ViewModels;
|
||||
using System.Net;
|
||||
|
||||
namespace Jugenddienst_Stunden;
|
||||
|
||||
@@ -9,15 +17,12 @@ namespace Jugenddienst_Stunden;
|
||||
/// Das Hauptprogramm.
|
||||
/// </summary>
|
||||
public static class MauiProgram {
|
||||
|
||||
public static MauiApp CreateMauiApp() {
|
||||
var builder = MauiApp.CreateBuilder();
|
||||
builder
|
||||
.UseMauiApp<App>()
|
||||
// Initialize the .NET MAUI Community Toolkit by adding the below line of code
|
||||
.UseMauiCommunityToolkit(options => {
|
||||
options.SetShouldEnableSnackbarOnWindows(true);
|
||||
})
|
||||
.UseMauiCommunityToolkit(options => { options.SetShouldEnableSnackbarOnWindows(true); })
|
||||
.ConfigureFonts(fonts => {
|
||||
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
|
||||
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
|
||||
@@ -25,19 +30,95 @@ public static class MauiProgram {
|
||||
//.UseBarcodeScanning();
|
||||
.UseBarcodeReader();
|
||||
|
||||
#if DEBUG
|
||||
if (GlobalVar.ApiKey == null) {
|
||||
GlobalVar.ApiKey = Preferences.Default.Get("apiKey", "MTQxfHNkdFptQkNZTXlPT3ZyMHNBZDl0UnVxNExMRXxodHRwOi8vaG91cnMuZGF1bmkubWluZS5udTo4MS9hcHBhcGk=");
|
||||
GlobalVar.Name = Preferences.Default.Get("name", "Testserver: Isabell");
|
||||
GlobalVar.Surname = Preferences.Default.Get("surname", "Biasi");
|
||||
GlobalVar.EmployeeId = Preferences.Default.Get("EmployeeId", 141);
|
||||
GlobalVar.ApiUrl = Preferences.Default.Get("apiUrl", "https://hours.dauni.mine.nu/appapi");
|
||||
//#if DEBUG
|
||||
// if (string.IsNullOrWhiteSpace(GlobalVar.ApiKey)) {
|
||||
// GlobalVar.ApiKey = Preferences.Default.Get("apiKey",
|
||||
// "MTQxfHNkdFptQkNZTXlPT3ZyMHxodHRwOi8vaG91cnMuZGF1bmkubWluZS5udTo4MS9hcHBhcGk=");
|
||||
// GlobalVar.Name = Preferences.Default.Get("name", "Testserver: Isabell");
|
||||
// GlobalVar.Surname = Preferences.Default.Get("surname", "Biasi");
|
||||
// GlobalVar.EmployeeId = Preferences.Default.Get("EmployeeId", 141);
|
||||
// GlobalVar.ApiUrl = Preferences.Default.Get("apiUrl", "https://hours.dauni.mine.nu/appapi");
|
||||
// }
|
||||
|
||||
// builder.Logging.AddDebug();
|
||||
//#endif
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -7,10 +7,7 @@ using System.Text.Json;
|
||||
namespace Jugenddienst_Stunden.Models;
|
||||
|
||||
internal static class BaseFunc {
|
||||
|
||||
|
||||
internal static async Task<string> GetApiDataWithAuthAsync(string url, string token) {
|
||||
|
||||
if (Connectivity.Current.NetworkAccess == NetworkAccess.None)
|
||||
throw new Exception("Bitte überprüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.");
|
||||
|
||||
@@ -19,11 +16,11 @@ internal static class BaseFunc {
|
||||
|
||||
// Erstellen eines HttpClient-Objekts
|
||||
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
|
||||
|
||||
client.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
|
||||
// Hinzufügen des Bearer-Tokens zum Authorization-Header
|
||||
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
client.DefaultRequestHeaders.Authorization =
|
||||
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
// Senden der Anfrage und Abrufen der Antwort
|
||||
using (HttpResponseMessage HttpResponseMessage = await client.GetAsync(url).ConfigureAwait(false)) {
|
||||
@@ -35,12 +32,11 @@ internal static class BaseFunc {
|
||||
if (HttpResponseMessage.StatusCode == System.Net.HttpStatusCode.OK) {
|
||||
return responseData;
|
||||
} else {
|
||||
var options = new JsonDocumentOptions {
|
||||
AllowTrailingCommas = true
|
||||
};
|
||||
var options = new JsonDocumentOptions { AllowTrailingCommas = true };
|
||||
using (JsonDocument doc = JsonDocument.Parse(responseData, options)) {
|
||||
JsonElement root = doc.RootElement;
|
||||
string message = root.GetProperty("message").GetString() ?? throw new Exception("Fehler: 'message' ist null.");
|
||||
string message = root.GetProperty("message").GetString() ??
|
||||
throw new Exception("Fehler: 'message' ist null.");
|
||||
throw new Exception(message);
|
||||
}
|
||||
}
|
||||
@@ -49,34 +45,26 @@ internal static class BaseFunc {
|
||||
}
|
||||
|
||||
|
||||
|
||||
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);
|
||||
|
||||
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
|
||||
|
||||
client.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
|
||||
// Senden der Anfrage und Abrufen der Antwort
|
||||
using (HttpResponseMessage HttpResponseMessage = await client.PostAsync(url, content).ConfigureAwait(false)) {
|
||||
if (!HttpResponseMessage.IsSuccessStatusCode)
|
||||
{
|
||||
using (HttpResponseMessage HttpResponseMessage =
|
||||
await client.PostAsync(url, content).ConfigureAwait(false)) {
|
||||
if (!HttpResponseMessage.IsSuccessStatusCode) {
|
||||
//throw new Exception("Fehler beim Einloggen " + HttpResponseMessage.Content);
|
||||
var byteArray = await HttpResponseMessage.Content.ReadAsByteArrayAsync();
|
||||
string responseData = Encoding.UTF8.GetString(byteArray);
|
||||
var options = new JsonDocumentOptions {
|
||||
AllowTrailingCommas = true
|
||||
};
|
||||
var options = new JsonDocumentOptions { AllowTrailingCommas = true };
|
||||
using (JsonDocument doc = JsonDocument.Parse(responseData, options)) {
|
||||
JsonElement root = doc.RootElement;
|
||||
string message = root.GetProperty("message").GetString() ?? throw new Exception("Fehler: 'message' ist null.");
|
||||
string message = root.GetProperty("message").GetString() ??
|
||||
throw new Exception("Fehler: 'message' ist null.");
|
||||
throw new Exception(message);
|
||||
}
|
||||
}
|
||||
@@ -87,14 +75,15 @@ internal static class BaseFunc {
|
||||
// Lesen und Rückgabe der Antwort als String
|
||||
|
||||
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");
|
||||
return res.user;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -108,16 +97,13 @@ internal static class BaseFunc {
|
||||
throw new FileNotFoundException("Unable to find file on local storage.", filename);
|
||||
|
||||
return
|
||||
new() {
|
||||
Date = File.GetLastWriteTime(filename)
|
||||
};
|
||||
new() { Date = File.GetLastWriteTime(filename) };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stundeneintrag speichern
|
||||
/// </summary>
|
||||
internal static async Task SaveItemAsync(string url, string token, DayTime item, bool isNewItem = false) {
|
||||
|
||||
//Uhrzeiten sollten sinnvolle Werte haben - außer bei Freistellungen, da wäre eigentlich null
|
||||
if (item.TimeSpanVon == item.TimeSpanBis && item.FreistellungAktiv == null) {
|
||||
throw new Exception("Beginn und Ende sind gleich");
|
||||
@@ -138,22 +124,27 @@ internal static class BaseFunc {
|
||||
if (item.GemeindeAktiv == null && GlobalVar.Settings.GemeindeAktivSet) {
|
||||
throw new Exception("Gemeinde nicht gewählt");
|
||||
}
|
||||
|
||||
//Projekt ist ein Pflichtfeld
|
||||
if (item.ProjektAktiv == null && GlobalVar.Settings.ProjektAktivSet) {
|
||||
throw new Exception("Projekt nicht gewählt");
|
||||
}
|
||||
|
||||
//Keine Beschreibung
|
||||
if (string.IsNullOrEmpty(item.Description) && item.FreistellungAktiv == null) {
|
||||
throw new Exception("Keine Beschreibung");
|
||||
}
|
||||
|
||||
//Keine Beschreibung
|
||||
if (string.IsNullOrEmpty(item.Description)) {
|
||||
item.Description = item.FreistellungAktiv.Name;
|
||||
}
|
||||
|
||||
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
|
||||
//HttpClient client = new HttpClient();
|
||||
client.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
client.DefaultRequestHeaders.Authorization =
|
||||
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
//string json = JsonSerializer.Serialize<DayTime>(item);
|
||||
string json = JsonConvert.SerializeObject(item);
|
||||
@@ -169,7 +160,6 @@ internal static class BaseFunc {
|
||||
throw new Exception("Fehler beim Speichern " + response.Content);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -177,10 +167,10 @@ internal static class BaseFunc {
|
||||
/// </summary>
|
||||
internal static async Task DeleteItemAsync(string url, string token) {
|
||||
using (HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }) {
|
||||
|
||||
//HttpClient client = new HttpClient();
|
||||
client.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
client.DefaultRequestHeaders.Authorization =
|
||||
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
HttpResponseMessage response = await client.DeleteAsync(url);
|
||||
|
||||
@@ -188,5 +178,4 @@ internal static class BaseFunc {
|
||||
throw new Exception("Fehler beim Löschen " + response.Content);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
21
Jugenddienst Stunden/Models/ConfirmationEventArgs.cs
Normal 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);
|
||||
}
|
||||
@@ -1,26 +1,32 @@
|
||||
using Jugenddienst_Stunden.Types;
|
||||
|
||||
namespace Jugenddienst_Stunden.Models;
|
||||
|
||||
internal static class GlobalVar {
|
||||
public static string ApiKey {
|
||||
get => Preferences.Default.Get("apiKey", "");
|
||||
set => Preferences.Default.Set("apiKey", value);
|
||||
}
|
||||
|
||||
public static int EmployeeId {
|
||||
get => Preferences.Default.Get("EmployeeId", 0);
|
||||
set => Preferences.Default.Set("EmployeeId", value);
|
||||
}
|
||||
|
||||
public static string Name {
|
||||
get => Preferences.Default.Get("name", "Nicht");
|
||||
set => Preferences.Default.Set("name", value);
|
||||
}
|
||||
|
||||
public static string Surname {
|
||||
get => Preferences.Default.Get("surname", "Eingeloggt");
|
||||
set => Preferences.Default.Set("surname", value);
|
||||
}
|
||||
|
||||
public static string ApiUrl {
|
||||
get => Preferences.Default.Get("apiUrl", "");
|
||||
set => Preferences.Default.Set("apiUrl", value);
|
||||
}
|
||||
|
||||
public static Settings Settings { get; set; }
|
||||
}
|
||||
@@ -4,14 +4,14 @@ using Newtonsoft.Json;
|
||||
namespace Jugenddienst_Stunden.Models;
|
||||
|
||||
internal static class HoursBase {
|
||||
|
||||
/// <summary>
|
||||
/// Laden ... what can be: "settings", "hours", date="YYYY-MM-DD", id=<int/>
|
||||
/// </summary>
|
||||
/// <returns>Entire response</returns>
|
||||
internal static async Task<BaseResponse> LoadBase(string what) {
|
||||
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?"+what, GlobalVar.ApiKey);
|
||||
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
|
||||
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?" + what, GlobalVar.ApiKey);
|
||||
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
|
||||
throw new Exception("Fehler beim Deserialisieren der Daten");
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@ internal static class HoursBase {
|
||||
/// <returns>Settings only</returns>
|
||||
internal static async Task<Settings> LoadSettings() {
|
||||
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?settings", GlobalVar.ApiKey);
|
||||
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
|
||||
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
|
||||
throw new Exception("Fehler beim Deserialisieren der Daten");
|
||||
return res.settings;
|
||||
}
|
||||
|
||||
@@ -31,7 +32,8 @@ internal static class HoursBase {
|
||||
/// <returns>Hours only</returns>
|
||||
internal static async Task<Hours> LoadData() {
|
||||
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?hours", GlobalVar.ApiKey);
|
||||
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
|
||||
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
|
||||
throw new Exception("Fehler beim Deserialisieren der Daten");
|
||||
return res.hour;
|
||||
}
|
||||
|
||||
@@ -41,7 +43,8 @@ internal static class HoursBase {
|
||||
/// <returns>User-Object</returns>
|
||||
public static async Task<User> LoadUser(string apiKey) {
|
||||
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl, apiKey);
|
||||
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
|
||||
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
|
||||
throw new Exception("Fehler beim Deserialisieren der Daten");
|
||||
return res.user;
|
||||
}
|
||||
|
||||
@@ -49,8 +52,10 @@ internal static class HoursBase {
|
||||
/// Zeiten eines Tages holen
|
||||
/// </summary>
|
||||
internal static async Task<List<DayTime>> LoadDay(DateTime date) {
|
||||
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?date=" + date.ToString("yyyy-MM-dd"), GlobalVar.ApiKey);
|
||||
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
|
||||
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?date=" + date.ToString("yyyy-MM-dd"),
|
||||
GlobalVar.ApiKey);
|
||||
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
|
||||
throw new Exception("Fehler beim Deserialisieren der Daten");
|
||||
return res.daytimes;
|
||||
}
|
||||
|
||||
@@ -59,7 +64,8 @@ internal static class HoursBase {
|
||||
/// </summary>
|
||||
internal static async Task<DayTime> LoadEntry(int id) {
|
||||
string data = await BaseFunc.GetApiDataWithAuthAsync(GlobalVar.ApiUrl + "?id=" + id, GlobalVar.ApiKey);
|
||||
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ?? throw new Exception("Fehler beim Deserialisieren der Daten");
|
||||
BaseResponse res = JsonConvert.DeserializeObject<BaseResponse>(data) ??
|
||||
throw new Exception("Fehler beim Deserialisieren der Daten");
|
||||
res.daytime.TimeSpanVon = res.daytime.Begin.ToTimeSpan();
|
||||
res.daytime.TimeSpanBis = res.daytime.End.ToTimeSpan();
|
||||
return res.daytime;
|
||||
@@ -68,7 +74,8 @@ internal static class HoursBase {
|
||||
/// <summary>
|
||||
/// Eintrag speichern
|
||||
/// </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;
|
||||
if (stunde.Id == null)
|
||||
isNew = true;
|
||||
|
||||
56
Jugenddienst Stunden/Models/JsonFlexibleIntConverters.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,17 @@ namespace Jugenddienst_Stunden.Models {
|
||||
return typeof(T).IsAssignableFrom(objectType);
|
||||
}
|
||||
|
||||
public override bool CanWrite { get { return false; } }
|
||||
public override bool CanWrite {
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
|
||||
JsonSerializer serializer) {
|
||||
var contract = serializer.ContractResolver.ResolveContract(objectType);
|
||||
if (!(contract is Newtonsoft.Json.Serialization.JsonObjectContract || contract is Newtonsoft.Json.Serialization.JsonDictionaryContract)) {
|
||||
throw new JsonSerializationException(string.Format("Unsupported objectType {0} at {1}.", objectType, reader.Path));
|
||||
if (!(contract is Newtonsoft.Json.Serialization.JsonObjectContract ||
|
||||
contract is Newtonsoft.Json.Serialization.JsonDictionaryContract)) {
|
||||
throw new JsonSerializationException(string.Format("Unsupported objectType {0} at {1}.", objectType,
|
||||
reader.Path));
|
||||
}
|
||||
|
||||
switch (reader.SkipComments().TokenType) {
|
||||
@@ -31,13 +36,15 @@ namespace Jugenddienst_Stunden.Models {
|
||||
default: {
|
||||
count++;
|
||||
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();
|
||||
serializer.Populate(reader, existingValue);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Should not come here.
|
||||
throw new JsonSerializationException(string.Format("Unclosed array at path {0}.", reader.Path));
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
namespace Jugenddienst_Stunden.Models;
|
||||
|
||||
internal class Note {
|
||||
public string Filename { get; set; }
|
||||
public string Text { get; set; }
|
||||
public DateTime Date { get; set; }
|
||||
|
||||
public void Save() =>
|
||||
File.WriteAllText(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename), Text);
|
||||
File.WriteAllText(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename), Text);
|
||||
|
||||
public void Delete() =>
|
||||
File.Delete(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename));
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Jugenddienst_Stunden.Models;
|
||||
|
||||
internal class Operator {
|
||||
public string? id;
|
||||
public string? name;
|
||||
@@ -17,5 +18,4 @@ internal class Operator {
|
||||
public string? year;
|
||||
|
||||
public event EventHandler<string>? AlertEvent;
|
||||
|
||||
}
|
||||
@@ -13,6 +13,7 @@ internal class TokenData {
|
||||
if (string.IsNullOrEmpty(ak)) {
|
||||
throw new ArgumentException("API key cannot be null or empty", nameof(ak));
|
||||
}
|
||||
|
||||
string dat = Encoding.UTF8.GetString(Convert.FromBase64String(ak));
|
||||
|
||||
string[] parts = dat.Split('|');
|
||||
@@ -20,9 +21,12 @@ internal class TokenData {
|
||||
throw new FormatException("API key format is invalid");
|
||||
}
|
||||
|
||||
Token = dat.Split('|')[1]; ;
|
||||
Url = dat.Split('|')[2]; ;
|
||||
Operator_id = dat.Split('|')[0]; ;
|
||||
Token = dat.Split('|')[1];
|
||||
;
|
||||
Url = dat.Split('|')[2];
|
||||
;
|
||||
Operator_id = dat.Split('|')[0];
|
||||
;
|
||||
ApiKey = ak;
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,8 @@
|
||||
android:label="Stunden"
|
||||
/>
|
||||
<!-- android:usesCleartextTraffic="true" -->
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
</manifest>
|
||||
@@ -3,6 +3,9 @@ using Android.Content.PM;
|
||||
using Android.OS;
|
||||
|
||||
namespace Jugenddienst_Stunden;
|
||||
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
|
||||
|
||||
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop,
|
||||
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode |
|
||||
ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
|
||||
public class MainActivity : MauiAppCompatActivity {
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<dict>
|
||||
<!-- The Mac App Store requires you specify if the app uses encryption. -->
|
||||
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption -->
|
||||
<!-- <key>ITSAppUsesNonExemptEncryption</key> -->
|
||||
@@ -34,5 +34,5 @@
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/appicon.appiconset</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="maui-application-id-placeholder" version="0.0.0" api-version="8" xmlns="http://tizen.org/ns/packages">
|
||||
<profile name="common" />
|
||||
<ui-application appid="maui-application-id-placeholder" exec="Jugenddienst Stunden.dll" multiple="false" nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
|
||||
<manifest package="maui-application-id-placeholder" version="0.0.0" api-version="8"
|
||||
xmlns="http://tizen.org/ns/packages">
|
||||
<profile name="common"/>
|
||||
<ui-application appid="maui-application-id-placeholder" exec="Jugenddienst Stunden.dll" multiple="false"
|
||||
nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
|
||||
<label>maui-application-title-placeholder</label>
|
||||
<icon>maui-appicon-placeholder</icon>
|
||||
<metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true" />
|
||||
<metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true"/>
|
||||
</ui-application>
|
||||
<shortcut-list />
|
||||
<shortcut-list/>
|
||||
<privileges>
|
||||
<privilege>http://tizen.org/privilege/internet</privilege>
|
||||
</privileges>
|
||||
<dependencies />
|
||||
<provides-appdefined-privileges />
|
||||
<dependencies/>
|
||||
<provides-appdefined-privileges/>
|
||||
</manifest>
|
||||
@@ -19,9 +19,5 @@ namespace Jugenddienst_Stunden.WinUI {
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -8,9 +8,10 @@
|
||||
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
|
||||
IgnorableNamespaces="uap rescap com desktop">
|
||||
|
||||
<Identity Name="JugenddienstStunden" Publisher="CN=User Name" Version="0.0.0.0" />
|
||||
<Identity Name="JugenddienstStunden" Publisher="CN=User Name" Version="0.0.0.0"/>
|
||||
|
||||
<mp:PhoneIdentity PhoneProductId="4BA4D7D7-E3C2-4BBF-92EF-0EDB5DB5CDB4" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
<mp:PhoneIdentity PhoneProductId="4BA4D7D7-E3C2-4BBF-92EF-0EDB5DB5CDB4"
|
||||
PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
|
||||
<Properties>
|
||||
<DisplayName>$placeholder$</DisplayName>
|
||||
@@ -19,12 +20,12 @@
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0"/>
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0"/>
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
<Resource Language="x-generate" />
|
||||
<Resource Language="x-generate"/>
|
||||
</Resources>
|
||||
|
||||
<Applications>
|
||||
@@ -35,7 +36,9 @@
|
||||
Square150x150Logo="Resources\Windows\$placeholder$.png"
|
||||
Square44x44Logo="Resources\Windows\$placeholder$.png"
|
||||
BackgroundColor="transparent">
|
||||
<uap:DefaultTile Square71x71Logo="Resources\Windows\Small\$placeholder$.png" Wide310x150Logo="Resources\Windows\Wide\$placeholder$.png" Square310x310Logo="Resources\Windows\$placeholder$.png" ShortName="Stunden"/>
|
||||
<uap:DefaultTile Square71x71Logo="Resources\Windows\Small\$placeholder$.png"
|
||||
Wide310x150Logo="Resources\Windows\Wide\$placeholder$.png"
|
||||
Square310x310Logo="Resources\Windows\$placeholder$.png" ShortName="Stunden"/>
|
||||
<uap:SplashScreen Image="Resources\Windows\Splash\$placeholder$.png" BackgroundColor="#F7931D"/>
|
||||
</uap:VisualElements>
|
||||
|
||||
@@ -43,15 +46,16 @@
|
||||
|
||||
<!-- Specify which CLSID to activate when notification is clicked -->
|
||||
<desktop:Extension Category="windows.toastNotificationActivation">
|
||||
<desktop:ToastNotificationActivation ToastActivatorCLSID="6e919706-2634-4d97-a93c-2213b2acc334" />
|
||||
<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:">
|
||||
<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:Class Id="6e919706-2634-4d97-a93c-2213b2acc334"/>
|
||||
</com:ExeServer>
|
||||
</com:ComServer>
|
||||
</com:Extension>
|
||||
@@ -62,7 +66,7 @@
|
||||
</Applications>
|
||||
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
<rescap:Capability Name="runFullTrust"/>
|
||||
<Capability Name="internetClient"/>
|
||||
<DeviceCapability Name="webcam"/>
|
||||
</Capabilities>
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
2) System < Windows 10 Anniversary Update
|
||||
-->
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor
|
||||
</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<dict>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIDeviceFamily</key>
|
||||
@@ -30,5 +30,5 @@
|
||||
<string>Assets.xcassets/appicon.appiconset</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// Dieser Code wurde von einem Tool generiert.
|
||||
// Laufzeitversion:4.0.30319.42000
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Änderungen an dieser Datei können falsches Verhalten verursachen und gehen verloren, wenn
|
||||
// der Code erneut generiert wird.
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -13,12 +12,12 @@ namespace Jugenddienst_Stunden.Properties {
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Eine stark typisierte Ressourcenklasse zum Suchen von lokalisierten Zeichenfolgen usw.
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// Diese Klasse wurde von der StronglyTypedResourceBuilder automatisch generiert
|
||||
// -Klasse über ein Tool wie ResGen oder Visual Studio automatisch generiert.
|
||||
// Um einen Member hinzuzufügen oder zu entfernen, bearbeiten Sie die .ResX-Datei und führen dann ResGen
|
||||
// mit der /str-Option erneut aus, oder Sie erstellen Ihr VS-Projekt neu.
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
@@ -33,7 +32,7 @@ namespace Jugenddienst_Stunden.Properties {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gibt die zwischengespeicherte ResourceManager-Instanz zurück, die von dieser Klasse verwendet wird.
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
@@ -47,8 +46,8 @@ namespace Jugenddienst_Stunden.Properties {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Überschreibt die CurrentUICulture-Eigenschaft des aktuellen Threads für alle
|
||||
/// Ressourcenzuordnungen, die diese stark typisierte Ressourcenklasse verwenden.
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
|
||||
@@ -59,27 +59,28 @@
|
||||
: 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:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1"/>
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
<xsd:attribute name="name" type="xsd:string" use="required"/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
@@ -93,9 +94,13 @@
|
||||
<value>1.3</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral,
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral,
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Windows Machine": {
|
||||
"commandName": "Project",
|
||||
"commandName": "MsixPackage",
|
||||
"nativeDebugging": false
|
||||
}
|
||||
}
|
||||
|
||||
80
Jugenddienst Stunden/Repositories/HoursRepository.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<svg width="100%" height="100%" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<rect x="0" y="0" width="456" height="456" style="fill:white;"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 521 B After Width: | Height: | Size: 531 B |
@@ -1,41 +1,54 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<svg width="100%" height="100%" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g id="JD" transform="matrix(0.96485,0,0,0.96485,104.499,104.499)">
|
||||
<g transform="matrix(12.0607,0,0,12.0607,101.169,256)">
|
||||
<path d="M0,-11.938L0,-4.95C0,-3.918 -0.071,-3.151 -0.214,-2.644C-0.359,-2.069 -0.604,-1.583 -0.95,-1.184C-1.624,-0.397 -2.515,0 -3.626,0C-4.117,0 -4.588,-0.075 -5.04,-0.226L-5.04,-2.305L-4.999,-2.375C-4.509,-2.048 -4.052,-1.884 -3.626,-1.884C-3.026,-1.884 -2.612,-2.112 -2.382,-2.565C-2.141,-3.019 -2.02,-3.817 -2.02,-4.95L-2.02,-11.938L0,-11.938" style="fill-rule:nonzero;"/>
|
||||
<path d="M0,-11.938L0,-4.95C0,-3.918 -0.071,-3.151 -0.214,-2.644C-0.359,-2.069 -0.604,-1.583 -0.95,-1.184C-1.624,-0.397 -2.515,0 -3.626,0C-4.117,0 -4.588,-0.075 -5.04,-0.226L-5.04,-2.305L-4.999,-2.375C-4.509,-2.048 -4.052,-1.884 -3.626,-1.884C-3.026,-1.884 -2.612,-2.112 -2.382,-2.565C-2.141,-3.019 -2.02,-3.817 -2.02,-4.95L-2.02,-11.938L0,-11.938"
|
||||
style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0,12.0607,12.0607,0,87.6884,17.5193)">
|
||||
<path d="M1.249,-1.246C0.559,-1.246 -0.002,-0.687 -0.002,0.002C-0.002,0.692 0.559,1.249 1.249,1.249C1.936,1.249 2.497,0.692 2.497,0.002C2.497,-0.687 1.936,-1.246 1.249,-1.246" style="fill-rule:nonzero;"/>
|
||||
<path d="M1.249,-1.246C0.559,-1.246 -0.002,-0.687 -0.002,0.002C-0.002,0.692 0.559,1.249 1.249,1.249C1.936,1.249 2.497,0.692 2.497,0.002C2.497,-0.687 1.936,-1.246 1.249,-1.246"
|
||||
style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(12.0607,0,0,12.0607,32.4324,105.748)">
|
||||
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018" style="fill:rgb(247,147,29);fill-rule:nonzero;"/>
|
||||
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018"
|
||||
style="fill:rgb(247,147,29);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(12.0607,0,0,12.0607,109.437,86.7078)">
|
||||
<path d="M0,7.464C0,4.526 2.384,2.142 5.325,2.142C6.633,2.142 7.832,2.61 8.758,3.395L8.758,-5.324L10.746,-5.324L10.746,12.598L8.758,12.598L8.758,11.534C7.832,12.318 6.633,12.788 5.325,12.788C2.384,12.788 0,10.404 0,7.464M2.113,7.401C2.113,9.234 3.598,10.719 5.432,10.719C7.263,10.719 8.747,9.234 8.747,7.401C8.747,5.569 7.263,4.083 5.432,4.083C3.598,4.083 2.113,5.569 2.113,7.401" style="fill-rule:nonzero;"/>
|
||||
<path d="M0,7.464C0,4.526 2.384,2.142 5.325,2.142C6.633,2.142 7.832,2.61 8.758,3.395L8.758,-5.324L10.746,-5.324L10.746,12.598L8.758,12.598L8.758,11.534C7.832,12.318 6.633,12.788 5.325,12.788C2.384,12.788 0,10.404 0,7.464M2.113,7.401C2.113,9.234 3.598,10.719 5.432,10.719C7.263,10.719 8.747,9.234 8.747,7.401C8.747,5.569 7.263,4.083 5.432,4.083C3.598,4.083 2.113,5.569 2.113,7.401"
|
||||
style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Uhr" transform="matrix(0.0220493,0,0,0.0220493,267.879,222.51)">
|
||||
<g transform="matrix(4.16667,0,0,4.16667,-6599.04,0)">
|
||||
<path d="M1638.49,981.86C1405.19,981.86 1216.06,792.734 1216.06,559.433C1216.06,326.132 1405.19,137.006 1638.49,137.006C1871.79,137.006 2060.92,326.132 2060.92,559.433C2060.92,792.734 1871.79,981.86 1638.49,981.86ZM1638.49,70.646C1368.54,70.646 1149.7,289.481 1149.7,559.433C1149.7,829.385 1368.54,1048.22 1638.49,1048.22C1908.44,1048.22 2127.28,829.385 2127.28,559.433C2127.28,289.481 1908.44,70.646 1638.49,70.646Z" style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
|
||||
<path d="M1638.49,981.86C1405.19,981.86 1216.06,792.734 1216.06,559.433C1216.06,326.132 1405.19,137.006 1638.49,137.006C1871.79,137.006 2060.92,326.132 2060.92,559.433C2060.92,792.734 1871.79,981.86 1638.49,981.86ZM1638.49,70.646C1368.54,70.646 1149.7,289.481 1149.7,559.433C1149.7,829.385 1368.54,1048.22 1638.49,1048.22C1908.44,1048.22 2127.28,829.385 2127.28,559.433C2127.28,289.481 1908.44,70.646 1638.49,70.646Z"
|
||||
style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(4.14211,-0.451661,0.451661,4.14211,-6813.16,744.211)">
|
||||
<path d="M1677.2,559.433C1677.2,580.825 1659.85,598.166 1638.45,598.145C1617.12,598.124 1599.75,580.168 1599.78,558.84C1599.88,474.921 1621.18,308.102 1632.14,254.433C1633.54,247.532 1643.41,247.537 1644.81,254.44C1655.78,308.457 1677.2,476.326 1677.2,559.433Z" style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
|
||||
<path d="M1677.2,559.433C1677.2,580.825 1659.85,598.166 1638.45,598.145C1617.12,598.124 1599.75,580.168 1599.78,558.84C1599.88,474.921 1621.18,308.102 1632.14,254.433C1633.54,247.532 1643.41,247.537 1644.81,254.44C1655.78,308.457 1677.2,476.326 1677.2,559.433Z"
|
||||
style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(-4.09852,-0.750473,-0.750473,4.09852,7333.98,1287.55)">
|
||||
<path d="M1595.39,590.238C1532.86,586.449 1465.21,572.938 1428.29,565.401C1422.75,564.267 1422.76,556.353 1428.29,555.229C1463.02,548.172 1526.22,535.741 1585.66,531.077C1585.22,540.595 1584.97,549.88 1584.96,558.834C1584.95,570.561 1587.38,581.586 1595.39,590.238Z" style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
|
||||
<path d="M1595.39,590.238C1532.86,586.449 1465.21,572.938 1428.29,565.401C1422.75,564.267 1422.76,556.353 1428.29,555.229C1463.02,548.172 1526.22,535.741 1585.66,531.077C1585.22,540.595 1584.97,549.88 1584.96,558.834C1584.95,570.561 1587.38,581.586 1595.39,590.238Z"
|
||||
style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(4.16667,0,0,4.16667,-6599.04,0)">
|
||||
<path d="M1656.19,176.808C1656.19,186.583 1648.27,194.507 1638.49,194.507C1628.72,194.507 1620.79,186.583 1620.79,176.808C1620.79,167.033 1628.72,159.108 1638.49,159.108C1648.27,159.108 1656.19,167.033 1656.19,176.808Z" style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
|
||||
<path d="M1656.19,176.808C1656.19,186.583 1648.27,194.507 1638.49,194.507C1628.72,194.507 1620.79,186.583 1620.79,176.808C1620.79,167.033 1628.72,159.108 1638.49,159.108C1648.27,159.108 1656.19,167.033 1656.19,176.808Z"
|
||||
style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(4.16667,0,0,4.16667,-6599.04,0)">
|
||||
<path d="M1255.87,541.734C1265.64,541.734 1273.57,549.658 1273.57,559.433C1273.57,569.208 1265.64,577.132 1255.87,577.132C1246.09,577.132 1238.17,569.208 1238.17,559.433C1238.17,549.658 1246.09,541.734 1255.87,541.734Z" style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
|
||||
<path d="M1255.87,541.734C1265.64,541.734 1273.57,549.658 1273.57,559.433C1273.57,569.208 1265.64,577.132 1255.87,577.132C1246.09,577.132 1238.17,569.208 1238.17,559.433C1238.17,549.658 1246.09,541.734 1255.87,541.734Z"
|
||||
style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(4.16667,0,0,4.16667,-6599.04,0)">
|
||||
<path d="M1620.79,942.059C1620.79,932.283 1628.72,924.359 1638.49,924.359C1648.27,924.359 1656.19,932.283 1656.19,942.059C1656.19,951.834 1648.27,959.758 1638.49,959.758C1628.72,959.758 1620.79,951.834 1620.79,942.059Z" style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
|
||||
<path d="M1620.79,942.059C1620.79,932.283 1628.72,924.359 1638.49,924.359C1648.27,924.359 1656.19,932.283 1656.19,942.059C1656.19,951.834 1648.27,959.758 1638.49,959.758C1628.72,959.758 1620.79,951.834 1620.79,942.059Z"
|
||||
style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(4.16667,0,0,4.16667,-6599.04,0)">
|
||||
<path d="M2021.12,577.132C2011.34,577.132 2003.42,569.208 2003.42,559.433C2003.42,549.658 2011.34,541.734 2021.12,541.734C2030.89,541.734 2038.82,549.658 2038.82,559.433C2038.82,569.208 2030.89,577.132 2021.12,577.132Z" style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
|
||||
<path d="M2021.12,577.132C2011.34,577.132 2003.42,569.208 2003.42,559.433C2003.42,549.658 2011.34,541.734 2021.12,541.734C2030.89,541.734 2038.82,549.658 2038.82,559.433C2038.82,569.208 2030.89,577.132 2021.12,577.132Z"
|
||||
style="fill:rgb(0,0,6);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.4 KiB |
@@ -1,21 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<svg width="100%" height="100%" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g>
|
||||
<g transform="matrix(12.0607,0,0,12.0607,201.169,356)">
|
||||
<path d="M0,-11.938L0,-4.95C0,-3.918 -0.071,-3.151 -0.214,-2.644C-0.359,-2.069 -0.604,-1.583 -0.95,-1.184C-1.624,-0.397 -2.515,0 -3.626,0C-4.117,0 -4.588,-0.075 -5.04,-0.226L-5.04,-2.305L-4.999,-2.375C-4.509,-2.048 -4.052,-1.884 -3.626,-1.884C-3.026,-1.884 -2.612,-2.112 -2.382,-2.565C-2.141,-3.019 -2.02,-3.817 -2.02,-4.95L-2.02,-11.938L0,-11.938" style="fill-rule:nonzero;"/>
|
||||
<path d="M0,-11.938L0,-4.95C0,-3.918 -0.071,-3.151 -0.214,-2.644C-0.359,-2.069 -0.604,-1.583 -0.95,-1.184C-1.624,-0.397 -2.515,0 -3.626,0C-4.117,0 -4.588,-0.075 -5.04,-0.226L-5.04,-2.305L-4.999,-2.375C-4.509,-2.048 -4.052,-1.884 -3.626,-1.884C-3.026,-1.884 -2.612,-2.112 -2.382,-2.565C-2.141,-3.019 -2.02,-3.817 -2.02,-4.95L-2.02,-11.938L0,-11.938"
|
||||
style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0,12.0607,12.0607,0,187.688,117.519)">
|
||||
<path d="M1.249,-1.246C0.559,-1.246 -0.002,-0.687 -0.002,0.002C-0.002,0.692 0.559,1.249 1.249,1.249C1.936,1.249 2.497,0.692 2.497,0.002C2.497,-0.687 1.936,-1.246 1.249,-1.246" style="fill-rule:nonzero;"/>
|
||||
<path d="M1.249,-1.246C0.559,-1.246 -0.002,-0.687 -0.002,0.002C-0.002,0.692 0.559,1.249 1.249,1.249C1.936,1.249 2.497,0.692 2.497,0.002C2.497,-0.687 1.936,-1.246 1.249,-1.246"
|
||||
style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(12.0607,0,0,12.0607,132.432,205.748)">
|
||||
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018" style="fill:rgb(247,147,29);fill-rule:nonzero;"/>
|
||||
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018"
|
||||
style="fill:rgb(247,147,29);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(12.0607,0,0,12.0607,132.432,205.748)">
|
||||
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018"
|
||||
style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(12.0607,0,0,12.0607,209.437,186.708)">
|
||||
<path d="M0,7.464C0,4.526 2.384,2.142 5.325,2.142C6.633,2.142 7.832,2.61 8.758,3.395L8.758,-5.324L10.746,-5.324L10.746,12.598L8.758,12.598L8.758,11.534C7.832,12.318 6.633,12.788 5.325,12.788C2.384,12.788 0,10.404 0,7.464M2.113,7.401C2.113,9.234 3.598,10.719 5.432,10.719C7.263,10.719 8.747,9.234 8.747,7.401C8.747,5.569 7.263,4.083 5.432,4.083C3.598,4.083 2.113,5.569 2.113,7.401" style="fill-rule:nonzero;"/>
|
||||
<path d="M0,7.464C0,4.526 2.384,2.142 5.325,2.142C6.633,2.142 7.832,2.61 8.758,3.395L8.758,-5.324L10.746,-5.324L10.746,12.598L8.758,12.598L8.758,11.534C7.832,12.318 6.633,12.788 5.325,12.788C2.384,12.788 0,10.404 0,7.464M2.113,7.401C2.113,9.234 3.598,10.719 5.432,10.719C7.263,10.719 8.747,9.234 8.747,7.401C8.747,5.569 7.263,4.083 5.432,4.083C3.598,4.083 2.113,5.569 2.113,7.401"
|
||||
style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.0 KiB |
@@ -1,21 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<svg width="100%" height="100%" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g>
|
||||
<g transform="matrix(12.0607,0,0,12.0607,201.169,356)">
|
||||
<path d="M0,-11.938L0,-4.95C0,-3.918 -0.071,-3.151 -0.214,-2.644C-0.359,-2.069 -0.604,-1.583 -0.95,-1.184C-1.624,-0.397 -2.515,0 -3.626,0C-4.117,0 -4.588,-0.075 -5.04,-0.226L-5.04,-2.305L-4.999,-2.375C-4.509,-2.048 -4.052,-1.884 -3.626,-1.884C-3.026,-1.884 -2.612,-2.112 -2.382,-2.565C-2.141,-3.019 -2.02,-3.817 -2.02,-4.95L-2.02,-11.938L0,-11.938" style="fill-rule:nonzero;"/>
|
||||
<path d="M0,-11.938L0,-4.95C0,-3.918 -0.071,-3.151 -0.214,-2.644C-0.359,-2.069 -0.604,-1.583 -0.95,-1.184C-1.624,-0.397 -2.515,0 -3.626,0C-4.117,0 -4.588,-0.075 -5.04,-0.226L-5.04,-2.305L-4.999,-2.375C-4.509,-2.048 -4.052,-1.884 -3.626,-1.884C-3.026,-1.884 -2.612,-2.112 -2.382,-2.565C-2.141,-3.019 -2.02,-3.817 -2.02,-4.95L-2.02,-11.938L0,-11.938"
|
||||
style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0,12.0607,12.0607,0,187.688,117.519)">
|
||||
<path d="M1.249,-1.246C0.559,-1.246 -0.002,-0.687 -0.002,0.002C-0.002,0.692 0.559,1.249 1.249,1.249C1.936,1.249 2.497,0.692 2.497,0.002C2.497,-0.687 1.936,-1.246 1.249,-1.246" style="fill-rule:nonzero;"/>
|
||||
<path d="M1.249,-1.246C0.559,-1.246 -0.002,-0.687 -0.002,0.002C-0.002,0.692 0.559,1.249 1.249,1.249C1.936,1.249 2.497,0.692 2.497,0.002C2.497,-0.687 1.936,-1.246 1.249,-1.246"
|
||||
style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(12.0607,0,0,12.0607,132.432,205.748)">
|
||||
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018" style="fill:rgb(247,147,29);fill-rule:nonzero;"/>
|
||||
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018"
|
||||
style="fill:rgb(247,147,29);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(12.0607,0,0,12.0607,132.432,205.748)">
|
||||
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M0,-5.018C0.88,-4.169 2.079,-3.647 3.398,-3.647C6.103,-3.647 8.293,-5.838 8.293,-8.539L8.288,-8.768L10.103,-8.768L10.107,-8.539C10.107,-5.953 8.64,-3.705 6.49,-2.586C7.743,-2.48 8.98,-2.02 10.042,-1.19L10.221,-1.044L9.103,0.384L8.926,0.242C6.796,-1.425 3.719,-1.044 2.055,1.084C1.43,1.878 1.096,2.811 1.03,3.75L-0.788,3.75C-0.72,2.42 -0.256,1.095 0.626,-0.033C1.239,-0.816 1.992,-1.427 2.82,-1.857C1.229,-1.993 -0.204,-2.685 -1.283,-3.737L0,-5.018"
|
||||
style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(12.0607,0,0,12.0607,209.437,186.708)">
|
||||
<path d="M0,7.464C0,4.526 2.384,2.142 5.325,2.142C6.633,2.142 7.832,2.61 8.758,3.395L8.758,-5.324L10.746,-5.324L10.746,12.598L8.758,12.598L8.758,11.534C7.832,12.318 6.633,12.788 5.325,12.788C2.384,12.788 0,10.404 0,7.464M2.113,7.401C2.113,9.234 3.598,10.719 5.432,10.719C7.263,10.719 8.747,9.234 8.747,7.401C8.747,5.569 7.263,4.083 5.432,4.083C3.598,4.083 2.113,5.569 2.113,7.401" style="fill-rule:nonzero;"/>
|
||||
<path d="M0,7.464C0,4.526 2.384,2.142 5.325,2.142C6.633,2.142 7.832,2.61 8.758,3.395L8.758,-5.324L10.746,-5.324L10.746,12.598L8.758,12.598L8.758,11.534C7.832,12.318 6.633,12.788 5.325,12.788C2.384,12.788 0,10.404 0,7.464M2.113,7.401C2.113,9.234 3.598,10.719 5.432,10.719C7.263,10.719 8.747,9.234 8.747,7.401C8.747,5.569 7.263,4.083 5.432,4.083C3.598,4.083 2.113,5.569 2.113,7.401"
|
||||
style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.0 KiB |
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xaml-comp compile="true" ?>
|
||||
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
|
||||
@@ -30,18 +31,18 @@
|
||||
<Color x:Key="Gray900">#212121</Color>
|
||||
<Color x:Key="Gray950">#141414</Color>
|
||||
|
||||
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}"/>
|
||||
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}"/>
|
||||
<SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}"/>
|
||||
<SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}"/>
|
||||
<SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}"/>
|
||||
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}" />
|
||||
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}" />
|
||||
<SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}" />
|
||||
<SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}" />
|
||||
<SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}" />
|
||||
|
||||
<SolidColorBrush x:Key="Gray100Brush" Color="{StaticResource Gray100}"/>
|
||||
<SolidColorBrush x:Key="Gray200Brush" Color="{StaticResource Gray200}"/>
|
||||
<SolidColorBrush x:Key="Gray300Brush" Color="{StaticResource Gray300}"/>
|
||||
<SolidColorBrush x:Key="Gray400Brush" Color="{StaticResource Gray400}"/>
|
||||
<SolidColorBrush x:Key="Gray500Brush" Color="{StaticResource Gray500}"/>
|
||||
<SolidColorBrush x:Key="Gray600Brush" Color="{StaticResource Gray600}"/>
|
||||
<SolidColorBrush x:Key="Gray900Brush" Color="{StaticResource Gray900}"/>
|
||||
<SolidColorBrush x:Key="Gray950Brush" Color="{StaticResource Gray950}"/>
|
||||
<SolidColorBrush x:Key="Gray100Brush" Color="{StaticResource Gray100}" />
|
||||
<SolidColorBrush x:Key="Gray200Brush" Color="{StaticResource Gray200}" />
|
||||
<SolidColorBrush x:Key="Gray300Brush" Color="{StaticResource Gray300}" />
|
||||
<SolidColorBrush x:Key="Gray400Brush" Color="{StaticResource Gray400}" />
|
||||
<SolidColorBrush x:Key="Gray500Brush" Color="{StaticResource Gray500}" />
|
||||
<SolidColorBrush x:Key="Gray600Brush" Color="{StaticResource Gray600}" />
|
||||
<SolidColorBrush x:Key="Gray900Brush" Color="{StaticResource Gray900}" />
|
||||
<SolidColorBrush x:Key="Gray950Brush" Color="{StaticResource Gray950}" />
|
||||
</ResourceDictionary>
|
||||
@@ -1,12 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xaml-comp compile="true" ?>
|
||||
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
|
||||
|
||||
<Style TargetType="ToolbarItem">
|
||||
<Style.Triggers>
|
||||
<Trigger TargetType="ToolbarItem" Property="VisualElement.BackgroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}">
|
||||
<Trigger TargetType="ToolbarItem" Property="VisualElement.BackgroundColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}">
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
@@ -16,39 +18,48 @@
|
||||
</Style>
|
||||
|
||||
<Style TargetType="IndicatorView">
|
||||
<Setter Property="IndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}"/>
|
||||
<Setter Property="SelectedIndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray100}}"/>
|
||||
<Setter Property="IndicatorColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
||||
<Setter Property="SelectedIndicatorColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray100}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Stroke" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
||||
<Setter Property="StrokeShape" Value="Rectangle"/>
|
||||
<Setter Property="StrokeThickness" Value="1"/>
|
||||
<Setter Property="Stroke"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
||||
<Setter Property="StrokeShape" Value="Rectangle" />
|
||||
<Setter Property="StrokeThickness" Value="1" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="BoxView">
|
||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
|
||||
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
|
||||
<Setter Property="Color"
|
||||
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource PrimaryDarkText}}" />
|
||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="BorderWidth" Value="0"/>
|
||||
<Setter Property="CornerRadius" Value="8"/>
|
||||
<Setter Property="Padding" Value="14,10"/>
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource PrimaryDarkText}}" />
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="BorderWidth" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
<Setter Property="Padding" Value="14,10" />
|
||||
<Setter Property="MinimumHeightRequest" Value="44" />
|
||||
<Setter Property="MinimumWidthRequest" Value="44" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
|
||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="PointerOver" />
|
||||
@@ -59,15 +70,16 @@
|
||||
|
||||
<Style TargetType="CheckBox">
|
||||
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="MinimumHeightRequest" Value="44" />
|
||||
<Setter Property="MinimumWidthRequest" Value="44" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="Color"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
@@ -76,19 +88,21 @@
|
||||
</Style>
|
||||
|
||||
<Style TargetType="DatePicker">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="MinimumHeightRequest" Value="44" />
|
||||
<Setter Property="MinimumWidthRequest" Value="44" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
@@ -97,20 +111,23 @@
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Editor">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="PlaceholderColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
||||
<Setter Property="MinimumHeightRequest" Value="44" />
|
||||
<Setter Property="MinimumWidthRequest" Value="44" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
@@ -119,20 +136,23 @@
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Entry">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="PlaceholderColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
||||
<Setter Property="MinimumHeightRequest" Value="44" />
|
||||
<Setter Property="MinimumWidthRequest" Value="44" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
@@ -142,18 +162,20 @@
|
||||
|
||||
<Style TargetType="Frame">
|
||||
<Setter Property="HasShadow" Value="False" />
|
||||
<Setter Property="BorderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
|
||||
<Setter Property="BorderColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ImageButton">
|
||||
<Setter Property="Opacity" Value="1" />
|
||||
<Setter Property="BorderColor" Value="Transparent"/>
|
||||
<Setter Property="BorderWidth" Value="0"/>
|
||||
<Setter Property="CornerRadius" Value="0"/>
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="BorderColor" Value="Transparent" />
|
||||
<Setter Property="BorderWidth" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="0" />
|
||||
<Setter Property="MinimumHeightRequest" Value="44" />
|
||||
<Setter Property="MinimumWidthRequest" Value="44" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
@@ -170,7 +192,8 @@
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Label">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
@@ -180,7 +203,8 @@
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
@@ -189,44 +213,53 @@
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Span">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Label" x:Key="Headline">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
|
||||
<Setter Property="FontSize" Value="32" />
|
||||
<Setter Property="HorizontalOptions" Value="Center" />
|
||||
<Setter Property="HorizontalTextAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Label" x:Key="SubHeadline">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
|
||||
<Setter Property="FontSize" Value="24" />
|
||||
<Setter Property="HorizontalOptions" Value="Center" />
|
||||
<Setter Property="HorizontalTextAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ListView">
|
||||
<Setter Property="SeparatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
||||
<Setter Property="RefreshControlColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
||||
<Setter Property="SeparatorColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
||||
<Setter Property="RefreshControlColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Picker">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
||||
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
||||
<Setter Property="TitleColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="MinimumHeightRequest" Value="44" />
|
||||
<Setter Property="MinimumWidthRequest" Value="44" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="TitleColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
@@ -235,14 +268,16 @@
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ProgressBar">
|
||||
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
<Setter Property="ProgressColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="ProgressColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
@@ -251,19 +286,21 @@
|
||||
</Style>
|
||||
|
||||
<Style TargetType="RadioButton">
|
||||
<Setter Property="BackgroundColor" Value="Transparent"/>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="MinimumHeightRequest" Value="44" />
|
||||
<Setter Property="MinimumWidthRequest" Value="44" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
@@ -272,26 +309,30 @@
|
||||
</Style>
|
||||
|
||||
<Style TargetType="RefreshView">
|
||||
<Setter Property="RefreshColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
||||
<Setter Property="RefreshColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="SearchBar">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
||||
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
|
||||
<Setter Property="CancelButtonColor" Value="{StaticResource Gray500}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="MinimumHeightRequest" Value="44" />
|
||||
<Setter Property="MinimumWidthRequest" Value="44" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="PlaceholderColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
@@ -300,7 +341,8 @@
|
||||
</Style>
|
||||
|
||||
<Style TargetType="SearchHandler">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
||||
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular" />
|
||||
@@ -311,8 +353,10 @@
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="PlaceholderColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
@@ -328,18 +372,24 @@
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Slider">
|
||||
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
<Setter Property="MinimumTrackColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
<Setter Property="MaximumTrackColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="ThumbColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
|
||||
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
|
||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
|
||||
<Setter Property="MinimumTrackColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="MaximumTrackColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="ThumbColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
@@ -348,11 +398,13 @@
|
||||
</Style>
|
||||
|
||||
<Style TargetType="SwipeItem">
|
||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Switch">
|
||||
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
<Setter Property="OnColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
<Setter Property="ThumbColor" Value="{StaticResource White}" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
@@ -360,19 +412,24 @@
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="OnColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="ThumbColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="On">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Secondary}, Dark={StaticResource Gray200}}" />
|
||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
<Setter Property="OnColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Secondary}, Dark={StaticResource Gray200}}" />
|
||||
<Setter Property="ThumbColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Off">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
|
||||
<Setter Property="ThumbColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
@@ -381,19 +438,21 @@
|
||||
</Style>
|
||||
|
||||
<Style TargetType="TimePicker">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent"/>
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="MinimumHeightRequest" Value="44" />
|
||||
<Setter Property="MinimumWidthRequest" Value="44" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
@@ -402,34 +461,51 @@
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Page" ApplyToDerivedTypes="True">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Shell" ApplyToDerivedTypes="True">
|
||||
<Setter Property="Shell.BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
|
||||
<Setter Property="Shell.ForegroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
|
||||
<Setter Property="Shell.TitleColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
|
||||
<Setter Property="Shell.DisabledColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
|
||||
<Setter Property="Shell.UnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray200}}" />
|
||||
<Setter Property="Shell.BackgroundColor"
|
||||
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
|
||||
<Setter Property="Shell.ForegroundColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
|
||||
<Setter Property="Shell.TitleColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
|
||||
<Setter Property="Shell.DisabledColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
|
||||
<Setter Property="Shell.UnselectedColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray200}}" />
|
||||
<Setter Property="Shell.NavBarHasShadow" Value="False" />
|
||||
<Setter Property="Shell.TabBarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
|
||||
<Setter Property="Shell.TabBarForegroundColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
|
||||
<Setter Property="Shell.TabBarTitleColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
|
||||
<Setter Property="Shell.TabBarUnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
||||
<Setter Property="Shell.TabBarBackgroundColor"
|
||||
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
|
||||
<Setter Property="Shell.TabBarForegroundColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
|
||||
<Setter Property="Shell.TabBarTitleColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
|
||||
<Setter Property="Shell.TabBarUnselectedColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="NavigationPage">
|
||||
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
|
||||
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
|
||||
<Setter Property="IconColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
|
||||
<Setter Property="BarBackgroundColor"
|
||||
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
|
||||
<Setter Property="BarTextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
|
||||
<Setter Property="IconColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="TabbedPage">
|
||||
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Gray950}}" />
|
||||
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
|
||||
<Setter Property="UnselectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
|
||||
<Setter Property="SelectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
|
||||
<Setter Property="BarBackgroundColor"
|
||||
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Gray950}}" />
|
||||
<Setter Property="BarTextColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
|
||||
<Setter Property="UnselectedTabColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
|
||||
<Setter Property="SelectedTabColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
94
Jugenddienst Stunden/Services/AuthService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
67
Jugenddienst Stunden/Services/HoursService.cs
Normal 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);
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
using Jugenddienst_Stunden.Models;
|
||||
using Jugenddienst_Stunden.Types;
|
||||
|
||||
namespace Jugenddienst_Stunden.Types;
|
||||
internal class BaseResponse {
|
||||
|
||||
internal class BaseResponse {
|
||||
public Settings settings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Monatsübersicht
|
||||
/// </summary>
|
||||
public Hours hour { get; set; }
|
||||
public Types.Hours hour { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stundenliste ... für die Katz?
|
||||
/// </summary>
|
||||
public List<Hours> hours { get; set; }
|
||||
public List<Types.Hours> hours { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Liste der Stundeneinträge
|
||||
@@ -29,6 +30,7 @@ internal class BaseResponse {
|
||||
/// Auch irgendwie doppelt ...
|
||||
/// </summary>
|
||||
public Operator operatorVar { get; set; }
|
||||
|
||||
public User user { get; set; }
|
||||
|
||||
public int error { get; set; }
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Jugenddienst_Stunden.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a day time entry for an employee.
|
||||
/// </summary>
|
||||
@@ -92,5 +93,4 @@ public class DayTime {
|
||||
public Freistellung? FreistellungAktiv { get; set; }
|
||||
|
||||
public int TimeTable { get; set; }
|
||||
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
namespace Jugenddienst_Stunden.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Freistellungen: Urlaub, Zeitausgleich, Krankheit, ...
|
||||
/// </summary>
|
||||
|
||||
@@ -1,43 +1,66 @@
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Jugenddienst_Stunden.Types;
|
||||
|
||||
internal partial class Hours : ObservableObject {
|
||||
public double? Zeit;
|
||||
public double? Nominal;
|
||||
//public Dictionary<DateOnly,NominalDay> nominal_day_api;
|
||||
public partial class Hours : ObservableObject {
|
||||
/// <summary>
|
||||
/// Total time in seconds for the current context.
|
||||
/// "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 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<string> time_line;
|
||||
public double? Zeit_total;
|
||||
|
||||
//https://stackoverflow.com/questions/29449641/deserialize-json-when-a-value-can-be-an-object-or-an-empty-array/29450279#29450279
|
||||
//[JsonConverter(typeof(JsonSingleOrEmptyArrayConverter<Hours>))]
|
||||
//public Dictionary<int,decimal> zeit_total_daily;
|
||||
/// <summary>
|
||||
/// Total time in seconds reported by the API for the current period. Nullable if not provided.
|
||||
/// </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;
|
||||
|
||||
/// <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<string> wochensumme;
|
||||
|
||||
[ObservableProperty]
|
||||
public double overtime_month;
|
||||
[ObservableProperty] public double overtime_month;
|
||||
|
||||
[ObservableProperty]
|
||||
public double overtime;
|
||||
[ObservableProperty] public double overtime;
|
||||
//public List<string> overtime_day;
|
||||
|
||||
[ObservableProperty]
|
||||
public double zeitausgleich;
|
||||
[ObservableProperty] public double zeitausgleich;
|
||||
|
||||
public double zeitausgleich_month;
|
||||
public double holiday;
|
||||
public double? holiday { get; set; }
|
||||
public double krankheit;
|
||||
public double weiterbildung;
|
||||
public double bereitschaft;
|
||||
|
||||
public double bereitschaft_month;
|
||||
|
||||
//public Operator operator_api;
|
||||
public DateTime Today;
|
||||
public DateTime Date;
|
||||
@@ -47,5 +70,4 @@ internal partial class Hours : ObservableObject {
|
||||
public Collection<Gemeinde> Gemeinden { get; set; }
|
||||
public Collection<Freistellung> Freistellungen { get; set; }
|
||||
public int EmployeeId { get; set; }
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
namespace Jugenddienst_Stunden.Types;
|
||||
internal class NominalDay {
|
||||
|
||||
public class NominalDay {
|
||||
public int day_number;
|
||||
public int month_number;
|
||||
public double hours;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Jugenddienst_Stunden.Types;
|
||||
|
||||
internal class NominalWeek {
|
||||
public class NominalWeek {
|
||||
public int Week_number;
|
||||
public double Hours;
|
||||
}
|
||||
@@ -29,10 +29,10 @@ public class Settings {
|
||||
/// </summary>
|
||||
public List<Freistellung>? Freistellungen { get; set; }
|
||||
|
||||
public List<Sollstunden> Nominal { get; set; }
|
||||
public List<Sollstunden>? Nominal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Version der API
|
||||
/// </summary>
|
||||
public string Version { get; set; }
|
||||
public string? Version { get; set; }
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
/// <summary>
|
||||
/// Summe der geleisteten Stunden.
|
||||
/// </summary>
|
||||
internal struct TimeDay {
|
||||
public struct TimeDay {
|
||||
public int Day { get; set; }
|
||||
public double Hours { get; set; }
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
namespace Jugenddienst_Stunden.Types;
|
||||
internal class User {
|
||||
|
||||
public class User {
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Surname { get; set; }
|
||||
|
||||
31
Jugenddienst Stunden/Validators/HoursValidator.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
/// Die Loginseite
|
||||
/// ViewModel für die Loginseite (MVVM)
|
||||
/// </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>
|
||||
/// Name der Anwendung
|
||||
/// </summary>
|
||||
@@ -14,18 +29,133 @@ public class LoginViewModel {
|
||||
/// </summary>
|
||||
public string Version => AppInfo.VersionString;
|
||||
|
||||
/// <summary>
|
||||
/// Kurze Mitteilung für den Anwender
|
||||
/// </summary>
|
||||
public string Message { get; set; } = "Scanne den QR-Code von deinem Benutzerprofil auf der Stundenseite.";
|
||||
[ObservableProperty]
|
||||
private string message = "Scanne den QR-Code von deinem Benutzerprofil auf der Stundenseite.";
|
||||
|
||||
/// <summary>
|
||||
/// Genutzer Server für die API
|
||||
/// </summary>
|
||||
public string Server { get; set; } = "Server: " + Preferences.Default.Get("apiUrl", "").Replace("/appapi", "").Replace("https://", "").Replace("http://", "");
|
||||
[ObservableProperty]
|
||||
private string? server;
|
||||
|
||||
/// <summary>
|
||||
/// Titel der Seite - im Moment der aktuelle Anwender
|
||||
/// </summary>
|
||||
public string Title { get; set; } = Preferences.Default.Get("name", "Nicht") + " " + Preferences.Default.Get("surname", "eingeloggt");
|
||||
[ObservableProperty]
|
||||
private string? serverLabel;
|
||||
|
||||
[ObservableProperty]
|
||||
private string title = Preferences.Default.Get("name", "Nicht") + " " + Preferences.Default.Get("surname", "eingeloggt");
|
||||
|
||||
[ObservableProperty]
|
||||
private string? username;
|
||||
|
||||
[ObservableProperty]
|
||||
private string? password;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isManualMode;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isBusy;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isDetecting;
|
||||
|
||||
// Explizite Command-Property für den QR-Scanner-Event, damit das Binding in XAML zuverlässig greift
|
||||
public IAsyncRelayCommand<object?> QrDetectedCommand { get; }
|
||||
|
||||
public LoginViewModel(IAuthService auth, IAppSettings settings) {
|
||||
_auth = auth;
|
||||
_settings = settings;
|
||||
|
||||
// gespeicherte Präferenz für Logintyp laden
|
||||
var lt = Preferences.Default.Get("logintype", "qr");
|
||||
isManualMode = string.Equals(lt, "manual", StringComparison.OrdinalIgnoreCase);
|
||||
// Scanner standardmäßig nur im QR-Modus aktivieren
|
||||
IsDetecting = !isManualMode;
|
||||
|
||||
// Serveranzeige vorbereiten
|
||||
var apiUrl = Preferences.Default.Get("apiUrl", string.Empty);
|
||||
if (!string.IsNullOrWhiteSpace(apiUrl)) {
|
||||
Server = apiUrl.Replace("/appapi", "").Replace("https://", "").Replace("http://", "");
|
||||
ServerLabel = "Server: " + Server;
|
||||
}
|
||||
|
||||
// Command initialisieren
|
||||
QrDetectedCommand = new AsyncRelayCommand<object?>(QrDetectedAsync);
|
||||
}
|
||||
|
||||
// DI-Konstruktor: AlertService anbinden und Alerts an VM-Event weiterreichen (analog StundeViewModel)
|
||||
internal LoginViewModel(IAuthService auth, IAppSettings settings, IAlertService alertService) : this(auth, settings) {
|
||||
_alerts = alertService;
|
||||
if (alertService is not null) {
|
||||
alertService.AlertRaised += (s, msg) => AlertEvent?.Invoke(this, msg);
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnIsManualModeChanged(bool value) {
|
||||
Preferences.Default.Set("logintype", value ? "manual" : "qr");
|
||||
// Scanner nur aktiv, wenn QR-Modus aktiv ist
|
||||
IsDetecting = !value;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task LoginAsync() {
|
||||
if (IsBusy) return;
|
||||
try {
|
||||
IsBusy = true;
|
||||
var user = await _auth.LoginWithCredentials(Username?.Trim() ?? string.Empty,
|
||||
Password ?? string.Empty,
|
||||
(Server ?? string.Empty).Trim());
|
||||
|
||||
Title = $"{user.Name} {user.Surname}";
|
||||
// Info zeigen und auf Bestätigung warten
|
||||
var args = new ConfirmationEventArgs("Information:", "Login erfolgreich");
|
||||
InfoEvent?.Invoke(this, args);
|
||||
bool confirmed = await args.Task;
|
||||
if (confirmed) {
|
||||
await Shell.Current.GoToAsync("//StundenPage");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
if (_alerts is not null) {
|
||||
_alerts.Raise(ex.Message);
|
||||
} else {
|
||||
AlertEvent?.Invoke(this, ex.Message);
|
||||
}
|
||||
} finally {
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task QrDetectedAsync(object? args) {
|
||||
var now = DateTime.Now;
|
||||
if ((now - _lastDetectionTime) <= _detectionInterval) return;
|
||||
_lastDetectionTime = now;
|
||||
|
||||
try {
|
||||
var token = ExtractFirstBarcodeValue(args);
|
||||
if (string.IsNullOrWhiteSpace(token)) return;
|
||||
|
||||
var user = await _auth.LoginWithToken(token);
|
||||
Title = $"{user.Name} {user.Surname}";
|
||||
|
||||
// Info zeigen und auf Bestätigung warten
|
||||
var infoArgs = new ConfirmationEventArgs("Information:", "Login erfolgreich");
|
||||
InfoEvent?.Invoke(this, infoArgs);
|
||||
bool confirmed = await infoArgs.Task;
|
||||
if (confirmed) {
|
||||
await Shell.Current.GoToAsync("//StundenPage");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
if (_alerts is not null) {
|
||||
_alerts.Raise(ex.Message);
|
||||
} else {
|
||||
AlertEvent?.Invoke(this, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ExtractFirstBarcodeValue(object? args) {
|
||||
try {
|
||||
if (args is ZXing.Net.Maui.BarcodeDetectionEventArgs e && e.Results is not null) {
|
||||
return e.Results.FirstOrDefault()?.Value;
|
||||
}
|
||||
} catch { }
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,10 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Jugenddienst_Stunden.ViewModels;
|
||||
|
||||
internal class NoteViewModel : ObservableObject, IQueryAttributable {
|
||||
private Models.Note _note;
|
||||
|
||||
public string Text {
|
||||
get => _note.Text;
|
||||
set {
|
||||
|
||||
@@ -47,7 +47,9 @@ internal class NotesViewModel : IQueryAttributable {
|
||||
}
|
||||
|
||||
// If note isn't found, it's new; add it.
|
||||
else { AllNotes.Insert(0, new NoteViewModel(Note.Load(noteId))); }
|
||||
else {
|
||||
AllNotes.Insert(0, new NoteViewModel(Note.Load(noteId)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,20 @@ using Jugenddienst_Stunden.Types;
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Input;
|
||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||
using Jugenddienst_Stunden.Interfaces;
|
||||
using Jugenddienst_Stunden.Repositories;
|
||||
using Jugenddienst_Stunden.Services;
|
||||
using Jugenddienst_Stunden.Infrastructure;
|
||||
using Jugenddienst_Stunden.Validators;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Jugenddienst_Stunden.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Viewmodel für die einzelnen Stundeneinträge / Bearbeitung
|
||||
/// </summary>
|
||||
public partial class StundeViewModel : ObservableObject, IQueryAttributable {
|
||||
private readonly IHoursService _hoursService;
|
||||
|
||||
public int Id { get; set; }
|
||||
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> InfoEvent;
|
||||
|
||||
public event Func<string, string, Task<bool>> ConfirmEvent;
|
||||
//public event Func<string, string, string?, string?, Task<bool>> ConfirmEvent;
|
||||
//public event EventHandler<ConfirmEventArgs> ConfirmEvent;
|
||||
@@ -28,47 +37,39 @@ public partial class StundeViewModel : ObservableObject, IQueryAttributable {
|
||||
/// <summary>
|
||||
/// Gemeinden für die Auswahlliste
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private List<Gemeinde> optionsGemeinde;
|
||||
[ObservableProperty] private List<Gemeinde> optionsGemeinde;
|
||||
|
||||
/// <summary>
|
||||
/// Projekte für die Auswahlliste
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private List<Projekt> optionsProjekt;
|
||||
[ObservableProperty] private List<Projekt> optionsProjekt;
|
||||
|
||||
/// <summary>
|
||||
/// Freistellungen für die Auswahlliste
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private List<Freistellung> optionsFreistellung;
|
||||
[ObservableProperty] private List<Freistellung> optionsFreistellung;
|
||||
|
||||
/// <summary>
|
||||
/// Vorhandene Zeiten anzeigen, wenn neuer Eintrag erstellt wird
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private List<DayTime> dayTimes;
|
||||
[ObservableProperty] private List<DayTime> dayTimes;
|
||||
|
||||
/// <summary>
|
||||
/// Aktueller Stundeneintrag
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private DayTime dayTime;
|
||||
[ObservableProperty] private DayTime dayTime;
|
||||
|
||||
/// <summary>
|
||||
/// Dürfen Gemeinden verwendet werden?
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private bool gemeindeAktivSet;
|
||||
[ObservableProperty] private bool gemeindeAktivSet;
|
||||
|
||||
/// <summary>
|
||||
/// Dürfen Projekte verwendet werden?
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private bool projektAktivSet;
|
||||
[ObservableProperty] private bool projektAktivSet;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool freistellungEnabled;
|
||||
[ObservableProperty] private bool freistellungEnabled;
|
||||
|
||||
public ICommand SaveCommand { 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 StundeViewModel() {
|
||||
public StundeViewModel(IHoursService hoursService, IAlertService alertService) {
|
||||
_hoursService = hoursService;
|
||||
SaveCommand = new AsyncRelayCommand(Save);
|
||||
//DeleteCommand = new AsyncRelayCommand(Delete);
|
||||
DeleteConfirmCommand = new Command(async () => await DeleteConfirm());
|
||||
|
||||
if (alertService is not null) {
|
||||
alertService.AlertRaised += (s, msg) => AlertEvent?.Invoke(this, msg);
|
||||
}
|
||||
|
||||
public StundeViewModel(DayTime stunde) {
|
||||
SaveCommand = new AsyncRelayCommand(Save);
|
||||
DeleteConfirmCommand = new AsyncRelayCommand(DeleteConfirm);
|
||||
//LoadSettingsAsync();
|
||||
}
|
||||
|
||||
private async void LoadSettingsAsync() {
|
||||
try {
|
||||
Settings = await HoursBase.LoadSettings();
|
||||
Settings = await _hoursService.GetSettingsAsync();
|
||||
GlobalVar.Settings = Settings;
|
||||
|
||||
OptionsGemeinde = Settings.Gemeinden;
|
||||
@@ -98,27 +100,57 @@ public partial class StundeViewModel : ObservableObject, IQueryAttributable {
|
||||
|
||||
GemeindeAktivSet = Settings.GemeindeAktivSet;
|
||||
ProjektAktivSet = Settings.ProjektAktivSet;
|
||||
|
||||
} catch (Exception e) {
|
||||
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() {
|
||||
bool exceptionOccurred = false;
|
||||
bool proceed = true;
|
||||
|
||||
//Arbeitszeit sollte nicht null sein
|
||||
if (DayTime.TimeSpanVon == DayTime.TimeSpanBis && DayTime.FreistellungAktiv.Name == null) {
|
||||
proceed = false;
|
||||
AlertEvent?.Invoke(this, "Uhrzeiten sollten unterschiedlich sein");
|
||||
}
|
||||
|
||||
//Projekt ist ein Pflichtfeld
|
||||
if (Settings.ProjektAktivSet) {
|
||||
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) {
|
||||
try {
|
||||
await HoursBase.SaveEntry(DayTime);
|
||||
await _hoursService.SaveEntryAsync(DayTime);
|
||||
} catch (Exception e) {
|
||||
AlertEvent?.Invoke(this, e.Message);
|
||||
exceptionOccurred = true;
|
||||
}
|
||||
|
||||
if (!exceptionOccurred) {
|
||||
if (DayTime.Id != null) {
|
||||
await Shell.Current.GoToAsync($"..?saved={DayTime.Id}");
|
||||
@@ -133,7 +165,7 @@ public partial class StundeViewModel : ObservableObject, IQueryAttributable {
|
||||
/// Löschen ohne Bestätigung
|
||||
/// </summary>
|
||||
private async Task Delete() {
|
||||
await HoursBase.DeleteEntry(DayTime);
|
||||
await _hoursService.DeleteEntryAsync(DayTime);
|
||||
await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}");
|
||||
}
|
||||
|
||||
@@ -142,11 +174,16 @@ public partial class StundeViewModel : ObservableObject, IQueryAttributable {
|
||||
/// </summary>
|
||||
private async Task DeleteConfirm() {
|
||||
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) {
|
||||
//Löschen
|
||||
await HoursBase.DeleteEntry(DayTime);
|
||||
try {
|
||||
await _hoursService.DeleteEntryAsync(DayTime);
|
||||
await Shell.Current.GoToAsync($"..?date={DayTime.Day.ToString("yyyy-MM-dd")}");
|
||||
} catch (Exception e) {
|
||||
AlertEvent?.Invoke(this, e.Message);
|
||||
}
|
||||
} else {
|
||||
//nicht Löschen
|
||||
}
|
||||
@@ -160,72 +197,63 @@ public partial class StundeViewModel : ObservableObject, IQueryAttributable {
|
||||
//load beinhaltet die ID: Eintrag bearbeiten
|
||||
//date beinhaltet einen Tag: Neuen Eintrag erstellen
|
||||
if (query.ContainsKey("load")) {
|
||||
|
||||
//DateTime heute = DateTime.Now;
|
||||
try {
|
||||
//_dayTime = await HoursBase.LoadEntry(Convert.ToInt32(query["load"]));
|
||||
BaseResponse dat = await HoursBase.LoadBase("id=" + Convert.ToInt32(query["load"]));
|
||||
GlobalVar.Settings = dat.settings;
|
||||
GemeindeAktivSet = dat.settings.GemeindeAktivSet;
|
||||
ProjektAktivSet = dat.settings.ProjektAktivSet;
|
||||
//var entry = await _hoursService.GetEntryAsync(Convert.ToInt32(query["load"]));
|
||||
var (entry, settings, daytimes) = await _hoursService.GetEntryWithSettingsAsync(Convert.ToInt32(query["load"]));
|
||||
UpdateSettingsAsync(settings);
|
||||
|
||||
DayTime = dat.daytime;
|
||||
DayTime.TimeSpanVon = dat.daytime.Begin.ToTimeSpan();
|
||||
DayTime.TimeSpanBis = dat.daytime.End.ToTimeSpan();
|
||||
DayTime = entry;
|
||||
DayTime.TimeSpanVon = entry.Begin.ToTimeSpan();
|
||||
DayTime.TimeSpanBis = entry.End.ToTimeSpan();
|
||||
|
||||
OptionsGemeinde = dat.settings.Gemeinden ?? new List<Gemeinde>();
|
||||
OptionsProjekt = dat.settings.Projekte ?? new List<Projekt>();
|
||||
OptionsFreistellung = dat.settings.Freistellungen ?? new List<Freistellung>();
|
||||
DayTime.GemeindeAktiv = OptionsGemeinde.FirstOrDefault(Gemeinde => Gemeinde.Id == DayTime.Gemeinde) ??
|
||||
new Gemeinde();
|
||||
DayTime.ProjektAktiv = OptionsProjekt.FirstOrDefault(Projekt => Projekt.Id == DayTime.Projekt) ??
|
||||
new Projekt();
|
||||
DayTime.FreistellungAktiv =
|
||||
OptionsFreistellung.FirstOrDefault(Freistellung => Freistellung.Id == DayTime.Free) ??
|
||||
new Freistellung();
|
||||
|
||||
DayTime.GemeindeAktiv = OptionsGemeinde.FirstOrDefault(Gemeinde => Gemeinde.Id == DayTime.Gemeinde) ?? new Gemeinde();
|
||||
DayTime.ProjektAktiv = OptionsProjekt.FirstOrDefault(Projekt => Projekt.Id == DayTime.Projekt) ?? new Projekt();
|
||||
DayTime.FreistellungAktiv = OptionsFreistellung.FirstOrDefault(Freistellung => Freistellung.Id == DayTime.Free) ?? new Freistellung();
|
||||
|
||||
//Evtl. noch die anderen Zeiten des gleichen Tages holen
|
||||
BaseResponse dat1 = await HoursBase.LoadBase("date=" + DayTime.Day.ToString("yyyy-MM-dd"));
|
||||
DayTimes = dat1.daytimes;
|
||||
OnPropertyChanged(nameof(DayTime));
|
||||
OnPropertyChanged(nameof(DayTimes));
|
||||
|
||||
} catch (Exception e) {
|
||||
AlertEvent?.Invoke(this, e.Message);
|
||||
} finally {
|
||||
|
||||
}
|
||||
|
||||
if (System.String.IsNullOrEmpty(DayTime.Description)) {
|
||||
InfoEvent?.Invoke(this, "Eintrag hat keinen Beschreibungstext");
|
||||
}
|
||||
|
||||
SubTitle = DayTime.Day.ToString("dddd, d. MMMM yyyy");
|
||||
OnPropertyChanged(nameof(SubTitle));
|
||||
|
||||
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")) {
|
||||
Title = "Neuer Eintrag";
|
||||
OnPropertyChanged(nameof(Title));
|
||||
|
||||
DateTime _date = DateTime.ParseExact((string)query["date"], "yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture);
|
||||
DateTime _date = DateTime.ParseExact((string)query["date"], "yyyy-MM-dd",
|
||||
System.Globalization.CultureInfo.InvariantCulture);
|
||||
//Bei neuem Eintrag die vorhandenen des gleichen Tages anzeigen
|
||||
try {
|
||||
//DayTimes = await HoursBase.LoadDay(_date);
|
||||
BaseResponse dat = await HoursBase.LoadBase("date=" + _date.ToString("yyyy-MM-dd"));
|
||||
GlobalVar.Settings = dat.settings;
|
||||
DayTimes = dat.daytimes;
|
||||
|
||||
OptionsGemeinde = dat.settings.Gemeinden;
|
||||
OptionsProjekt = dat.settings.Projekte;
|
||||
OptionsFreistellung = dat.settings.Freistellungen;
|
||||
|
||||
GemeindeAktivSet = dat.settings.GemeindeAktivSet;
|
||||
ProjektAktivSet = dat.settings.ProjektAktivSet;
|
||||
|
||||
var (list, settings) = await _hoursService.GetDayWithSettingsAsync(_date);
|
||||
UpdateSettingsAsync(settings);
|
||||
DayTimes = list;
|
||||
OnPropertyChanged(nameof(DayTimes));
|
||||
} catch (Exception) {
|
||||
//Ein Tag ohne Einträge gibt eine Fehlermeldung,
|
||||
//die soll aber ignoriert werden, weil beim Neueintrag ist das ja Wurscht
|
||||
//In dem Fall müssen die Settings aber nochmal geholt werden, weil die dann nicht geladen wurden
|
||||
LoadSettingsAsync();
|
||||
// LoadSettingsAsync();
|
||||
} finally {
|
||||
DayTime = new DayTime();
|
||||
DayTime.Day = _date;
|
||||
@@ -240,8 +268,6 @@ public partial class StundeViewModel : ObservableObject, IQueryAttributable {
|
||||
OnPropertyChanged(nameof(SubTitle));
|
||||
//OnPropertyChanged(nameof(DayTime));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,16 +5,16 @@ using Jugenddienst_Stunden.Types;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows.Input;
|
||||
using CommunityToolkit.Maui.Alerts;
|
||||
using CommunityToolkit.Maui.Core;
|
||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||
using Jugenddienst_Stunden.Interfaces;
|
||||
|
||||
|
||||
namespace Jugenddienst_Stunden.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel für die Stundenliste
|
||||
/// </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 SelectEntryCommand { get; }
|
||||
@@ -30,8 +30,7 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
|
||||
/// <summary>
|
||||
/// Beschriftung Button Monatsübersicht
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string loadOverview;
|
||||
[ObservableProperty] private string loadOverview;
|
||||
|
||||
//private HoursBase HoursBase = new HoursBase();
|
||||
internal Settings Settings = new Settings();
|
||||
@@ -39,25 +38,21 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
|
||||
/// <summary>
|
||||
/// Zu leistende Stunden
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private TimeOnly sollstunden;
|
||||
[ObservableProperty] private TimeOnly sollstunden;
|
||||
|
||||
/// <summary>
|
||||
/// Geleistete Stunden an einem Tag
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private TimeOnly dayTotal;
|
||||
[ObservableProperty] private TimeOnly dayTotal;
|
||||
|
||||
/// <summary>
|
||||
/// Liste der Tageszeiten
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private List<DayTime> dayTimes = new List<DayTime>();
|
||||
[ObservableProperty] private List<DayTime> dayTimes = new List<DayTime>();
|
||||
|
||||
public string Title { get; set; } = GlobalVar.Name + " " + GlobalVar.Surname;
|
||||
|
||||
[ObservableProperty]
|
||||
private Hours hours;
|
||||
[ObservableProperty] private Hours hours;
|
||||
|
||||
|
||||
/// <summary>
|
||||
@@ -78,14 +73,26 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
|
||||
/// Heutiges Datum, wenn das Datum geändert wird, wird auch der Tag geladen
|
||||
/// </summary>
|
||||
private DateTime dateToday = DateTime.Today;
|
||||
|
||||
public DateTime DateToday {
|
||||
get => dateToday;
|
||||
set {
|
||||
if (dateToday != value) {
|
||||
dateToday = value;
|
||||
LoadOverview = "Lade Summen für " + dateToday.ToString("MMMM yy");
|
||||
//OnPropertyChanged();
|
||||
Task.Run(() => LoadDay(value));
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,14 +101,14 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
|
||||
/// Monatsübersicht: Geleistete Stunden
|
||||
/// </summary>
|
||||
public double? ZeitCalculated {
|
||||
get => Hours.Zeit_total;
|
||||
get => Hours.zeit_total;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Monatsübersicht: Sollstunden
|
||||
/// </summary>
|
||||
public double? Nominal {
|
||||
get => Hours.Nominal;
|
||||
get => Hours.nominal;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -121,6 +128,7 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
|
||||
public double Zeitausgleich {
|
||||
get => Hours.zeitausgleich;
|
||||
}
|
||||
|
||||
public double ZeitausgleichMonth {
|
||||
get => Hours.zeitausgleich_month;
|
||||
}
|
||||
@@ -128,15 +136,14 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
|
||||
/// <summary>
|
||||
/// Monatsübersicht: Resturlaub
|
||||
/// </summary>
|
||||
public double Holiday {
|
||||
public double? Holiday {
|
||||
get => Hours.holiday;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seite neu laden
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private bool isRefreshing;
|
||||
[ObservableProperty] private bool isRefreshing;
|
||||
|
||||
|
||||
/// <summary>
|
||||
@@ -152,11 +159,11 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
|
||||
private bool doContinue = true;
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// CTOR
|
||||
/// CTOR (DI)
|
||||
/// </summary>
|
||||
public StundenViewModel() {
|
||||
public StundenViewModel(IHoursService hoursService) {
|
||||
_hoursService = hoursService;
|
||||
Hours = new Hours();
|
||||
|
||||
LoadOverview = "Lade Summen für " + DateToday.ToString("MMMM");
|
||||
@@ -167,8 +174,22 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
|
||||
RefreshListCommand = new AsyncRelayCommand(RefreshList);
|
||||
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>
|
||||
@@ -200,13 +221,16 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
|
||||
/// </summary>
|
||||
private async Task LoadData() {
|
||||
try {
|
||||
BaseResponse dat = await HoursBase.LoadBase("hours&year=" + DateToday.ToString("yyyy") + "&month=" + DateToday.ToString("MM"));
|
||||
Hours = dat.hour;
|
||||
Settings = dat.settings;
|
||||
var (hours, settings) = await _hoursService.GetMonthSummaryAsync(DateToday);
|
||||
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)");
|
||||
InfoEvent?.Invoke(this,
|
||||
"Version: " + Settings.Version + " verfügbar (" + AppInfo.Current.VersionString.Substring(0, 5) +
|
||||
" installiert)");
|
||||
}
|
||||
|
||||
//_hour = await HoursBase.LoadData();
|
||||
RefreshProperties();
|
||||
} catch (Exception e) {
|
||||
@@ -219,19 +243,25 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
|
||||
/// Lädt die Arbeitszeiten für einen Tag
|
||||
/// </summary>
|
||||
public async Task LoadDay(DateTime date) {
|
||||
// kleine Initialwerte sind ok, aber UI-Thread sicher setzen:
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
DayTotal = new TimeOnly(0);
|
||||
Sollstunden = new TimeOnly(0);
|
||||
});
|
||||
try {
|
||||
//_dayTimes = await HoursBase.LoadDay(date);
|
||||
BaseResponse dat = await HoursBase.LoadBase("date=" + date.ToString("yyyy-MM-dd"));
|
||||
var (dayTimes, settings) = await _hoursService.GetDayWithSettingsAsync(date);
|
||||
|
||||
DayTimes = dat.daytimes;
|
||||
Settings = dat.settings;
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
DayTimes = dayTimes;
|
||||
Settings = settings;
|
||||
GemeindeAktivSet = Settings.GemeindeAktivSet;
|
||||
ProjektAktivSet = Settings.ProjektAktivSet;
|
||||
|
||||
OnPropertyChanged(nameof(GemeindeAktivSet));
|
||||
OnPropertyChanged(nameof(ProjektAktivSet));
|
||||
});
|
||||
|
||||
List<Sollstunden> _soll;
|
||||
TimeSpan span = TimeSpan.Zero;
|
||||
@@ -243,32 +273,54 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
|
||||
span += dt.Night.ToTimeSpan() * .5;
|
||||
merker = true;
|
||||
}
|
||||
|
||||
_soll = Settings.Nominal.Where(w => w.Timetable == dt.TimeTable && w.Wochentag == dt.Wday).ToList();
|
||||
if (_soll.Count > 0)
|
||||
Sollstunden = TimeOnly.FromTimeSpan(TimeSpan.FromHours(_soll[0].Zeit));
|
||||
{
|
||||
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
|
||||
if (DayTimes != null) {
|
||||
BaseResponse dat1 = await HoursBase.LoadBase("date=" + date.ToString("yyyy-MM-dd") + "&tilldate=" + date.AddDays(3).ToString("yyyy-MM-dd"));
|
||||
if (dat1.daytimes != null)
|
||||
DayTimes = dat.daytimes.Concat(dat1.daytimes).ToList();
|
||||
var more = await _hoursService.GetDayRangeAsync(date.AddDays(1), date.AddDays(3));
|
||||
if (more != null && more.Count > 0)
|
||||
{
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
DayTimes = DayTimes.Concat(more).ToList()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
DayTimes = new List<DayTime>();
|
||||
//TODO: hier könnte auch ein Fehler kommen, dann wäre InfoEvent falsch.
|
||||
|
||||
if (Settings.Version != null && Settings.Version != AppInfo.Current.VersionString.Substring(0, 5)) {
|
||||
InfoEvent?.Invoke(this, "Version: " + Settings.Version + " verfügbar (" + AppInfo.Current.VersionString.Substring(0, 5) + " installiert)");
|
||||
} else { InfoEvent?.Invoke(this, e.Message); }
|
||||
InfoEvent?.Invoke(this,
|
||||
"Version: " + Settings.Version + " verfügbar (" + AppInfo.Current.VersionString.Substring(0, 5) +
|
||||
" installiert)");
|
||||
} else {
|
||||
InfoEvent?.Invoke(this, e.Message);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
} finally {
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
OnPropertyChanged(nameof(DayTotal));
|
||||
OnPropertyChanged(nameof(Sollstunden));
|
||||
OnPropertyChanged(nameof(DateToday));
|
||||
OnPropertyChanged(nameof(LoadOverview));
|
||||
//OnPropertyChanged(nameof(DayTimes));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,6 +367,4 @@ internal partial class StundenViewModel : ObservableObject, IQueryAttributable,
|
||||
//Console.WriteLine($"Fehler bei OnPropertyChanged: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
|
||||
@@ -13,7 +14,8 @@
|
||||
|
||||
<!-- Add an item to the toolbar -->
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="Neue Notiz" Command="{Binding NewCommand}" IconImageSource="{FontImage Glyph='+', Color=Black, Size=22}" />
|
||||
<ToolbarItem Text="Neue Notiz" Command="{Binding NewCommand}"
|
||||
IconImageSource="{FontImage Glyph='+', Color=Black, Size=22}" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Behaviors>
|
||||
@@ -23,7 +25,7 @@
|
||||
</ContentPage.Behaviors>
|
||||
|
||||
<VerticalStackLayout Margin="20,0,0,0">
|
||||
<Label Text="Werden nur lokal gespeichert"/>
|
||||
<Label Text="Werden nur lokal gespeichert" />
|
||||
|
||||
<!-- Display notes in a list -->
|
||||
<CollectionView x:Name="notesCollection"
|
||||
@@ -42,8 +44,8 @@
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackLayout>
|
||||
<Label Text="{Binding Text}" FontSize="22"/>
|
||||
<Label Text="{Binding Date}" FontSize="14" TextColor="Silver"/>
|
||||
<Label Text="{Binding Text}" FontSize="22" />
|
||||
<Label Text="{Binding Date}" FontSize="14" TextColor="Silver" />
|
||||
</StackLayout>
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
namespace Jugenddienst_Stunden.Views;
|
||||
|
||||
public partial class AllNotesPage : ContentPage
|
||||
{
|
||||
public AllNotesPage()
|
||||
{
|
||||
public partial class AllNotesPage : ContentPage {
|
||||
public AllNotesPage() {
|
||||
InitializeComponent();
|
||||
|
||||
}
|
||||
|
||||
private void ContentPage_NavigatedTo(object sender, NavigatedToEventArgs e) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
|
||||
@@ -9,13 +10,13 @@
|
||||
Title="{Binding Title}">
|
||||
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<models:LoginViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
<!-- BindingContext wird via DI im Code-Behind gesetzt -->
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<conv:StringVisibilityConverter x:Key="StringVisibilityConverter" />
|
||||
<conv:InverseBoolConverter x:Key="InverseBoolConverter" />
|
||||
<conv:EventArgsPassThroughConverter x:Key="EventArgsPassThroughConverter" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
@@ -29,40 +30,48 @@
|
||||
<VerticalStackLayout Spacing="10" Margin="15,0">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<HorizontalStackLayout Spacing="5" HorizontalOptions="Start" Grid.Column="0">
|
||||
<Label FontSize="20" FontAttributes="Bold" Text="{Binding AppTitle}" Margin="0,7,0,0" />
|
||||
<Label FontSize="16" Text="{Binding Version}" Margin="0,11,0,0" />
|
||||
</HorizontalStackLayout>
|
||||
<Grid Grid.Column="1" ColumnDefinitions="*,50" ColumnSpacing="10">
|
||||
<Label Text="Login QR/manuell" VerticalOptions="Center" Grid.Column="0"/>
|
||||
<Switch x:Name="LoginSwitch" IsToggled="False" Toggled="Switch_Toggled" VerticalOptions="Center" Grid.Column="1"/>
|
||||
<Label Text="Login QR/manuell" VerticalOptions="Center" Grid.Column="0" />
|
||||
<Switch x:Name="LoginSwitch" IsToggled="{Binding IsManualMode}" VerticalOptions="Center"
|
||||
Grid.Column="1" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Label x:Name="ServerLabel" Text="{Binding Server}" IsVisible="{Binding Server, Converter={StaticResource StringVisibilityConverter}}" />
|
||||
<Label x:Name="ServerLabel" Text="{Binding ServerLabel}"
|
||||
IsVisible="{Binding Server, Converter={StaticResource StringVisibilityConverter}}" />
|
||||
|
||||
<VerticalStackLayout x:Name="LoginQR" Margin="0,20,0,0">
|
||||
<VerticalStackLayout x:Name="LoginQR" Margin="0,20,0,0" IsVisible="{Binding IsManualMode, Converter={StaticResource InverseBoolConverter}}">
|
||||
<Label Text="Login mit QR-Code" FontSize="32" HorizontalOptions="Start" />
|
||||
<Label x:Name="Message" Text="{Binding Message}" Margin="0,15" />
|
||||
|
||||
<Border HeightRequest="300" Padding="0">
|
||||
<zxing:CameraBarcodeReaderView
|
||||
x:Name="barcodeScannerView"
|
||||
BarcodesDetected="BarcodesDetected"
|
||||
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>
|
||||
</VerticalStackLayout>
|
||||
|
||||
<VerticalStackLayout x:Name="LoginManual" Spacing="25">
|
||||
<VerticalStackLayout x:Name="LoginManual" Spacing="25" IsVisible="{Binding IsManualMode}">
|
||||
<Label Text="Manueller Login" FontSize="32" HorizontalOptions="Start" Margin="0, 20, 0, 0" />
|
||||
<Entry x:Name="UsernameEntry" Placeholder="Benutzername (Mailadresse)" Keyboard="Email" />
|
||||
<Entry x:Name="PasswordEntry" Placeholder="Passwort" IsPassword="True" />
|
||||
<Entry x:Name="ServerEntry" Placeholder="Server (gleich wie im Browser)" Keyboard="Url" />
|
||||
<Button Text="Login" Clicked="OnLoginButtonClicked" TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}" />
|
||||
<Entry x:Name="UsernameEntry" Text="{Binding Username}" Placeholder="Benutzername (Mailadresse)" Keyboard="Email" />
|
||||
<Entry x:Name="PasswordEntry" Text="{Binding Password}" Placeholder="Passwort" IsPassword="True" />
|
||||
<Entry x:Name="ServerEntry" Text="{Binding Server}" Placeholder="Server (gleich wie im Browser)" Keyboard="Url" />
|
||||
<Button Text="Login" Command="{Binding LoginCommand}"
|
||||
TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}" />
|
||||
</VerticalStackLayout>
|
||||
</VerticalStackLayout>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Jugenddienst_Stunden.Models;
|
||||
using Jugenddienst_Stunden.Types;
|
||||
using Jugenddienst_Stunden.ViewModels;
|
||||
using ZXing.Net.Maui;
|
||||
|
||||
|
||||
@@ -9,7 +10,6 @@ namespace Jugenddienst_Stunden.Views;
|
||||
/// Die Loginseite mit dem Barcodescanner
|
||||
/// </summary>
|
||||
public partial class LoginPage : ContentPage {
|
||||
|
||||
private DateTime _lastDetectionTime;
|
||||
private readonly TimeSpan _detectionInterval = TimeSpan.FromSeconds(5);
|
||||
|
||||
@@ -20,10 +20,43 @@ public partial class LoginPage : ContentPage {
|
||||
public LoginPage() {
|
||||
InitializeComponent();
|
||||
|
||||
barcodeScannerView.Options = new BarcodeReaderOptions {
|
||||
Formats = BarcodeFormat.QrCode,
|
||||
AutoRotate = true,
|
||||
Multiple = false
|
||||
// BindingContext via DI beziehen, falls nicht bereits gesetzt
|
||||
try {
|
||||
if (BindingContext is null) {
|
||||
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) {
|
||||
@@ -33,19 +66,7 @@ public partial class LoginPage : ContentPage {
|
||||
//}
|
||||
|
||||
//Zwischen manuellem und automatischem Login (mit QR-Code) umschalten und die Schalterstellung merken
|
||||
bool sqr = true;
|
||||
bool sma = false;
|
||||
if (Preferences.Default.Get("logintype", "") == "manual") {
|
||||
sqr = false;
|
||||
sma = true;
|
||||
LoginSwitch.IsToggled = true;
|
||||
Message.IsVisible = false;
|
||||
} else {
|
||||
LoginSwitch.IsToggled = false;
|
||||
Message.IsVisible = true;
|
||||
}
|
||||
LoginQR.IsVisible = sqr;
|
||||
LoginManual.IsVisible = sma;
|
||||
// MVVM übernimmt Umschalten über IsManualMode im ViewModel; keine Code-Behind-Umschaltung mehr
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +75,6 @@ public partial class LoginPage : ContentPage {
|
||||
/// ZXing.Net.Maui.Controls 0.4.4
|
||||
/// </summary>
|
||||
private void BarcodesDetected(object sender, BarcodeDetectionEventArgs e) {
|
||||
|
||||
var currentTime = DateTime.Now;
|
||||
if ((currentTime - _lastDetectionTime) > _detectionInterval) {
|
||||
_lastDetectionTime = currentTime;
|
||||
@@ -75,22 +95,21 @@ public partial class LoginPage : ContentPage {
|
||||
|
||||
Title = user.Name + " " + user.Surname;
|
||||
//Auf der Loginseite wird der Server als Info ohne Protokoll und ohne /appapi angezeigt
|
||||
ServerLabel.Text = "Server: " + tokendata.Url.Replace("/appapi", "").Replace("https://", "").Replace("http://", "");
|
||||
ServerLabel.Text = "Server: " + tokendata.Url.Replace("/appapi", "").Replace("https://", "")
|
||||
.Replace("http://", "");
|
||||
|
||||
|
||||
await DisplayAlert("Login erfolgreich", user.Name + " " + user.Surname, "OK");
|
||||
if (Navigation.NavigationStack.Count > 1) {
|
||||
//Beim ersten Start ohne Login, wird man automatisch auf die Loginseite geleitet. Danach in der History zur<75>ck
|
||||
//Beim ersten Start ohne Login, wird man automatisch auf die Loginseite geleitet. Danach in der History zur<75>ck
|
||||
await Navigation.PopAsync();
|
||||
} else {
|
||||
//Beim manuellen Wechsel auf die Loginseite leiten wir nach erfolgreichem Login auf die Stunden<65>bersicht
|
||||
//Beim manuellen Wechsel auf die Loginseite leiten wir nach erfolgreichem Login auf die Stunden<65>bersicht
|
||||
await Shell.Current.GoToAsync($"//StundenPage");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
await DisplayAlert("Fehler", e.Message, "OK");
|
||||
}
|
||||
|
||||
});
|
||||
} else {
|
||||
MainThread.InvokeOnMainThreadAsync(() => {
|
||||
@@ -101,20 +120,19 @@ public partial class LoginPage : ContentPage {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected override void OnDisappearing() {
|
||||
base.OnDisappearing();
|
||||
|
||||
barcodeScannerView.CameraLocation = CameraLocation.Front;
|
||||
barcodeScannerView.IsDetecting = false;
|
||||
// IsDetecting wird via Binding vom ViewModel gesteuert
|
||||
}
|
||||
|
||||
protected override void OnAppearing() {
|
||||
base.OnAppearing();
|
||||
|
||||
barcodeScannerView.IsDetecting = true;
|
||||
// IsDetecting wird via Binding vom ViewModel gesteuert
|
||||
barcodeScannerView.CameraLocation = CameraLocation.Rear;
|
||||
}
|
||||
|
||||
@@ -123,6 +141,7 @@ public partial class LoginPage : ContentPage {
|
||||
if (status != PermissionStatus.Granted) {
|
||||
status = Permissions.RequestAsync<Permissions.Camera>().Result;
|
||||
}
|
||||
|
||||
return status != PermissionStatus.Granted;
|
||||
}
|
||||
|
||||
@@ -133,13 +152,15 @@ public partial class LoginPage : ContentPage {
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
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.Name = response.Name;
|
||||
@@ -169,29 +190,16 @@ public partial class LoginPage : ContentPage {
|
||||
if (!url.StartsWith("http://") && !url.StartsWith("https://")) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
|
||||
if (url.StartsWith("http://")) {
|
||||
url = url.Replace("http://", "https://");
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
//Zwischen manuellem und automatischem Login (mit QR-Code) umschalten und die Schalterstellung merken
|
||||
private void Switch_Toggled(object sender, ToggledEventArgs e) {
|
||||
var switcher = (Switch)sender;
|
||||
|
||||
if (switcher.IsToggled) {
|
||||
LoginQR.IsVisible = false;
|
||||
LoginManual.IsVisible = true;
|
||||
Message.IsVisible = false;
|
||||
Preferences.Default.Set("logintype", "manual");
|
||||
} else {
|
||||
LoginQR.IsVisible = true;
|
||||
LoginManual.IsVisible = false;
|
||||
Message.IsVisible = true;
|
||||
Preferences.Default.Set("logintype", "qr");
|
||||
}
|
||||
|
||||
}
|
||||
// Umschalt-Logik erfolgt über Binding an IsManualMode im ViewModel
|
||||
|
||||
//private void Vm_AlertEvent(object? sender, string e) {
|
||||
// DisplayAlert("Fehler:", e, "OK");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace Jugenddienst_Stunden.Views;
|
||||
/// Einzelne Notiz
|
||||
/// </summary>
|
||||
public partial class NotePage : ContentPage {
|
||||
|
||||
/// <summary>
|
||||
/// CTOR
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
|
||||
@@ -7,10 +8,6 @@
|
||||
x:Class="Jugenddienst_Stunden.Views.StundePage"
|
||||
Title="{Binding Title}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<models:StundeViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<conv:IntBoolConverter x:Key="IntBoolConverter" />
|
||||
@@ -31,20 +28,26 @@
|
||||
<Border>
|
||||
<Border.Padding>
|
||||
<OnPlatform x:TypeArguments="Thickness" Default="0,15,10,0">
|
||||
<On Platform="Android" Value="0,4,10,8"/>
|
||||
<On Platform="WPF" Value="0,15,10,0"/>
|
||||
<On Platform="Android" Value="0,4,10,8" />
|
||||
<On Platform="WPF" Value="0,15,10,0" />
|
||||
</OnPlatform>
|
||||
</Border.Padding>
|
||||
|
||||
<FlexLayout Direction="Row" AlignItems="Start" Wrap="Wrap" JustifyContent="SpaceBetween">
|
||||
<HorizontalStackLayout Spacing="10">
|
||||
<Label Text="Beginn" VerticalTextAlignment="Center" HorizontalTextAlignment="End" MinimumWidthRequest="60"></Label>
|
||||
<TimePicker x:Name="TimeBegin" HorizontalOptions="Center" Format="HH:mm" MinimumWidthRequest="80" Time="{Binding DayTime.TimeSpanVon}" />
|
||||
<Label Text="Beginn" VerticalTextAlignment="Center" HorizontalTextAlignment="End"
|
||||
MinimumWidthRequest="60">
|
||||
</Label>
|
||||
<TimePicker x:Name="TimeBegin" HorizontalOptions="Center" Format="HH:mm" MinimumWidthRequest="80"
|
||||
Time="{Binding DayTime.TimeSpanVon}" />
|
||||
</HorizontalStackLayout>
|
||||
|
||||
<HorizontalStackLayout Spacing="10">
|
||||
<Label Text="Ende" VerticalTextAlignment="Center" HorizontalTextAlignment="End" MinimumWidthRequest="60"></Label>
|
||||
<TimePicker x:Name="TimeEnd" Format="HH:mm" MinimumWidthRequest="80" Time="{Binding DayTime.TimeSpanBis}" />
|
||||
<Label Text="Ende" VerticalTextAlignment="Center" HorizontalTextAlignment="End"
|
||||
MinimumWidthRequest="60">
|
||||
</Label>
|
||||
<TimePicker x:Name="TimeEnd" Format="HH:mm" MinimumWidthRequest="80"
|
||||
Time="{Binding DayTime.TimeSpanBis}" />
|
||||
</HorizontalStackLayout>
|
||||
</FlexLayout>
|
||||
</Border>
|
||||
@@ -52,29 +55,28 @@
|
||||
<Border>
|
||||
<Border.Padding>
|
||||
<OnPlatform x:TypeArguments="Thickness" Default="5">
|
||||
<On Platform="Android" Value="5,4,5,8"/>
|
||||
<On Platform="WPF" Value="5"/>
|
||||
<On Platform="Android" Value="5,4,5,8" />
|
||||
<On Platform="WPF" Value="5" />
|
||||
</OnPlatform>
|
||||
</Border.Padding>
|
||||
<!--<Grid ColumnDefinitions="*,*,*">
|
||||
<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>
|
||||
<Picker x:Name="pick_gemeinde" Title="Gemeinde" ItemsSource="{Binding OptionsGemeinde}" SelectedItem="{Binding DayTime.GemeindeAktiv, Mode=TwoWay}" ItemDisplayBinding="{Binding Name}" IsVisible="{Binding GemeindeAktivSet}">
|
||||
<Picker x:Name="pick_gemeinde" Title="Gemeinde" ItemsSource="{Binding OptionsGemeinde}"
|
||||
SelectedItem="{Binding DayTime.GemeindeAktiv, Mode=TwoWay}" ItemDisplayBinding="{Binding Name}"
|
||||
IsVisible="{Binding GemeindeAktivSet}">
|
||||
</Picker>
|
||||
<Picker x:Name="pick_projekt" Title="Projekt" ItemsSource="{Binding OptionsProjekt}" SelectedItem="{Binding DayTime.ProjektAktiv, Mode=TwoWay}" ItemDisplayBinding="{Binding Name}" IsVisible="{Binding ProjektAktivSet}">
|
||||
<Picker x:Name="pick_projekt" Title="Projekt" ItemsSource="{Binding OptionsProjekt}"
|
||||
SelectedItem="{Binding DayTime.ProjektAktiv, Mode=TwoWay}" ItemDisplayBinding="{Binding Name}"
|
||||
IsVisible="{Binding ProjektAktivSet}">
|
||||
</Picker>
|
||||
<Picker x:Name="pick_freistellung" Title="Freistellung" ItemsSource="{Binding OptionsFreistellung}" SelectedItem="{Binding DayTime.FreistellungAktiv, Mode=TwoWay}" ItemDisplayBinding="{Binding Name}" IsEnabled="{Binding FreistellungEnabled}">
|
||||
<Picker x:Name="pick_freistellung" Title="Freistellung" ItemsSource="{Binding OptionsFreistellung}"
|
||||
SelectedItem="{Binding DayTime.FreistellungAktiv, Mode=TwoWay}"
|
||||
ItemDisplayBinding="{Binding Name}" IsEnabled="{Binding FreistellungEnabled}">
|
||||
</Picker>
|
||||
</HorizontalStackLayout>
|
||||
</Border>
|
||||
|
||||
<Editor Placeholder="Beschreibung" Text="{Binding DayTime.Description}" MinimumHeightRequest="40" AutoSize="TextChanges" FontSize="18" />
|
||||
<Editor Placeholder="Beschreibung" Text="{Binding DayTime.Description}" MinimumHeightRequest="40"
|
||||
AutoSize="TextChanges" FontSize="18" />
|
||||
|
||||
<Grid ColumnDefinitions="*,*" ColumnSpacing="4">
|
||||
<Button Grid.Column="1" Text="Speichern"
|
||||
@@ -85,15 +87,18 @@
|
||||
IsEnabled="{Binding DayTime.Id, Converter={StaticResource IntBoolConverter}}"
|
||||
IsVisible="{Binding FreistellungEnabled}"
|
||||
BackgroundColor="{StaticResource Gray500}"
|
||||
TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}"/>
|
||||
TextColor="{AppThemeBinding Dark={StaticResource White}, Light={StaticResource White}}" />
|
||||
</Grid>
|
||||
|
||||
|
||||
<BoxView HeightRequest="1" Margin="3,10" />
|
||||
|
||||
<Label Text="Noch keine Einträge vorhanden" IsVisible="{Binding DayTimes, Converter={StaticResource CollectionVisibilityConverter}, ConverterParameter=Invert}" Margin="6,0,0,0"/>
|
||||
<Label Text="Noch keine Einträge vorhanden"
|
||||
IsVisible="{Binding DayTimes, Converter={StaticResource CollectionVisibilityConverter}, ConverterParameter=Invert}"
|
||||
Margin="6,0,0,0" />
|
||||
|
||||
<StackLayout Margin="6,0,0,0" IsVisible="{Binding DayTimes, Converter={StaticResource CollectionVisibilityConverter}}">
|
||||
<StackLayout Margin="6,0,0,0"
|
||||
IsVisible="{Binding DayTimes, Converter={StaticResource CollectionVisibilityConverter}}">
|
||||
<Label>
|
||||
<Label.FormattedText>
|
||||
<FormattedString>
|
||||
@@ -129,15 +134,18 @@
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<HorizontalStackLayout Grid.Row="0" Grid.Column="0">
|
||||
<Label Grid.Column="0" Text="{Binding Begin}"/>
|
||||
<Label Text="bis" Padding="5,0,5,0"/>
|
||||
<Label Text="{Binding End}"/>
|
||||
<Label Text="{Binding GemeindeAktiv.Name}" Margin="10,0,0,0" IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.GemeindeAktivSet}"/>
|
||||
<Label Text="{Binding ProjektAktiv.Name}" Margin="10,0,0,0" IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.ProjektAktivSet}"/>
|
||||
<Label Text="{Binding FreistellungAktiv.Name}" Margin="10,0,0,0"/>
|
||||
<Label Grid.Column="0" Text="{Binding Begin}" />
|
||||
<Label Text="bis" Padding="5,0,5,0" />
|
||||
<Label Text="{Binding End}" />
|
||||
<Label Text="{Binding GemeindeAktiv.Name}" Margin="10,0,0,0"
|
||||
IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.GemeindeAktivSet}" />
|
||||
<Label Text="{Binding ProjektAktiv.Name}" Margin="10,0,0,0"
|
||||
IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.ProjektAktivSet}" />
|
||||
<Label Text="{Binding FreistellungAktiv.Name}" Margin="10,0,0,0" />
|
||||
</HorizontalStackLayout>
|
||||
|
||||
<Label Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Text="{Binding Description}" Padding="0,0,0,15"/>
|
||||
<Label Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Text="{Binding Description}"
|
||||
Padding="0,0,0,15" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
|
||||
@@ -10,19 +10,17 @@ namespace Jugenddienst_Stunden.Views;
|
||||
/// Einzelner Stundeneintrag
|
||||
/// </summary>
|
||||
public partial class StundePage : ContentPage {
|
||||
|
||||
/// <summary>
|
||||
/// CTOR
|
||||
/// </summary>
|
||||
public StundePage() {
|
||||
public StundePage(StundeViewModel vm) {
|
||||
InitializeComponent();
|
||||
|
||||
if (BindingContext is StundeViewModel vm) {
|
||||
BindingContext = vm;
|
||||
vm.AlertEvent += Vm_AlertEvent;
|
||||
vm.InfoEvent += Vm_InfoEvent;
|
||||
vm.ConfirmEvent += ShowConfirm;
|
||||
}
|
||||
}
|
||||
|
||||
private void Vm_AlertEvent(object? sender, string e) {
|
||||
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);
|
||||
// e.Result = result;
|
||||
//}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
|
||||
@@ -8,9 +9,6 @@
|
||||
x:Class="Jugenddienst_Stunden.Views.StundenPage"
|
||||
Title="{Binding Title}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<models:StundenViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
@@ -18,7 +16,7 @@
|
||||
<FontImageSource x:Key="ToolbarIcon"
|
||||
Glyph="+"
|
||||
Size="22"
|
||||
Color="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}"/>
|
||||
Color="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
@@ -30,10 +28,12 @@
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<!--<ToolbarItem Text="Lade Liste" Command="{Binding RefreshListCommand}"/>-->
|
||||
<ToolbarItem Text="Neuer Eintrag" IconImageSource="{StaticResource ToolbarIcon}" Command="{Binding NewEntryCommand}" />
|
||||
<ToolbarItem Text="Neuer Eintrag" IconImageSource="{StaticResource ToolbarIcon}"
|
||||
Command="{Binding NewEntryCommand}" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<RefreshView x:Name="MyRefreshView" Command="{Binding RefreshCommand}" IsRefreshing="{Binding IsRefreshing}" Margin="10" Padding="10">
|
||||
<RefreshView x:Name="MyRefreshView" Command="{Binding RefreshCommand}" IsRefreshing="{Binding IsRefreshing}"
|
||||
Margin="10" Padding="10">
|
||||
<Grid RowDefinitions="50,*,Auto,80">
|
||||
<!--<VerticalStackLayout Spacing="10" Margin="10">-->
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<Border Grid.Column="1" Margin="15,0,0,0" Padding="15,0,0,0" ToolTipProperties.Text="Tagessumme">
|
||||
<HorizontalStackLayout>
|
||||
<Label Text="{Binding DayTotal,StringFormat='{}{0:HH:mm}'}" VerticalOptions="Center"></Label>
|
||||
<Label Text="/" VerticalOptions="Center" Margin="3,0"/>
|
||||
<Label Text="/" VerticalOptions="Center" Margin="3,0" />
|
||||
<Label Text="{Binding Sollstunden,StringFormat='{}{0:HH:mm}'}" VerticalOptions="Center"></Label>
|
||||
</HorizontalStackLayout>
|
||||
</Border>
|
||||
@@ -72,27 +72,29 @@
|
||||
<VisualStateGroup Name="CommonStates">
|
||||
<VisualState Name="Normal">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource TransparentColor}}" />
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource TransparentColor}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState Name="Selected">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
|
||||
|
||||
|
||||
<HorizontalStackLayout>
|
||||
<HorizontalStackLayout.Triggers>
|
||||
<DataTrigger TargetType="HorizontalStackLayout" Binding="{Binding Approved}" Value="True">
|
||||
<DataTrigger TargetType="HorizontalStackLayout" Binding="{Binding Approved}"
|
||||
Value="True">
|
||||
<Setter Property="BackgroundColor" Value="LightCoral" />
|
||||
<Setter Property="Padding" Value="4"/>
|
||||
<Setter Property="Padding" Value="4" />
|
||||
</DataTrigger>
|
||||
</HorizontalStackLayout.Triggers>
|
||||
<Label Text="{Binding Day, StringFormat='{0:dddd, dd. MMMM}'}"/>
|
||||
<Label Text="{Binding Day, StringFormat='{0:dddd, dd. MMMM}'}" />
|
||||
<Label Text="von" Padding="5,0,5,0" />
|
||||
<Label Text="{Binding Begin}" />
|
||||
<Label Text="bis" Padding="5,0,5,0" />
|
||||
@@ -101,17 +103,20 @@
|
||||
|
||||
<HorizontalStackLayout HorizontalOptions="FillAndExpand">
|
||||
<HorizontalStackLayout.Triggers>
|
||||
<DataTrigger TargetType="HorizontalStackLayout" Binding="{Binding Approved}" Value="True">
|
||||
<DataTrigger TargetType="HorizontalStackLayout" Binding="{Binding Approved}"
|
||||
Value="True">
|
||||
<Setter Property="BackgroundColor" Value="LightCoral" />
|
||||
<Setter Property="Padding" Value="4"/>
|
||||
<Setter Property="Padding" Value="4" />
|
||||
</DataTrigger>
|
||||
</HorizontalStackLayout.Triggers>
|
||||
<Label Text="{Binding GemeindeAktiv.Name}" Margin="0,0,10,0" IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.GemeindeAktivSet}" />
|
||||
<Label Text="{Binding ProjektAktiv.Name}" Margin="0,0,10,0" IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.ProjektAktivSet}" />
|
||||
<Label Text="{Binding GemeindeAktiv.Name}" Margin="0,0,10,0"
|
||||
IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.GemeindeAktivSet}" />
|
||||
<Label Text="{Binding ProjektAktiv.Name}" Margin="0,0,10,0"
|
||||
IsVisible="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.ProjektAktivSet}" />
|
||||
<Label Text="{Binding FreistellungAktiv.Name}" IsVisible="{Binding Approved}" />
|
||||
</HorizontalStackLayout>
|
||||
|
||||
<Label Text="{Binding Description}" Padding="0,0,0,15"/>
|
||||
<Label Text="{Binding Description}" Padding="0,0,0,15" />
|
||||
</VerticalStackLayout>
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
@@ -133,12 +138,20 @@
|
||||
<Label Grid.Row="1" Grid.Column="2" Text="Zeitausgleich:" Margin="15,0,0,0" />
|
||||
<Label Grid.Row="2" Grid.Column="2" Text="Resturlaub:" Margin="15,0,0,0" />
|
||||
|
||||
<Label Grid.Row="0" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding Nominal, Converter={StaticResource secToTime}}" ToolTipProperties.Text="Sollstunden" />
|
||||
<Label Grid.Row="1" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding ZeitCalculated, Converter={StaticResource secToTime}}" ToolTipProperties.Text="Geleistete Stunden" />
|
||||
<Label Grid.Row="2" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding OvertimeMonth, Converter={StaticResource secToTime}}" />
|
||||
<Label Grid.Row="0" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding Overtime, Converter={StaticResource secToTime}}" />
|
||||
<Label Grid.Row="1" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding Zeitausgleich, Converter={StaticResource secToTime}}" />
|
||||
<Label Grid.Row="2" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0" Text="{Binding Holiday, Converter={StaticResource secToTime}}" />
|
||||
<Label Grid.Row="0" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0"
|
||||
Text="{Binding Nominal, Converter={StaticResource secToTime}}"
|
||||
ToolTipProperties.Text="Sollstunden" />
|
||||
<Label Grid.Row="1" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0"
|
||||
Text="{Binding ZeitCalculated, Converter={StaticResource secToTime}}"
|
||||
ToolTipProperties.Text="Geleistete Stunden" />
|
||||
<Label Grid.Row="2" Grid.Column="1" HorizontalTextAlignment="End" Padding="0,0,5,0"
|
||||
Text="{Binding OvertimeMonth, Converter={StaticResource secToTime}}" />
|
||||
<Label Grid.Row="0" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0"
|
||||
Text="{Binding Overtime, Converter={StaticResource secToTime}}" />
|
||||
<Label Grid.Row="1" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0"
|
||||
Text="{Binding Zeitausgleich, Converter={StaticResource secToTime}}" />
|
||||
<Label Grid.Row="2" Grid.Column="3" HorizontalTextAlignment="End" Padding="0,0,5,0"
|
||||
Text="{Binding Holiday, Converter={StaticResource secToTime}}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
|
||||
@@ -9,28 +9,35 @@ namespace Jugenddienst_Stunden.Views;
|
||||
/// Code-Behind f<>r die Stunden-<2D>bersicht
|
||||
/// </summary>
|
||||
public partial class StundenPage : ContentPage {
|
||||
/// <summary>
|
||||
/// CTOR (f<>r Shell/XAML DataTemplate erforderlich)
|
||||
/// </summary>
|
||||
public StundenPage() : this(
|
||||
(Application.Current?.Handler?.MauiContext?.Services
|
||||
?? throw new InvalidOperationException("DI container ist nicht verf<72>gbar."))
|
||||
.GetRequiredService<StundenViewModel>()) {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CTOR
|
||||
/// CTOR (DI)
|
||||
/// </summary>
|
||||
public StundenPage() {
|
||||
public StundenPage(StundenViewModel vm) {
|
||||
InitializeComponent();
|
||||
BindingContext = vm;
|
||||
|
||||
if (BindingContext is StundenViewModel vm) {
|
||||
vm.AlertEvent += Vm_AlertEvent;
|
||||
vm.InfoEvent += Vm_InfoEvent;
|
||||
}
|
||||
if (!CheckLogin()) {
|
||||
NavigateToTargetPage();
|
||||
}
|
||||
|
||||
// Navigation NICHT im CTOR ausf<73>hren (Shell/Navigation-Stack ist hier oft noch nicht ?ready?)
|
||||
// if (!CheckLogin()) {
|
||||
// NavigateToTargetPage();
|
||||
// }
|
||||
}
|
||||
|
||||
private void Vm_AlertEvent(object? sender, string e) {
|
||||
MainThread.BeginInvokeOnMainThread(async () => {
|
||||
await DisplayAlert("Fehler:", e, "OK");
|
||||
});
|
||||
MainThread.BeginInvokeOnMainThread(async () => { await DisplayAlert("Fehler:", e, "OK"); });
|
||||
}
|
||||
|
||||
//private void Vm_InfoEvent(object? sender, string e) {
|
||||
// DisplayAlert("Information:", e, "OK");
|
||||
//}
|
||||
@@ -52,17 +59,30 @@ public partial class StundenPage : ContentPage {
|
||||
/// <summary>
|
||||
/// Beim Laden der Seite den Titel setzen
|
||||
/// </summary>
|
||||
protected override void OnAppearing() {
|
||||
protected override async void OnAppearing() {
|
||||
base.OnAppearing();
|
||||
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() {
|
||||
return Preferences.Default.Get("apiKey", "") != "";
|
||||
}
|
||||
|
||||
private async void NavigateToTargetPage() {
|
||||
await Navigation.PushAsync(new LoginPage());
|
||||
}
|
||||
// private async void NavigateToTargetPage() {
|
||||
// await Navigation.PushAsync(new LoginPage());
|
||||
// }
|
||||
|
||||
private Task NavigateToTargetPage() {
|
||||
// Shell-Navigation statt Navigation.PushAsync
|
||||
// Voraussetzung: LoginPage-Route ist in AppShell registriert (Routing.RegisterRoute(...))
|
||||
return Shell.Current.GoToAsync(nameof(Views.LoginPage));
|
||||
}
|
||||
}
|
||||