Updated EllieHub to 1.0.2.0

This commit is contained in:
Toastie (DCS Team) 2024-07-02 01:55:23 +12:00
parent fe5c273143
commit 7429e0298a
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
95 changed files with 948 additions and 437 deletions

View file

@ -1,4 +1,3 @@
[*] [*]
charset = utf-8 charset = utf-8
end_of_line = lf end_of_line = lf
@ -10,11 +9,74 @@ indent_size = 4 # A property with the same name was updated with a value 2 in a
[{*.yaml,*.yml}] [{*.yaml,*.yml}]
indent_style = space indent_style = space
indent_size = 2 # A property with the same name was updated with a value 4 in a section [*]; with a value 4 in a section [*.{appxmanifest,asax,ascx,aspx,axaml,build,cg,cginc,compute,cs,cshtml,dtd,hlsl,hlsli,hlslinc,master,nuspec,paml,razor,resw,resx,skin,usf,ush,vb,xaml,xamlx,xoml,xsd}] indent_size = 2 # A property with the same name was updated with a value 4 in a section [*]; with a value 4 in a section [*.{appxmanifest,asax,ascx,aspx,axaml,build,cg,cginc,compute,cs,cshtml,dtd,hlsl,hlsli,hlslinc,master,nuspec,paml,razor,resw,resx,skin,usf,ush,vb,xaml,xamlx,xoml,xsd}]
dotnet_diagnostic.CA1047.severity = error
[*.{appxmanifest,asax,ascx,aspx,axaml,build,cg,cginc,compute,cs,cshtml,dtd,hlsl,hlsli,hlslinc,master,nuspec,paml,razor,resw,resx,skin,usf,ush,vb,xaml,xamlx,xoml,xsd}] [*.{appxmanifest,asax,ascx,aspx,axaml,build,cg,cginc,compute,cs,cshtml,dtd,hlsl,hlsli,hlslinc,master,nuspec,paml,razor,resw,resx,skin,usf,ush,vb,xaml,xamlx,xoml,xsd}]
indent_style = space indent_style = space
indent_size = 4 # A property with the same name was updated with a value 2 in a section [{*.yaml,*.yml}] indent_size = 4 # A property with the same name was updated with a value 2 in a section [{*.yaml,*.yml}]
tab_width = 4 tab_width = 4
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:error
dotnet_style_prefer_auto_properties = true:warning
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_return = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
dotnet_style_namespace_match_folder = true:suggestion
dotnet_style_readonly_field = true:warning
dotnet_style_predefined_type_for_locals_parameters_members = true:warning
dotnet_style_predefined_type_for_member_access = true:warning
dotnet_style_require_accessibility_modifiers = always:error
dotnet_style_allow_multiple_blank_lines_experimental = false:silent
dotnet_style_allow_statement_immediately_after_block_experimental = false:silent
dotnet_code_quality_unused_parameters = all:warning
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_property = false:silent
dotnet_style_qualification_for_method = false:silent
dotnet_style_qualification_for_event = false:silent
dotnet_diagnostic.CA1310.severity = warning
dotnet_diagnostic.CA1509.severity = error
dotnet_diagnostic.CA1000.severity = suggestion
dotnet_diagnostic.CA1001.severity = error
dotnet_diagnostic.CA1005.severity = none
dotnet_diagnostic.CA1008.severity = suggestion
dotnet_diagnostic.CA1010.severity = suggestion
dotnet_diagnostic.CA1031.severity = silent
dotnet_diagnostic.CA1050.severity = error
dotnet_diagnostic.CA1051.severity = error
dotnet_diagnostic.CA1069.severity = suggestion
dotnet_diagnostic.CA1724.severity = error
dotnet_diagnostic.CA1826.severity = warning
dotnet_diagnostic.CA1827.severity = warning
dotnet_diagnostic.CA1828.severity = warning
dotnet_diagnostic.CA1829.severity = warning
dotnet_diagnostic.CA1830.severity = warning
dotnet_diagnostic.CA1832.severity = warning
dotnet_diagnostic.CA1833.severity = warning
dotnet_diagnostic.CA1842.severity = warning
dotnet_diagnostic.CA1843.severity = warning
dotnet_diagnostic.CA1836.severity = none
dotnet_diagnostic.CA1839.severity = warning
dotnet_diagnostic.CA1840.severity = warning
dotnet_diagnostic.CA1846.severity = warning
dotnet_diagnostic.CA1848.severity = suggestion
dotnet_diagnostic.CA1852.severity = suggestion
dotnet_diagnostic.CA2012.severity = warning
dotnet_diagnostic.CA2019.severity = warning
dotnet_diagnostic.CA2211.severity = warning
[*.cs] [*.cs]
@ -79,49 +141,49 @@ dotnet_style_allow_statement_immediately_after_block_experimental = false
#### C# Coding Conventions #### #### C# Coding Conventions ####
# var preferences # var preferences
csharp_style_var_elsewhere = true:error csharp_style_var_elsewhere = true:warning
csharp_style_var_for_built_in_types = true:error csharp_style_var_for_built_in_types = true:warning
csharp_style_var_when_type_is_apparent = true:error csharp_style_var_when_type_is_apparent = true:warning
# Expression-bodied members # Expression-bodied members
csharp_style_expression_bodied_accessors = true:suggestion csharp_style_expression_bodied_accessors = true:suggestion
csharp_style_expression_bodied_constructors = when_on_single_line:suggestion csharp_style_expression_bodied_constructors = when_on_single_line:suggestion
csharp_style_expression_bodied_indexers = true:suggestion csharp_style_expression_bodied_indexers = true:suggestion
csharp_style_expression_bodied_lambdas = true:suggestion csharp_style_expression_bodied_lambdas = when_on_single_line:suggestion
csharp_style_expression_bodied_local_functions = true:suggestion csharp_style_expression_bodied_local_functions = true:suggestion
csharp_style_expression_bodied_methods = when_on_single_line:suggestion csharp_style_expression_bodied_methods = when_on_single_line:suggestion
csharp_style_expression_bodied_operators = when_on_single_line:suggestion csharp_style_expression_bodied_operators = when_on_single_line:suggestion
csharp_style_expression_bodied_properties = true:suggestion csharp_style_expression_bodied_properties = true:suggestion
# Pattern matching preferences # Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true:error csharp_style_pattern_matching_over_as_with_null_check = true:warning
csharp_style_pattern_matching_over_is_with_cast_check = true:error csharp_style_pattern_matching_over_is_with_cast_check = true:warning
csharp_style_prefer_not_pattern = true:error csharp_style_prefer_not_pattern = true:warning
csharp_style_prefer_pattern_matching = true:suggestion csharp_style_prefer_pattern_matching = true:suggestion
csharp_style_prefer_switch_expression = true csharp_style_prefer_switch_expression = true:suggestion
# Null-checking preferences # Null-checking preferences
csharp_style_conditional_delegate_call = true:error csharp_style_conditional_delegate_call = true:warning
# Modifier preferences # Modifier preferences
csharp_prefer_static_local_function = true csharp_prefer_static_local_function = true:suggestion
csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async
# Code-block preferences # Code-block preferences
csharp_prefer_braces = when_multiline:suggestion csharp_prefer_braces = when_multiline:suggestion
csharp_prefer_simple_using_statement = true csharp_prefer_simple_using_statement = true:suggestion
# Expression-level preferences # Expression-level preferences
csharp_prefer_simple_default_expression = true csharp_prefer_simple_default_expression = true:suggestion
csharp_style_deconstructed_variable_declaration = true csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_implicit_object_creation_when_type_is_apparent = true:error csharp_style_implicit_object_creation_when_type_is_apparent = true:warning
csharp_style_inlined_variable_declaration = true:warning csharp_style_inlined_variable_declaration = true:warning
csharp_style_pattern_local_over_anonymous_function = true csharp_style_pattern_local_over_anonymous_function = true
csharp_style_prefer_index_operator = true csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_range_operator = true csharp_style_prefer_range_operator = true:suggestion
csharp_style_throw_expression = true:error csharp_style_throw_expression = true:error
csharp_style_unused_value_assignment_preference = discard_variable:warning csharp_style_unused_value_assignment_preference = discard_variable:warning
csharp_style_unused_value_expression_statement_preference = discard_variable csharp_style_unused_value_expression_statement_preference = discard_variable:silent
# 'using' directive preferences # 'using' directive preferences
csharp_using_directive_placement = outside_namespace:error csharp_using_directive_placement = outside_namespace:error
@ -130,9 +192,9 @@ csharp_using_directive_placement = outside_namespace:error
csharp_style_namespace_declarations = file_scoped:error csharp_style_namespace_declarations = file_scoped:error
# New line preferences # New line preferences
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:silent
csharp_style_allow_embedded_statements_on_same_line_experimental = true csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
#### C# Formatting Rules #### #### C# Formatting Rules ####
@ -189,40 +251,40 @@ csharp_preserve_single_line_statements = true
dotnet_naming_rule.class_should_be_pascal_case.import_to_resharper = as_predefined dotnet_naming_rule.class_should_be_pascal_case.import_to_resharper = as_predefined
dotnet_naming_rule.class_should_be_pascal_case.severity = error dotnet_naming_rule.class_should_be_pascal_case.severity = error
dotnet_naming_rule.class_should_be_pascal_case.symbols = class dotnet_naming_rule.class_should_be_pascal_case.symbols = class
dotnet_naming_rule.class_should_be_pascal_case.style = pascal_case dotnet_naming_rule.class_should_be_pascal_case.style = upper_camel_case_style
dotnet_naming_rule.struct_should_be_pascal_case.import_to_resharper = as_predefined dotnet_naming_rule.struct_should_be_pascal_case.import_to_resharper = as_predefined
dotnet_naming_rule.struct_should_be_pascal_case.severity = error dotnet_naming_rule.struct_should_be_pascal_case.severity = error
dotnet_naming_rule.struct_should_be_pascal_case.symbols = struct dotnet_naming_rule.struct_should_be_pascal_case.symbols = struct
dotnet_naming_rule.struct_should_be_pascal_case.style = pascal_case dotnet_naming_rule.struct_should_be_pascal_case.style = upper_camel_case_style
dotnet_naming_rule.interface_should_be_begins_with_i.import_to_resharper = as_predefined dotnet_naming_rule.interface_should_be_begins_with_i.import_to_resharper = as_predefined
dotnet_naming_rule.interface_should_be_begins_with_i.severity = error dotnet_naming_rule.interface_should_be_begins_with_i.severity = error
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 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.interface_should_be_begins_with_i.style = i_upper_camel_case_style
dotnet_naming_rule.types_should_be_pascal_case.import_to_resharper = as_predefined dotnet_naming_rule.types_should_be_pascal_case.import_to_resharper = as_predefined
dotnet_naming_rule.types_should_be_pascal_case.severity = error dotnet_naming_rule.types_should_be_pascal_case.severity = error
dotnet_naming_rule.types_should_be_pascal_case.symbols = types dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case dotnet_naming_rule.types_should_be_pascal_case.style = upper_camel_case_style
dotnet_naming_rule.enum_should_be_pascal_case.import_to_resharper = as_predefined dotnet_naming_rule.enum_should_be_pascal_case.import_to_resharper = as_predefined
dotnet_naming_rule.enum_should_be_pascal_case.severity = error dotnet_naming_rule.enum_should_be_pascal_case.severity = error
dotnet_naming_rule.enum_should_be_pascal_case.symbols = enum dotnet_naming_rule.enum_should_be_pascal_case.symbols = enum
dotnet_naming_rule.enum_should_be_pascal_case.style = pascal_case dotnet_naming_rule.enum_should_be_pascal_case.style = upper_camel_case_style
dotnet_naming_rule.property_should_be_pascal_case.import_to_resharper = as_predefined dotnet_naming_rule.property_should_be_pascal_case.import_to_resharper = as_predefined
dotnet_naming_rule.property_should_be_pascal_case.severity = error dotnet_naming_rule.property_should_be_pascal_case.severity = error
dotnet_naming_rule.property_should_be_pascal_case.symbols = property dotnet_naming_rule.property_should_be_pascal_case.symbols = property
dotnet_naming_rule.property_should_be_pascal_case.style = pascal_case dotnet_naming_rule.property_should_be_pascal_case.style = upper_camel_case_style
dotnet_naming_rule.method_should_be_pascal_case.import_to_resharper = as_predefined dotnet_naming_rule.method_should_be_pascal_case.import_to_resharper = as_predefined
dotnet_naming_rule.method_should_be_pascal_case.severity = error dotnet_naming_rule.method_should_be_pascal_case.severity = error
dotnet_naming_rule.method_should_be_pascal_case.symbols = method dotnet_naming_rule.method_should_be_pascal_case.symbols = method
dotnet_naming_rule.method_should_be_pascal_case.style = pascal_case dotnet_naming_rule.method_should_be_pascal_case.style = upper_camel_case_style
dotnet_naming_rule.async_method_should_be_ends_with_async.import_to_resharper = as_predefined dotnet_naming_rule.async_method_should_be_ends_with_async.import_to_resharper = as_predefined
dotnet_naming_rule.async_method_should_be_ends_with_async.severity = error dotnet_naming_rule.async_method_should_be_ends_with_async.severity = warning
dotnet_naming_rule.async_method_should_be_ends_with_async.symbols = async_method dotnet_naming_rule.async_method_should_be_ends_with_async.symbols = async_method
dotnet_naming_rule.async_method_should_be_ends_with_async.style = ends_with_async dotnet_naming_rule.async_method_should_be_ends_with_async.style = ends_with_async
@ -234,17 +296,17 @@ dotnet_naming_rule.private_field_should_be_begins_with_underscore.style = begins
dotnet_naming_rule.non_field_members_should_be_pascal_case.import_to_resharper = as_predefined dotnet_naming_rule.non_field_members_should_be_pascal_case.import_to_resharper = as_predefined
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = error dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = error
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 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 dotnet_naming_rule.non_field_members_should_be_pascal_case.style = upper_camel_case_style
dotnet_naming_rule.local_variable_should_be_camel_case.import_to_resharper = as_predefined dotnet_naming_rule.local_variable_should_be_camel_case.import_to_resharper = as_predefined
dotnet_naming_rule.local_variable_should_be_camel_case.severity = error dotnet_naming_rule.local_variable_should_be_camel_case.severity = error
dotnet_naming_rule.local_variable_should_be_camel_case.symbols = local_variable dotnet_naming_rule.local_variable_should_be_camel_case.symbols = local_variable
dotnet_naming_rule.local_variable_should_be_camel_case.style = camel_case dotnet_naming_rule.local_variable_should_be_camel_case.style = lower_camel_case_style_1
dotnet_naming_rule.public_anything_should_be_pascal_case.import_to_resharper = as_predefined dotnet_naming_rule.public_anything_should_be_pascal_case.import_to_resharper = as_predefined
dotnet_naming_rule.public_anything_should_be_pascal_case.severity = error dotnet_naming_rule.public_anything_should_be_pascal_case.severity = error
dotnet_naming_rule.public_anything_should_be_pascal_case.symbols = public_anything dotnet_naming_rule.public_anything_should_be_pascal_case.symbols = public_anything
dotnet_naming_rule.public_anything_should_be_pascal_case.style = pascal_case dotnet_naming_rule.public_anything_should_be_pascal_case.style = upper_camel_case_style
dotnet_naming_rule.constants_rule.import_to_resharper = as_predefined dotnet_naming_rule.constants_rule.import_to_resharper = as_predefined
dotnet_naming_rule.constants_rule.severity = error dotnet_naming_rule.constants_rule.severity = error
@ -288,7 +350,7 @@ dotnet_naming_rule.parameters_rule.symbols = parameters_symbols
dotnet_naming_rule.private_constants_rule.import_to_resharper = as_predefined dotnet_naming_rule.private_constants_rule.import_to_resharper = as_predefined
dotnet_naming_rule.private_constants_rule.severity = error dotnet_naming_rule.private_constants_rule.severity = error
dotnet_naming_rule.private_constants_rule.style = lower_camel_case_style dotnet_naming_rule.private_constants_rule.style = begins_with_underscore
dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols
dotnet_naming_rule.private_instance_fields_rule.import_to_resharper = as_predefined dotnet_naming_rule.private_instance_fields_rule.import_to_resharper = as_predefined
@ -298,12 +360,12 @@ dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_field
dotnet_naming_rule.private_static_fields_rule.import_to_resharper = as_predefined dotnet_naming_rule.private_static_fields_rule.import_to_resharper = as_predefined
dotnet_naming_rule.private_static_fields_rule.severity = error dotnet_naming_rule.private_static_fields_rule.severity = error
dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style dotnet_naming_rule.private_static_fields_rule.style = begins_with_underscore
dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols
dotnet_naming_rule.private_static_readonly_rule.import_to_resharper = as_predefined dotnet_naming_rule.private_static_readonly_rule.import_to_resharper = as_predefined
dotnet_naming_rule.private_static_readonly_rule.severity = error dotnet_naming_rule.private_static_readonly_rule.severity = error
dotnet_naming_rule.private_static_readonly_rule.style = lower_camel_case_style dotnet_naming_rule.private_static_readonly_rule.style = begins_with_underscore
dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols
dotnet_naming_rule.property_rule.import_to_resharper = as_predefined dotnet_naming_rule.property_rule.import_to_resharper = as_predefined
@ -318,7 +380,7 @@ dotnet_naming_rule.public_fields_rule.symbols = public_fields_symbols
dotnet_naming_rule.public_fields_rule.import_to_resharper = as_predefined dotnet_naming_rule.public_fields_rule.import_to_resharper = as_predefined
dotnet_naming_rule.public_fields_rule.severity = error dotnet_naming_rule.public_fields_rule.severity = error
dotnet_naming_rule.public_fields_rule.style = camel_case dotnet_naming_rule.public_fields_rule.style = lower_camel_case_style_1
dotnet_naming_rule.public_fields_rule.symbols = protected_fields_symbols dotnet_naming_rule.public_fields_rule.symbols = protected_fields_symbols
dotnet_naming_rule.static_readonly_rule.import_to_resharper = as_predefined dotnet_naming_rule.static_readonly_rule.import_to_resharper = as_predefined
@ -482,23 +544,92 @@ dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities = *
dotnet_naming_symbols.type_parameters_symbols.applicable_kinds = type_parameter dotnet_naming_symbols.type_parameters_symbols.applicable_kinds = type_parameter
#### Visual Studio Specific Rules #### #### Style Rules ####
# CA1822: Mark members as static csharp_style_prefer_method_group_conversion = true:silent
dotnet_diagnostic.CA1822.severity = none csharp_style_prefer_top_level_statements = false:suggestion
csharp_style_prefer_primary_constructors = false:warning
csharp_style_prefer_null_check_over_type_check = true:suggestion
csharp_style_prefer_local_over_anonymous_function = true:suggestion
csharp_style_prefer_tuple_swap = true:suggestion
csharp_style_prefer_utf8_string_literals = true:suggestion
csharp_style_prefer_readonly_struct = true:suggestion
csharp_style_prefer_readonly_struct_member = true:suggestion
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
csharp_style_prefer_extended_property_pattern = true:warning
dotnet_diagnostic.CA1311.severity = suggestion
dotnet_diagnostic.CA1507.severity = warning
dotnet_diagnostic.IDE0047.severity = none
dotnet_diagnostic.IDE0052.severity = warning
dotnet_diagnostic.IDE0051.severity = warning
dotnet_diagnostic.IDE0076.severity = error
dotnet_diagnostic.IDE0077.severity = error
dotnet_diagnostic.IDE0043.severity = warning
dotnet_diagnostic.CA1070.severity = error
dotnet_diagnostic.CA1200.severity = suggestion
dotnet_diagnostic.CA1805.severity = suggestion
dotnet_diagnostic.CA1825.severity = warning
dotnet_diagnostic.IDE0009.severity = suggestion
dotnet_diagnostic.IDE0016.severity = suggestion
dotnet_diagnostic.IDE0017.severity = suggestion
dotnet_diagnostic.IDE0018.severity = suggestion
dotnet_diagnostic.IDE0019.severity = suggestion
dotnet_diagnostic.IDE0020.severity = suggestion
dotnet_diagnostic.IDE0028.severity = suggestion
dotnet_diagnostic.IDE0029.severity = suggestion
dotnet_diagnostic.IDE0030.severity = suggestion
dotnet_diagnostic.IDE0031.severity = suggestion
dotnet_diagnostic.IDE0032.severity = suggestion
dotnet_diagnostic.IDE0034.severity = suggestion
dotnet_diagnostic.IDE0039.severity = suggestion
dotnet_diagnostic.IDE0040.severity = warning
dotnet_diagnostic.IDE0041.severity = suggestion
dotnet_diagnostic.IDE0044.severity = suggestion
dotnet_diagnostic.IDE0045.severity = suggestion
dotnet_diagnostic.IDE0046.severity = suggestion
dotnet_diagnostic.IDE0048.severity = suggestion
dotnet_diagnostic.IDE0053.severity = suggestion
dotnet_diagnostic.IDE0059.severity = warning
dotnet_diagnostic.IDE0060.severity = warning
dotnet_diagnostic.IDE0062.severity = suggestion
dotnet_diagnostic.IDE0063.severity = suggestion
dotnet_diagnostic.IDE0071.severity = suggestion
dotnet_diagnostic.IDE0082.severity = warning
dotnet_diagnostic.IDE0080.severity = suggestion
dotnet_diagnostic.IDE0130.severity = warning
dotnet_diagnostic.IDE0120.severity = suggestion
dotnet_diagnostic.IDE0100.severity = warning
dotnet_diagnostic.IDE0150.severity = suggestion
dotnet_diagnostic.IDE0161.severity = warning
dotnet_diagnostic.IDE0170.severity = suggestion
dotnet_diagnostic.IDE0180.severity = suggestion
dotnet_diagnostic.IDE0200.severity = suggestion
dotnet_diagnostic.IDE0211.severity = suggestion
dotnet_diagnostic.IDE0230.severity = suggestion
dotnet_diagnostic.IDE0240.severity = suggestion
dotnet_diagnostic.IDE0241.severity = suggestion
dotnet_diagnostic.IDE0251.severity = suggestion
dotnet_diagnostic.IDE0250.severity = suggestion
dotnet_diagnostic.IDE0260.severity = suggestion
dotnet_diagnostic.IDE0270.severity = suggestion
dotnet_diagnostic.IDE0280.severity = warning
dotnet_diagnostic.IDE1005.severity = suggestion
dotnet_diagnostic.IDE1006.severity = warning
# IDE0004: Cast is redundant # IDE0004: Cast is redundant
dotnet_diagnostic.IDE0004.severity = error dotnet_diagnostic.IDE0004.severity = error
# IDE0058: Expression value is never used
dotnet_diagnostic.IDE0058.severity = none
# IDE0011: Add braces to 'if'/'else' statement # IDE0011: Add braces to 'if'/'else' statement
dotnet_diagnostic.IDE0011.severity = none dotnet_diagnostic.IDE0011.severity = none
# IDE0290: Use primary constructor # IDE0058: Expression value is never used
dotnet_diagnostic.IDE0058.severity = none
# Use primary constructor
dotnet_diagnostic.IDE0290.severity = none dotnet_diagnostic.IDE0290.severity = none
#### ReSharper Properties #### #### ReSharper Properties ####
resharper_accessor_owner_body = expression_body resharper_accessor_owner_body = expression_body

View file

@ -3,7 +3,7 @@
xmlns:local="using:EllieHub" xmlns:local="using:EllieHub"
RequestedThemeVariant="Default" RequestedThemeVariant="Default"
x:Class="EllieHub.App"> x:Class="EllieHub.App">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. --> <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.DataTemplates> <Application.DataTemplates>
<local:ViewLocator/> <local:ViewLocator/>
@ -14,9 +14,9 @@
<Application.Resources> <Application.Resources>
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="/Resources/Colors.axaml" /> <ResourceInclude Source="/Avalonia/Resources/Colors.axaml" />
<ResourceInclude Source="/Resources/Fonts.axaml" /> <ResourceInclude Source="/Avalonia/Resources/Fonts.axaml" />
<ResourceInclude Source="/Resources/Images.axaml" /> <ResourceInclude Source="/Avalonia/Resources/Images.axaml" />
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>
@ -24,27 +24,27 @@
<!--Styles--> <!--Styles-->
<!--Think of them like CSS classes--> <!--Think of them like CSS classes-->
<Application.Styles> <Application.Styles>
<StyleInclude Source="/Styles/EllieStyles.axaml" /> <StyleInclude Source="/Avalonia/Styles/EllieStyles.axaml" />
<!--Generated Here: https://theme.xaml.live/--> <!--Generated Here: https://theme.xaml.live/-->
<FluentTheme> <FluentTheme>
<FluentTheme.Palettes> <FluentTheme.Palettes>
<ColorPaletteResources x:Key="Light" Accent="#ff0073cf" AltHigh="White" AltLow="White" AltMedium="White" AltMediumHigh="White" AltMediumLow="White" BaseHigh="Black" BaseLow="#6fcccccc" BaseMedium="#c5898989" BaseMediumHigh="#ff5d5d5d" BaseMediumLow="#e2737373" ChromeAltLow="#ff5d5d5d" ChromeBlackHigh="Black" ChromeBlackLow="#6fcccccc" ChromeBlackMedium="#ff5d5d5d" ChromeBlackMediumLow="#c5898989" ChromeDisabledHigh="#6fcccccc" ChromeDisabledLow="#c5898989" ChromeGray="#e2737373" ChromeHigh="#6fcccccc" ChromeLow="#ffececec" ChromeMedium="#e2e6e6e6" ChromeMediumLow="#ffececec" ChromeWhite="White" ListLow="#e2e6e6e6" ListMedium="#6fcccccc" RegionColor="White" /> <ColorPaletteResources x:Key="Light" Accent="#ff0073cf" AltHigh="White" AltLow="White" AltMedium="White" AltMediumHigh="White" AltMediumLow="White" BaseHigh="Black" BaseLow="#6fcccccc" BaseMedium="#c5898989" BaseMediumHigh="#ff5d5d5d" BaseMediumLow="#e2737373" ChromeAltLow="#ff5d5d5d" ChromeBlackHigh="Black" ChromeBlackLow="#6fcccccc" ChromeBlackMedium="#ff5d5d5d" ChromeBlackMediumLow="#c5898989" ChromeDisabledHigh="#6fcccccc" ChromeDisabledLow="#c5898989" ChromeGray="#e2737373" ChromeHigh="#6fcccccc" ChromeLow="#ffececec" ChromeMedium="#e2e6e6e6" ChromeMediumLow="#ffececec" ChromeWhite="White" ListLow="#e2e6e6e6" ListMedium="#6fcccccc" RegionColor="White" />
<ColorPaletteResources x:Key="Dark" Accent="#ff0073cf" AltHigh="Black" AltLow="Black" AltMedium="Black" AltMediumHigh="Black" AltMediumLow="Black" BaseHigh="White" BaseLow="#ff333333" BaseMedium="#ff9a9a9a" BaseMediumHigh="#ffb4b4b4" BaseMediumLow="#ff676767" ChromeAltLow="#ffb4b4b4" ChromeBlackHigh="Black" ChromeBlackLow="#ffb4b4b4" ChromeBlackMedium="Black" ChromeBlackMediumLow="Black" ChromeDisabledHigh="#ff333333" ChromeDisabledLow="#ff9a9a9a" ChromeGray="Gray" ChromeHigh="Gray" ChromeLow="#ff151515" ChromeMedium="#ff1d1d1d" ChromeMediumLow="#ff2c2c2c" ChromeWhite="White" ListLow="#ff1d1d1d" ListMedium="#ff333333" RegionColor="#ff181818" /> <ColorPaletteResources x:Key="Dark" Accent="#ff0073cf" AltHigh="Black" AltLow="Black" AltMedium="Black" AltMediumHigh="Black" AltMediumLow="Black" BaseHigh="White" BaseLow="#ff333333" BaseMedium="#ff9a9a9a" BaseMediumHigh="#ffb4b4b4" BaseMediumLow="#ff676767" ChromeAltLow="#ffb4b4b4" ChromeBlackHigh="Black" ChromeBlackLow="#ffb4b4b4" ChromeBlackMedium="Black" ChromeBlackMediumLow="Black" ChromeDisabledHigh="#ff333333" ChromeDisabledLow="#ff9a9a9a" ChromeGray="Gray" ChromeHigh="Gray" ChromeLow="#ff151515" ChromeMedium="#ff1d1d1d" ChromeMediumLow="#ff2c2c2c" ChromeWhite="White" ListLow="#ff1d1d1d" ListMedium="#ff333333" RegionColor="#ff181818" />
</FluentTheme.Palettes> </FluentTheme.Palettes>
</FluentTheme> </FluentTheme>
</Application.Styles> </Application.Styles>
<TrayIcon.Icons> <TrayIcon.Icons>
<TrayIcons> <TrayIcons>
<TrayIcon Clicked="TrayDoubleClick" ToolTipText="Ellie Updater" Icon="{DynamicResource EllieHubIcon}"> <TrayIcon Clicked="TrayDoubleClick" ToolTipText="Ellie Updater" Icon="{DynamicResource EllieHubIcon}">
<TrayIcon.Menu> <TrayIcon.Menu>
<NativeMenu> <NativeMenu>
<NativeMenuItem Header="Open" Click="ShowApp" /> <NativeMenuItem Header="Open" Click="ShowApp" />
<NativeMenuItemSeparator /> <NativeMenuItemSeparator />
<NativeMenuItem Header="Close" Click="CloseApp" /> <NativeMenuItem Header="Close" Click="CloseApp" />
</NativeMenu> </NativeMenu>
</TrayIcon.Menu> </TrayIcon.Menu>
</TrayIcon> </TrayIcon>
</TrayIcons> </TrayIcons>
</TrayIcon.Icons> </TrayIcon.Icons>
</Application> </Application>

View file

@ -3,7 +3,7 @@ using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using EllieHub.Views.Windows; using EllieHub.Features.AppWindow.Views.Windows;
using System.Reflection; using System.Reflection;
namespace EllieHub; namespace EllieHub;

View file

@ -1,7 +1,7 @@
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
namespace EllieHub.DesignData.Common; namespace EllieHub.Avalonia.DesignData.Common;
/// <summary> /// <summary>
/// Defines objects useful at design-time. /// Defines objects useful at design-time.

View file

@ -1,11 +1,14 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using EllieHub.DesignData.Common; using EllieHub.Avalonia.DesignData.Common;
using EllieHub.Services.Abstractions; using EllieHub.Features.AppConfig.Services.Mocks;
using EllieHub.Services.Mocks; using EllieHub.Features.AppWindow.ViewModels;
using EllieHub.ViewModels.Controls; using EllieHub.Features.AppWindow.Views.Windows;
using EllieHub.Views.Windows; using EllieHub.Features.BotConfig.Services.Abstractions;
using EllieHub.Features.BotConfig.Services.Mocks;
using EllieHub.Features.BotConfig.ViewModels;
using EllieHub.Features.Shared.ViewModels;
namespace EllieHub.DesignData.Controls; namespace EllieHub.Avalonia.DesignData.Controls;
/// <summary> /// <summary>
/// Mock view-model for <see cref="BotConfigViewModel"/>. /// Mock view-model for <see cref="BotConfigViewModel"/>.

View file

@ -1,12 +1,12 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using EllieHub.DesignData.Common; using EllieHub.Avalonia.DesignData.Common;
using EllieHub.Services.Abstractions; using EllieHub.Features.AppConfig.Services.Abstractions;
using EllieHub.Services.Mocks; using EllieHub.Features.AppConfig.Services.Mocks;
using EllieHub.ViewModels.Controls; using EllieHub.Features.AppConfig.ViewModels;
using EllieHub.ViewModels.Windows; using EllieHub.Features.AppWindow.Views.Windows;
using EllieHub.Views.Windows; using EllieHub.Features.Shared.ViewModels;
namespace EllieHub.DesignData.Controls; namespace EllieHub.Avalonia.DesignData.Controls;
/// <summary> /// <summary>
/// Mock view-model for <see cref="ConfigViewModel"/>. /// Mock view-model for <see cref="ConfigViewModel"/>.

View file

@ -1,9 +1,9 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using EllieHub.DesignData.Common; using EllieHub.Avalonia.DesignData.Common;
using EllieHub.ViewModels.Controls; using EllieHub.Features.AppWindow.Views.Windows;
using EllieHub.Views.Windows; using EllieHub.Features.Shared.ViewModels;
namespace EllieHub.DesignData.Controls; namespace EllieHub.Avalonia.DesignData.Controls;
/// <summary> /// <summary>
/// Mock view-model for <see cref="DependencyButtonViewModel"/>. /// Mock view-model for <see cref="DependencyButtonViewModel"/>.

View file

@ -1,6 +1,6 @@
using EllieHub.ViewModels.Controls; using EllieHub.Features.BotConfig.ViewModels;
namespace EllieHub.DesignData.Controls; namespace EllieHub.Avalonia.DesignData.Controls;
/// <summary> /// <summary>
/// Mock view-model for <see cref="FakeConsoleViewModel"/>. /// Mock view-model for <see cref="FakeConsoleViewModel"/>.

View file

@ -1,6 +1,6 @@
using EllieHub.ViewModels.Controls; using EllieHub.Features.Home.ViewModels;
namespace EllieHub.DesignData.Controls; namespace EllieHub.Avalonia.DesignData.Controls;
/// <summary> /// <summary>
/// Mock view-model for <see cref="HomeViewModel"/>. /// Mock view-model for <see cref="HomeViewModel"/>.

View file

@ -1,9 +1,9 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using EllieHub.DesignData.Common; using EllieHub.Avalonia.DesignData.Common;
using EllieHub.Services.Mocks; using EllieHub.Features.AppConfig.Services.Mocks;
using EllieHub.ViewModels.Controls; using EllieHub.Features.AppWindow.ViewModels;
namespace EllieHub.DesignData.Controls; namespace EllieHub.Avalonia.DesignData.Controls;
/// <summary> /// <summary>
/// Mock view-model for <see cref="LateralBarViewModel"/>. /// Mock view-model for <see cref="LateralBarViewModel"/>.

View file

@ -1,9 +1,9 @@
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using EllieHub.DesignData.Common; using EllieHub.Avalonia.DesignData.Common;
using EllieHub.ViewModels.Controls; using EllieHub.Features.Shared.ViewModels;
namespace EllieHub.DesignData.Controls; namespace EllieHub.Avalonia.DesignData.Controls;
/// <summary> /// <summary>
/// Mock view-model for <see cref="UriInputBarViewModel"/>. /// Mock view-model for <see cref="UriInputBarViewModel"/>.

View file

@ -1,6 +1,6 @@
using EllieHub.ViewModels.Windows; using EllieHub.Features.AppConfig.ViewModels;
namespace EllieHub.DesignData.Windows; namespace EllieHub.Avalonia.DesignData.Windows;
/// <summary> /// <summary>
/// Mock view-model for <see cref="AboutMeViewModel"/>. /// Mock view-model for <see cref="AboutMeViewModel"/>.

View file

@ -1,9 +1,9 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using EllieHub.DesignData.Common; using EllieHub.Avalonia.DesignData.Common;
using EllieHub.ViewModels.Controls; using EllieHub.Features.AppWindow.ViewModels;
using EllieHub.ViewModels.Windows; using EllieHub.Features.Home.ViewModels;
namespace EllieHub.DesignData.Windows; namespace EllieHub.Avalonia.DesignData.Windows;
/// <summary> /// <summary>
/// Mock view-model for <see cref="AppViewModel"/>. /// Mock view-model for <see cref="AppViewModel"/>.

View file

@ -1,6 +1,6 @@
using EllieHub.ViewModels.Windows; using EllieHub.Features.Home.ViewModels;
namespace EllieHub.DesignData.Windows; namespace EllieHub.Avalonia.DesignData.Windows;
/// <summary> /// <summary>
/// Mock view-model for <see cref="UpdateViewModel"/>. /// Mock view-model for <see cref="UpdateViewModel"/>.

View file

@ -63,7 +63,7 @@
<Style Selector="Button.accent"> <Style Selector="Button.accent">
<Setter Property="FontSize" Value="28" /> <Setter Property="FontSize" Value="28" />
<Setter Property="FontWeight" Value="Bold" /> <Setter Property="FontWeight" Value="Bold" />
<Setter Property="Padding" Value="12 2 12 2" /> <Setter Property="Padding" Value="13 2 13 2" />
<Setter Property="HorizontalContentAlignment" Value="Center" /> <Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" /> <Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontFamily" Value="{StaticResource NotoSansBoldFont}" /> <Setter Property="FontFamily" Value="{StaticResource NotoSansBoldFont}" />

View file

@ -14,4 +14,9 @@ public static class AppConstants
/// The name for an <see cref="HttpClient"/> that does not automatically follow redirect responses. /// The name for an <see cref="HttpClient"/> that does not automatically follow redirect responses.
/// </summary> /// </summary>
public const string NoRedirectClient = "NoRedirect"; public const string NoRedirectClient = "NoRedirect";
/// <summary>
/// The name for an <see cref="HttpClient"/> that makes calls to the Toastielab API.
/// </summary>
public const string ToastielabClient = "NoRedirect";
} }

View file

@ -1,6 +1,7 @@
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using System.Reflection;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace EllieHub.Common; namespace EllieHub.Common;
@ -62,6 +63,12 @@ public static partial class AppStatics
} }
}; };
/// <summary>
/// The version of this application.
/// </summary>
public static string AppVersion { get; } = Assembly.GetExecutingAssembly().GetName().Version?.ToString()
?? throw new InvalidOperationException("Version is missing from application assembly.");
/// <summary> /// <summary>
/// Matches the version of Ffmpeg from its CLI output. /// Matches the version of Ffmpeg from its CLI output.
/// </summary> /// </summary>

View file

@ -26,7 +26,7 @@ internal static class Utilities
/// <exception cref="FileNotFoundException">Occurs when the embeded resource does not exist.</exception> /// <exception cref="FileNotFoundException">Occurs when the embeded resource does not exist.</exception>
public static SKBitmap LoadEmbededImage(string? uri = default) public static SKBitmap LoadEmbededImage(string? uri = default)
{ {
return (string.IsNullOrWhiteSpace(uri) || !uri.StartsWith("avares://")) return (string.IsNullOrWhiteSpace(uri) || !uri.StartsWith("avares://", StringComparison.Ordinal))
? SKBitmap.Decode(AssetLoader.Open(new Uri(AppConstants.BotAvatarUri))) ? SKBitmap.Decode(AssetLoader.Open(new Uri(AppConstants.BotAvatarUri)))
: SKBitmap.Decode(AssetLoader.Open(new Uri(uri))); : SKBitmap.Decode(AssetLoader.Open(new Uri(uri)));
} }

View file

@ -8,12 +8,12 @@ public static class WindowConstants
/// <summary> /// <summary>
/// Defines the default width of the window. /// Defines the default width of the window.
/// </summary> /// </summary>
public const string DefaultWindowWidth = "885"; public const string DefaultWindowWidth = "900";
/// <summary> /// <summary>
/// Defines the default height of the window. /// Defines the default height of the window.
/// </summary> /// </summary>
public const string DefaultWindowHeight = "570"; public const string DefaultWindowHeight = "575";
/// <summary> /// <summary>
/// Defines the minimum height of the window. /// Defines the minimum height of the window.
@ -33,5 +33,5 @@ public static class WindowConstants
/// <summary> /// <summary>
/// Defines the message that should be shown when a view's parameterless constructor should not be used. /// Defines the message that should be shown when a view's parameterless constructor should not be used.
/// </summary> /// </summary>
public const string DesignerCtorWarning = "This constructor exists to satisfy Avalonia's designer. Please, use the parameterized constructor instead."; public const string DesignerCtorWarning = "This constructor exists to satisfy Avalonia's previewer. Please, use the parameterized constructor instead.";
} }

View file

@ -20,7 +20,7 @@
<DebugType>embedded</DebugType> <DebugType>embedded</DebugType>
<!--Version--> <!--Version-->
<VersionPrefix>1.0.1.0</VersionPrefix> <VersionPrefix>1.0.2.0</VersionPrefix>
<!--Avalonia Settings--> <!--Avalonia Settings-->
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
@ -39,19 +39,20 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.4" /> <PackageReference Include="Avalonia" Version="11.0.10" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.4" /> <PackageReference Include="Avalonia.Desktop" Version="11.0.10" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.4" /> <PackageReference Include="Avalonia.ReactiveUI" Version="11.0.10" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.4" /> <PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.10" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.4" /> <PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.10" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.4" /> <PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.10" />
<PackageReference Include="Toastie.Events" Version="2.1.0" /> <PackageReference Include="Toastie.Events" Version="2.2.1" />
<PackageReference Include="MessageBox.Avalonia" Version="3.1.4" /> <PackageReference Include="MessageBox.Avalonia" Version="3.1.5.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.--> <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.4" /> <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.10" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="SingleFileExtractor.Core" Version="2.2.0" />
<PackageReference Include="SkiaImageView.Avalonia11" Version="1.5.0" /> <PackageReference Include="SkiaImageView.Avalonia11" Version="1.5.0" />
</ItemGroup> </ItemGroup>

View file

@ -1,10 +1,16 @@
using Avalonia.Controls; using Avalonia.Controls;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using EllieHub.Models.Config; using EllieHub.Features.AppConfig.Models;
using EllieHub.Features.AppConfig.Services;
using EllieHub.Features.AppConfig.Services.Abstractions;
using EllieHub.Features.AppConfig.Services.Mocks;
using EllieHub.Features.AppWindow.Views.Windows;
using EllieHub.Features.BotConfig.Services;
using EllieHub.Features.BotConfig.Services.Abstractions;
using EllieHub.Features.BotConfig.Services.Mocks;
using EllieHub.Features.Home.Services;
using EllieHub.Features.Home.Services.Abstractions;
using EllieHub.Services; using EllieHub.Services;
using EllieHub.Services.Abstractions;
using EllieHub.Services.Mocks;
using EllieHub.Views.Windows;
using ReactiveUI; using ReactiveUI;
using System.Reflection; using System.Reflection;
using System.Text.Json; using System.Text.Json;
@ -61,14 +67,25 @@ public static class IServiceCollectionExt
// Web requests // Web requests
serviceCollection.AddHttpClient(); serviceCollection.AddHttpClient();
serviceCollection.AddHttpClient(AppConstants.NoRedirectClient) // Client that doesn't allow automatic reditections serviceCollection.AddHttpClient(AppConstants.NoRedirectClient) // Client that doesn't allow automatic reditections
.ConfigureHttpMessageHandlerBuilder(builder => builder.PrimaryHandler = new HttpClientHandler() { AllowAutoRedirect = false }); .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { AllowAutoRedirect = false });
serviceCollection.AddHttpClient(AppConstants.ToastielabClient)
.ConfigureHttpClient(x =>
{
x.DefaultRequestHeaders.Add("Accept", "application/vnd.toastielab+json");
x.DefaultRequestHeaders.Add("X-Toastielab-Api-Version", "2022-11-28");
#if DEBUG
x.DefaultRequestHeaders.UserAgent.TryParseAdd($"EllieHub v{AppStatics.AppVersion}-Debug");
#else
x.DefaultRequestHeaders.UserAgent.TryParseAdd($"EllieHub v{AppStatics.AppVersion}");
#endif
});
// App settings // App settings
serviceCollection.AddSingleton<IAppConfigManager, AppConfigManager>(); serviceCollection.AddSingleton<IAppConfigManager, AppConfigManager>();
serviceCollection.AddSingleton<ReadOnlyAppConfig>(); serviceCollection.AddSingleton<ReadOnlyAppSettings>();
serviceCollection.AddSingleton(_ => serviceCollection.AddSingleton(_ =>
(File.Exists(AppStatics.AppConfigUri)) (File.Exists(AppStatics.AppConfigUri))
? JsonSerializer.Deserialize<AppConfig>(File.ReadAllText(AppStatics.AppConfigUri)) ?? new() ? JsonSerializer.Deserialize<AppSettings>(File.ReadAllText(AppStatics.AppConfigUri)) ?? new()
: new() : new()
); );

View file

@ -1,6 +1,6 @@
using ReactiveUI; using ReactiveUI;
namespace EllieHub.ViewModels.Abstractions; namespace EllieHub.Features.Abstractions;
/// <summary> /// <summary>
/// The base view-model. /// The base view-model.

View file

@ -1,6 +1,6 @@
using ReactiveUI; using ReactiveUI;
namespace EllieHub.ViewModels.Abstractions; namespace EllieHub.Features.Abstractions;
/// <summary> /// <summary>
/// The base view-model. /// The base view-model.

View file

@ -1,6 +1,6 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace EllieHub.Models.Api; namespace EllieHub.Features.AppConfig.Models.Api.Evermeet;
/// <summary> /// <summary>
/// Represents download information for a <see cref="EvermeetInfo"/> component. /// Represents download information for a <see cref="EvermeetInfo"/> component.

View file

@ -1,6 +1,6 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace EllieHub.Models.Api; namespace EllieHub.Features.AppConfig.Models.Api.Evermeet;
/// <summary> /// <summary>
/// Represents a response from the "https://evermeet.cx/ffmpeg/info" endpoint. /// Represents a response from the "https://evermeet.cx/ffmpeg/info" endpoint.

View file

@ -1,13 +1,14 @@
using EllieHub.Enums; using EllieHub.Enums;
using EllieHub.Features.AppWindow.Models;
using System.Collections.Concurrent; using System.Collections.Concurrent;
namespace EllieHub.Models.Config; namespace EllieHub.Features.AppConfig.Models;
/// <summary> /// <summary>
/// Represents the settings of the application. /// Represents the settings of the application.
/// </summary> /// </summary>
/// <remarks>Prefer using <see cref="ReadOnlyAppConfig"/> in dependency injection, if possible.</remarks> /// <remarks>Prefer using <see cref="ReadOnlyAppSettings"/> in dependency injection, if possible.</remarks>
public sealed class AppConfig public sealed class AppSettings
{ {
/// <summary> /// <summary>
/// The absolute path to the directory where the bot instances are stored. /// The absolute path to the directory where the bot instances are stored.

View file

@ -1,13 +1,14 @@
using EllieHub.Enums; using EllieHub.Enums;
using EllieHub.Features.AppWindow.Models;
namespace EllieHub.Models.Config; namespace EllieHub.Features.AppConfig.Models;
/// <summary> /// <summary>
/// Represents a read-only version of <see cref="AppConfig"/>. /// Represents a read-only version of <see cref="AppSettings"/>.
/// </summary> /// </summary>
public sealed class ReadOnlyAppConfig public sealed class ReadOnlyAppSettings
{ {
private readonly AppConfig _appConfig; private readonly AppSettings _appConfig;
/// <summary> /// <summary>
/// The absolute path to the directory where the bot instances are stored. /// The absolute path to the directory where the bot instances are stored.
@ -64,9 +65,9 @@ public sealed class ReadOnlyAppConfig
=> _appConfig.BotEntries; => _appConfig.BotEntries;
/// <summary> /// <summary>
/// Initializes a read-only version of <see cref="AppConfig"/>. /// Initializes a read-only version of <see cref="AppSettings"/>.
/// </summary> /// </summary>
/// <param name="appConfig">The application settings to read from.</param> /// <param name="appConfig">The application settings to read from.</param>
public ReadOnlyAppConfig(AppConfig appConfig) public ReadOnlyAppSettings(AppSettings appConfig)
=> _appConfig = appConfig; => _appConfig = appConfig;
} }

View file

@ -1,4 +1,4 @@
namespace EllieHub.Models; namespace EllieHub.Features.AppConfig.Models;
/// <summary> /// <summary>
/// Represents the dimensions of the application's window. /// Represents the dimensions of the application's window.

View file

@ -1,11 +1,11 @@
namespace EllieHub.Services.Abstractions; namespace EllieHub.Features.AppConfig.Services.Abstractions;
/// <summary> /// <summary>
/// Base class for a service that checks, downloads, installs, and updates ffmpeg. /// Base class for a service that checks, downloads, installs, and updates ffmpeg.
/// </summary> /// </summary>
public abstract class FfmpegResolver : IFfmpegResolver public abstract class FfmpegResolver : IFfmpegResolver
{ {
private readonly string _programVerifier = (Environment.OSVersion.Platform is PlatformID.Win32NT) ? "where" : "which"; private readonly string _programVerifier = Environment.OSVersion.Platform is PlatformID.Win32NT ? "where" : "which";
/// <summary> /// <summary>
/// The name of the Ffmpeg process. /// The name of the Ffmpeg process.
@ -13,7 +13,7 @@ public abstract class FfmpegResolver : IFfmpegResolver
protected const string FfmpegProcessName = "ffmpeg"; protected const string FfmpegProcessName = "ffmpeg";
/// <inheritdoc/> /// <inheritdoc/>
public string DependencyName { get; } = "FFMPEG"; public string DependencyName { get; } = "Ffmpeg";
/// <inheritdoc/> /// <inheritdoc/>
public abstract string FileName { get; } public abstract string FileName { get; }

View file

@ -1,7 +1,7 @@
using EllieHub.Models; using EllieHub.Features.AppConfig.Models;
using EllieHub.Models.Config; using EllieHub.Features.AppWindow.Models;
namespace EllieHub.Services.Abstractions; namespace EllieHub.Features.AppConfig.Services.Abstractions;
/// <summary> /// <summary>
/// Represents a service that manages the application's settings. /// Represents a service that manages the application's settings.
@ -11,7 +11,7 @@ public interface IAppConfigManager
/// <summary> /// <summary>
/// The application settings. /// The application settings.
/// </summary> /// </summary>
ReadOnlyAppConfig AppConfig { get; } ReadOnlyAppSettings AppConfig { get; }
/// <summary> /// <summary>
/// Creates a bot entry. /// Creates a bot entry.
@ -52,5 +52,5 @@ public interface IAppConfigManager
/// </summary> /// </summary>
/// <param name="action">The action to be performed on the configuration file.</param> /// <param name="action">The action to be performed on the configuration file.</param>
/// <param name="cToken">The cancellation token.</param> /// <param name="cToken">The cancellation token.</param>
ValueTask UpdateConfigAsync(Action<AppConfig> action, CancellationToken cToken = default); ValueTask UpdateConfigAsync(Action<AppSettings> action, CancellationToken cToken = default);
} }

View file

@ -1,4 +1,6 @@
namespace EllieHub.Services.Abstractions; using EllieHub.Features.Shared.Services.Abstractions;
namespace EllieHub.Features.AppConfig.Services.Abstractions;
/// <summary> /// <summary>
/// Represents a service that checks, downloads, installs, and updates ffmpeg. /// Represents a service that checks, downloads, installs, and updates ffmpeg.

View file

@ -1,4 +1,6 @@
namespace EllieHub.Services.Abstractions; using EllieHub.Features.Shared.Services.Abstractions;
namespace EllieHub.Features.AppConfig.Services.Abstractions;
/// <summary> /// <summary>
/// Represents a service that checks, downloads, installs, and updates yt-dlp. /// Represents a service that checks, downloads, installs, and updates yt-dlp.

View file

@ -1,9 +1,9 @@
using EllieHub.Models; using EllieHub.Features.AppConfig.Models;
using EllieHub.Models.Config; using EllieHub.Features.AppConfig.Services.Abstractions;
using EllieHub.Services.Abstractions; using EllieHub.Features.AppWindow.Models;
using System.Text.Json; using System.Text.Json;
namespace EllieHub.Services; namespace EllieHub.Features.AppConfig.Services;
/// <summary> /// <summary>
/// Defines a service that manages the application's settings. /// Defines a service that manages the application's settings.
@ -11,15 +11,15 @@ namespace EllieHub.Services;
public sealed class AppConfigManager : IAppConfigManager public sealed class AppConfigManager : IAppConfigManager
{ {
private readonly JsonSerializerOptions _jsonSerializerOptions = new() { WriteIndented = true }; private readonly JsonSerializerOptions _jsonSerializerOptions = new() { WriteIndented = true };
private readonly AppConfig _appConfig; private readonly AppSettings _appConfig;
/// <inheritdoc/> /// <inheritdoc/>
public ReadOnlyAppConfig AppConfig { get; } public ReadOnlyAppSettings AppConfig { get; }
/// <summary> /// <summary>
/// Creates a service that manages the application's settings. /// Creates a service that manages the application's settings.
/// </summary> /// </summary>
public AppConfigManager(AppConfig appConfig, ReadOnlyAppConfig readOnlyAppConfig) public AppConfigManager(AppSettings appConfig, ReadOnlyAppSettings readOnlyAppConfig)
{ {
_appConfig = appConfig; _appConfig = appConfig;
AppConfig = readOnlyAppConfig; AppConfig = readOnlyAppConfig;
@ -31,7 +31,7 @@ public sealed class AppConfigManager : IAppConfigManager
public async ValueTask<BotEntry> CreateBotEntryAsync(CancellationToken cToken = default) public async ValueTask<BotEntry> CreateBotEntryAsync(CancellationToken cToken = default)
{ {
var newId = CreateNewId(); var newId = CreateNewId();
var newPosition = (_appConfig.BotEntries.Count is 0) ? 0 : _appConfig.BotEntries.Values.Max(x => x.Position) + 1; var newPosition = _appConfig.BotEntries.Count is 0 ? 0 : _appConfig.BotEntries.Values.Max(x => x.Position) + 1;
var newBotName = "NewBot_" + newPosition; var newBotName = "NewBot_" + newPosition;
var newEntry = new BotInstanceInfo(newBotName, Path.Combine(_appConfig.BotsDirectoryUri, newBotName), newPosition); var newEntry = new BotInstanceInfo(newBotName, Path.Combine(_appConfig.BotsDirectoryUri, newBotName), newPosition);
@ -90,7 +90,7 @@ public sealed class AppConfigManager : IAppConfigManager
} }
/// <inheritdoc/> /// <inheritdoc/>
public ValueTask UpdateConfigAsync(Action<AppConfig> action, CancellationToken cToken = default) public ValueTask UpdateConfigAsync(Action<AppSettings> action, CancellationToken cToken = default)
{ {
action(_appConfig); action(_appConfig);
return SaveAsync(cToken); return SaveAsync(cToken);

View file

@ -1,4 +1,4 @@
using EllieHub.Services.Abstractions; using EllieHub.Features.AppConfig.Services.Abstractions;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;

View file

@ -1,9 +1,9 @@
using EllieHub.Models.Api; using EllieHub.Features.AppConfig.Models.Api.Evermeet;
using EllieHub.Services.Abstractions; using EllieHub.Features.AppConfig.Services.Abstractions;
using System.IO.Compression; using System.IO.Compression;
using System.Runtime.Versioning; using System.Runtime.Versioning;
namespace EllieHub.Services; namespace EllieHub.Features.AppConfig.Services;
/// <summary> /// <summary>
/// Service that checks, downloads, installs, and updates ffmpeg on MacOS. /// Service that checks, downloads, installs, and updates ffmpeg on MacOS.

View file

@ -1,9 +1,9 @@
using EllieHub.Services.Abstractions; using EllieHub.Features.AppConfig.Services.Abstractions;
using System.IO.Compression; using System.IO.Compression;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Versioning; using System.Runtime.Versioning;
namespace EllieHub.Services; namespace EllieHub.Features.AppConfig.Services;
/// <summary> /// <summary>
/// Service that checks, downloads, installs, and updates ffmpeg on Windows. /// Service that checks, downloads, installs, and updates ffmpeg on Windows.
@ -30,7 +30,7 @@ public sealed class FfmpegWindowsResolver : FfmpegResolver
public override ValueTask<bool?> CanUpdateAsync(CancellationToken cToken = default) public override ValueTask<bool?> CanUpdateAsync(CancellationToken cToken = default)
{ {
// I could not find any ARM build of ffmpeg for Windows. // I could not find any ARM build of ffmpeg for Windows.
return (RuntimeInformation.OSArchitecture is Architecture.X64) return RuntimeInformation.OSArchitecture is Architecture.X64
? base.CanUpdateAsync(cToken) ? base.CanUpdateAsync(cToken)
: ValueTask.FromResult<bool?>(false); : ValueTask.FromResult<bool?>(false);
} }

View file

@ -1,6 +1,6 @@
using EllieHub.Services.Abstractions; using EllieHub.Features.AppConfig.Services.Abstractions;
namespace EllieHub.Services.Mocks; namespace EllieHub.Features.AppConfig.Services.Mocks;
/// <summary> /// <summary>
/// Service that pretends to check, download, install, and update ffmpeg. /// Service that pretends to check, download, install, and update ffmpeg.

View file

@ -1,8 +1,8 @@
using EllieHub.Models; using EllieHub.Features.AppConfig.Models;
using EllieHub.Models.Config; using EllieHub.Features.AppConfig.Services.Abstractions;
using EllieHub.Services.Abstractions; using EllieHub.Features.AppWindow.Models;
namespace EllieHub.Services.Mocks; namespace EllieHub.Features.AppConfig.Services.Mocks;
/// <summary> /// <summary>
/// Represents a service that pretends to manage the application's settings. /// Represents a service that pretends to manage the application's settings.
@ -10,7 +10,7 @@ namespace EllieHub.Services.Mocks;
internal sealed class MockAppConfigManager : IAppConfigManager internal sealed class MockAppConfigManager : IAppConfigManager
{ {
/// <inheritdoc/> /// <inheritdoc/>
public ReadOnlyAppConfig AppConfig { get; } = new(new() { BotEntries = new() { [Guid.Empty] = new("MockBot", Path.Combine(AppStatics.AppDefaultBotDirectoryUri, "MockBot"), 0) } }); public ReadOnlyAppSettings AppConfig { get; } = new(new() { BotEntries = new() { [Guid.Empty] = new("MockBot", Path.Combine(AppStatics.AppDefaultBotDirectoryUri, "MockBot"), 0) } });
/// <inheritdoc/> /// <inheritdoc/>
public ValueTask<BotEntry> CreateBotEntryAsync(CancellationToken cToken = default) public ValueTask<BotEntry> CreateBotEntryAsync(CancellationToken cToken = default)
@ -29,6 +29,6 @@ internal sealed class MockAppConfigManager : IAppConfigManager
=> ValueTask.FromResult(false); => ValueTask.FromResult(false);
/// <inheritdoc/> /// <inheritdoc/>
public ValueTask UpdateConfigAsync(Action<AppConfig> action, CancellationToken cToken = default) public ValueTask UpdateConfigAsync(Action<AppSettings> action, CancellationToken cToken = default)
=> ValueTask.CompletedTask; => ValueTask.CompletedTask;
} }

View file

@ -1,8 +1,8 @@
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using EllieHub.Services.Abstractions; using EllieHub.Features.AppConfig.Services.Abstractions;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace EllieHub.Services; namespace EllieHub.Features.AppConfig.Services;
/// <summary> /// <summary>
/// Service that checks, downloads, installs, and updates yt-dlp. /// Service that checks, downloads, installs, and updates yt-dlp.
@ -18,10 +18,10 @@ public sealed class YtdlpResolver : IYtdlpResolver
private readonly IMemoryCache _memoryCache; private readonly IMemoryCache _memoryCache;
/// <inheritdoc /> /// <inheritdoc />
public string DependencyName { get; } = "Youtube-dlp"; public string DependencyName { get; } = "Yt-dlp";
/// <inheritdoc /> /// <inheritdoc />
public string FileName { get; } = (OperatingSystem.IsWindows()) ? "yt-dlp.exe" : "yt-dlp"; public string FileName { get; } = OperatingSystem.IsWindows() ? "yt-dlp.exe" : "yt-dlp";
/// <summary> /// <summary>
/// Creates a service that checks, downloads, installs, and updates yt-dlp. /// Creates a service that checks, downloads, installs, and updates yt-dlp.

View file

@ -1,8 +1,8 @@
using EllieHub.ViewModels.Abstractions; using EllieHub.Features.Abstractions;
using EllieHub.Views.Windows; using EllieHub.Features.AppConfig.Views.Windows;
using System.Diagnostics; using System.Diagnostics;
namespace EllieHub.ViewModels.Windows; namespace EllieHub.Features.AppConfig.ViewModels;
/// <summary> /// <summary>
/// View-model for the about me dialog window. /// View-model for the about me dialog window.

View file

@ -2,22 +2,25 @@ using Avalonia.Controls;
using Avalonia.Styling; using Avalonia.Styling;
using MsBox.Avalonia.Enums; using MsBox.Avalonia.Enums;
using EllieHub.Enums; using EllieHub.Enums;
using EllieHub.Services.Abstractions; using EllieHub.Features.Abstractions;
using EllieHub.ViewModels.Abstractions; using EllieHub.Features.AppConfig.Services.Abstractions;
using EllieHub.ViewModels.Windows; using EllieHub.Features.AppConfig.Views.Controls;
using EllieHub.Views.Controls; using EllieHub.Features.AppConfig.Views.Windows;
using EllieHub.Views.Windows; using EllieHub.Features.AppWindow.ViewModels;
using EllieHub.Features.AppWindow.Views.Windows;
using EllieHub.Features.Shared.Services.Abstractions;
using EllieHub.Features.Shared.ViewModels;
using ReactiveUI; using ReactiveUI;
using System.Diagnostics; using System.Diagnostics;
namespace EllieHub.ViewModels.Controls; namespace EllieHub.Features.AppConfig.ViewModels;
/// <summary> /// <summary>
/// The view-model for the application's settings. /// The view-model for the application's settings.
/// </summary> /// </summary>
public class ConfigViewModel : ViewModelBase<ConfigView> public class ConfigViewModel : ViewModelBase<ConfigView>
{ {
private static readonly string _unixNotice = (Environment.OSVersion.Platform is not PlatformID.Unix) private static readonly string _unixNotice = Environment.OSVersion.Platform is not PlatformID.Unix
? string.Empty ? string.Empty
: Environment.NewLine + "To make the dependencies accessible to your bot instances without this updater, consider installing " + : Environment.NewLine + "To make the dependencies accessible to your bot instances without this updater, consider installing " +
$"them through your package manager or adding the directory \"{AppStatics.AppDepsUri}\" to your PATH environment variable."; $"them through your package manager or adding the directory \"{AppStatics.AppDepsUri}\" to your PATH environment variable.";
@ -25,6 +28,7 @@ public class ConfigViewModel : ViewModelBase<ConfigView>
private readonly IAppConfigManager _appConfigManager; private readonly IAppConfigManager _appConfigManager;
private readonly AboutMeViewModel _aboutMeViewModel; private readonly AboutMeViewModel _aboutMeViewModel;
private readonly AppView _mainWindow; private readonly AppView _mainWindow;
private readonly LateralBarViewModel _lateralBarViewModel;
private double _maxLogSize; private double _maxLogSize;
private int _selectedThemeIndex; private int _selectedThemeIndex;
@ -95,6 +99,7 @@ public class ConfigViewModel : ViewModelBase<ConfigView>
_maxLogSize = _appConfigManager.AppConfig.LogMaxSizeMb; _maxLogSize = _appConfigManager.AppConfig.LogMaxSizeMb;
_selectedThemeIndex = (int)_appConfigManager.AppConfig.Theme; _selectedThemeIndex = (int)_appConfigManager.AppConfig.Theme;
_aboutMeViewModel = aboutMeViewModel; _aboutMeViewModel = aboutMeViewModel;
_lateralBarViewModel = mainWindow.ViewModel!.LateralBarInstance;
BotsUriBar = botsUriBar; BotsUriBar = botsUriBar;
BotsUriBar.CurrentUri = appConfigManager.AppConfig.BotsDirectoryUri; BotsUriBar.CurrentUri = appConfigManager.AppConfig.BotsDirectoryUri;
@ -148,7 +153,7 @@ public class ConfigViewModel : ViewModelBase<ConfigView>
if (MaxLogSize is 0.0 && spinDirection is SpinDirection.Decrease) if (MaxLogSize is 0.0 && spinDirection is SpinDirection.Decrease)
return; return;
MaxLogSize = (spinDirection is SpinDirection.Increase) MaxLogSize = spinDirection is SpinDirection.Increase
? Math.Round(Math.Max(0.0, MaxLogSize + 0.1), 2) ? Math.Round(Math.Max(0.0, MaxLogSize + 0.1), 2)
: Math.Round(Math.Max(0.0, MaxLogSize - 0.1), 2); : Math.Round(Math.Max(0.0, MaxLogSize - 0.1), 2);
@ -196,11 +201,12 @@ public class ConfigViewModel : ViewModelBase<ConfigView>
dependencyButton.Click += async (buttonViewModel, _) => await HandleDependencyAsync(buttonViewModel, dependencyResolver); dependencyButton.Click += async (buttonViewModel, _) => await HandleDependencyAsync(buttonViewModel, dependencyResolver);
var canUpdate = await dependencyResolver.CanUpdateAsync(); var canUpdate = await dependencyResolver.CanUpdateAsync();
dependencyButton.Status = (canUpdate is null) dependencyButton.Status = canUpdate switch
? DependencyStatus.Install {
: (canUpdate is true) true => DependencyStatus.Update,
? DependencyStatus.Update false => DependencyStatus.Installed,
: DependencyStatus.Installed; null => DependencyStatus.Install
};
} }
/// <summary> /// <summary>
@ -213,6 +219,7 @@ public class ConfigViewModel : ViewModelBase<ConfigView>
/// </exception> /// </exception>
private async ValueTask HandleDependencyAsync(DependencyButtonViewModel buttonViewModel, IDependencyResolver dependencyResolver) private async ValueTask HandleDependencyAsync(DependencyButtonViewModel buttonViewModel, IDependencyResolver dependencyResolver)
{ {
_lateralBarViewModel.ToggleEnable(false);
var originalStatus = buttonViewModel.Status; var originalStatus = buttonViewModel.Status;
buttonViewModel.Status = DependencyStatus.Updating; buttonViewModel.Status = DependencyStatus.Updating;
@ -234,5 +241,9 @@ public class ConfigViewModel : ViewModelBase<ConfigView>
await _mainWindow.ShowDialogWindowAsync($"An error occurred while updating {dependencyResolver.DependencyName}:\n{ex.Message}", DialogType.Error, Icon.Error); await _mainWindow.ShowDialogWindowAsync($"An error occurred while updating {dependencyResolver.DependencyName}:\n{ex.Message}", DialogType.Error, Icon.Error);
buttonViewModel.Status = originalStatus; buttonViewModel.Status = originalStatus;
} }
finally
{
_lateralBarViewModel.ToggleEnable(true);
}
} }
} }

View file

@ -2,14 +2,14 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:EllieHub.ViewModels.Controls" xmlns:vm="using:EllieHub.Features.AppConfig.ViewModels"
xmlns:views="using:EllieHub.Views.Controls" xmlns:views="using:EllieHub.Features.Shared.Views.Controls"
xmlns:const="using:EllieHub.Common" xmlns:const="using:EllieHub.Common"
xmlns:dd="using:EllieHub.DesignData.Controls" xmlns:dd="using:EllieHub.Avalonia.DesignData.Controls"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignWidth="{x:Static const:WindowConstants.MinWindowWidth}" d:DesignWidth="{x:Static const:WindowConstants.MinWindowWidth}"
d:DesignHeight="{x:Static const:WindowConstants.DefaultWindowHeight}" d:DesignHeight="{x:Static const:WindowConstants.DefaultWindowHeight}"
x:Class="EllieHub.Views.Controls.ConfigView" x:Class="EllieHub.Features.AppConfig.Views.Controls.ConfigView"
x:DataType="vm:ConfigViewModel"> x:DataType="vm:ConfigViewModel">
<Design.DataContext> <Design.DataContext>
@ -39,7 +39,7 @@
<TextBlock Text="Dependencies" <TextBlock Text="Dependencies"
FontSize="22"/> FontSize="22"/>
<TextBlock Text="Make sure the dependencies below are installed if you want to use Ellie to play music."/> <TextBlock Text="Make sure the dependencies below are installed if you want to use EllieBot to play music."/>
<!--Dependency Buttons--> <!--Dependency Buttons-->
<ItemsRepeater ItemsSource="{Binding DependencyButtons}"> <ItemsRepeater ItemsSource="{Binding DependencyButtons}">

View file

@ -1,8 +1,8 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using EllieHub.ViewModels.Controls; using EllieHub.Features.AppConfig.ViewModels;
namespace EllieHub.Views.Controls; namespace EllieHub.Features.AppConfig.Views.Controls;
/// <summary> /// <summary>
/// The view for the application's settings. /// The view for the application's settings.

View file

@ -2,12 +2,12 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:EllieHub.ViewModels.Windows" xmlns:vm="using:EllieHub.Features.AppConfig.ViewModels"
xmlns:const="using:EllieHub.Common" xmlns:const="using:EllieHub.Common"
xmlns:dd="using:EllieHub.DesignData.Windows" xmlns:dd="using:EllieHub.Avalonia.DesignData.Windows"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="325" mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="305"
Width="400" Height="325" Width="400" Height="305"
x:Class="EllieHub.Views.Windows.AboutMeView" x:Class="EllieHub.Features.AppConfig.Views.Windows.AboutMeView"
x:DataType="vm:AboutMeViewModel" x:DataType="vm:AboutMeViewModel"
Title="About EllieHub" Title="About EllieHub"
Icon="{DynamicResource EllieHubIcon}" Icon="{DynamicResource EllieHubIcon}"
@ -18,7 +18,7 @@
</Design.DataContext> </Design.DataContext>
<StackPanel Margin="10 20 10 20"> <StackPanel Margin="10 20 10 20">
<TextBlock Text="Ellie is a general purpose open-source Discord bot created by Toastie. Support the project!" <TextBlock Text="EllieBot is a general purpose open-source Discord bot created by Toastie. Support the project!"
TextAlignment="Center" TextAlignment="Center"
Margin="0 0 0 10"/> Margin="0 0 0 10"/>
@ -42,7 +42,7 @@
</StackPanel> </StackPanel>
</Button> </Button>
<Button CommandParameter="https://www.patreon.com/emotionchild" <Button CommandParameter="https://www.patreon.com/toastiet0ast"
Command="{Binding OpenUrl}"> Command="{Binding OpenUrl}">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock Text="Patreon " /> <TextBlock Text="Patreon " />
@ -55,7 +55,8 @@
<Separator Margin="20" <Separator Margin="20"
HorizontalAlignment="Stretch" /> HorizontalAlignment="Stretch" />
<TextBlock Text="This tool was made by Toastie. If it has been useful to you, consider showing your support by buying me a coffee." <TextBlock Text="This tool was made by Toastie. If it has been useful to you, consider showing your support on Ko-fi."
TextAlignment="Center"
Margin="0 0 0 10" /> Margin="0 0 0 10" />
<Button HorizontalAlignment="Center" <Button HorizontalAlignment="Center"
@ -68,7 +69,7 @@
</StackPanel> </StackPanel>
</Button> </Button>
<TextBlock Text="© 2023 Toastie" <TextBlock Text="© 2024 Toastie"
FontSize="11" FontSize="11"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Margin="0 30 0 0"/> Margin="0 30 0 0"/>

View file

@ -1,7 +1,7 @@
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using EllieHub.ViewModels.Windows; using EllieHub.Features.AppConfig.ViewModels;
namespace EllieHub.Views.Windows; namespace EllieHub.Features.AppConfig.Views.Windows;
/// <summary> /// <summary>
/// Represents the about me dialog window. /// Represents the about me dialog window.

View file

@ -1,6 +1,6 @@
using EllieHub.ViewModels.Controls; using EllieHub.Features.AppWindow.ViewModels;
namespace EllieHub.Models; namespace EllieHub.Features.AppWindow.Models;
/// <summary> /// <summary>
/// Represents a bot entry in the <see cref="LateralBarViewModel"/>. /// Represents a bot entry in the <see cref="LateralBarViewModel"/>.

View file

@ -1,4 +1,4 @@
namespace EllieHub.Models; namespace EllieHub.Features.AppWindow.Models;
/// <summary> /// <summary>
/// Represents the information of a bot instance. /// Represents the information of a bot instance.

View file

@ -0,0 +1,11 @@
namespace EllieHub.Features.AppWindow.Models;
/// <summary>
/// Represents a bot entry from EllieUpdater.
/// </summary>
/// <param name="Guid">The Id of the bot.</param>
/// <param name="Name">The name of the bot.</param>
/// <param name="IconUri">The path to the avatar image.</param>
/// <param name="Version">The version of the bot.</param>
/// <param name="PathUri">The path to the bot files.</param>
internal sealed record OldUpdaterBotEntry(Guid Guid, string Name, string? IconUri, string? Version, string? PathUri);

View file

@ -1,10 +1,10 @@
using EllieHub.ViewModels.Abstractions; using EllieHub.Features.Abstractions;
using EllieHub.ViewModels.Controls; using EllieHub.Features.AppWindow.Views.Controls;
using EllieHub.Views.Controls; using EllieHub.Features.AppWindow.Views.Windows;
using EllieHub.Views.Windows; using EllieHub.Features.Home.ViewModels;
using ReactiveUI; using ReactiveUI;
namespace EllieHub.ViewModels.Windows; namespace EllieHub.Features.AppWindow.ViewModels;
/// <summary> /// <summary>
/// View-model for the main window. /// View-model for the main window.

View file

@ -1,25 +1,35 @@
using Avalonia.Controls; using Avalonia.Controls;
using EllieHub.Models; using EllieHub.Features.Abstractions;
using EllieHub.Services.Abstractions; using EllieHub.Features.AppConfig.Services.Abstractions;
using EllieHub.ViewModels.Abstractions; using EllieHub.Features.AppWindow.Models;
using EllieHub.Views.Controls; using EllieHub.Features.AppWindow.Views.Controls;
using ReactiveUI; using ReactiveUI;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Reactive.Disposables; using System.Reactive.Disposables;
namespace EllieHub.ViewModels.Controls; namespace EllieHub.Features.AppWindow.ViewModels;
/// <summary> /// <summary>
/// View-model for <see cref="LateralBarView"/>, the lateral bar with home, bot, and configuration buttons. /// View-model for <see cref="LateralBarView"/>, the lateral bar with home, bot, and configuration buttons.
/// </summary> /// </summary>
public class LateralBarViewModel : ViewModelBase<LateralBarView> public class LateralBarViewModel : ViewModelBase<LateralBarView>
{ {
private bool _isLateralBarEnabled = true;
private readonly IAppConfigManager _appConfigManager;
/// <summary> /// <summary>
/// Collection of buttons for bot instances. /// Collection of buttons for bot instances.
/// </summary> /// </summary>
public ObservableCollection<Button> BotButtonList { get; } = new(); public ObservableCollection<Button> BotButtonList { get; } = [];
private readonly IAppConfigManager _appConfigManager; /// <summary>
/// Determines whether the buttons on the lateral bar are enabled or not.
/// </summary>
public bool IsLateralBarEnabled
{
get => _isLateralBarEnabled;
private set => this.RaiseAndSetIfChanged(ref _isLateralBarEnabled, value);
}
/// <summary> /// <summary>
/// Creates the view-model for the <see cref="LateralBarView"/>. /// Creates the view-model for the <see cref="LateralBarView"/>.
@ -67,11 +77,23 @@ public class LateralBarViewModel : ViewModelBase<LateralBarView>
this.RaisePropertyChanged(nameof(BotButtonList)); this.RaisePropertyChanged(nameof(BotButtonList));
} }
/// <summary>
/// Enables or disables the buttons on the lateral bar.
/// </summary>
/// <param name="enable"><see langword="true"/> to enable the buttons, <see langword="false"/> otherwise.</param>
public void ToggleEnable(bool enable)
{
IsLateralBarEnabled = enable;
foreach (var button in BotButtonList)
button.IsEnabled = enable;
}
/// <summary> /// <summary>
/// Loads the bot buttons to the lateral bar. /// Loads the bot buttons to the lateral bar.
/// </summary> /// </summary>
/// <param name="botEntires">The bot entries.</param> /// <param name="botEntires">The bot entries.</param>
private void ReloadBotButtons(IReadOnlyDictionary<Guid, BotInstanceInfo> botEntires) public void ReloadBotButtons(IReadOnlyDictionary<Guid, BotInstanceInfo> botEntires)
{ {
BotButtonList.Clear(); BotButtonList.Clear();

View file

@ -3,11 +3,11 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:siv="https://github.com/kekyo/SkiaImageView" xmlns:siv="https://github.com/kekyo/SkiaImageView"
xmlns:vm="using:EllieHub.ViewModels.Controls" xmlns:vm="using:EllieHub.Features.AppWindow.ViewModels"
xmlns:dd="using:EllieHub.DesignData.Controls" xmlns:dd="using:EllieHub.Avalonia.DesignData.Controls"
xmlns:const="using:EllieHub.Common" xmlns:const="using:EllieHub.Common"
mc:Ignorable="d" d:DesignWidth="70" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="70" d:DesignHeight="450"
x:Class="EllieHub.Views.Controls.LateralBarView" x:Class="EllieHub.Features.AppWindow.Views.Controls.LateralBarView"
x:DataType="vm:LateralBarViewModel"> x:DataType="vm:LateralBarViewModel">
<Design.DataContext> <Design.DataContext>
@ -20,7 +20,8 @@
<Button Classes="transparent" <Button Classes="transparent"
Name="HomeButton" Name="HomeButton"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"> HorizontalContentAlignment="Center"
IsEnabled="{Binding IsLateralBarEnabled}">
<Image Classes="icon" <Image Classes="icon"
Source="{DynamicResource HomeIcon}"/> Source="{DynamicResource HomeIcon}"/>
</Button> </Button>
@ -34,7 +35,8 @@
<Button Classes="accent" <Button Classes="accent"
Content="+" Content="+"
Cursor="Hand" Cursor="Hand"
Command="{Binding AddBotButtonAsync}"/> Command="{Binding AddBotButtonAsync}"
IsEnabled="{Binding IsLateralBarEnabled}"/>
</Border> </Border>
</StackPanel> </StackPanel>
@ -77,7 +79,8 @@
Classes="transparent" Classes="transparent"
Name="ConfigButton" Name="ConfigButton"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Center"> VerticalAlignment="Center"
IsEnabled="{Binding IsLateralBarEnabled}">
<Image MaxHeight="17" <Image MaxHeight="17"
Source="{DynamicResource ConfigIcon}"/> Source="{DynamicResource ConfigIcon}"/>
</Button> </Button>

View file

@ -5,15 +5,15 @@ using Avalonia.Interactivity;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using EllieHub.DesignData.Common; using EllieHub.Avalonia.DesignData.Common;
using EllieHub.Models.Config; using EllieHub.Features.AppConfig.Models;
using EllieHub.Models.EventArguments; using EllieHub.Features.AppConfig.Services.Mocks;
using EllieHub.Services.Mocks; using EllieHub.Features.AppWindow.ViewModels;
using EllieHub.ViewModels.Controls; using EllieHub.Features.BotConfig.Models;
using SkiaImageView; using SkiaImageView;
using SkiaSharp; using SkiaSharp;
namespace EllieHub.Views.Controls; namespace EllieHub.Features.AppWindow.Views.Controls;
/// <summary> /// <summary>
/// View for the lateral bar with home, bot, and configuration buttons. /// View for the lateral bar with home, bot, and configuration buttons.
@ -22,7 +22,7 @@ public partial class LateralBarView : ReactiveUserControl<LateralBarViewModel>
{ {
private static readonly Cursor _pointingHandCursor = new(StandardCursorType.Hand); private static readonly Cursor _pointingHandCursor = new(StandardCursorType.Hand);
private static readonly Cursor _arrow = new(StandardCursorType.Arrow); private static readonly Cursor _arrow = new(StandardCursorType.Arrow);
private readonly ReadOnlyAppConfig _appConfig; private readonly ReadOnlyAppSettings _appConfig;
/// <summary> /// <summary>
/// Raised when the user clicks a bot button. /// Raised when the user clicks a bot button.
@ -41,7 +41,7 @@ public partial class LateralBarView : ReactiveUserControl<LateralBarViewModel>
/// Creates the lateral bar of the application. /// Creates the lateral bar of the application.
/// </summary> /// </summary>
/// <param name="appConfig">The application settings.</param> /// <param name="appConfig">The application settings.</param>
public LateralBarView(ReadOnlyAppConfig appConfig) public LateralBarView(ReadOnlyAppSettings appConfig)
{ {
_appConfig = appConfig; _appConfig = appConfig;
InitializeComponent(); InitializeComponent();
@ -93,7 +93,11 @@ public partial class LateralBarView : ReactiveUserControl<LateralBarViewModel>
/// <param name="sender">The bot button that was clicked.</param> /// <param name="sender">The bot button that was clicked.</param>
/// <param name="eventArgs">The event arguments.</param> /// <param name="eventArgs">The event arguments.</param>
private void LoadBotViewModel(object sender, RoutedEventArgs eventArgs) private void LoadBotViewModel(object sender, RoutedEventArgs eventArgs)
=> BotButtonClick?.Invoke((Button)sender, eventArgs); {
// "sender", for some reason, is not one of the buttons stored in the lateral bar's view-model.
if (Utilities.TryCastTo<Button>(sender, out var button) && this.ViewModel!.BotButtonList.First(x => x.Content == button.Content).IsEnabled)
BotButtonClick?.Invoke(button, eventArgs);
}
/// <summary> /// <summary>
/// Loads the bot avatar when the buttons on the lateral bar are rendered. /// Loads the bot avatar when the buttons on the lateral bar are rendered.

View file

@ -2,13 +2,13 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:EllieHub.ViewModels.Windows" xmlns:vm="using:EllieHub.Features.AppWindow.ViewModels"
xmlns:const="using:EllieHub.Common" xmlns:const="using:EllieHub.Common"
xmlns:dd="using:EllieHub.DesignData.Windows" xmlns:dd="using:EllieHub.Avalonia.DesignData.Windows"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignWidth="{x:Static const:WindowConstants.MinWindowWidth}" d:DesignWidth="{x:Static const:WindowConstants.MinWindowWidth}"
d:DesignHeight="{x:Static const:WindowConstants.MinWindowHeight}" d:DesignHeight="{x:Static const:WindowConstants.MinWindowHeight}"
x:Class="EllieHub.Views.Windows.AppView" x:Class="EllieHub.Features.AppWindow.Views.Windows.AppView"
x:DataType="vm:AppViewModel" x:DataType="vm:AppViewModel"
Icon="{DynamicResource EllieHubIcon}" Icon="{DynamicResource EllieHubIcon}"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"

View file

@ -2,18 +2,26 @@ using Avalonia.Controls;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.Styling; using Avalonia.Styling;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using EllieHub.DesignData.Common; using EllieHub.Avalonia.DesignData.Common;
using EllieHub.Enums; using EllieHub.Enums;
using EllieHub.Features.Abstractions;
using EllieHub.Features.AppConfig.Services.Abstractions;
using EllieHub.Features.AppConfig.ViewModels;
using EllieHub.Features.AppWindow.Models;
using EllieHub.Features.AppWindow.ViewModels;
using EllieHub.Features.AppWindow.Views.Controls;
using EllieHub.Features.BotConfig.Services.Abstractions;
using EllieHub.Features.BotConfig.ViewModels;
using EllieHub.Features.Home.Services.Abstractions;
using EllieHub.Features.Home.ViewModels;
using EllieHub.Features.Home.Views.Windows;
using EllieHub.Services; using EllieHub.Services;
using EllieHub.Services.Abstractions;
using EllieHub.ViewModels.Abstractions;
using EllieHub.ViewModels.Controls;
using EllieHub.ViewModels.Windows;
using EllieHub.Views.Controls;
using ReactiveUI; using ReactiveUI;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.Versioning;
using System.Text.Json;
namespace EllieHub.Views.Windows; namespace EllieHub.Features.AppWindow.Views.Windows;
/// <summary> /// <summary>
/// Represents the main window of the application. /// Represents the main window of the application.
@ -103,7 +111,7 @@ public partial class AppView : ReactiveWindow<AppViewModel>
base.OnResized(eventArgs); base.OnResized(eventArgs);
} }
/// <inheritdoc/> /// <inheritdoc />
/// <exception cref="UnreachableException">Occurs when <see cref="ThemeType"/> has an unimplemented value.</exception> /// <exception cref="UnreachableException">Occurs when <see cref="ThemeType"/> has an unimplemented value.</exception>
protected override void OnOpened(EventArgs eventArgs) protected override void OnOpened(EventArgs eventArgs)
{ {
@ -127,6 +135,10 @@ public partial class AppView : ReactiveWindow<AppViewModel>
// Update the application, if one is available // Update the application, if one is available
_ = UpdateAndCloseAsync(); _ = UpdateAndCloseAsync();
// Import bots from the old updater, if available
if (OperatingSystem.IsWindows())
_ = MigrateOldBotsAsync();
base.OnOpened(eventArgs); base.OnOpened(eventArgs);
} }
@ -248,6 +260,27 @@ public partial class AppView : ReactiveWindow<AppViewModel>
base.Close(); base.Close();
} }
/// <summary>
/// Migrates bots created by the EllieUpdater when EllieHub is run for the first time.
/// </summary>
[SupportedOSPlatform("windows")]
private async Task MigrateOldBotsAsync()
{
var configFileUri = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "EllieBotUpdater", "bots.json");
if (File.Exists(AppStatics.AppConfigUri) || !File.Exists(configFileUri))
return;
var bots = (JsonSerializer.Deserialize<OldUpdaterBotEntry[]>(await File.ReadAllTextAsync(configFileUri)) ?? [])
.Where(x => !string.IsNullOrWhiteSpace(x.PathUri) && File.Exists(Path.Combine(x.PathUri, "EllieBot.exe")))
.Select((x, y) => new BotEntry(x.Guid, new(x.Name, x.PathUri!, (uint)y, x.Version, x.IconUri)));
foreach (var botEntry in bots)
await _appConfigManager.UpdateConfigAsync(x => x.BotEntries.TryAdd(botEntry.Id, botEntry.BotInfo));
_lateralBarView.ViewModel?.ReloadBotButtons(_appConfigManager.AppConfig.BotEntries);
}
/// <summary> /// <summary>
/// Gets a <see cref="BotConfigViewModel"/> from the <paramref name="scopeFactory"/> and initializes /// Gets a <see cref="BotConfigViewModel"/> from the <paramref name="scopeFactory"/> and initializes
/// its properties with user data. /// its properties with user data.

View file

@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace EllieHub.Features.BotConfig.Models.Api.Toastielab;
/// <summary>
/// The assets of a Toastielab release.
/// </summary>
/// <param name="Name">The name of the release file.</param>
/// <param name="Url">The url to the release file.</param>
public sealed record Assets(
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("browser_download_url")] string Url
);

View file

@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace EllieHub.Features.BotConfig.Models.Api.Toastielab;
/// <summary>
/// Represents a release from the Toastielab API.
/// </summary>
/// <param name="Tag">The tag of the release.</param>
/// <param name="Assets">The assets of the release.</param>
public sealed record ToastielabRelease(
[property: JsonPropertyName("tag_name")] string Tag,
[property: JsonPropertyName("assets")] Assets[] Assets
);

View file

@ -1,6 +1,6 @@
using SkiaSharp; using SkiaSharp;
namespace EllieHub.Models.EventArguments; namespace EllieHub.Features.BotConfig.Models;
/// <summary> /// <summary>
/// Defines the event arguments for when the user sets a new avatar for a bot instance. /// Defines the event arguments for when the user sets a new avatar for a bot instance.

View file

@ -1,4 +1,4 @@
namespace EllieHub.Models.EventArguments; namespace EllieHub.Features.BotConfig.Models;
/// <summary> /// <summary>
/// Defines the event arguments when a bot process exits. /// Defines the event arguments when a bot process exits.

View file

@ -1,4 +1,4 @@
namespace EllieHub.Models.EventArguments; namespace EllieHub.Features.BotConfig.Models;
/// <summary> /// <summary>
/// Defines the event arguments when a log is written to disk. /// Defines the event arguments when a log is written to disk.

View file

@ -1,4 +1,4 @@
namespace EllieHub.Models.EventArguments; namespace EllieHub.Features.BotConfig.Models;
/// <summary> /// <summary>
/// Defines the event arguments when a bot process writes to stdout or stderr. /// Defines the event arguments when a bot process writes to stdout or stderr.

View file

@ -1,9 +1,9 @@
using EllieHub.Models.EventArguments; using EllieHub.Features.BotConfig.Models;
namespace EllieHub.Services.Abstractions; namespace EllieHub.Features.BotConfig.Services.Abstractions;
/// <summary> /// <summary>
/// Represents an object that coordinates multiple running processes of Ellie. /// Represents an object that coordinates multiple running processes of EllieBot.
/// </summary> /// </summary>
public interface IBotOrchestrator public interface IBotOrchestrator
{ {

View file

@ -1,4 +1,6 @@
namespace EllieHub.Services.Abstractions; using EllieHub.Features.Shared.Services.Abstractions;
namespace EllieHub.Features.BotConfig.Services.Abstractions;
/// <summary> /// <summary>
/// Represents a service that checks, downloads, installs, and updates a bot instance. /// Represents a service that checks, downloads, installs, and updates a bot instance.
@ -10,6 +12,11 @@ public interface IBotResolver : IDependencyResolver
/// </summary> /// </summary>
string BotName { get; } string BotName { get; }
/// <summary>
/// Defines whether there is an ongoing update.
/// </summary>
bool IsUpdateInProgress { get; }
/// <summary> /// <summary>
/// The Id of the bot. /// The Id of the bot.
/// </summary> /// </summary>

View file

@ -1,7 +1,7 @@
using EllieHub.Models.EventArguments; using EllieHub.Features.BotConfig.Models;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
namespace EllieHub.Services.Abstractions; namespace EllieHub.Features.BotConfig.Services.Abstractions;
/// <summary> /// <summary>
/// Represents a service that writes logs of bot instances to the disk. /// Represents a service that writes logs of bot instances to the disk.

View file

@ -1,9 +1,9 @@
using EllieHub.Models.Config; using EllieHub.Features.AppConfig.Models;
using EllieHub.Models.EventArguments; using EllieHub.Features.BotConfig.Models;
using EllieHub.Services.Abstractions; using EllieHub.Features.BotConfig.Services.Abstractions;
using System.Diagnostics; using System.Diagnostics;
namespace EllieHub.Services; namespace EllieHub.Features.BotConfig.Services;
/// <summary> /// <summary>
/// Defines an object that coordinates multiple running processes of EllieBot. /// Defines an object that coordinates multiple running processes of EllieBot.
@ -11,8 +11,8 @@ namespace EllieHub.Services;
public sealed class EllieOrchestrator : IBotOrchestrator public sealed class EllieOrchestrator : IBotOrchestrator
{ {
private readonly Dictionary<Guid, Process> _runningBots = new(); private readonly Dictionary<Guid, Process> _runningBots = new();
private readonly ReadOnlyAppConfig _appConfig; private readonly ReadOnlyAppSettings _appConfig;
private readonly string _fileName = (OperatingSystem.IsWindows()) ? "NadekoBot.exe" : "NadekoBot"; private readonly string _fileName = OperatingSystem.IsWindows() ? "EllieBot.exe" : "EllieBot";
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<IBotOrchestrator, BotExitEventArgs>? OnBotExit; public event EventHandler<IBotOrchestrator, BotExitEventArgs>? OnBotExit;
@ -27,7 +27,7 @@ public sealed class EllieOrchestrator : IBotOrchestrator
/// Creates an object that coordinates multiple running processes of EllieBot. /// Creates an object that coordinates multiple running processes of EllieBot.
/// </summary> /// </summary>
/// <param name="appConfig">The application settings.</param> /// <param name="appConfig">The application settings.</param>
public EllieOrchestrator(ReadOnlyAppConfig appConfig) public EllieOrchestrator(ReadOnlyAppSettings appConfig)
=> _appConfig = appConfig; => _appConfig = appConfig;
/// <inheritdoc/> /// <inheritdoc/>

View file

@ -1,29 +1,42 @@
using EllieHub.Services.Abstractions; using Microsoft.Extensions.Caching.Memory;
using EllieHub.Features.AppConfig.Services.Abstractions;
using EllieHub.Features.BotConfig.Models.Api.Toastielab;
using EllieHub.Features.BotConfig.Services.Abstractions;
using SingleFileExtractor.Core;
using System.Formats.Tar; using System.Formats.Tar;
using System.IO.Compression; using System.IO.Compression;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace EllieHub.Services; namespace EllieHub.Services;
/// <summary> /// <summary>
/// Service that checks, downloads, installs, and updates a NadekoBot instance. /// Service that checks, downloads, installs, and updates a EllieBot instance.
/// </summary> /// </summary>
/// <remarks>Source: https://gitlab.com/Kwoth/nadekobot/-/releases/permalink/latest</remarks> /// <remarks>Source: https://toastielab.dev/Emotions-stuff/elliebot/releases/latest</remarks>
public sealed partial class EllieResolver : IBotResolver public sealed partial class EllieResolver : IBotResolver
{ {
private static readonly HashSet<Guid> _updateIdOngoing = new(); private const string _cachedCurrentVersionKey = "currentVersion:EllieBot";
private const string _toastielabReleasesEndpointUrl = "https://toastielab.dev/api/v1/repos/Emotions-stuff/elliebot/releases/latest";
private const string _toastielabReleasesRepoUrl = "https://toastielab.dev/Emotions-stuff/elliebot/releases/latest";
private static readonly HashSet<Guid> _updateIdOngoing = [];
private static readonly string _tempDirectory = Path.GetTempPath(); private static readonly string _tempDirectory = Path.GetTempPath();
private static readonly Regex _unzipedDirRegex = GenerateUnzipedDirRegex(); private static readonly Regex _unzipedDirRegex = GenerateUnzipedDirRegex();
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly IMemoryCache _memoryCache;
private readonly IAppConfigManager _appConfigManager; private readonly IAppConfigManager _appConfigManager;
/// <inheritdoc/> /// <inheritdoc/>
public string DependencyName { get; } = "NadekoBot"; public string DependencyName { get; } = "EllieBot";
/// <inheritdoc/> /// <inheritdoc/>
public string FileName { get; } = (OperatingSystem.IsWindows()) ? "NadekoBot.exe" : "NadekoBot"; public string FileName { get; } = (OperatingSystem.IsWindows()) ? "EllieBot.exe" : "EllieBot";
/// <inheritdoc/>
public bool IsUpdateInProgress
=> _updateIdOngoing.Contains(Id);
/// <inheritdoc/> /// <inheritdoc/>
public Guid Id { get; } public Guid Id { get; }
@ -32,14 +45,16 @@ public sealed partial class EllieResolver : IBotResolver
public string BotName { get; } public string BotName { get; }
/// <summary> /// <summary>
/// Creates a service that checks, downloads, installs, and updates a NadekoBot instance. /// Creates a service that checks, downloads, installs, and updates a EllieBot instance.
/// </summary> /// </summary>
/// <param name="httpClientFactory">The HTTP client factory.</param> /// <param name="httpClientFactory">The HTTP client factory.</param>
/// <param name="memoryCache">The memory cache.</param>
/// <param name="appConfigManager">The application's settings.</param> /// <param name="appConfigManager">The application's settings.</param>
/// <param name="botId">The Id of the bot.</param> /// <param name="botId">The Id of the bot.</param>
public EllieResolver(IHttpClientFactory httpClientFactory, IAppConfigManager appConfigManager, Guid botId) public EllieResolver(IHttpClientFactory httpClientFactory, IMemoryCache memoryCache, IAppConfigManager appConfigManager, Guid botId)
{ {
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_memoryCache = memoryCache;
_appConfigManager = appConfigManager; _appConfigManager = appConfigManager;
Id = botId; Id = botId;
BotName = _appConfigManager.AppConfig.BotEntries[Id].Name; BotName = _appConfigManager.AppConfig.BotEntries[Id].Name;
@ -55,13 +70,13 @@ public sealed partial class EllieResolver : IBotResolver
var latestVersion = await GetLatestVersionAsync(cToken); var latestVersion = await GetLatestVersionAsync(cToken);
if (latestVersion.Equals(currentVersion, StringComparison.Ordinal)) if (Version.Parse(latestVersion) <= Version.Parse(currentVersion))
return false; return false;
var http = _httpClientFactory.CreateClient(); var http = _httpClientFactory.CreateClient();
return await http.IsUrlValidAsync( return await http.IsUrlValidAsync(
$"https://gitlab.com/api/v4/projects/9321079/packages/generic/NadekoBot-build/{latestVersion}/{GetDownloadFileName(latestVersion)}", await GetDownloadUrlAsync(latestVersion, cToken),
cToken cToken
); );
} }
@ -78,7 +93,7 @@ public sealed partial class EllieResolver : IBotResolver
var now = DateTimeOffset.Now; var now = DateTimeOffset.Now;
var date = new DateOnly(now.Year, now.Month, now.Day).ToShortDateString().Replace('/', '-'); var date = new DateOnly(now.Year, now.Month, now.Day).ToShortDateString().Replace('/', '-');
var backupZipName = $"{botInstance.Name}_{date}-{now.ToUnixTimeMilliseconds()}.zip"; var backupZipName = $"{botInstance.Name}_v{botInstance.Version}_{date}-{now.ToUnixTimeMilliseconds()}.zip";
var destinationUri = Path.Combine(_appConfigManager.AppConfig.BotsBackupDirectoryUri, backupZipName); var destinationUri = Path.Combine(_appConfigManager.AppConfig.BotsBackupDirectoryUri, backupZipName);
// ZipFile does not provide asynchronous implementations, so we have to schedule its // ZipFile does not provide asynchronous implementations, so we have to schedule its
@ -92,42 +107,60 @@ public sealed partial class EllieResolver : IBotResolver
public async ValueTask<string?> GetCurrentVersionAsync(CancellationToken cToken = default) public async ValueTask<string?> GetCurrentVersionAsync(CancellationToken cToken = default)
{ {
var botEntry = _appConfigManager.AppConfig.BotEntries[Id]; var botEntry = _appConfigManager.AppConfig.BotEntries[Id];
var executableUri = Path.Combine(botEntry.InstanceDirectoryUri, FileName);
if (!File.Exists(executableUri))
{
await _appConfigManager.UpdateBotEntryAsync(Id, x => x with { Version = null }, cToken);
return null;
}
if (!string.IsNullOrWhiteSpace(botEntry.Version)) if (!string.IsNullOrWhiteSpace(botEntry.Version))
return botEntry.Version; return botEntry.Version;
var assemblyUri = Path.Combine(botEntry.InstanceDirectoryUri, "NadekoBot.dll"); // Ellie is published as a single-file binary, so we have to extract
// its contents first in order to read the assembly for its version.
using var executableReader = new ExecutableReader(executableUri);
var extractDirectoryUri = Path.Combine(_tempDirectory, "EllieBotExtract");
var extractAssemblyUri = Path.Combine(extractDirectoryUri, "EllieBot.dll");
if (!File.Exists(assemblyUri)) try
return null; {
var nadekoAssembly = Assembly.LoadFile(assemblyUri); await executableReader.ExtractToDirectoryAsync(extractDirectoryUri, cToken);
var version = nadekoAssembly.GetName().Version
?? throw new InvalidOperationException($"Could not find version of the assembly at {assemblyUri}.");
var currentVersion = $"{version.Major}.{version.Minor}.{version.Build}"; var nadekoAssembly = Assembly.LoadFile(extractAssemblyUri);
var version = nadekoAssembly.GetName().Version
?? throw new InvalidOperationException($"Could not find version of the assembly at {extractAssemblyUri}.");
await _appConfigManager.UpdateBotEntryAsync(Id, x => x with { Version = currentVersion }, cToken); var currentVersion = $"{version.Major}.{version.Minor}.{version.Build}";
return currentVersion; await _appConfigManager.UpdateBotEntryAsync(Id, x => x with { Version = currentVersion }, cToken);
return currentVersion;
}
finally
{
Utilities.TryDeleteDirectory(extractDirectoryUri);
}
} }
/// <inheritdoc/> /// <inheritdoc/>
public async ValueTask<string> GetLatestVersionAsync(CancellationToken cToken = default) public async ValueTask<string> GetLatestVersionAsync(CancellationToken cToken = default)
{ {
var http = _httpClientFactory.CreateClient(AppConstants.NoRedirectClient); try
{
var response = await http.GetAsync("https://gitlab.com/Kwoth/nadekobot/-/releases/permalink/latest", cToken); return (await GetLatestVersionFromApiAsync(cToken)).Tag;
}
var lastSlashIndex = response.Headers.Location?.OriginalString.LastIndexOf('/') catch (InvalidOperationException)
?? throw new InvalidOperationException("Failed to get the latest NadekoBot version."); {
return await GetLatestVersionFromUrlAsync(cToken);
return response.Headers.Location.OriginalString[(lastSlashIndex + 1)..]; }
} }
/// <inheritdoc/> /// <inheritdoc/>
public async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string installationUri, CancellationToken cToken = default) public async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string installationUri, CancellationToken cToken = default)
{ {
if (_updateIdOngoing.Contains(Id)) if (IsUpdateInProgress)
return (null, null); return (null, null);
_updateIdOngoing.Add(Id); _updateIdOngoing.Add(Id);
@ -135,8 +168,8 @@ public sealed partial class EllieResolver : IBotResolver
var currentVersion = await GetCurrentVersionAsync(cToken); var currentVersion = await GetCurrentVersionAsync(cToken);
var latestVersion = await GetLatestVersionAsync(cToken); var latestVersion = await GetLatestVersionAsync(cToken);
// Update // Already up-to-date, quit
if (latestVersion == currentVersion) if (currentVersion is not null && Version.Parse(latestVersion) <= Version.Parse(currentVersion))
{ {
_updateIdOngoing.Remove(Id); _updateIdOngoing.Remove(Id);
return (currentVersion, null); return (currentVersion, null);
@ -152,13 +185,13 @@ public sealed partial class EllieResolver : IBotResolver
var http = _httpClientFactory.CreateClient(); var http = _httpClientFactory.CreateClient();
var downloadFileName = GetDownloadFileName(latestVersion); var downloadFileName = GetDownloadFileName(latestVersion);
var botTempLocation = Path.Combine(_tempDirectory, "nadekobot-" + _unzipedDirRegex.Match(downloadFileName).Groups[1].Value); var botTempLocation = Path.Combine(_tempDirectory, "elliebot-" + _unzipedDirRegex.Match(downloadFileName).Groups[1].Value);
var zipTempLocation = Path.Combine(_tempDirectory, downloadFileName); var zipTempLocation = Path.Combine(_tempDirectory, downloadFileName);
try try
{ {
using var downloadStream = await http.GetStreamAsync( using var downloadStream = await http.GetStreamAsync(
$"https://gitlab.com/api/v4/projects/9321079/packages/generic/NadekoBot-build/{latestVersion}/{downloadFileName}", await GetDownloadUrlAsync(latestVersion, cToken),
cToken cToken
); );
@ -194,7 +227,7 @@ public sealed partial class EllieResolver : IBotResolver
} }
/// <summary> /// <summary>
/// Installs the Nadeko instance on a Unix system. /// Installs the Ellie instance on a Unix system.
/// </summary> /// </summary>
/// <param name="downloadStream">The stream of data downloaded from the source.</param> /// <param name="downloadStream">The stream of data downloaded from the source.</param>
/// <param name="installationUri">The absolute path to the directory the bot got installed to.</param> /// <param name="installationUri">The absolute path to the directory the bot got installed to.</param>
@ -215,7 +248,7 @@ public sealed partial class EllieResolver : IBotResolver
} }
/// <summary> /// <summary>
/// Installs the Nadeko instance on a non-Unix system. /// Installs the Ellie instance on a non-Unix system.
/// </summary> /// </summary>
/// <param name="downloadStream">The stream of data downloaded from the source.</param> /// <param name="downloadStream">The stream of data downloaded from the source.</param>
/// <param name="installationUri">The absolute path to the directory the bot got installed to.</param> /// <param name="installationUri">The absolute path to the directory the bot got installed to.</param>
@ -246,8 +279,8 @@ public sealed partial class EllieResolver : IBotResolver
using var zipFile = ZipFile.OpenRead(backupFileUri); using var zipFile = ZipFile.OpenRead(backupFileUri);
var zippedFiles = zipFile.Entries var zippedFiles = zipFile.Entries
.Where(x => .Where(x =>
x.Name is "creds.yml" or "creds_example.yml" x.Name is "creds.yml" // Restore creds.yml and everything in the "data" folder, except the stuff in the "strings" folder.
|| (!string.IsNullOrWhiteSpace(x.Name) && x.FullName.Contains("data/")) || (!string.IsNullOrWhiteSpace(x.Name) && x.FullName.Contains("data/") && !x.FullName.Contains("strings/"))
); );
foreach (var zippedFile in zippedFiles) foreach (var zippedFile in zippedFiles)
@ -277,7 +310,7 @@ public sealed partial class EllieResolver : IBotResolver
/// <summary> /// <summary>
/// Gets the name of the file to be downloaded. /// Gets the name of the file to be downloaded.
/// </summary> /// </summary>
/// <param name="version">The version of NadekoBot.</param> /// <param name="version">The version of EllieBot.</param>
/// <returns>The name of the file to download.</returns> /// <returns>The name of the file to download.</returns>
/// <exception cref="NotSupportedException">Occurs when this method is executed in an unsupported platform.</exception> /// <exception cref="NotSupportedException">Occurs when this method is executed in an unsupported platform.</exception>
private static string GetDownloadFileName(string version) private static string GetDownloadFileName(string version)
@ -295,10 +328,75 @@ public sealed partial class EllieResolver : IBotResolver
// MacOS // MacOS
Architecture.X64 when OperatingSystem.IsMacOS() => "-osx-x64-build.tar", Architecture.X64 when OperatingSystem.IsMacOS() => "-osx-x64-build.tar",
Architecture.Arm64 when OperatingSystem.IsMacOS() => "-osx-arm64-build.tar", Architecture.Arm64 when OperatingSystem.IsMacOS() => "-osx-arm64-build.tar",
_ => throw new NotSupportedException($"Architecture of type {RuntimeInformation.OSArchitecture} is not supported by NadekoBot on this OS.") _ => throw new NotSupportedException($"Architecture of type {RuntimeInformation.OSArchitecture} is not supported by EllieBot on this OS.")
}; };
} }
/// <summary>
/// Gets the download url to the latest bot release.
/// </summary>
/// <param name="latestVersion">The latest version of the bot.</param>
/// <param name="cToken">The cancellation token.</param>
/// <returns>The url to the latest bot release.</returns>
private async ValueTask<string> GetDownloadUrlAsync(string latestVersion, CancellationToken cToken = default)
{
var downloadFileName = GetDownloadFileName(latestVersion);
try
{
// The first release is the most recent one.
return (await GetLatestVersionFromApiAsync(cToken)).Assets
.First(x => x.Name.Equals(downloadFileName, StringComparison.Ordinal))
.Url;
}
catch (InvalidOperationException)
{
return $"https://toastielab.dev/Emotions-stuff/elliebot/releases/download/{latestVersion}/{downloadFileName}";
}
}
/// <summary>
/// Gets the latest bot version from the Toastielab latest release URL.
/// </summary>
/// <param name="cToken">The cancellation token.</param>
/// <returns>The latest version of the bot.</returns>
/// <exception cref="InvalidOperationException">Occurs when parsing of the response fails.</exception>
private async ValueTask<string> GetLatestVersionFromUrlAsync(CancellationToken cToken = default)
{
var http = _httpClientFactory.CreateClient(AppConstants.NoRedirectClient);
var response = await http.GetAsync(_toastielabReleasesRepoUrl, cToken);
var lastSlashIndex = response.Headers.Location?.OriginalString.LastIndexOf('/')
?? throw new InvalidOperationException("Failed to get the latest EllieBot version.");
return response.Headers.Location.OriginalString[(lastSlashIndex + 1)..];
}
/// <summary>
/// Gets the latest bot version from the Toastielab API.
/// </summary>
/// <param name="cToken">The cancellation token.</param>
/// <returns>The latest version of the bot.</returns>
/// <exception cref="InvalidOperationException">Occurs when the API call fails.</exception>
private async ValueTask<ToastielabRelease> GetLatestVersionFromApiAsync(CancellationToken cToken = default)
{
if (_memoryCache.TryGetValue(_cachedCurrentVersionKey, out var cachedObject) && cachedObject is ToastielabRelease cachedResponse)
return cachedResponse;
var http = _httpClientFactory.CreateClient();
var httpResponse = await http.GetAsync(_toastielabReleasesEndpointUrl, cToken);
if (!httpResponse.IsSuccessStatusCode)
throw new InvalidOperationException("The call to the Toastielab API failed.");
var response = JsonSerializer.Deserialize<ToastielabRelease>(await httpResponse.Content.ReadAsStringAsync(cToken))
?? throw new InvalidOperationException("Failed deserializing Toastielab's response.");
_memoryCache.Set(_cachedCurrentVersionKey, response, TimeSpan.FromMinutes(1));
return response;
}
[GeneratedRegex(@"^(?:\S+\-)(\S+\-\S+)\-", RegexOptions.Compiled)] [GeneratedRegex(@"^(?:\S+\-)(\S+\-\S+)\-", RegexOptions.Compiled)]
private static partial Regex GenerateUnzipedDirRegex(); private static partial Regex GenerateUnzipedDirRegex();
} }

View file

@ -1,10 +1,10 @@
using EllieHub.Models.Config; using EllieHub.Features.AppConfig.Models;
using EllieHub.Models.EventArguments; using EllieHub.Features.BotConfig.Models;
using EllieHub.Services.Abstractions; using EllieHub.Features.BotConfig.Services.Abstractions;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text; using System.Text;
namespace EllieHub.Services; namespace EllieHub.Features.BotConfig.Services;
/// <summary> /// <summary>
/// Defines a service that writes logs of bot instances to the disk. /// Defines a service that writes logs of bot instances to the disk.
@ -12,7 +12,7 @@ namespace EllieHub.Services;
public sealed class LogWriter : ILogWriter public sealed class LogWriter : ILogWriter
{ {
private readonly Dictionary<Guid, StringBuilder> _botLogs = new(); private readonly Dictionary<Guid, StringBuilder> _botLogs = new();
private readonly ReadOnlyAppConfig _appConfig; private readonly ReadOnlyAppSettings _appConfig;
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<ILogWriter, LogFlushEventArgs>? OnLogCreated; public event EventHandler<ILogWriter, LogFlushEventArgs>? OnLogCreated;
@ -21,7 +21,7 @@ public sealed class LogWriter : ILogWriter
/// Creates a service that writes logs of bot instances to the disk. /// Creates a service that writes logs of bot instances to the disk.
/// </summary> /// </summary>
/// <param name="appConfig">The application settings.</param> /// <param name="appConfig">The application settings.</param>
public LogWriter(ReadOnlyAppConfig appConfig) public LogWriter(ReadOnlyAppSettings appConfig)
=> _appConfig = appConfig; => _appConfig = appConfig;
/// <inheritdoc/> /// <inheritdoc/>
@ -73,7 +73,7 @@ public sealed class LogWriter : ILogWriter
logStringBuilder.AppendLine(message); logStringBuilder.AppendLine(message);
if ((logStringBuilder.Length > _appConfig.LogMaxSizeMb * 1_000_000)) if (logStringBuilder.Length > _appConfig.LogMaxSizeMb * 1_000_000)
_ = FlushAsync(botId); _ = FlushAsync(botId);
return true; return true;

View file

@ -1,6 +1,6 @@
using EllieHub.Services.Abstractions; using EllieHub.Features.BotConfig.Services.Abstractions;
namespace EllieHub.Services.Mocks; namespace EllieHub.Features.BotConfig.Services.Mocks;
/// <summary> /// <summary>
/// Defines a service that pretends to check, download, install, and update a bot instance. /// Defines a service that pretends to check, download, install, and update a bot instance.
@ -14,10 +14,13 @@ internal sealed class MockEllieResolver : IBotResolver
public Guid Id { get; } = Guid.Empty; public Guid Id { get; } = Guid.Empty;
/// <inheritdoc/> /// <inheritdoc/>
public string DependencyName { get; } = "NadekoBot"; public string DependencyName { get; } = "EllieBot";
/// <inheritdoc/> /// <inheritdoc/>
public string FileName { get; } = "NadekoBot"; public string FileName { get; } = "EllieBot";
/// <inheritdoc/>
public bool IsUpdateInProgress { get; } = false;
/// <inheritdoc/> /// <inheritdoc/>
public ValueTask<bool?> CanUpdateAsync(CancellationToken cToken = default) public ValueTask<bool?> CanUpdateAsync(CancellationToken cToken = default)

View file

@ -1,17 +1,21 @@
using Avalonia.Controls; using Avalonia.Controls;
using MsBox.Avalonia.Enums; using MsBox.Avalonia.Enums;
using EllieHub.Enums; using EllieHub.Enums;
using EllieHub.Models.EventArguments; using EllieHub.Features.Abstractions;
using EllieHub.Services.Abstractions; using EllieHub.Features.AppConfig.Services.Abstractions;
using EllieHub.ViewModels.Abstractions; using EllieHub.Features.AppWindow.ViewModels;
using EllieHub.Views.Controls; using EllieHub.Features.AppWindow.Views.Controls;
using EllieHub.Views.Windows; using EllieHub.Features.AppWindow.Views.Windows;
using EllieHub.Features.BotConfig.Models;
using EllieHub.Features.BotConfig.Services.Abstractions;
using EllieHub.Features.BotConfig.Views.Controls;
using EllieHub.Features.Shared.ViewModels;
using ReactiveUI; using ReactiveUI;
using SkiaSharp; using SkiaSharp;
using System.Diagnostics; using System.Diagnostics;
using System.Reactive.Disposables; using System.Reactive.Disposables;
namespace EllieHub.ViewModels.Controls; namespace EllieHub.Features.BotConfig.ViewModels;
/// <summary> /// <summary>
/// View-model for <see cref="BotConfigView"/>, the window with settings and controls for a specific bot instance. /// View-model for <see cref="BotConfigView"/>, the window with settings and controls for a specific bot instance.
@ -28,6 +32,7 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
private readonly AppView _mainWindow; private readonly AppView _mainWindow;
private readonly IBotOrchestrator _botOrchestrator; private readonly IBotOrchestrator _botOrchestrator;
private readonly ILogWriter _logWriter; private readonly ILogWriter _logWriter;
private readonly LateralBarViewModel _lateralBarViewModel;
/// <summary> /// <summary>
/// Raised when the user deletes the bot instance associated with this view-model. /// Raised when the user deletes the bot instance associated with this view-model.
@ -39,8 +44,6 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
/// </summary> /// </summary>
public event AsyncEventHandler<BotConfigViewModel, AvatarChangedEventArgs>? AvatarChanged; public event AsyncEventHandler<BotConfigViewModel, AvatarChangedEventArgs>? AvatarChanged;
/// <summary> /// <summary>
/// The name of the bot as defined in the settings file. /// The name of the bot as defined in the settings file.
/// </summary> /// </summary>
@ -150,6 +153,7 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
_mainWindow = mainWindow; _mainWindow = mainWindow;
_botOrchestrator = botOrchestrator; _botOrchestrator = botOrchestrator;
_logWriter = logWriter; _logWriter = logWriter;
_lateralBarViewModel = mainWindow.ViewModel!.LateralBarInstance;
BotDirectoryUriBar = botDirectoryUriBar; BotDirectoryUriBar = botDirectoryUriBar;
UpdateBar = updateBotBar; UpdateBar = updateBotBar;
FakeConsole = fakeConsole; FakeConsole = fakeConsole;
@ -162,6 +166,7 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
var botEntry = _appConfigManager.AppConfig.BotEntries[botResolver.Id]; var botEntry = _appConfigManager.AppConfig.BotEntries[botResolver.Id];
_ = LoadUpdateBarAsync(botResolver, updateBotBar);
_logWriter.TryRead(botResolver.Id, out var logContent); _logWriter.TryRead(botResolver.Id, out var logContent);
FakeConsole.Content = logContent ?? string.Empty; FakeConsole.Content = logContent ?? string.Empty;
FakeConsole.Watermark = "Waiting for the bot to start..."; FakeConsole.Watermark = "Waiting for the bot to start...";
@ -170,15 +175,12 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
_botAvatar = Utilities.LoadLocalImage(botEntry.AvatarUri); _botAvatar = Utilities.LoadLocalImage(botEntry.AvatarUri);
BotName = botResolver.BotName; BotName = botResolver.BotName;
Id = botResolver.Id; Id = botResolver.Id;
UpdateBar.DependencyName = "Checking...";
IsBotRunning = botOrchestrator.IsBotRunning(botResolver.Id); IsBotRunning = botOrchestrator.IsBotRunning(botResolver.Id);
if (IsBotRunning) if (IsBotRunning)
EnableButtons(true, false); EnableButtons(true, false);
else else
EnableButtons(!Directory.Exists(botEntry.InstanceDirectoryUri), true); EnableButtons(!File.Exists(Path.Combine(botEntry.InstanceDirectoryUri, Resolver.FileName)), true);
_ = LoadUpdateBarAsync(botResolver, updateBotBar);
// Dispose when the view is deactivated // Dispose when the view is deactivated
this.WhenActivated(disposables => Disposable.Create(() => Dispose()).DisposeWith(disposables)); this.WhenActivated(disposables => Disposable.Create(() => Dispose()).DisposeWith(disposables));
@ -243,7 +245,12 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
} }
finally finally
{ {
EnableButtons(!wereButtonsUnlocked, true); _ = LoadUpdateBarAsync(Resolver, UpdateBar);
if (!wereButtonsUnlocked && File.Exists(Path.Combine(BotDirectoryUriBar.CurrentUri, Resolver.FileName)))
EnableButtons(false, true);
else
EnableButtons(!wereButtonsUnlocked, true);
} }
} }
@ -306,7 +313,7 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
var backupUri = await Resolver.CreateBackupAsync(); var backupUri = await Resolver.CreateBackupAsync();
await ((string.IsNullOrWhiteSpace(backupUri)) await (string.IsNullOrWhiteSpace(backupUri)
? _mainWindow.ShowDialogWindowAsync($"Bot {ActualBotName} not found.", DialogType.Error, Icon.Error) ? _mainWindow.ShowDialogWindowAsync($"Bot {ActualBotName} not found.", DialogType.Error, Icon.Error)
: _mainWindow.ShowDialogWindowAsync($"Successfully backed up {ActualBotName} to:{Environment.NewLine}{backupUri}", iconType: Icon.Success)); : _mainWindow.ShowDialogWindowAsync($"Successfully backed up {ActualBotName} to:{Environment.NewLine}{backupUri}", iconType: Icon.Success));
@ -322,7 +329,7 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
{ {
ButtonDefinitions = ButtonEnum.OkCancel, ButtonDefinitions = ButtonEnum.OkCancel,
ContentTitle = "Are you sure?", ContentTitle = "Are you sure?",
ContentMessage = $"Are you sure you want to delete {ActualBotName}?{Environment.NewLine}This action cannot be undone.", ContentMessage = $"Are you sure you want to delete {ActualBotName}?{Environment.NewLine}This action cannot be reversed.",
MaxWidth = int.Parse(WindowConstants.DefaultWindowWidth) / 2.0, MaxWidth = int.Parse(WindowConstants.DefaultWindowWidth) / 2.0,
SizeToContent = SizeToContent.WidthAndHeight, SizeToContent = SizeToContent.WidthAndHeight,
ShowInCenter = true, ShowInCenter = true,
@ -366,6 +373,7 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
} }
EnableButtons(true, false); EnableButtons(true, false);
_lateralBarViewModel.ToggleEnable(false);
var originalStatus = UpdateBar.Status; var originalStatus = UpdateBar.Status;
UpdateBar.Status = DependencyStatus.Updating; UpdateBar.Status = DependencyStatus.Updating;
@ -391,6 +399,10 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
await _mainWindow.ShowDialogWindowAsync($"An error occurred while updating {Resolver.DependencyName}:\n{ex.Message}", DialogType.Error, Icon.Error); await _mainWindow.ShowDialogWindowAsync($"An error occurred while updating {Resolver.DependencyName}:\n{ex.Message}", DialogType.Error, Icon.Error);
UpdateBar.Status = originalStatus; UpdateBar.Status = originalStatus;
} }
finally
{
_lateralBarViewModel.ToggleEnable(true);
}
} }
/// <summary> /// <summary>
@ -417,23 +429,28 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
/// <param name="updateBotBar">The update bar.</param> /// <param name="updateBotBar">The update bar.</param>
private async static Task LoadUpdateBarAsync(IBotResolver botResolver, DependencyButtonViewModel updateBotBar) private async static Task LoadUpdateBarAsync(IBotResolver botResolver, DependencyButtonViewModel updateBotBar)
{ {
updateBotBar.DependencyName = "Checking...";
updateBotBar.Status = DependencyStatus.Checking;
var currentVersion = await botResolver.GetCurrentVersionAsync(); var currentVersion = await botResolver.GetCurrentVersionAsync();
updateBotBar.DependencyName = (string.IsNullOrWhiteSpace(currentVersion)) updateBotBar.DependencyName = string.IsNullOrWhiteSpace(currentVersion)
? "Not Installed" ? "Not Installed"
: "EllieBot v" + currentVersion; : "EllieBot v" + currentVersion;
var canUpdate = await botResolver.CanUpdateAsync(); var canUpdate = await botResolver.CanUpdateAsync();
updateBotBar.Status = (canUpdate is true) updateBotBar.Status = canUpdate switch
? DependencyStatus.Update {
: (canUpdate is null) true => DependencyStatus.Update,
? DependencyStatus.Install false => DependencyStatus.Installed,
: DependencyStatus.Installed; null when botResolver.IsUpdateInProgress => DependencyStatus.Updating,
null => DependencyStatus.Install
};
} }
/// <summary> /// <summary>
/// Locks or unlocks the settings buttons of this view-model. /// Locks or unlocks the settings buttons of this view-model.
/// </summary> /// </summary>
/// <param name="lockButtons">Whether the settings buttons should be locked.</param> /// <param name="lockButtons">Whether the setting buttons should be locked.</param>
/// <param name="isIdle">Whether this view-model is currently undergoing an operation.</param> /// <param name="isIdle">Whether this view-model is currently undergoing an operation.</param>
private void EnableButtons(bool lockButtons, bool isIdle) private void EnableButtons(bool lockButtons, bool isIdle)
{ {
@ -453,8 +470,8 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
_logWriter.TryAdd(eventArgs.Id, eventArgs.Output); _logWriter.TryAdd(eventArgs.Id, eventArgs.Output);
FakeConsole.Content = (FakeConsole.Content.Length > 100_000) FakeConsole.Content = FakeConsole.Content.Length > 100_000
? FakeConsole.Content[FakeConsole.Content.IndexOf(Environment.NewLine, 60_000)..] + eventArgs.Output + Environment.NewLine ? FakeConsole.Content[FakeConsole.Content.IndexOf(Environment.NewLine, 60_000, StringComparison.Ordinal)..] + eventArgs.Output + Environment.NewLine
: FakeConsole.Content + eventArgs.Output + Environment.NewLine; : FakeConsole.Content + eventArgs.Output + Environment.NewLine;
} }

View file

@ -1,8 +1,8 @@
using EllieHub.ViewModels.Abstractions; using EllieHub.Features.Abstractions;
using EllieHub.Views.Controls; using EllieHub.Features.AppConfig.Views.Controls;
using ReactiveUI; using ReactiveUI;
namespace EllieHub.ViewModels.Controls; namespace EllieHub.Features.BotConfig.ViewModels;
/// <summary> /// <summary>
/// View-model for <see cref="FakeConsole"/>, the fake console that displays text. /// View-model for <see cref="FakeConsole"/>, the fake console that displays text.

View file

@ -3,13 +3,13 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:siv="https://github.com/kekyo/SkiaImageView" xmlns:siv="https://github.com/kekyo/SkiaImageView"
xmlns:vm="using:EllieHub.ViewModels.Controls" xmlns:vm="using:EllieHub.Features.BotConfig.ViewModels"
xmlns:dd="using:EllieHub.DesignData.Controls" xmlns:dd="using:EllieHub.Avalonia.DesignData.Controls"
xmlns:const="using:EllieHub.Common" xmlns:const="using:EllieHub.Common"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignWidth="{x:Static const:WindowConstants.MinWindowWidth}" d:DesignWidth="{x:Static const:WindowConstants.MinWindowWidth}"
d:DesignHeight="{x:Static const:WindowConstants.MinWindowHeight}" d:DesignHeight="{x:Static const:WindowConstants.MinWindowHeight}"
x:Class="EllieHub.Views.Controls.BotConfigView" x:Class="EllieHub.Features.BotConfig.Views.Controls.BotConfigView"
x:DataType="vm:BotConfigViewModel"> x:DataType="vm:BotConfigViewModel">
<Design.DataContext> <Design.DataContext>

View file

@ -1,9 +1,9 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using EllieHub.ViewModels.Controls; using EllieHub.Features.BotConfig.ViewModels;
namespace EllieHub.Views.Controls; namespace EllieHub.Features.BotConfig.Views.Controls;
/// <summary> /// <summary>
/// Represents the view with settings and controls for a specific bot instance. /// Represents the view with settings and controls for a specific bot instance.

View file

@ -2,10 +2,10 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:EllieHub.ViewModels.Controls" xmlns:vm="using:EllieHub.Features.BotConfig.ViewModels"
xmlns:dd="using:EllieHub.DesignData.Controls" xmlns:dd="using:EllieHub.Avalonia.DesignData.Controls"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="250" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="250"
x:Class="EllieHub.Views.Controls.FakeConsole" x:Class="EllieHub.Features.AppConfig.Views.Controls.FakeConsole"
x:DataType="vm:FakeConsoleViewModel"> x:DataType="vm:FakeConsoleViewModel">
<Design.DataContext> <Design.DataContext>
<dd:DesignFakeConsoleViewModel/> <dd:DesignFakeConsoleViewModel/>

View file

@ -1,7 +1,7 @@
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using EllieHub.ViewModels.Controls; using EllieHub.Features.BotConfig.ViewModels;
namespace EllieHub.Views.Controls; namespace EllieHub.Features.AppConfig.Views.Controls;
/// <summary> /// <summary>
/// Represents a control that mimics the appearance of a terminal emulator. /// Represents a control that mimics the appearance of a terminal emulator.

View file

@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace EllieHub.Features.Home.Models.Api.Toastielab;
/// <summary>
/// The assets of a Toastielab release.
/// </summary>
/// <param name="Name">The name of the release file.</param>
/// <param name="Url">The url of the release file.</param>
public sealed record Assets(
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("browser_download_url")] string Url
);

View file

@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace EllieHub.Features.Home.Models.Api.Toastielab;
/// <summary>
/// Represents a release from the Toastielab API.
/// </summary>
/// <param name="Tag">The tag of the release.</param>
/// <param name="Assets">The assets of the release.</param>
public sealed record ToastielabRelease(
[property: JsonPropertyName("tag_name")] string Tag,
[property: JsonPropertyName("assets")] Assets[] Assets
);

View file

@ -1,4 +1,6 @@
namespace EllieHub.Services.Abstractions; using EllieHub.Features.Shared.Services.Abstractions;
namespace EllieHub.Features.Home.Services.Abstractions;
/// <summary> /// <summary>
/// Represents a service that updates this application. /// Represents a service that updates this application.

View file

@ -1,19 +1,24 @@
using EllieHub.Services.Abstractions; using Microsoft.Extensions.Caching.Memory;
using EllieHub.Features.Home.Models.Api.Toastielab;
using EllieHub.Features.Home.Services.Abstractions;
using System.IO.Compression; using System.IO.Compression;
using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.Json;
namespace EllieHub.Services; namespace EllieHub.Features.Home.Services;
/// <summary> /// <summary>
/// Defines a service that updates this application. /// Defines a service that updates this application.
/// </summary> /// </summary>
public sealed class AppResolver : IAppResolver public sealed class AppResolver : IAppResolver
{ {
private const string _cachedCurrentVersionKey = "currentVersion:EllieHub";
private const string _toastielabReleasesEndpointUrl = "https://toastielab.dev/api/v1/repos/Emotions-stuff/EllieHub/releases/latest";
private const string _toastielabReleasesRepoUrl = "https://toastielab.dev/Emotions-stuff/EllieHub/releases/latest";
private static readonly string _tempDirectory = Path.GetTempPath(); private static readonly string _tempDirectory = Path.GetTempPath();
private static readonly string _downloadedFileName = GetDownloadFileName(); private static readonly string _downloadedFileName = GetDownloadFileName();
private static readonly string? _currentUpdaterVersion = Assembly.GetEntryAssembly()?.GetName().Version?.ToString();
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly IMemoryCache _memoryCache;
/// <inheritdoc/> /// <inheritdoc/>
public string DependencyName { get; } = "EllieHub"; public string DependencyName { get; } = "EllieHub";
@ -31,16 +36,18 @@ public sealed class AppResolver : IAppResolver
/// Creates a service that updates this application. /// Creates a service that updates this application.
/// </summary> /// </summary>
/// <param name="httpClientFactory">The Http client factory.</param> /// <param name="httpClientFactory">The Http client factory.</param>
public AppResolver(IHttpClientFactory httpClientFactory) /// <param name="memoryCache">The memory cache.</param>
public AppResolver(IHttpClientFactory httpClientFactory, IMemoryCache memoryCache)
{ {
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
FileName = (OperatingSystem.IsWindows()) ? "EllieHub.exe" : "EllieHub"; _memoryCache = memoryCache;
FileName = OperatingSystem.IsWindows() ? "EllieHub.exe" : "EllieHub";
BinaryUri = Path.Combine(AppContext.BaseDirectory, FileName); BinaryUri = Path.Combine(AppContext.BaseDirectory, FileName);
} }
/// <inheritdoc/> /// <inheritdoc/>
public ValueTask<string?> GetCurrentVersionAsync(CancellationToken cToken = default) public ValueTask<string?> GetCurrentVersionAsync(CancellationToken cToken = default)
=> ValueTask.FromResult(_currentUpdaterVersion); => ValueTask.FromResult<string?>(AppStatics.AppVersion);
/// <inheritdoc/> /// <inheritdoc/>
public void LaunchNewVersion() public void LaunchNewVersion()
@ -64,12 +71,15 @@ public sealed class AppResolver : IAppResolver
var latestVersion = await GetLatestVersionAsync(cToken); var latestVersion = await GetLatestVersionAsync(cToken);
if (latestVersion.Equals(currentVersion, StringComparison.Ordinal)) if (Version.Parse(latestVersion) <= Version.Parse(currentVersion))
return false; return false;
var http = _httpClientFactory.CreateClient(); var http = _httpClientFactory.CreateClient();
return await http.IsUrlValidAsync($"https://toastielab.dev/ToastieSharp/EllieHub/releases/download/{latestVersion}/{_downloadedFileName}", cToken); return await http.IsUrlValidAsync(
await GetDownloadUrlAsync(latestVersion, cToken),
cToken
);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -77,7 +87,7 @@ public sealed class AppResolver : IAppResolver
{ {
var result = false; var result = false;
foreach (var file in Directory.GetFiles(AppContext.BaseDirectory).Where(x => x.EndsWith(OldFileSuffix))) foreach (var file in Directory.GetFiles(AppContext.BaseDirectory).Where(x => x.EndsWith(OldFileSuffix, StringComparison.Ordinal)))
result |= Utilities.TryDeleteFile(file); result |= Utilities.TryDeleteFile(file);
return result; return result;
@ -86,14 +96,14 @@ public sealed class AppResolver : IAppResolver
/// <inheritdoc/> /// <inheritdoc/>
public async ValueTask<string> GetLatestVersionAsync(CancellationToken cToken = default) public async ValueTask<string> GetLatestVersionAsync(CancellationToken cToken = default)
{ {
var http = _httpClientFactory.CreateClient(AppConstants.NoRedirectClient); try
{
var response = await http.GetAsync("https://toastielab.dev/ToastieSharp/EllieHub/releases/latest", cToken); return (await GetLatestVersionFromApiAsync(cToken)).Tag;
}
var lastSlashIndex = response.Headers.Location?.OriginalString.LastIndexOf('/') catch (InvalidOperationException)
?? throw new InvalidOperationException("Failed to get the latest EllieBotUpdater version."); {
return await GetLatestVersionFromUrlAsync(cToken);
return response.Headers.Location.OriginalString[(lastSlashIndex + 1)..]; }
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -102,16 +112,19 @@ public sealed class AppResolver : IAppResolver
var currentVersion = await GetCurrentVersionAsync(cToken); var currentVersion = await GetCurrentVersionAsync(cToken);
var latestVersion = await GetLatestVersionAsync(cToken); var latestVersion = await GetLatestVersionAsync(cToken);
if (latestVersion.Equals(currentVersion, StringComparison.Ordinal)) if (currentVersion is not null && Version.Parse(latestVersion) <= Version.Parse(currentVersion))
return (currentVersion, null); return (currentVersion, null);
var http = _httpClientFactory.CreateClient(); var http = _httpClientFactory.CreateClient(); // Do not initialize a ToastielabClient here, it returns 302 with no data
var appTempLocation = Path.Combine(_tempDirectory, _downloadedFileName[..(_downloadedFileName.LastIndexOf('.'))]); var appTempLocation = Path.Combine(_tempDirectory, _downloadedFileName[.._downloadedFileName.LastIndexOf('.')]);
var zipTempLocation = Path.Combine(_tempDirectory, _downloadedFileName); var zipTempLocation = Path.Combine(_tempDirectory, _downloadedFileName);
try try
{ {
using var downloadStream = await http.GetStreamAsync($"https://toastielab.dev/ToastieSharp/EllieHub/releases/download/{latestVersion}/{_downloadedFileName}", cToken); using var downloadStream = await http.GetStreamAsync(
await GetDownloadUrlAsync(latestVersion, cToken),
cToken
);
// Save the zip file // Save the zip file
using (var fileStream = new FileStream(zipTempLocation, FileMode.Create)) using (var fileStream = new FileStream(zipTempLocation, FileMode.Create))
@ -182,4 +195,67 @@ public sealed class AppResolver : IAppResolver
_ => throw new NotSupportedException($"Architecture of type {RuntimeInformation.OSArchitecture} is not supported by EllieHub on this OS.") _ => throw new NotSupportedException($"Architecture of type {RuntimeInformation.OSArchitecture} is not supported by EllieHub on this OS.")
}; };
} }
/// <summary>
/// Gets the download url to the latest bot release.
/// </summary>
/// <param name="latestVersion">The latest version of the bot.</param>
/// <param name="cToken">The cancellation token.</param>
/// <returns>The url to the latest bot release.</returns>
private async ValueTask<string> GetDownloadUrlAsync(string latestVersion, CancellationToken cToken = default)
{
try
{
// The first release is the most recent one.
return (await GetLatestVersionFromApiAsync(cToken)).Assets
.First(x => x.Name.Equals(_downloadedFileName, StringComparison.Ordinal))
.Url;
}
catch (InvalidOperationException)
{
return $"https://toastielab.dev/Emotions-stuff/EllieHub/releases/download/{latestVersion}/{_downloadedFileName}";
}
}
/// <summary>
/// Gets the latest bot version from the Toastielab latest release URL.
/// </summary>
/// <param name="cToken">The cancellation token.</param>
/// <returns>The latest version of the bot.</returns>
/// <exception cref="InvalidOperationException">Occurs when parsing of the response fails.</exception>
private async ValueTask<string> GetLatestVersionFromUrlAsync(CancellationToken cToken = default)
{
var http = _httpClientFactory.CreateClient(AppConstants.NoRedirectClient);
var response = await http.GetAsync(_toastielabReleasesRepoUrl, cToken);
var lastSlashIndex = response.Headers.Location?.OriginalString.LastIndexOf('/')
?? throw new InvalidOperationException("Failed to get the latest EllieBot version.");
return response.Headers.Location.OriginalString[(lastSlashIndex + 1)..];
}
/// <summary>
/// Gets the latest bot version from the Toastielab API.
/// </summary>
/// <param name="cToken">The cancellation token.</param>
/// <returns>The latest version of the bot.</returns>
/// <exception cref="InvalidOperationException">Occurs when the API call fails.</exception>
private async ValueTask<ToastielabRelease> GetLatestVersionFromApiAsync(CancellationToken cToken = default)
{
if (_memoryCache.TryGetValue(_cachedCurrentVersionKey, out var cachedObject) && cachedObject is ToastielabRelease cachedResponse)
return cachedResponse;
var http = _httpClientFactory.CreateClient(AppConstants.ToastielabClient);
var httpResponse = await http.GetAsync(_toastielabReleasesEndpointUrl, cToken);
if (!httpResponse.IsSuccessStatusCode)
throw new InvalidOperationException("The call to the Toastielab API failed.");
var response = JsonSerializer.Deserialize<ToastielabRelease>(await httpResponse.Content.ReadAsStringAsync(cToken))
?? throw new InvalidOperationException("Failed deserializing Toastielab's response.");
_memoryCache.Set(_cachedCurrentVersionKey, response, TimeSpan.FromMinutes(1));
return response;
}
} }

View file

@ -1,8 +1,8 @@
using EllieHub.ViewModels.Abstractions; using EllieHub.Features.Abstractions;
using EllieHub.Views.Controls; using EllieHub.Features.Home.Views.Controls;
using System.Diagnostics; using System.Diagnostics;
namespace EllieHub.ViewModels.Controls; namespace EllieHub.Features.Home.ViewModels;
/// <summary> /// <summary>
/// View-model for the home window, with links to Ellie resources. /// View-model for the home window, with links to Ellie resources.

View file

@ -1,6 +1,7 @@
using EllieHub.ViewModels.Abstractions; using EllieHub.Features.Abstractions;
using EllieHub.Features.Home.Views.Windows;
namespace EllieHub.ViewModels.Windows; namespace EllieHub.Features.Home.ViewModels;
/// <summary> /// <summary>
/// View-model for the update dialog window. /// View-model for the update dialog window.

View file

@ -2,13 +2,13 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:EllieHub.ViewModels.Controls" xmlns:vm="using:EllieHub.Features.Home.ViewModels"
xmlns:const="using:EllieHub.Common" xmlns:const="using:EllieHub.Common"
xmlns:dd="using:EllieHub.DesignData.Controls" xmlns:dd="using:EllieHub.Avalonia.DesignData.Controls"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignWidth="{x:Static const:WindowConstants.MinWindowWidth}" d:DesignWidth="{x:Static const:WindowConstants.MinWindowWidth}"
d:DesignHeight="{x:Static const:WindowConstants.MinWindowHeight}" d:DesignHeight="{x:Static const:WindowConstants.MinWindowHeight}"
x:Class="EllieHub.Views.Controls.HomeView" x:Class="EllieHub.Features.Home.Views.Controls.HomeView"
x:DataType="vm:HomeViewModel"> x:DataType="vm:HomeViewModel">
<Design.DataContext> <Design.DataContext>
<dd:DesignHomeViewModel/> <dd:DesignHomeViewModel/>
@ -64,7 +64,7 @@
Classes="transparent" Classes="transparent"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Center" VerticalAlignment="Center"
CommandParameter="https://commands.elliebot.net" CommandParameter="https://commands.elliebot.net/"
Command="{Binding OpenUrl}"> Command="{Binding OpenUrl}">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<Image Classes="icon" <Image Classes="icon"
@ -92,7 +92,7 @@
Classes="transparent" Classes="transparent"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Center" VerticalAlignment="Center"
CommandParameter="https://docs.elliebot.net" CommandParameter="https://docs.elliebot.net/ellie/"
Command="{Binding OpenUrl}"> Command="{Binding OpenUrl}">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<Image Classes="icon" <Image Classes="icon"
@ -106,7 +106,7 @@
Classes="transparent" Classes="transparent"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Center" VerticalAlignment="Center"
CommandParameter="https://discord.elliebot.net/" CommandParameter="https://discord.gg/etQdZxSyEH/"
Command="{Binding OpenUrl}"> Command="{Binding OpenUrl}">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<Image Classes="icon" <Image Classes="icon"
@ -170,12 +170,12 @@
<Button Grid.Row="5" <Button Grid.Row="5"
Grid.Column="1" Grid.Column="1"
Classes="transparent" Classes="transparent"
Content="Support Ellie" Content="Support EllieBot"
Foreground="White" Foreground="White"
FontSize="12" FontSize="12"
Cursor="Hand" Cursor="Hand"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
CommandParameter="https://patreon.com/emotionchild" CommandParameter="https://patreon.com/toastiet0ast"
Command="{Binding OpenUrl}"/> Command="{Binding OpenUrl}"/>
<Image Grid.Row="5" <Image Grid.Row="5"

View file

@ -1,7 +1,7 @@
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using EllieHub.ViewModels.Controls; using EllieHub.Features.Home.ViewModels;
namespace EllieHub.Views.Controls; namespace EllieHub.Features.Home.Views.Controls;
/// <summary> /// <summary>
/// View for the home window, with buttons linking to official Ellie resources. /// View for the home window, with buttons linking to official Ellie resources.

View file

@ -2,10 +2,10 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:EllieHub.ViewModels.Windows" xmlns:vm="using:EllieHub.Features.Home.ViewModels"
xmlns:dd="using:EllieHub.DesignData.Windows" xmlns:dd="using:EllieHub.Avalonia.DesignData.Windows"
mc:Ignorable="d" d:DesignWidth="420" d:DesignHeight="100" mc:Ignorable="d" d:DesignWidth="420" d:DesignHeight="100"
x:Class="EllieHub.UpdateView" x:Class="EllieHub.Features.Home.Views.Windows.UpdateView"
x:DataType="vm:UpdateViewModel" x:DataType="vm:UpdateViewModel"
Title="Update in Progress" Title="Update in Progress"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"

View file

@ -1,7 +1,7 @@
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using EllieHub.ViewModels.Windows; using EllieHub.Features.Home.ViewModels;
namespace EllieHub; namespace EllieHub.Features.Home.Views.Windows;
/// <summary> /// <summary>
/// Represents the update dialog window of the application. /// Represents the update dialog window of the application.

View file

@ -1,6 +1,6 @@
using EllieHub.ViewModels.Controls; using EllieHub.Features.Shared.ViewModels;
namespace EllieHub.Models.EventArguments; namespace EllieHub.Features.Shared.Models;
/// <summary> /// <summary>
/// Defines the event arguments for when a valid uri is set to a <see cref="UriInputBarViewModel"/>. /// Defines the event arguments for when a valid uri is set to a <see cref="UriInputBarViewModel"/>.

View file

@ -1,4 +1,4 @@
namespace EllieHub.Services.Abstractions; namespace EllieHub.Features.Shared.Services.Abstractions;
/// <summary> /// <summary>
/// Represents a service that checks, downloads, installs, and updates a dependency. /// Represents a service that checks, downloads, installs, and updates a dependency.

View file

@ -1,12 +1,12 @@
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using EllieHub.Enums; using EllieHub.Enums;
using EllieHub.ViewModels.Abstractions; using EllieHub.Features.Abstractions;
using EllieHub.Views.Controls; using EllieHub.Features.AppWindow.Views.Windows;
using EllieHub.Views.Windows; using EllieHub.Features.Shared.Views.Controls;
using ReactiveUI; using ReactiveUI;
using System.Diagnostics; using System.Diagnostics;
namespace EllieHub.ViewModels.Controls; namespace EllieHub.Features.Shared.ViewModels;
/// <summary> /// <summary>
/// Defines the view-model for a button that installs a dependency for Ellie. /// Defines the view-model for a button that installs a dependency for Ellie.

View file

@ -1,11 +1,11 @@
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using EllieHub.Models.EventArguments; using EllieHub.Features.Abstractions;
using EllieHub.ViewModels.Abstractions; using EllieHub.Features.Shared.Models;
using EllieHub.Views.Controls; using EllieHub.Features.Shared.Views.Controls;
using ReactiveUI; using ReactiveUI;
using System.Diagnostics; using System.Diagnostics;
namespace EllieHub.ViewModels.Controls; namespace EllieHub.Features.Shared.ViewModels;
/// <summary> /// <summary>
/// Represents a text box for inputting the absolute path of a directory. /// Represents a text box for inputting the absolute path of a directory.

View file

@ -2,10 +2,10 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:EllieHub.ViewModels.Controls" xmlns:vm="using:EllieHub.Features.Shared.ViewModels"
xmlns:dd="using:EllieHub.DesignData.Controls" xmlns:dd="using:EllieHub.Avalonia.DesignData.Controls"
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="50" mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="50"
x:Class="EllieHub.Views.Controls.DependencyButton" x:Class="EllieHub.Features.Shared.Views.Controls.DependencyButton"
x:DataType="vm:DependencyButtonViewModel"> x:DataType="vm:DependencyButtonViewModel">
<Design.DataContext> <Design.DataContext>

View file

@ -1,7 +1,7 @@
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using EllieHub.ViewModels.Controls; using EllieHub.Features.Shared.ViewModels;
namespace EllieHub.Views.Controls; namespace EllieHub.Features.Shared.Views.Controls;
/// <summary> /// <summary>
/// Represents a button that installs a dependency for Ellie. /// Represents a button that installs a dependency for Ellie.

View file

@ -2,10 +2,10 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:EllieHub.ViewModels.Controls" xmlns:vm="using:EllieHub.Features.Shared.ViewModels"
xmlns:dd="using:EllieHub.DesignData.Controls" xmlns:dd="using:EllieHub.Avalonia.DesignData.Controls"
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="33" mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="33"
x:Class="EllieHub.Views.Controls.UriInputBar" x:Class="EllieHub.Features.Shared.Views.Controls.UriInputBar"
x:DataType="vm:UriInputBarViewModel"> x:DataType="vm:UriInputBarViewModel">
<Design.DataContext> <Design.DataContext>

View file

@ -1,7 +1,7 @@
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using EllieHub.ViewModels.Controls; using EllieHub.Features.Shared.ViewModels;
namespace EllieHub.Views.Controls; namespace EllieHub.Features.Shared.Views.Controls;
/// <summary> /// <summary>
/// Represents a text box that receives the absolute path to a directory. /// Represents a text box that receives the absolute path to a directory.

View file

@ -2,7 +2,7 @@ using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Media; using Avalonia.Media;
using EllieHub.ViewModels.Abstractions; using EllieHub.Features.Abstractions;
namespace EllieHub; namespace EllieHub;