diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b0b59f7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,1744 @@ + +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = false +insert_final_newline = false +indent_style = space +indent_size = 4 # A property with the same name was updated with a value 2 in a section [{*.yaml,*.yml}] + +[{*.yaml,*.yml}] +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}] + +[*.{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_size = 4 # A property with the same name was updated with a value 2 in a section [{*.yaml,*.yml}] +tab_width = 4 + +[*.cs] + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Parentheses preferences +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_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = always:error + +# Expression-level preferences +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true:warning +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:error +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true:suggestion + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:warning + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = false +dotnet_style_allow_statement_immediately_after_block_experimental = false + + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = true:error +csharp_style_var_for_built_in_types = true:error +csharp_style_var_when_type_is_apparent = true:error + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:suggestion +csharp_style_expression_bodied_constructors = when_on_single_line:suggestion +csharp_style_expression_bodied_indexers = true:suggestion +csharp_style_expression_bodied_lambdas = true:suggestion +csharp_style_expression_bodied_local_functions = true: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_properties = true:suggestion + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:error +csharp_style_pattern_matching_over_is_with_cast_check = true:error +csharp_style_prefer_not_pattern = true:error +csharp_style_prefer_pattern_matching = true:suggestion +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:error + +# Modifier preferences +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async + +# Code-block preferences +csharp_prefer_braces = when_multiline:suggestion +csharp_prefer_simple_using_statement = true + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true:error +csharp_style_inlined_variable_declaration = true:warning +csharp_style_pattern_local_over_anonymous_function = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_range_operator = true +csharp_style_throw_expression = true:error +csharp_style_unused_value_assignment_preference = discard_variable:warning +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:error + +# Enforce file-scoped namespaces +csharp_style_namespace_declarations = file_scoped:error + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false +csharp_style_allow_embedded_statements_on_same_line_experimental = true + + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +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.symbols = class +dotnet_naming_rule.class_should_be_pascal_case.style = pascal_case + +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.symbols = struct +dotnet_naming_rule.struct_should_be_pascal_case.style = pascal_case + +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.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +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.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +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.symbols = enum +dotnet_naming_rule.enum_should_be_pascal_case.style = pascal_case + +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.symbols = property +dotnet_naming_rule.property_should_be_pascal_case.style = pascal_case + +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.symbols = method +dotnet_naming_rule.method_should_be_pascal_case.style = pascal_case + +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.symbols = async_method +dotnet_naming_rule.async_method_should_be_ends_with_async.style = ends_with_async + +dotnet_naming_rule.private_field_should_be_begins_with_underscore.import_to_resharper = as_predefined +dotnet_naming_rule.private_field_should_be_begins_with_underscore.severity = error +dotnet_naming_rule.private_field_should_be_begins_with_underscore.symbols = private_field +dotnet_naming_rule.private_field_should_be_begins_with_underscore.style = begins_with_underscore + +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.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +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.symbols = local_variable +dotnet_naming_rule.local_variable_should_be_camel_case.style = camel_case + +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.symbols = public_anything +dotnet_naming_rule.public_anything_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.constants_rule.import_to_resharper = as_predefined +dotnet_naming_rule.constants_rule.severity = error +dotnet_naming_rule.constants_rule.style = upper_camel_case_style +dotnet_naming_rule.constants_rule.symbols = constants_symbols + +dotnet_naming_rule.event_rule.import_to_resharper = as_predefined +dotnet_naming_rule.event_rule.severity = error +dotnet_naming_rule.event_rule.style = upper_camel_case_style +dotnet_naming_rule.event_rule.symbols = event_symbols + +dotnet_naming_rule.interfaces_rule.import_to_resharper = as_predefined +dotnet_naming_rule.interfaces_rule.severity = error +dotnet_naming_rule.interfaces_rule.style = i_upper_camel_case_style +dotnet_naming_rule.interfaces_rule.symbols = interfaces_symbols + +dotnet_naming_rule.locals_rule.import_to_resharper = as_predefined +dotnet_naming_rule.locals_rule.severity = error +dotnet_naming_rule.locals_rule.style = lower_camel_case_style_1 +dotnet_naming_rule.locals_rule.symbols = locals_symbols + +dotnet_naming_rule.local_constants_rule.import_to_resharper = as_predefined +dotnet_naming_rule.local_constants_rule.severity = error +dotnet_naming_rule.local_constants_rule.style = lower_camel_case_style_1 +dotnet_naming_rule.local_constants_rule.symbols = local_constants_symbols + +dotnet_naming_rule.local_functions_rule.import_to_resharper = as_predefined +dotnet_naming_rule.local_functions_rule.severity = error +dotnet_naming_rule.local_functions_rule.style = upper_camel_case_style +dotnet_naming_rule.local_functions_rule.symbols = local_functions_symbols + +dotnet_naming_rule.method_rule.import_to_resharper = as_predefined +dotnet_naming_rule.method_rule.severity = error +dotnet_naming_rule.method_rule.style = upper_camel_case_style +dotnet_naming_rule.method_rule.symbols = method_symbols + +dotnet_naming_rule.parameters_rule.import_to_resharper = as_predefined +dotnet_naming_rule.parameters_rule.severity = error +dotnet_naming_rule.parameters_rule.style = lower_camel_case_style_1 +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.severity = error +dotnet_naming_rule.private_constants_rule.style = lower_camel_case_style +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.severity = error +dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols + +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.style = lower_camel_case_style +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.severity = error +dotnet_naming_rule.private_static_readonly_rule.style = lower_camel_case_style +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.severity = error +dotnet_naming_rule.property_rule.style = upper_camel_case_style +dotnet_naming_rule.property_rule.symbols = property_symbols + +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.style = upper_camel_case_style +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.severity = error +dotnet_naming_rule.public_fields_rule.style = camel_case +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.severity = error +dotnet_naming_rule.static_readonly_rule.style = upper_camel_case_style +dotnet_naming_rule.static_readonly_rule.symbols = static_readonly_symbols + +dotnet_naming_rule.types_and_namespaces_rule.import_to_resharper = as_predefined +dotnet_naming_rule.types_and_namespaces_rule.severity = error +dotnet_naming_rule.types_and_namespaces_rule.style = upper_camel_case_style +dotnet_naming_rule.types_and_namespaces_rule.symbols = types_and_namespaces_symbols + +dotnet_naming_rule.type_parameters_rule.import_to_resharper = as_predefined +dotnet_naming_rule.type_parameters_rule.severity = error +dotnet_naming_rule.type_parameters_rule.style = t_upper_camel_case_style +dotnet_naming_rule.type_parameters_rule.symbols = type_parameters_symbols + +# Symbol specifications + +dotnet_naming_symbols.class.applicable_kinds = class +dotnet_naming_symbols.class.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.class.required_modifiers = + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.struct.applicable_kinds = struct +dotnet_naming_symbols.struct.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.struct.required_modifiers = + +dotnet_naming_symbols.enum.applicable_kinds = enum +dotnet_naming_symbols.enum.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.enum.required_modifiers = + +dotnet_naming_symbols.method.applicable_kinds = method +dotnet_naming_symbols.method.applicable_accessibilities = public +dotnet_naming_symbols.method.required_modifiers = + +dotnet_naming_symbols.property.applicable_kinds = property +dotnet_naming_symbols.property.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.property.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +dotnet_naming_symbols.private_field.applicable_kinds = field +dotnet_naming_symbols.private_field.applicable_accessibilities = private +dotnet_naming_symbols.private_field.required_modifiers = + +dotnet_naming_symbols.async_method.applicable_kinds = method, local_function +dotnet_naming_symbols.async_method.applicable_accessibilities = * +dotnet_naming_symbols.async_method.required_modifiers = async + +dotnet_naming_symbols.local_variable.applicable_kinds = parameter, local +dotnet_naming_symbols.local_variable.applicable_accessibilities = local +dotnet_naming_symbols.local_variable.required_modifiers = + +dotnet_naming_symbols.public_anything.applicable_kinds = property, field, event, class, struct, interface, enum, delegate, method +dotnet_naming_symbols.public_anything.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_anything.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.begins_with_underscore.required_prefix = _ +dotnet_naming_style.begins_with_underscore.required_suffix = +dotnet_naming_style.begins_with_underscore.word_separator = +dotnet_naming_style.begins_with_underscore.capitalization = camel_case + +dotnet_naming_style.ends_with_async.required_prefix = +dotnet_naming_style.ends_with_async.required_suffix = Async +dotnet_naming_style.ends_with_async.word_separator = +dotnet_naming_style.ends_with_async.capitalization = pascal_case + +dotnet_naming_style.camel_case.required_prefix = +dotnet_naming_style.camel_case.required_suffix = +dotnet_naming_style.camel_case.word_separator = +dotnet_naming_style.camel_case.capitalization = camel_case + +dotnet_naming_style.i_upper_camel_case_style.capitalization = pascal_case +dotnet_naming_style.i_upper_camel_case_style.required_prefix = I + +dotnet_naming_style.lower_camel_case_style.capitalization = camel_case +dotnet_naming_style.lower_camel_case_style.required_prefix = _ +dotnet_naming_style.lower_camel_case_style_1.capitalization = camel_case + +dotnet_naming_style.t_upper_camel_case_style.capitalization = pascal_case +dotnet_naming_style.t_upper_camel_case_style.required_prefix = T + +dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case + +dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.constants_symbols.applicable_kinds = field +dotnet_naming_symbols.constants_symbols.required_modifiers = const + +dotnet_naming_symbols.event_symbols.applicable_accessibilities = * +dotnet_naming_symbols.event_symbols.applicable_kinds = event + +dotnet_naming_symbols.interfaces_symbols.applicable_accessibilities = * +dotnet_naming_symbols.interfaces_symbols.applicable_kinds = interface + +dotnet_naming_symbols.locals_symbols.applicable_accessibilities = * +dotnet_naming_symbols.locals_symbols.applicable_kinds = local + +dotnet_naming_symbols.local_constants_symbols.applicable_accessibilities = * +dotnet_naming_symbols.local_constants_symbols.applicable_kinds = local +dotnet_naming_symbols.local_constants_symbols.required_modifiers = const +dotnet_naming_symbols.local_functions_symbols.applicable_accessibilities = * +dotnet_naming_symbols.local_functions_symbols.applicable_kinds = local_function + +dotnet_naming_symbols.method_symbols.applicable_accessibilities = * +dotnet_naming_symbols.method_symbols.applicable_kinds = method + +dotnet_naming_symbols.parameters_symbols.applicable_accessibilities = * +dotnet_naming_symbols.parameters_symbols.applicable_kinds = parameter + +dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field +dotnet_naming_symbols.private_constants_symbols.required_modifiers = const + +dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field + +dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static + +dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly + +dotnet_naming_symbols.property_symbols.applicable_accessibilities = * +dotnet_naming_symbols.property_symbols.applicable_kinds = property + +dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_fields_symbols.applicable_kinds = field + +dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.static_readonly_symbols.applicable_kinds = field +dotnet_naming_symbols.static_readonly_symbols.required_modifiers = static,readonly + +dotnet_naming_symbols.types_and_namespaces_symbols.applicable_accessibilities = * +dotnet_naming_symbols.types_and_namespaces_symbols.applicable_kinds = namespace,class,struct,enum,delegate + +dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities = * +dotnet_naming_symbols.type_parameters_symbols.applicable_kinds = type_parameter + + +#### Visual Studio Specific Rules #### + +# CA1822: Mark members as static +dotnet_diagnostic.CA1822.severity = none + +# IDE0004: Cast is redundant +dotnet_diagnostic.IDE0004.severity = error + +# IDE0058: Expression value is never used +dotnet_diagnostic.IDE0058.severity = none + +# IDE0011: Add braces to 'if'/'else' statement +dotnet_diagnostic.IDE0011.severity = none + +# IDE0290: Use primary constructor +dotnet_diagnostic.IDE0290.severity = none + +#### ReSharper Properties #### + +resharper_accessor_owner_body = expression_body +resharper_alignment_tab_fill_style = use_spaces +resharper_align_first_arg_by_paren = false +resharper_align_linq_query = false +resharper_align_multiline_array_and_object_initializer = false +resharper_align_multiline_array_initializer = true +resharper_align_multiline_binary_expressions_chain = false +resharper_align_multiline_binary_patterns = false +resharper_align_multiline_ctor_init = true +resharper_align_multiline_expression_braces = false +resharper_align_multiline_implements_list = true +resharper_align_multiline_property_pattern = false +resharper_align_multiline_statement_conditions = true +resharper_align_multiline_switch_expression = false +resharper_align_multiline_type_argument = true +resharper_align_multiline_type_parameter = true +resharper_align_multline_type_parameter_constrains = true +resharper_align_multline_type_parameter_list = false +resharper_align_ternary = align_not_nested +resharper_align_tuple_components = false +resharper_allow_alias = true +resharper_allow_comment_after_lbrace = false +resharper_allow_far_alignment = false +resharper_always_use_end_of_line_brace_style = false +resharper_apply_auto_detected_rules = true +resharper_apply_on_completion = false +resharper_arguments_anonymous_function = positional +resharper_arguments_literal = positional +resharper_arguments_named = positional +resharper_arguments_other = positional +resharper_arguments_skip_single = false +resharper_arguments_string_literal = positional +resharper_attribute_style = do_not_touch +resharper_autodetect_indent_settings = true +resharper_blank_lines_after_block_statements = 1 +resharper_blank_lines_after_case = 0 +resharper_blank_lines_after_control_transfer_statements = 1 +resharper_blank_lines_after_file_scoped_namespace_directive = 1 +resharper_blank_lines_after_imports = 1 +resharper_blank_lines_after_multiline_statements = 1 +resharper_blank_lines_after_options = 1 +resharper_blank_lines_after_start_comment = 1 +resharper_blank_lines_after_using_list = 1 +resharper_blank_lines_around_accessor = 0 +resharper_blank_lines_around_auto_property = 1 +resharper_blank_lines_around_block_case_section = 0 +resharper_blank_lines_around_class_definition = 1 +resharper_blank_lines_around_field = 1 +resharper_blank_lines_around_function_declaration = 0 +resharper_blank_lines_around_function_definition = 1 +resharper_blank_lines_around_global_attribute = 0 +resharper_blank_lines_around_invocable = 1 +resharper_blank_lines_around_local_method = 1 +resharper_blank_lines_around_multiline_case_section = 1 +resharper_blank_lines_around_namespace = 1 +resharper_blank_lines_around_other_declaration = 0 +resharper_blank_lines_around_property = 1 +resharper_blank_lines_around_razor_functions = 1 +resharper_blank_lines_around_razor_helpers = 1 +resharper_blank_lines_around_razor_sections = 1 +resharper_blank_lines_around_region = 1 +resharper_blank_lines_around_single_line_accessor = 0 +resharper_blank_lines_around_single_line_auto_property = 0 +resharper_blank_lines_around_single_line_field = 0 +resharper_blank_lines_around_single_line_function_definition = 0 +resharper_blank_lines_around_single_line_invocable = 0 +resharper_blank_lines_around_single_line_local_method = 0 +resharper_blank_lines_around_single_line_property = 0 +resharper_blank_lines_around_single_line_type = 0 +resharper_blank_lines_around_type = 1 +resharper_blank_lines_before_block_statements = 1 +resharper_blank_lines_before_case = 0 +resharper_blank_lines_before_control_transfer_statements = 1 +resharper_blank_lines_before_multiline_statements = 1 +resharper_blank_lines_before_single_line_comment = 1 +resharper_blank_lines_inside_namespace = 0 +resharper_blank_lines_inside_region = 1 +resharper_blank_lines_inside_type = 0 +resharper_blank_line_after_pi = true +resharper_braces_for_dowhile = required +resharper_braces_for_fixed = required +resharper_braces_for_for = required_for_multiline +resharper_braces_for_foreach = required_for_multiline +resharper_braces_for_ifelse = required_for_multiline +resharper_braces_for_lock = required +resharper_braces_for_using = required +resharper_braces_for_while = required_for_multiline +resharper_braces_redundant = true +resharper_break_template_declaration = line_break +resharper_can_use_global_alias = false +resharper_configure_await_analysis_mode = disabled +resharper_constructor_or_destructor_body = expression_body +resharper_continuous_indent_multiplier = 1 +resharper_continuous_line_indent = single +resharper_csharp_align_multiline_argument = false +resharper_csharp_align_multiline_calls_chain = false +resharper_csharp_align_multiline_expression = false +resharper_csharp_align_multiline_extends_list = false +resharper_csharp_align_multiline_for_stmt = false +resharper_csharp_align_multiline_parameter = false +resharper_csharp_align_multiple_declaration = false +resharper_csharp_int_align_comments = true +resharper_csharp_max_line_length = 120 +resharper_csharp_naming_rule.enum_member = AaBb +resharper_csharp_naming_rule.method_property_event = AaBb +resharper_csharp_naming_rule.other = AaBb +resharper_csharp_new_line_before_while = false +resharper_csharp_prefer_qualified_reference = false +resharper_csharp_space_after_unary_operator = false +resharper_csharp_stick_comment = false +resharper_csharp_wrap_after_declaration_lpar = true +resharper_csharp_wrap_after_invocation_lpar = true +resharper_csharp_wrap_arguments_style = chop_if_long +resharper_csharp_wrap_before_declaration_rpar = true +resharper_csharp_wrap_before_invocation_rpar = true +resharper_csharp_wrap_lines = true +resharper_csharp_wrap_parameters_style = chop_if_long +resharper_cxxcli_property_declaration_braces = next_line +resharper_default_exception_variable_name = e +resharper_default_value_when_type_evident = default_literal +resharper_default_value_when_type_not_evident = default_literal +resharper_delete_quotes_from_solid_values = false +resharper_disable_blank_line_changes = false +resharper_disable_formatter = false +resharper_disable_indenter = false +resharper_disable_int_align = false +resharper_disable_line_break_changes = false +resharper_disable_line_break_removal = false +resharper_disable_space_changes = false +resharper_disable_space_changes_before_trailing_comment = false +resharper_dont_remove_extra_blank_lines = false +resharper_empty_block_style = multiline +resharper_enable_wrapping = false +resharper_enforce_line_ending_style = true +resharper_event_handler_pattern_long = $object$On$event$ +resharper_event_handler_pattern_short = On$event$ +resharper_expression_braces = inside +resharper_expression_pars = inside +resharper_extra_spaces = remove_all +resharper_force_attribute_style = join +resharper_force_chop_compound_do_expression = false +resharper_force_chop_compound_if_expression = false +resharper_force_chop_compound_while_expression = false +resharper_format_leading_spaces_decl = false +resharper_free_block_braces = next_line +resharper_function_declaration_return_type_style = do_not_change +resharper_function_definition_return_type_style = do_not_change +resharper_generator_mode = false +resharper_html_attribute_indent = align_by_first_attribute +resharper_html_linebreak_before_elements = body,div,p,form,h1,h2,h3 +resharper_html_max_blank_lines_between_tags = 2 +resharper_html_max_line_length = 120 +resharper_html_pi_attribute_style = on_single_line +resharper_html_space_before_self_closing = false +resharper_html_wrap_lines = true +resharper_ignore_space_preservation = false +resharper_include_prefix_comment_in_indent = false +resharper_indent_access_specifiers_from_class = false +resharper_indent_aligned_ternary = true +resharper_indent_anonymous_method_block = false +resharper_indent_braces_inside_statement_conditions = true +resharper_indent_case_from_select = true +resharper_indent_child_elements = OneIndent +resharper_indent_class_members_from_access_specifiers = false +resharper_indent_comment = true +resharper_indent_inside_namespace = true +resharper_indent_invocation_pars = inside +resharper_indent_method_decl_pars = inside +resharper_indent_nested_fixed_stmt = false +resharper_indent_nested_foreach_stmt = false +resharper_indent_nested_for_stmt = false +resharper_indent_nested_lock_stmt = false +resharper_indent_nested_usings_stmt = false +resharper_indent_nested_while_stmt = false +resharper_indent_pars = inside +resharper_indent_preprocessor_directives = none +resharper_indent_preprocessor_if = no_indent +resharper_indent_preprocessor_other = no_indent +resharper_indent_preprocessor_region = usual_indent +resharper_indent_statement_pars = inside +resharper_indent_text = OneIndent +resharper_indent_typearg_angles = inside +resharper_indent_typeparam_angles = inside +resharper_indent_type_constraints = true +resharper_indent_wrapped_function_names = false +resharper_instance_members_qualify_declared_in = base_class +resharper_int_align_assignments = false +resharper_int_align_binary_expressions = false +resharper_int_align_declaration_names = false +resharper_int_align_eq = false +resharper_int_align_fields = false +resharper_int_align_fix_in_adjacent = true +resharper_int_align_invocations = false +resharper_int_align_methods = false +resharper_int_align_nested_ternary = true +resharper_int_align_parameters = false +resharper_int_align_properties = false +resharper_int_align_property_patterns = false +resharper_int_align_switch_expressions = false +resharper_int_align_switch_sections = false +resharper_int_align_variables = false +resharper_keep_blank_lines_in_code = 2 +resharper_keep_blank_lines_in_declarations = 2 +resharper_keep_existing_attribute_arrangement = false +resharper_keep_existing_declaration_block_arrangement = false +resharper_keep_existing_declaration_parens_arrangement = true +resharper_keep_existing_embedded_arrangement = true +resharper_keep_existing_embedded_block_arrangement = false +resharper_keep_existing_enum_arrangement = false +resharper_keep_existing_expr_member_arrangement = false +resharper_keep_existing_invocation_parens_arrangement = true +resharper_keep_existing_property_patterns_arrangement = true +resharper_keep_existing_switch_expression_arrangement = false +resharper_keep_nontrivial_alias = true +resharper_keep_user_linebreaks = true +resharper_keep_user_wrapping = true +resharper_linebreaks_around_razor_statements = true +resharper_linebreaks_inside_tags_for_elements_longer_than = 2147483647 +resharper_linebreaks_inside_tags_for_elements_with_child_elements = true +resharper_linebreaks_inside_tags_for_multiline_elements = true +resharper_linebreak_before_all_elements = false +resharper_linebreak_before_multiline_elements = true +resharper_linebreak_before_singleline_elements = false +resharper_line_break_after_colon_in_member_initializer_lists = do_not_change +resharper_line_break_after_comma_in_member_initializer_lists = false +resharper_line_break_before_comma_in_member_initializer_lists = false +resharper_line_break_before_requires_clause = do_not_change +resharper_linkage_specification_braces = end_of_line +resharper_linkage_specification_indentation = none +resharper_local_function_body = expression_body +resharper_macro_block_begin = +resharper_macro_block_end = +resharper_max_array_initializer_elements_on_line = 10000 +resharper_max_attribute_length_for_same_line = 38 +resharper_max_enum_members_on_line = 3 +resharper_max_formal_parameters_on_line = 10000 +resharper_max_initializer_elements_on_line = 5 +resharper_max_invocation_arguments_on_line = 10000 +resharper_member_initializer_list_style = do_not_change +resharper_method_or_operator_body = expression_body +resharper_namespace_body = file_scoped +resharper_namespace_declaration_braces = next_line +resharper_namespace_indentation = all +resharper_nested_ternary_style = expanded +resharper_new_line_before_catch = true +resharper_new_line_before_else = true +resharper_new_line_before_enumerators = true +resharper_normalize_tag_names = false +resharper_no_indent_inside_elements = html,body,thead,tbody,tfoot +resharper_no_indent_inside_if_element_longer_than = 200 +resharper_object_creation_when_type_evident = target_typed +resharper_object_creation_when_type_not_evident = target_typed +resharper_old_engine = false +resharper_outdent_binary_ops = false +resharper_outdent_binary_pattern_ops = false +resharper_outdent_commas = false +resharper_outdent_dots = false +resharper_outdent_namespace_member = false +resharper_outdent_statement_labels = false +resharper_outdent_ternary_ops = false +resharper_parentheses_non_obvious_operations = none, conditional, conditional_or, conditional_and, bitwise, bitwise_inclusive_or, bitwise_exclusive_or, shift, arithmetic, additive, multiplicative, bitwise_and +resharper_parentheses_redundancy_style = remove_if_not_clarifies_precedence +resharper_parentheses_same_type_operations = false +resharper_pi_attributes_indent = align_by_first_attribute +resharper_place_accessorholder_attribute_on_same_line = false +resharper_place_accessor_attribute_on_same_line = if_owner_is_single_line +resharper_place_comments_at_first_column = false +resharper_place_constructor_initializer_on_same_line = true +resharper_place_event_attribute_on_same_line = false +resharper_place_expr_accessor_on_single_line = if_owner_is_single_line +resharper_place_expr_method_on_single_line = false +resharper_place_expr_property_on_single_line = false +resharper_place_field_attribute_on_same_line = true +resharper_place_linq_into_on_new_line = false +resharper_place_method_attribute_on_same_line = false +resharper_place_namespace_definitions_on_same_line = false +resharper_place_property_attribute_on_same_line = false +resharper_place_simple_case_statement_on_same_line = false +resharper_place_simple_embedded_statement_on_same_line = false +resharper_place_simple_enum_on_single_line = true +resharper_place_simple_initializer_on_single_line = true +resharper_place_simple_property_pattern_on_single_line = true +resharper_place_simple_switch_expression_on_single_line = false +resharper_place_type_attribute_on_same_line = false +resharper_place_type_constraints_on_same_line = true +resharper_prefer_explicit_discard_declaration = false +resharper_prefer_separate_deconstructed_variables_declaration = true +resharper_preserve_spaces_inside_tags = pre,textarea +resharper_qualified_using_at_nested_scope = false +resharper_quote_style = doublequoted +resharper_razor_prefer_qualified_reference = true +resharper_remove_blank_lines_near_braces = false +resharper_remove_blank_lines_near_braces_in_code = true +resharper_remove_blank_lines_near_braces_in_declarations = true +resharper_remove_this_qualifier = true +resharper_requires_expression_braces = next_line +resharper_resx_attribute_indent = single_indent +resharper_resx_linebreak_before_elements = +resharper_resx_max_blank_lines_between_tags = 0 +resharper_resx_max_line_length = 2147483647 +resharper_resx_pi_attribute_style = do_not_touch +resharper_resx_space_before_self_closing = false +resharper_resx_wrap_lines = false +resharper_resx_wrap_tags_and_pi = false +resharper_resx_wrap_text = false +resharper_show_autodetect_configure_formatting_tip = true +resharper_simple_block_style = do_not_change +resharper_simple_case_statement_style = do_not_change +resharper_simple_embedded_statement_style = do_not_change +resharper_sort_attributes = false +resharper_sort_class_selectors = false +resharper_sort_usings = true +resharper_sort_usings_lowercase_first = false +resharper_spaces_around_eq_in_attribute = false +resharper_spaces_around_eq_in_pi_attribute = false +resharper_spaces_inside_tags = false +resharper_space_after_attributes = true +resharper_space_after_attribute_target_colon = true +resharper_space_after_colon = true +resharper_space_after_colon_in_case = true +resharper_space_after_colon_in_inheritance_clause = true +resharper_space_after_comma = true +resharper_space_after_for_colon = true +resharper_space_after_keywords_in_control_flow_statements = true +resharper_space_after_last_attribute = false +resharper_space_after_last_pi_attribute = false +resharper_space_after_operator_keyword = true +resharper_space_after_ptr_in_data_member = true +resharper_space_after_ptr_in_data_members = false +resharper_space_after_ptr_in_method = true +resharper_space_after_ref_in_data_member = true +resharper_space_after_ref_in_data_members = false +resharper_space_after_ref_in_method = true +resharper_space_after_semicolon_in_for_statement = true +resharper_space_after_ternary_colon = true +resharper_space_after_ternary_quest = true +resharper_space_after_triple_slash = true +resharper_space_after_type_parameter_constraint_colon = true +resharper_space_around_additive_op = true +resharper_space_around_alias_eq = true +resharper_space_around_assignment_op = true +resharper_space_around_assignment_operator = true +resharper_space_around_deref_in_trailing_return_type = true +resharper_space_around_lambda_arrow = true +resharper_space_around_member_access_operator = false +resharper_space_around_relational_op = true +resharper_space_around_shift_op = true +resharper_space_around_stmt_colon = true +resharper_space_around_ternary_operator = true +resharper_space_before_array_rank_parentheses = false +resharper_space_before_attribute_target_colon = false +resharper_space_before_checked_parentheses = false +resharper_space_before_colon = false +resharper_space_before_colon_in_case = false +resharper_space_before_colon_in_inheritance_clause = true +resharper_space_before_comma = false +resharper_space_before_default_parentheses = false +resharper_space_before_empty_invocation_parentheses = false +resharper_space_before_empty_method_parentheses = false +resharper_space_before_for_colon = true +resharper_space_before_initializer_braces = false +resharper_space_before_invocation_parentheses = false +resharper_space_before_label_colon = false +resharper_space_before_lambda_parentheses = false +resharper_space_before_method_parentheses = false +resharper_space_before_nameof_parentheses = false +resharper_space_before_new_parentheses = false +resharper_space_before_nullable_mark = false +resharper_space_before_open_square_brackets = false +resharper_space_before_pointer_asterik_declaration = false +resharper_space_before_ptr_in_abstract_decl = false +resharper_space_before_ptr_in_data_member = false +resharper_space_before_ptr_in_data_members = true +resharper_space_before_ptr_in_method = false +resharper_space_before_ref_in_abstract_decl = false +resharper_space_before_ref_in_data_member = false +resharper_space_before_ref_in_data_members = true +resharper_space_before_ref_in_method = false +resharper_space_before_semicolon = false +resharper_space_before_semicolon_in_for_statement = false +resharper_space_before_singleline_accessorholder = true +resharper_space_before_sizeof_parentheses = false +resharper_space_before_template_args = false +resharper_space_before_template_params = true +resharper_space_before_ternary_colon = true +resharper_space_before_ternary_quest = true +resharper_space_before_trailing_comment = true +resharper_space_before_typeof_parentheses = false +resharper_space_before_type_argument_angle = false +resharper_space_before_type_parameter_angle = false +resharper_space_before_type_parameter_constraint_colon = true +resharper_space_before_type_parameter_parentheses = true +resharper_space_between_accessors_in_singleline_property = true +resharper_space_between_attribute_sections = true +resharper_space_between_closing_angle_brackets_in_template_args = false +resharper_space_between_keyword_and_expression = true +resharper_space_between_keyword_and_type = true +resharper_space_between_method_call_empty_parameter_list_parentheses = false +resharper_space_between_method_call_name_and_opening_parenthesis = false +resharper_space_between_method_call_parameter_list_parentheses = false +resharper_space_between_method_declaration_empty_parameter_list_parentheses = false +resharper_space_between_method_declaration_name_and_open_parenthesis = false +resharper_space_between_method_declaration_parameter_list_parentheses = false +resharper_space_between_parentheses_of_control_flow_statements = false +resharper_space_between_square_brackets = false +resharper_space_between_typecast_parentheses = false +resharper_space_in_singleline_accessorholder = true +resharper_space_in_singleline_anonymous_method = true +resharper_space_in_singleline_method = true +resharper_space_near_postfix_and_prefix_op = false +resharper_space_within_array_initialization_braces = false +resharper_space_within_array_rank_empty_parentheses = false +resharper_space_within_array_rank_parentheses = false +resharper_space_within_attribute_angles = false +resharper_space_within_checked_parentheses = false +resharper_space_within_default_parentheses = false +resharper_space_within_empty_braces = true +resharper_space_within_empty_initializer_braces = false +resharper_space_within_empty_invocation_parentheses = false +resharper_space_within_empty_method_parentheses = false +resharper_space_within_empty_template_params = false +resharper_space_within_expression_parentheses = false +resharper_space_within_initializer_braces = false +resharper_space_within_invocation_parentheses = false +resharper_space_within_method_parentheses = false +resharper_space_within_nameof_parentheses = false +resharper_space_within_new_parentheses = false +resharper_space_within_parentheses = false +resharper_space_within_single_line_array_initializer_braces = false +resharper_space_within_sizeof_parentheses = false +resharper_space_within_template_args = false +resharper_space_within_template_params = false +resharper_space_within_tuple_parentheses = false +resharper_space_within_typeof_parentheses = false +resharper_space_within_type_argument_angles = false +resharper_space_within_type_parameter_angles = false +resharper_space_within_type_parameter_parentheses = false +resharper_special_else_if_treatment = true +resharper_static_members_qualify_members = none +resharper_static_members_qualify_with = declared_type +resharper_support_vs_event_naming_pattern = true +resharper_T4_max_line_length = 120 +resharper_T4_wrap_lines = true +resharper_toplevel_function_declaration_return_type_style = do_not_change +resharper_toplevel_function_definition_return_type_style = do_not_change +resharper_trailing_comma_in_multiline_lists = false +resharper_trailing_comma_in_singleline_lists = false +resharper_use_continuous_indent_inside_initializer_braces = true +resharper_use_continuous_indent_inside_parens = true +resharper_use_continuous_line_indent_in_expression_braces = false +resharper_use_continuous_line_indent_in_method_pars = false +resharper_use_heuristics_for_body_style = true +resharper_use_indents_from_main_language_in_file = true +resharper_use_indent_from_previous_element = true +resharper_use_indent_from_vs = false +resharper_use_roslyn_logic_for_evident_types = true +resharper_wrap_after_binary_opsign = true +resharper_wrap_after_dot = false +resharper_wrap_after_dot_in_method_calls = false +resharper_wrap_after_expression_lbrace = true +resharper_wrap_around_elements = true +resharper_wrap_array_initializer_style = chop_if_long +resharper_wrap_base_clause_style = wrap_if_long +resharper_wrap_before_arrow_with_expressions = true +resharper_wrap_before_binary_opsign = false +resharper_wrap_before_binary_pattern_op = true +resharper_wrap_before_colon = false +resharper_wrap_before_comma = false +resharper_wrap_before_comma_in_base_clause = false +resharper_wrap_before_declaration_lpar = false +resharper_wrap_before_eq = false +resharper_wrap_before_expression_rbrace = true +resharper_wrap_before_extends_colon = false +resharper_wrap_before_first_type_parameter_constraint = false +resharper_wrap_before_invocation_lpar = false +resharper_wrap_before_linq_expression = false +resharper_wrap_before_ternary_opsigns = true +resharper_wrap_before_type_parameter_langle = false +resharper_wrap_braced_init_list_style = wrap_if_long +resharper_wrap_chained_binary_expressions = chop_if_long +resharper_wrap_chained_binary_patterns = wrap_if_long +resharper_wrap_chained_method_calls = chop_if_long +resharper_wrap_ctor_initializer_style = wrap_if_long +resharper_wrap_enumeration_style = chop_if_long +resharper_wrap_enum_declaration = chop_always +resharper_wrap_extends_list_style = wrap_if_long +resharper_wrap_for_stmt_header_style = chop_if_long +resharper_wrap_linq_expressions = chop_always +resharper_wrap_multiple_declaration_style = chop_if_long +resharper_wrap_multiple_type_parameter_constraints_style = chop_if_long +resharper_wrap_object_and_collection_initializer_style = chop_if_long +resharper_wrap_property_pattern = chop_if_long +resharper_wrap_switch_expression = chop_always +resharper_wrap_ternary_expr_style = chop_if_long +resharper_wrap_verbatim_interpolated_strings = no_wrap +resharper_xmldoc_attribute_indent = single_indent +resharper_xmldoc_linebreak_before_elements = summary,remarks,example,returns,param,typeparam,value,para +resharper_xmldoc_max_blank_lines_between_tags = 0 +resharper_xmldoc_max_line_length = 120 +resharper_xmldoc_pi_attribute_style = do_not_touch +resharper_xmldoc_space_before_self_closing = true +resharper_xmldoc_wrap_lines = true +resharper_xmldoc_wrap_tags_and_pi = true +resharper_xmldoc_wrap_text = true +resharper_xml_attribute_indent = align_by_first_attribute +resharper_xml_linebreak_before_elements = +resharper_xml_max_blank_lines_between_tags = 2 +resharper_xml_max_line_length = 120 +resharper_xml_pi_attribute_style = do_not_touch +resharper_xml_space_before_self_closing = true +resharper_xml_wrap_lines = true +resharper_xml_wrap_tags_and_pi = true +resharper_xml_wrap_text = false + +# ReSharper inspection severities +resharper_access_rights_in_text_highlighting = warning +resharper_access_to_disposed_closure_highlighting = warning +resharper_access_to_for_each_variable_in_closure_highlighting = warning +resharper_access_to_modified_closure_highlighting = warning +resharper_access_to_static_member_via_derived_type_highlighting = warning +resharper_address_of_marshal_by_ref_object_highlighting = warning +resharper_angular_html_banana_highlighting = warning +resharper_annotate_can_be_null_parameter_highlighting = none +resharper_annotate_can_be_null_type_member_highlighting = none +resharper_annotate_not_null_parameter_highlighting = none +resharper_annotate_not_null_type_member_highlighting = none +resharper_annotation_conflict_in_hierarchy_highlighting = warning +resharper_annotation_redundancy_at_value_type_highlighting = warning +resharper_annotation_redundancy_in_hierarchy_highlighting = warning +resharper_anonymous_object_destructuring_problem_highlighting = warning +resharper_arguments_style_anonymous_function_highlighting = hint +resharper_arguments_style_literal_highlighting = hint +resharper_arguments_style_named_expression_highlighting = hint +resharper_arguments_style_other_highlighting = hint +resharper_arguments_style_string_literal_highlighting = hint +resharper_arrange_accessor_owner_body_highlighting = error +resharper_arrange_attributes_highlighting = none +resharper_arrange_constructor_or_destructor_body_highlighting = error +resharper_arrange_default_value_when_type_evident_highlighting = suggestion +resharper_arrange_default_value_when_type_not_evident_highlighting = suggestion +resharper_arrange_local_function_body_highlighting = error +resharper_arrange_method_or_operator_body_highlighting = error +resharper_arrange_missing_parentheses_highlighting = hint +resharper_arrange_namespace_body_highlighting = error +resharper_arrange_object_creation_when_type_evident_highlighting = suggestion +resharper_arrange_object_creation_when_type_not_evident_highlighting = suggestion +resharper_arrange_redundant_parentheses_highlighting = hint +resharper_arrange_static_member_qualifier_highlighting = hint +resharper_arrange_trailing_comma_in_multiline_lists_highlighting = hint +resharper_arrange_trailing_comma_in_singleline_lists_highlighting = hint +resharper_arrange_var_keywords_in_deconstructing_declaration_highlighting = suggestion +resharper_assignment_in_conditional_expression_highlighting = warning +resharper_assignment_is_fully_discarded_highlighting = warning +resharper_assign_null_to_not_null_attribute_highlighting = warning +resharper_asxx_path_error_highlighting = warning +resharper_async_iterator_invocation_without_await_foreach_highlighting = warning +resharper_async_void_lambda_highlighting = warning +resharper_async_void_method_highlighting = warning +resharper_auto_property_can_be_made_get_only_global_highlighting = suggestion +resharper_auto_property_can_be_made_get_only_local_highlighting = suggestion +resharper_bad_attribute_brackets_spaces_highlighting = none +resharper_bad_braces_spaces_highlighting = none +resharper_bad_child_statement_indent_highlighting = warning +resharper_bad_colon_spaces_highlighting = none +resharper_bad_comma_spaces_highlighting = none +resharper_bad_control_braces_indent_highlighting = suggestion +resharper_bad_control_braces_line_breaks_highlighting = none +resharper_bad_declaration_braces_indent_highlighting = none +resharper_bad_declaration_braces_line_breaks_highlighting = none +resharper_bad_empty_braces_line_breaks_highlighting = none +resharper_bad_expression_braces_indent_highlighting = none +resharper_bad_expression_braces_line_breaks_highlighting = none +resharper_bad_generic_brackets_spaces_highlighting = none +resharper_bad_indent_highlighting = none +resharper_bad_linq_line_breaks_highlighting = none +resharper_bad_list_line_breaks_highlighting = none +resharper_bad_member_access_spaces_highlighting = none +resharper_bad_namespace_braces_indent_highlighting = none +resharper_bad_parens_line_breaks_highlighting = none +resharper_bad_parens_spaces_highlighting = none +resharper_bad_preprocessor_indent_highlighting = none +resharper_bad_semicolon_spaces_highlighting = none +resharper_bad_spaces_after_keyword_highlighting = none +resharper_bad_square_brackets_spaces_highlighting = none +resharper_bad_switch_braces_indent_highlighting = none +resharper_bad_symbol_spaces_highlighting = none +resharper_base_member_has_params_highlighting = warning +resharper_base_method_call_with_default_parameter_highlighting = warning +resharper_base_object_equals_is_object_equals_highlighting = warning +resharper_base_object_get_hash_code_call_in_get_hash_code_highlighting = warning +resharper_bitwise_operator_on_enum_without_flags_highlighting = warning +resharper_built_in_type_reference_style_for_member_access_highlighting = hint +resharper_built_in_type_reference_style_highlighting = hint +resharper_by_ref_argument_is_volatile_field_highlighting = warning +resharper_cannot_apply_equality_operator_to_type_highlighting = warning +resharper_center_tag_is_obsolete_highlighting = warning +resharper_check_for_reference_equality_instead_1_highlighting = suggestion +resharper_check_for_reference_equality_instead_2_highlighting = suggestion +resharper_check_for_reference_equality_instead_3_highlighting = suggestion +resharper_check_for_reference_equality_instead_4_highlighting = suggestion +resharper_check_namespace_highlighting = warning +resharper_class_cannot_be_instantiated_highlighting = warning +resharper_class_can_be_sealed_global_highlighting = none +resharper_class_can_be_sealed_local_highlighting = none +resharper_class_never_instantiated_global_highlighting = none +resharper_class_never_instantiated_local_highlighting = suggestion +resharper_class_with_virtual_members_never_inherited_global_highlighting = suggestion +resharper_class_with_virtual_members_never_inherited_local_highlighting = suggestion +resharper_clear_attribute_is_obsolete_all_highlighting = warning +resharper_clear_attribute_is_obsolete_highlighting = warning +resharper_collection_never_queried_global_highlighting = warning +resharper_collection_never_queried_local_highlighting = warning +resharper_collection_never_updated_global_highlighting = warning +resharper_collection_never_updated_local_highlighting = warning +resharper_comment_typo_highlighting = suggestion +resharper_compare_non_constrained_generic_with_null_highlighting = none +resharper_compare_of_floats_by_equality_operator_highlighting = warning +resharper_complex_object_destructuring_problem_highlighting = warning +resharper_complex_object_in_context_destructuring_problem_highlighting = warning +resharper_conditional_ternary_equal_branch_highlighting = warning +resharper_condition_is_always_true_or_false_highlighting = warning +resharper_confusing_char_as_integer_in_constructor_highlighting = warning +resharper_constant_conditional_access_qualifier_highlighting = warning +resharper_constant_null_coalescing_condition_highlighting = warning +resharper_constructor_initializer_loop_highlighting = warning +resharper_container_annotation_redundancy_highlighting = warning +resharper_contextual_logger_problem_highlighting = warning +resharper_context_value_is_provided_highlighting = none +resharper_contract_annotation_not_parsed_highlighting = warning +resharper_convert_closure_to_method_group_highlighting = suggestion +resharper_convert_conditional_ternary_expression_to_switch_expression_highlighting = hint +resharper_convert_if_do_to_while_highlighting = suggestion +resharper_convert_if_statement_to_conditional_ternary_expression_highlighting = suggestion +resharper_convert_if_statement_to_null_coalescing_assignment_highlighting = suggestion +resharper_convert_if_statement_to_null_coalescing_expression_highlighting = suggestion +resharper_convert_if_statement_to_return_statement_highlighting = hint +resharper_convert_if_statement_to_switch_expression_highlighting = hint +resharper_convert_if_statement_to_switch_statement_highlighting = hint +resharper_convert_if_to_or_expression_highlighting = suggestion +resharper_convert_nullable_to_short_form_highlighting = suggestion +resharper_convert_switch_statement_to_switch_expression_highlighting = hint +resharper_convert_to_auto_property_highlighting = suggestion +resharper_convert_to_auto_property_when_possible_highlighting = hint +resharper_convert_to_auto_property_with_private_setter_highlighting = hint +resharper_convert_to_compound_assignment_highlighting = hint +resharper_convert_to_constant_global_highlighting = hint +resharper_convert_to_constant_local_highlighting = hint +resharper_convert_to_lambda_expression_highlighting = suggestion +resharper_convert_to_lambda_expression_when_possible_highlighting = none +resharper_convert_to_local_function_highlighting = suggestion +resharper_convert_to_null_coalescing_compound_assignment_highlighting = suggestion +resharper_convert_to_primary_constructor_highlighting = suggestion +resharper_convert_to_static_class_highlighting = suggestion +resharper_convert_to_using_declaration_highlighting = suggestion +resharper_convert_to_vb_auto_property_highlighting = suggestion +resharper_convert_to_vb_auto_property_when_possible_highlighting = hint +resharper_convert_to_vb_auto_property_with_private_setter_highlighting = hint +resharper_convert_type_check_pattern_to_null_check_highlighting = warning +resharper_convert_type_check_to_null_check_highlighting = warning +resharper_co_variant_array_conversion_highlighting = warning +resharper_c_declaration_with_implicit_int_type_highlighting = warning +resharper_c_sharp_build_cs_invalid_module_name_highlighting = warning +resharper_c_sharp_missing_plugin_dependency_highlighting = warning +resharper_default_value_attribute_for_optional_parameter_highlighting = warning +resharper_dl_tag_contains_non_dt_or_dd_elements_highlighting = hint +resharper_double_negation_in_pattern_highlighting = suggestion +resharper_double_negation_operator_highlighting = suggestion +resharper_duplicate_resource_highlighting = warning +resharper_dynamic_shift_right_op_is_not_int_highlighting = warning +resharper_empty_constructor_highlighting = warning +resharper_empty_destructor_highlighting = warning +resharper_empty_embedded_statement_highlighting = warning +resharper_empty_for_statement_highlighting = warning +resharper_empty_general_catch_clause_highlighting = warning +resharper_empty_namespace_highlighting = warning +resharper_empty_statement_highlighting = warning +resharper_empty_title_tag_highlighting = hint +resharper_enforce_do_while_statement_braces_highlighting = error +resharper_enforce_fixed_statement_braces_highlighting = error +resharper_enforce_foreach_statement_braces_highlighting = error +resharper_enforce_for_statement_braces_highlighting = error +resharper_enforce_if_statement_braces_highlighting = error +resharper_enforce_lock_statement_braces_highlighting = error +resharper_enforce_using_statement_braces_highlighting = error +resharper_enforce_while_statement_braces_highlighting = error +resharper_entity_name_captured_only_global_highlighting = warning +resharper_entity_name_captured_only_local_highlighting = warning +resharper_enumerable_sum_in_explicit_unchecked_context_highlighting = warning +resharper_enum_underlying_type_is_int_highlighting = warning +resharper_equal_expression_comparison_highlighting = warning +resharper_escaped_keyword_highlighting = warning +resharper_event_never_invoked_global_highlighting = suggestion +resharper_event_never_subscribed_to_global_highlighting = suggestion +resharper_event_never_subscribed_to_local_highlighting = suggestion +resharper_event_unsubscription_via_anonymous_delegate_highlighting = warning +resharper_exception_passed_as_template_argument_problem_highlighting = warning +resharper_explicit_caller_info_argument_highlighting = warning +resharper_expression_is_always_null_highlighting = warning +resharper_field_can_be_made_read_only_global_highlighting = suggestion +resharper_field_can_be_made_read_only_local_highlighting = suggestion +resharper_field_hides_interface_property_with_default_implementation_highlighting = warning +resharper_foreach_can_be_converted_to_query_using_another_get_enumerator_highlighting = hint +resharper_foreach_can_be_partly_converted_to_query_using_another_get_enumerator_highlighting = hint +resharper_format_string_placeholders_mismatch_highlighting = warning +resharper_format_string_problem_highlighting = warning +resharper_for_can_be_converted_to_foreach_highlighting = suggestion +resharper_for_statement_condition_is_true_highlighting = warning +resharper_function_complexity_overflow_highlighting = none +resharper_function_never_returns_highlighting = warning +resharper_function_recursive_on_all_paths_highlighting = warning +resharper_gc_suppress_finalize_for_type_without_destructor_highlighting = warning +resharper_generic_enumerator_not_disposed_highlighting = warning +resharper_heap_view_boxing_allocation_highlighting = hint +resharper_heap_view_can_avoid_closure_highlighting = suggestion +resharper_heap_view_closure_allocation_highlighting = hint +resharper_heap_view_delegate_allocation_highlighting = hint +resharper_heap_view_object_allocation_evident_highlighting = hint +resharper_heap_view_object_allocation_highlighting = hint +resharper_heap_view_object_allocation_possible_highlighting = hint +resharper_heap_view_possible_boxing_allocation_highlighting = hint +resharper_heuristic_unreachable_code_highlighting = warning +resharper_identifier_typo_highlighting = suggestion +resharper_ignored_directive_highlighting = warning +resharper_inactive_preprocessor_branch_highlighting = warning +resharper_inconsistently_synchronized_field_highlighting = warning +resharper_inconsistent_context_log_property_naming_highlighting = warning +resharper_inconsistent_log_property_naming_highlighting = warning +resharper_inconsistent_naming_highlighting = warning +resharper_inconsistent_order_of_locks_highlighting = warning +resharper_incorrect_blank_lines_near_braces_highlighting = none +resharper_indexing_by_invalid_range_highlighting = warning +resharper_inheritdoc_consider_usage_highlighting = none +resharper_inheritdoc_invalid_usage_highlighting = warning +resharper_inline_out_variable_declaration_highlighting = suggestion +resharper_inline_temporary_variable_highlighting = hint +resharper_internal_or_private_member_not_documented_highlighting = none +resharper_interpolated_string_expression_is_not_i_formattable_highlighting = warning +resharper_introduce_optional_parameters_global_highlighting = suggestion +resharper_introduce_optional_parameters_local_highlighting = suggestion +resharper_int_division_by_zero_highlighting = warning +resharper_int_variable_overflow_highlighting = warning +resharper_int_variable_overflow_in_checked_context_highlighting = warning +resharper_int_variable_overflow_in_unchecked_context_highlighting = warning +resharper_invalid_value_type_highlighting = warning +resharper_invalid_xml_doc_comment_highlighting = warning +resharper_invert_condition_1_highlighting = hint +resharper_invert_if_highlighting = hint +resharper_invocation_is_skipped_highlighting = hint +resharper_invoke_as_extension_method_highlighting = suggestion +resharper_is_expression_always_false_highlighting = warning +resharper_is_expression_always_true_highlighting = warning +resharper_iterator_method_result_is_ignored_highlighting = warning +resharper_iterator_never_returns_highlighting = warning +resharper_join_declaration_and_initializer_highlighting = suggestion +resharper_join_null_check_with_usage_highlighting = suggestion +resharper_join_null_check_with_usage_when_possible_highlighting = none +resharper_lambda_expression_can_be_made_static_highlighting = none +resharper_lambda_expression_must_be_static_highlighting = suggestion +resharper_lambda_should_not_capture_context_highlighting = warning +resharper_localizable_element_highlighting = warning +resharper_local_function_can_be_made_static_highlighting = none +resharper_local_function_hides_method_highlighting = warning +resharper_local_variable_hides_member_highlighting = warning +resharper_log_message_is_sentence_problem_highlighting = warning +resharper_long_literal_ending_lower_l_highlighting = warning +resharper_loop_can_be_converted_to_query_highlighting = hint +resharper_loop_can_be_partly_converted_to_query_highlighting = none +resharper_loop_variable_is_never_changed_inside_loop_highlighting = warning +resharper_markup_attribute_typo_highlighting = suggestion +resharper_markup_text_typo_highlighting = suggestion +resharper_math_abs_method_is_redundant_highlighting = warning +resharper_math_clamp_min_greater_than_max_highlighting = warning +resharper_meaningless_default_parameter_value_highlighting = warning +resharper_member_can_be_internal_highlighting = none +resharper_member_can_be_made_static_global_highlighting = hint +resharper_member_can_be_made_static_local_highlighting = hint +resharper_member_can_be_private_global_highlighting = suggestion +resharper_member_can_be_private_local_highlighting = suggestion +resharper_member_can_be_protected_global_highlighting = suggestion +resharper_member_can_be_protected_local_highlighting = suggestion +resharper_member_hides_interface_member_with_default_implementation_highlighting = warning +resharper_member_hides_static_from_outer_class_highlighting = warning +resharper_member_initializer_value_ignored_highlighting = warning +resharper_merge_and_pattern_highlighting = suggestion +resharper_merge_cast_with_type_check_highlighting = suggestion +resharper_merge_conditional_expression_highlighting = suggestion +resharper_merge_conditional_expression_when_possible_highlighting = none +resharper_merge_into_logical_pattern_highlighting = hint +resharper_merge_into_negated_pattern_highlighting = hint +resharper_merge_into_pattern_highlighting = suggestion +resharper_merge_nested_property_patterns_highlighting = suggestion +resharper_merge_sequential_checks_highlighting = hint +resharper_merge_sequential_checks_when_possible_highlighting = none +resharper_method_has_async_overload_highlighting = suggestion +resharper_method_has_async_overload_with_cancellation_highlighting = suggestion +resharper_method_overload_with_optional_parameter_highlighting = warning +resharper_method_supports_cancellation_highlighting = suggestion +resharper_missing_alt_attribute_in_img_tag_highlighting = hint +resharper_missing_blank_lines_highlighting = none +resharper_missing_body_tag_highlighting = warning +resharper_missing_head_and_body_tags_highlighting = warning +resharper_missing_head_tag_highlighting = warning +resharper_missing_indent_highlighting = none +resharper_missing_linebreak_highlighting = none +resharper_missing_space_highlighting = none +resharper_missing_title_tag_highlighting = hint +resharper_more_specific_foreach_variable_type_available_highlighting = suggestion +resharper_move_to_existing_positional_deconstruction_pattern_highlighting = hint +resharper_multiple_nullable_attributes_usage_highlighting = warning +resharper_multiple_order_by_highlighting = warning +resharper_multiple_resolve_candidates_in_text_highlighting = warning +resharper_multiple_spaces_highlighting = none +resharper_multiple_statements_on_one_line_highlighting = none +resharper_multiple_type_members_on_one_line_highlighting = none +resharper_must_use_return_value_highlighting = warning +resharper_negation_of_relational_pattern_highlighting = suggestion +resharper_negative_equality_expression_highlighting = suggestion +resharper_negative_index_highlighting = warning +resharper_nested_string_interpolation_highlighting = suggestion +resharper_non_atomic_compound_operator_highlighting = warning +resharper_non_constant_equality_expression_has_constant_result_highlighting = warning +resharper_non_parsable_element_highlighting = warning +resharper_non_readonly_member_in_get_hash_code_highlighting = warning +resharper_non_volatile_field_in_double_check_locking_highlighting = warning +resharper_not_accessed_field_global_highlighting = suggestion +resharper_not_accessed_field_local_highlighting = warning +resharper_not_accessed_positional_property_global_highlighting = warning +resharper_not_accessed_positional_property_local_highlighting = warning +resharper_not_accessed_variable_highlighting = warning +resharper_not_assigned_out_parameter_highlighting = warning +resharper_not_declared_in_parent_culture_highlighting = warning +resharper_not_null_member_is_not_initialized_highlighting = warning +resharper_not_observable_annotation_redundancy_highlighting = warning +resharper_not_overridden_in_specific_culture_highlighting = warning +resharper_not_resolved_in_text_highlighting = warning +resharper_object_creation_as_statement_highlighting = warning +resharper_obsolete_element_error_highlighting = error +resharper_obsolete_element_highlighting = warning +resharper_ol_tag_contains_non_li_elements_highlighting = hint +resharper_one_way_operation_contract_with_return_type_highlighting = warning +resharper_operation_contract_without_service_contract_highlighting = warning +resharper_operator_is_can_be_used_highlighting = warning +resharper_optional_parameter_hierarchy_mismatch_highlighting = warning +resharper_optional_parameter_ref_out_highlighting = warning +resharper_other_tags_inside_script1_highlighting = error +resharper_other_tags_inside_script2_highlighting = error +resharper_other_tags_inside_unclosed_script_highlighting = error +resharper_outdent_is_off_prev_level_highlighting = none +resharper_out_parameter_value_is_always_discarded_global_highlighting = suggestion +resharper_out_parameter_value_is_always_discarded_local_highlighting = warning +resharper_overridden_with_empty_value_highlighting = warning +resharper_overridden_with_same_value_highlighting = suggestion +resharper_parameter_hides_member_highlighting = warning +resharper_parameter_only_used_for_precondition_check_global_highlighting = suggestion +resharper_parameter_only_used_for_precondition_check_local_highlighting = warning +resharper_parameter_type_can_be_enumerable_global_highlighting = hint +resharper_parameter_type_can_be_enumerable_local_highlighting = hint +resharper_partial_method_parameter_name_mismatch_highlighting = warning +resharper_partial_method_with_single_part_highlighting = warning +resharper_partial_type_with_single_part_highlighting = warning +resharper_pass_string_interpolation_highlighting = hint +resharper_pattern_always_matches_highlighting = warning +resharper_pattern_is_always_true_or_false_highlighting = warning +resharper_pattern_never_matches_highlighting = warning +resharper_polymorphic_field_like_event_invocation_highlighting = warning +resharper_positional_property_used_problem_highlighting = warning +resharper_possible_infinite_inheritance_highlighting = warning +resharper_possible_intended_rethrow_highlighting = warning +resharper_possible_interface_member_ambiguity_highlighting = warning +resharper_possible_invalid_cast_exception_highlighting = warning +resharper_possible_invalid_cast_exception_in_foreach_loop_highlighting = warning +resharper_possible_invalid_operation_exception_highlighting = warning +resharper_possible_loss_of_fraction_highlighting = warning +resharper_possible_mistaken_argument_highlighting = warning +resharper_possible_mistaken_call_to_get_type_1_highlighting = warning +resharper_possible_mistaken_call_to_get_type_2_highlighting = warning +resharper_possible_multiple_enumeration_highlighting = warning +resharper_possible_multiple_write_access_in_double_check_locking_highlighting = warning +resharper_possible_null_reference_exception_highlighting = warning +resharper_possible_struct_member_modification_of_non_variable_struct_highlighting = warning +resharper_possible_unintended_linear_search_in_set_highlighting = warning +resharper_possible_unintended_queryable_as_enumerable_highlighting = suggestion +resharper_possible_unintended_reference_comparison_highlighting = warning +resharper_possible_write_to_me_highlighting = warning +resharper_possibly_impure_method_call_on_readonly_variable_highlighting = warning +resharper_possibly_missing_indexer_initializer_comma_highlighting = warning +resharper_possibly_mistaken_use_of_interpolated_string_insert_highlighting = warning +resharper_possibly_mistaken_use_of_params_method_highlighting = warning +resharper_private_field_can_be_converted_to_local_variable_highlighting = warning +resharper_property_can_be_made_init_only_global_highlighting = suggestion +resharper_property_can_be_made_init_only_local_highlighting = suggestion +resharper_property_not_resolved_highlighting = error +resharper_public_constructor_in_abstract_class_highlighting = suggestion +resharper_pure_attribute_on_void_method_highlighting = warning +resharper_read_access_in_double_check_locking_highlighting = warning +resharper_redundant_abstract_modifier_highlighting = warning +resharper_redundant_always_match_subpattern_highlighting = suggestion +resharper_redundant_anonymous_type_property_name_highlighting = warning +resharper_redundant_argument_default_value_highlighting = warning +resharper_redundant_array_creation_expression_highlighting = hint +resharper_redundant_array_lower_bound_specification_highlighting = warning +resharper_redundant_assignment_highlighting = warning +resharper_redundant_attribute_parentheses_highlighting = hint +resharper_redundant_attribute_usage_property_highlighting = suggestion +resharper_redundant_base_constructor_call_highlighting = warning +resharper_redundant_base_qualifier_highlighting = warning +resharper_redundant_blank_lines_highlighting = none +resharper_redundant_bool_compare_highlighting = warning +resharper_redundant_case_label_highlighting = warning +resharper_redundant_cast_highlighting = warning +resharper_redundant_catch_clause_highlighting = warning +resharper_redundant_check_before_assignment_highlighting = warning +resharper_redundant_collection_initializer_element_braces_highlighting = hint +resharper_redundant_configure_await_highlighting = suggestion +resharper_redundant_declaration_semicolon_highlighting = hint +resharper_redundant_default_member_initializer_highlighting = warning +resharper_redundant_delegate_creation_highlighting = warning +resharper_redundant_disable_warning_comment_highlighting = warning +resharper_redundant_discard_designation_highlighting = suggestion +resharper_redundant_empty_case_else_highlighting = warning +resharper_redundant_empty_finally_block_highlighting = warning +resharper_redundant_empty_object_creation_argument_list_highlighting = hint +resharper_redundant_empty_object_or_collection_initializer_highlighting = warning +resharper_redundant_empty_switch_section_highlighting = warning +resharper_redundant_enumerable_cast_call_highlighting = warning +resharper_redundant_enum_case_label_for_default_section_highlighting = none +resharper_redundant_explicit_array_creation_highlighting = warning +resharper_redundant_explicit_array_size_highlighting = warning +resharper_redundant_explicit_nullable_creation_highlighting = warning +resharper_redundant_explicit_params_array_creation_highlighting = suggestion +resharper_redundant_explicit_positional_property_declaration_highlighting = warning +resharper_redundant_explicit_tuple_component_name_highlighting = warning +resharper_redundant_extends_list_entry_highlighting = warning +resharper_redundant_fixed_pointer_declaration_highlighting = suggestion +resharper_redundant_if_else_block_highlighting = hint +resharper_redundant_if_statement_then_keyword_highlighting = none +resharper_redundant_immediate_delegate_invocation_highlighting = suggestion +resharper_redundant_include_highlighting = warning +resharper_redundant_is_before_relational_pattern_highlighting = suggestion +resharper_redundant_iterator_keyword_highlighting = warning +resharper_redundant_jump_statement_highlighting = warning +resharper_redundant_lambda_parameter_type_highlighting = warning +resharper_redundant_lambda_signature_parentheses_highlighting = hint +resharper_redundant_linebreak_highlighting = none +resharper_redundant_logical_conditional_expression_operand_highlighting = warning +resharper_redundant_me_qualifier_highlighting = warning +resharper_redundant_my_base_qualifier_highlighting = warning +resharper_redundant_my_class_qualifier_highlighting = warning +resharper_redundant_name_qualifier_highlighting = warning +resharper_redundant_not_null_constraint_highlighting = warning +resharper_redundant_nullable_annotation_on_reference_type_constraint_highlighting = warning +resharper_redundant_nullable_annotation_on_type_constraint_has_non_nullable_base_type_highlighting = warning +resharper_redundant_nullable_annotation_on_type_constraint_has_non_nullable_type_kind_highlighting = warning +resharper_redundant_nullable_flow_attribute_highlighting = warning +resharper_redundant_nullable_type_mark_highlighting = warning +resharper_redundant_nullness_attribute_with_nullable_reference_types_highlighting = warning +resharper_redundant_overflow_checking_context_highlighting = warning +resharper_redundant_overload_global_highlighting = suggestion +resharper_redundant_overload_local_highlighting = suggestion +resharper_redundant_overridden_member_highlighting = warning +resharper_redundant_params_highlighting = warning +resharper_redundant_parentheses_highlighting = none +resharper_redundant_pattern_parentheses_highlighting = hint +resharper_redundant_property_parentheses_highlighting = hint +resharper_redundant_property_pattern_clause_highlighting = suggestion +resharper_redundant_qualifier_highlighting = warning +resharper_redundant_query_order_by_ascending_keyword_highlighting = hint +resharper_redundant_range_bound_highlighting = suggestion +resharper_redundant_readonly_modifier_highlighting = suggestion +resharper_redundant_record_body_highlighting = warning +resharper_redundant_record_class_keyword_highlighting = warning +resharper_redundant_setter_value_parameter_declaration_highlighting = hint +resharper_redundant_space_highlighting = none +resharper_redundant_string_format_call_highlighting = warning +resharper_redundant_string_interpolation_highlighting = suggestion +resharper_redundant_string_to_char_array_call_highlighting = warning +resharper_redundant_string_type_highlighting = suggestion +resharper_redundant_suppress_nullable_warning_expression_highlighting = warning +resharper_redundant_ternary_expression_highlighting = warning +resharper_redundant_to_string_call_for_value_type_highlighting = hint +resharper_redundant_to_string_call_highlighting = warning +resharper_redundant_type_arguments_of_method_highlighting = warning +resharper_redundant_type_check_in_pattern_highlighting = warning +resharper_redundant_unsafe_context_highlighting = warning +resharper_redundant_using_directive_global_highlighting = warning +resharper_redundant_using_directive_highlighting = warning +resharper_redundant_verbatim_prefix_highlighting = suggestion +resharper_redundant_verbatim_string_prefix_highlighting = suggestion +resharper_redundant_with_expression_highlighting = suggestion +resharper_reference_equals_with_value_type_highlighting = warning +resharper_reg_exp_inspections_highlighting = warning +resharper_remove_constructor_invocation_highlighting = none +resharper_remove_redundant_braces_highlighting = warning +resharper_remove_redundant_or_statement_false_highlighting = suggestion +resharper_remove_redundant_or_statement_true_highlighting = suggestion +resharper_remove_to_list_1_highlighting = suggestion +resharper_remove_to_list_2_highlighting = suggestion +resharper_replace_auto_property_with_computed_property_highlighting = hint +resharper_replace_object_pattern_with_var_pattern_highlighting = suggestion +resharper_replace_slice_with_range_indexer_highlighting = hint +resharper_replace_substring_with_range_indexer_highlighting = hint +resharper_replace_with_first_or_default_1_highlighting = suggestion +resharper_replace_with_first_or_default_2_highlighting = suggestion +resharper_replace_with_first_or_default_3_highlighting = suggestion +resharper_replace_with_first_or_default_4_highlighting = suggestion +resharper_replace_with_last_or_default_1_highlighting = suggestion +resharper_replace_with_last_or_default_2_highlighting = suggestion +resharper_replace_with_last_or_default_3_highlighting = suggestion +resharper_replace_with_last_or_default_4_highlighting = suggestion +resharper_replace_with_of_type_1_highlighting = suggestion +resharper_replace_with_of_type_2_highlighting = suggestion +resharper_replace_with_of_type_3_highlighting = suggestion +resharper_replace_with_of_type_any_1_highlighting = suggestion +resharper_replace_with_of_type_any_2_highlighting = suggestion +resharper_replace_with_of_type_count_1_highlighting = suggestion +resharper_replace_with_of_type_count_2_highlighting = suggestion +resharper_replace_with_of_type_first_1_highlighting = suggestion +resharper_replace_with_of_type_first_2_highlighting = suggestion +resharper_replace_with_of_type_first_or_default_1_highlighting = suggestion +resharper_replace_with_of_type_first_or_default_2_highlighting = suggestion +resharper_replace_with_of_type_last_1_highlighting = suggestion +resharper_replace_with_of_type_last_2_highlighting = suggestion +resharper_replace_with_of_type_last_or_default_1_highlighting = suggestion +resharper_replace_with_of_type_last_or_default_2_highlighting = suggestion +resharper_replace_with_of_type_long_count_highlighting = suggestion +resharper_replace_with_of_type_single_1_highlighting = suggestion +resharper_replace_with_of_type_single_2_highlighting = suggestion +resharper_replace_with_of_type_single_or_default_1_highlighting = suggestion +resharper_replace_with_of_type_single_or_default_2_highlighting = suggestion +resharper_replace_with_of_type_where_highlighting = suggestion +resharper_replace_with_simple_assignment_false_highlighting = suggestion +resharper_replace_with_simple_assignment_true_highlighting = suggestion +resharper_replace_with_single_assignment_false_highlighting = suggestion +resharper_replace_with_single_assignment_true_highlighting = suggestion +resharper_replace_with_single_call_to_any_highlighting = suggestion +resharper_replace_with_single_call_to_count_highlighting = suggestion +resharper_replace_with_single_call_to_first_highlighting = suggestion +resharper_replace_with_single_call_to_first_or_default_highlighting = suggestion +resharper_replace_with_single_call_to_last_highlighting = suggestion +resharper_replace_with_single_call_to_last_or_default_highlighting = suggestion +resharper_replace_with_single_call_to_single_highlighting = suggestion +resharper_replace_with_single_call_to_single_or_default_highlighting = suggestion +resharper_replace_with_single_or_default_1_highlighting = suggestion +resharper_replace_with_single_or_default_2_highlighting = suggestion +resharper_replace_with_single_or_default_3_highlighting = suggestion +resharper_replace_with_single_or_default_4_highlighting = suggestion +resharper_replace_with_string_is_null_or_empty_highlighting = suggestion +resharper_required_base_types_conflict_highlighting = warning +resharper_required_base_types_direct_conflict_highlighting = warning +resharper_required_base_types_is_not_inherited_highlighting = warning +resharper_resource_item_not_resolved_highlighting = error +resharper_resource_not_resolved_highlighting = error +resharper_resx_not_resolved_highlighting = warning +resharper_return_type_can_be_enumerable_global_highlighting = hint +resharper_return_type_can_be_enumerable_local_highlighting = hint +resharper_return_type_can_be_not_nullable_highlighting = warning +resharper_return_value_of_pure_method_is_not_used_highlighting = warning +resharper_route_templates_action_route_prefix_can_be_extracted_to_controller_route_highlighting = hint +resharper_route_templates_ambiguous_matching_constraint_constructor_highlighting = warning +resharper_route_templates_ambiguous_route_match_highlighting = warning +resharper_route_templates_constraint_argument_cannot_be_converted_highlighting = warning +resharper_route_templates_controller_route_parameter_is_not_passed_to_methods_highlighting = hint +resharper_route_templates_duplicated_parameter_highlighting = warning +resharper_route_templates_matching_constraint_constructor_not_resolved_highlighting = warning +resharper_route_templates_method_missing_route_parameters_highlighting = hint +resharper_route_templates_optional_parameter_can_be_preceded_only_by_single_period_highlighting = warning +resharper_route_templates_optional_parameter_must_be_at_the_end_of_segment_highlighting = warning +resharper_route_templates_parameter_constraint_can_be_specified_highlighting = hint +resharper_route_templates_parameter_type_and_constraints_mismatch_highlighting = warning +resharper_route_templates_parameter_type_can_be_made_stricter_highlighting = suggestion +resharper_route_templates_route_parameter_constraint_not_resolved_highlighting = warning +resharper_route_templates_route_parameter_is_not_passed_to_method_highlighting = hint +resharper_route_templates_route_token_not_resolved_highlighting = warning +resharper_route_templates_symbol_not_resolved_highlighting = warning +resharper_route_templates_syntax_error_highlighting = warning +resharper_safe_cast_is_used_as_type_check_highlighting = warning +resharper_script_tag_has_both_src_and_content_attributes_highlighting = error +resharper_script_tag_with_content_before_includes_highlighting = hint +resharper_sealed_member_in_sealed_class_highlighting = warning +resharper_separate_control_transfer_statement_highlighting = none +resharper_service_contract_without_operations_highlighting = warning +resharper_shift_expression_real_shift_count_is_zero_highlighting = warning +resharper_shift_expression_result_equals_zero_highlighting = warning +resharper_shift_expression_right_operand_not_equal_real_count_highlighting = warning +resharper_shift_expression_zero_left_operand_highlighting = warning +resharper_similar_anonymous_type_nearby_highlighting = hint +resharper_simplify_conditional_operator_highlighting = suggestion +resharper_simplify_conditional_ternary_expression_highlighting = suggestion +resharper_simplify_i_if_highlighting = suggestion +resharper_simplify_linq_expression_use_all_highlighting = suggestion +resharper_simplify_linq_expression_use_any_highlighting = suggestion +resharper_simplify_string_interpolation_highlighting = suggestion +resharper_specify_a_culture_in_string_conversion_explicitly_highlighting = warning +resharper_specify_string_comparison_highlighting = hint +resharper_spin_lock_in_readonly_field_highlighting = warning +resharper_stack_alloc_inside_loop_highlighting = warning +resharper_static_member_initializer_referes_to_member_below_highlighting = warning +resharper_static_member_in_generic_type_highlighting = warning +resharper_static_problem_in_text_highlighting = warning +resharper_string_compare_is_culture_specific_1_highlighting = warning +resharper_string_compare_is_culture_specific_2_highlighting = warning +resharper_string_compare_is_culture_specific_3_highlighting = warning +resharper_string_compare_is_culture_specific_4_highlighting = warning +resharper_string_compare_is_culture_specific_5_highlighting = warning +resharper_string_compare_is_culture_specific_6_highlighting = warning +resharper_string_compare_to_is_culture_specific_highlighting = warning +resharper_string_ends_with_is_culture_specific_highlighting = none +resharper_string_index_of_is_culture_specific_1_highlighting = warning +resharper_string_index_of_is_culture_specific_2_highlighting = warning +resharper_string_index_of_is_culture_specific_3_highlighting = warning +resharper_string_last_index_of_is_culture_specific_1_highlighting = warning +resharper_string_last_index_of_is_culture_specific_2_highlighting = warning +resharper_string_last_index_of_is_culture_specific_3_highlighting = warning +resharper_string_literal_as_interpolation_argument_highlighting = suggestion +resharper_string_literal_typo_highlighting = suggestion +resharper_string_starts_with_is_culture_specific_highlighting = none +resharper_structured_message_template_problem_highlighting = warning +resharper_struct_can_be_made_read_only_highlighting = suggestion +resharper_struct_member_can_be_made_read_only_highlighting = none +resharper_suggest_base_type_for_parameter_highlighting = hint +resharper_suggest_base_type_for_parameter_in_constructor_highlighting = hint +resharper_suggest_discard_declaration_var_style_highlighting = hint +resharper_suggest_var_or_type_deconstruction_declarations_highlighting = hint +resharper_suppress_nullable_warning_expression_as_inverted_is_expression_highlighting = warning +resharper_suspicious_lock_over_synchronization_primitive_highlighting = warning +resharper_suspicious_math_sign_method_highlighting = warning +resharper_suspicious_parameter_name_in_argument_null_exception_highlighting = warning +resharper_suspicious_type_conversion_global_highlighting = warning +resharper_swap_via_deconstruction_highlighting = suggestion +resharper_switch_expression_handles_some_known_enum_values_with_exception_in_default_highlighting = hint +resharper_switch_statement_for_enum_misses_default_section_highlighting = hint +resharper_switch_statement_handles_some_known_enum_values_with_default_highlighting = hint +resharper_switch_statement_missing_some_enum_cases_no_default_highlighting = hint +resharper_symbol_from_not_copied_locally_reference_used_warning_highlighting = warning +resharper_tabs_and_spaces_mismatch_highlighting = none +resharper_tabs_are_disallowed_highlighting = none +resharper_tabs_outside_indent_highlighting = none +resharper_tail_recursive_call_highlighting = hint +resharper_template_duplicate_property_problem_highlighting = warning +resharper_template_format_string_problem_highlighting = warning +resharper_template_is_not_compile_time_constant_problem_highlighting = warning +resharper_thread_static_at_instance_field_highlighting = warning +resharper_thread_static_field_has_initializer_highlighting = warning +resharper_too_wide_local_variable_scope_highlighting = suggestion +resharper_try_cast_always_succeeds_highlighting = suggestion +resharper_try_statements_can_be_merged_highlighting = hint +resharper_type_parameter_can_be_variant_highlighting = suggestion +resharper_ul_tag_contains_non_li_elements_highlighting = hint +resharper_unassigned_field_global_highlighting = suggestion +resharper_unassigned_field_local_highlighting = warning +resharper_unassigned_get_only_auto_property_highlighting = warning +resharper_unassigned_readonly_field_highlighting = warning +resharper_unclosed_script_highlighting = error +resharper_unexpected_attribute_highlighting = warning +resharper_unexpected_directive_highlighting = warning +resharper_unnecessary_whitespace_highlighting = none +resharper_unreachable_switch_arm_due_to_integer_analysis_highlighting = warning +resharper_unreachable_switch_case_due_to_integer_analysis_highlighting = warning +resharper_unreal_header_tool_error_highlighting = error +resharper_unreal_header_tool_parser_error_highlighting = error +resharper_unreal_header_tool_warning_highlighting = warning +resharper_unsupported_required_base_type_highlighting = warning +resharper_unused_anonymous_method_signature_highlighting = warning +resharper_unused_auto_property_accessor_global_highlighting = warning +resharper_unused_auto_property_accessor_local_highlighting = warning +resharper_unused_import_clause_highlighting = warning +resharper_unused_local_function_highlighting = warning +resharper_unused_local_function_parameter_highlighting = warning +resharper_unused_local_function_return_value_highlighting = warning +resharper_unused_member_global_highlighting = suggestion +resharper_unused_member_hierarchy_global_highlighting = suggestion +resharper_unused_member_hierarchy_local_highlighting = warning +resharper_unused_member_in_super_global_highlighting = suggestion +resharper_unused_member_in_super_local_highlighting = warning +resharper_unused_member_local_highlighting = warning +resharper_unused_method_return_value_global_highlighting = suggestion +resharper_unused_method_return_value_local_highlighting = warning +resharper_unused_parameter_global_highlighting = suggestion +resharper_unused_parameter_in_partial_method_highlighting = warning +resharper_unused_parameter_local_highlighting = warning +resharper_unused_tuple_component_in_return_value_highlighting = warning +resharper_unused_type_global_highlighting = suggestion +resharper_unused_type_local_highlighting = warning +resharper_unused_type_parameter_highlighting = warning +resharper_unused_variable_highlighting = warning +resharper_useless_binary_operation_highlighting = warning +resharper_useless_comparison_to_integral_constant_highlighting = warning +resharper_use_array_creation_expression_1_highlighting = suggestion +resharper_use_array_creation_expression_2_highlighting = suggestion +resharper_use_array_empty_method_highlighting = suggestion +resharper_use_await_using_highlighting = suggestion +resharper_use_cancellation_token_for_i_async_enumerable_highlighting = suggestion +resharper_use_collection_count_property_highlighting = suggestion +resharper_use_configure_await_false_for_async_disposable_highlighting = none +resharper_use_configure_await_false_highlighting = suggestion +resharper_use_deconstruction_highlighting = hint +resharper_use_deconstruction_on_parameter_highlighting = hint +resharper_use_empty_types_field_highlighting = suggestion +resharper_use_event_args_empty_field_highlighting = suggestion +resharper_use_format_specifier_in_format_string_highlighting = suggestion +resharper_use_implicitly_typed_variable_evident_highlighting = hint +resharper_use_implicitly_typed_variable_highlighting = none +resharper_use_implicit_by_val_modifier_highlighting = hint +resharper_use_indexed_property_highlighting = suggestion +resharper_use_index_from_end_expression_highlighting = suggestion +resharper_use_is_operator_1_highlighting = suggestion +resharper_use_is_operator_2_highlighting = suggestion +resharper_use_method_any_0_highlighting = suggestion +resharper_use_method_any_1_highlighting = suggestion +resharper_use_method_any_2_highlighting = suggestion +resharper_use_method_any_3_highlighting = suggestion +resharper_use_method_any_4_highlighting = suggestion +resharper_use_method_is_instance_of_type_highlighting = suggestion +resharper_use_nameof_expression_for_part_of_the_string_highlighting = none +resharper_use_nameof_expression_highlighting = suggestion +resharper_use_name_of_instead_of_type_of_highlighting = suggestion +resharper_use_negated_pattern_in_is_expression_highlighting = hint +resharper_use_negated_pattern_matching_highlighting = hint +resharper_use_nullable_annotation_instead_of_attribute_highlighting = suggestion +resharper_use_nullable_attributes_supported_by_compiler_highlighting = suggestion +resharper_use_nullable_reference_types_annotation_syntax_highlighting = warning +resharper_use_null_propagation_highlighting = hint +resharper_use_null_propagation_when_possible_highlighting = none +resharper_use_object_or_collection_initializer_highlighting = suggestion +resharper_use_pattern_matching_highlighting = suggestion +resharper_use_positional_deconstruction_pattern_highlighting = none +resharper_use_string_interpolation_highlighting = suggestion +resharper_use_switch_case_pattern_variable_highlighting = suggestion +resharper_use_throw_if_null_method_highlighting = none +resharper_use_verbatim_string_highlighting = hint +resharper_value_parameter_not_used_highlighting = warning +resharper_value_range_attribute_violation_highlighting = warning +resharper_variable_can_be_not_nullable_highlighting = warning +resharper_variable_hides_outer_variable_highlighting = warning +resharper_virtual_member_call_in_constructor_highlighting = warning +resharper_virtual_member_never_overridden_global_highlighting = suggestion +resharper_virtual_member_never_overridden_local_highlighting = suggestion +resharper_void_method_with_must_use_return_value_attribute_highlighting = warning +resharper_with_expression_instead_of_initializer_highlighting = suggestion +resharper_wrong_indent_size_highlighting = none +resharper_xunit_xunit_test_with_console_output_highlighting = warning \ No newline at end of file diff --git a/.gitignore b/.gitignore index ca1c7a3..03e89d4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -# ---> VisualStudio ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## @@ -35,6 +34,8 @@ bld/ # Visual Studio 2015/2017 cache/options directory .vs/ +# Visual Studio Code +.vscode/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ @@ -58,11 +59,14 @@ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ -# .NET Core +# .NET project.lock.json project.fragment.lock.json artifacts/ +# Tye +.tye/ + # ASP.NET Scaffolding ScaffoldingReadMe.txt @@ -398,3 +402,78 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk \ No newline at end of file diff --git a/EllieHub.sln b/EllieHub.sln new file mode 100644 index 0000000..0dd6fb8 --- /dev/null +++ b/EllieHub.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33829.357 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieHub", "EllieHub\EllieHub.csproj", "{6E399AD5-2130-4F97-A08F-397EFCE5872A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6E399AD5-2130-4F97-A08F-397EFCE5872A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6E399AD5-2130-4F97-A08F-397EFCE5872A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6E399AD5-2130-4F97-A08F-397EFCE5872A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6E399AD5-2130-4F97-A08F-397EFCE5872A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CB52F80B-4BF5-4BF2-AC3A-939FF8588802} + EndGlobalSection +EndGlobal diff --git a/EllieHub/App.axaml b/EllieHub/App.axaml new file mode 100644 index 0000000..a729d4c --- /dev/null +++ b/EllieHub/App.axaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/EllieHub/App.axaml.cs b/EllieHub/App.axaml.cs new file mode 100644 index 0000000..64f278f --- /dev/null +++ b/EllieHub/App.axaml.cs @@ -0,0 +1,80 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Microsoft.Extensions.DependencyInjection; +using EllieHub.Views.Windows; +using System.Reflection; + +namespace EllieHub; + +/// +/// Defines the Avalonia application. +/// +public partial class App : Application +{ + private DateTimeOffset _trayClickTime = DateTimeOffset.UnixEpoch; + + /// + /// IoC container with all services required by the application. + /// + public IServiceProvider Services { get; } = new ServiceCollection() + .RegisterViewsAndViewModels(Assembly.GetExecutingAssembly()) + .RegisterServices() + .BuildServiceProvider(true); + + /// + public override void Initialize() + => AvaloniaXamlLoader.Load(this); + + /// + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + desktop.MainWindow = Services.GetRequiredService(); + + base.OnFrameworkInitializationCompleted(); + } + + /// + /// Executed when the "Open" menu in the tray icon is clicked. + /// + /// A . + /// A . + private void ShowApp(object sender, EventArgs eventArgs) + => Services.GetRequiredService().Show(); + + /// + /// Executed when the "Close" menu in the tray icon is clicked. + /// + /// A . + /// A . + private void CloseApp(object sender, EventArgs eventArgs) + => Services.GetRequiredService().Close(); + + /// + /// Executed when the tray icon is clicked. + /// + /// A . + /// A . + /// Shows or hides the application when the tray icon is double-clicked. + private void TrayDoubleClick(object sender, EventArgs eventArgs) + { + // If this is the first click or if the second click took longer than 0.3 seconds, exit. + if (_trayClickTime == DateTimeOffset.UnixEpoch || DateTimeOffset.Now.Subtract(_trayClickTime) > TimeSpan.FromSeconds(0.3)) + { + _trayClickTime = DateTimeOffset.Now; + return; + } + + // User has double-clicked the tray icon. Reset the timer. + _trayClickTime = DateTimeOffset.UnixEpoch; + + var mainWindow = Services.GetRequiredService(); + + if (mainWindow.IsVisible) + mainWindow.Hide(); + else + mainWindow.Show(); + } +} \ No newline at end of file diff --git a/EllieHub/Assets/Dark/check_for_updates.png b/EllieHub/Assets/Dark/check_for_updates.png new file mode 100644 index 0000000..3b6068e Binary files /dev/null and b/EllieHub/Assets/Dark/check_for_updates.png differ diff --git a/EllieHub/Assets/Dark/config.png b/EllieHub/Assets/Dark/config.png new file mode 100644 index 0000000..6c3f53c Binary files /dev/null and b/EllieHub/Assets/Dark/config.png differ diff --git a/EllieHub/Assets/Dark/deps.png b/EllieHub/Assets/Dark/deps.png new file mode 100644 index 0000000..a1ac4b2 Binary files /dev/null and b/EllieHub/Assets/Dark/deps.png differ diff --git a/EllieHub/Assets/Dark/docs.png b/EllieHub/Assets/Dark/docs.png new file mode 100644 index 0000000..1095ab6 Binary files /dev/null and b/EllieHub/Assets/Dark/docs.png differ diff --git a/EllieHub/Assets/Dark/ellieupdatericon.ico b/EllieHub/Assets/Dark/ellieupdatericon.ico new file mode 100644 index 0000000..f9e2f35 Binary files /dev/null and b/EllieHub/Assets/Dark/ellieupdatericon.ico differ diff --git a/EllieHub/Assets/Dark/home.png b/EllieHub/Assets/Dark/home.png new file mode 100644 index 0000000..a5a1d2f Binary files /dev/null and b/EllieHub/Assets/Dark/home.png differ diff --git a/EllieHub/Assets/Dark/icon-commands.png b/EllieHub/Assets/Dark/icon-commands.png new file mode 100644 index 0000000..cb730aa Binary files /dev/null and b/EllieHub/Assets/Dark/icon-commands.png differ diff --git a/EllieHub/Assets/Dark/icon-embeds.png b/EllieHub/Assets/Dark/icon-embeds.png new file mode 100644 index 0000000..f7a7fd3 Binary files /dev/null and b/EllieHub/Assets/Dark/icon-embeds.png differ diff --git a/EllieHub/Assets/Dark/icon-link.png b/EllieHub/Assets/Dark/icon-link.png new file mode 100644 index 0000000..3378fe0 Binary files /dev/null and b/EllieHub/Assets/Dark/icon-link.png differ diff --git a/EllieHub/Assets/Dark/icon-suggest.png b/EllieHub/Assets/Dark/icon-suggest.png new file mode 100644 index 0000000..88e02d8 Binary files /dev/null and b/EllieHub/Assets/Dark/icon-suggest.png differ diff --git a/EllieHub/Assets/Dark/icon-support.png b/EllieHub/Assets/Dark/icon-support.png new file mode 100644 index 0000000..15e0803 Binary files /dev/null and b/EllieHub/Assets/Dark/icon-support.png differ diff --git a/EllieHub/Assets/Dark/terminal.png b/EllieHub/Assets/Dark/terminal.png new file mode 100644 index 0000000..466abfb Binary files /dev/null and b/EllieHub/Assets/Dark/terminal.png differ diff --git a/EllieHub/Assets/Light/check_for_updates.png b/EllieHub/Assets/Light/check_for_updates.png new file mode 100644 index 0000000..33ce32c Binary files /dev/null and b/EllieHub/Assets/Light/check_for_updates.png differ diff --git a/EllieHub/Assets/Light/config.png b/EllieHub/Assets/Light/config.png new file mode 100644 index 0000000..2824920 Binary files /dev/null and b/EllieHub/Assets/Light/config.png differ diff --git a/EllieHub/Assets/Light/deps.png b/EllieHub/Assets/Light/deps.png new file mode 100644 index 0000000..b3b2739 Binary files /dev/null and b/EllieHub/Assets/Light/deps.png differ diff --git a/EllieHub/Assets/Light/docs.png b/EllieHub/Assets/Light/docs.png new file mode 100644 index 0000000..c221819 Binary files /dev/null and b/EllieHub/Assets/Light/docs.png differ diff --git a/EllieHub/Assets/Light/ellieupdatericon.ico b/EllieHub/Assets/Light/ellieupdatericon.ico new file mode 100644 index 0000000..f9e2f35 Binary files /dev/null and b/EllieHub/Assets/Light/ellieupdatericon.ico differ diff --git a/EllieHub/Assets/Light/home.png b/EllieHub/Assets/Light/home.png new file mode 100644 index 0000000..bd1875e Binary files /dev/null and b/EllieHub/Assets/Light/home.png differ diff --git a/EllieHub/Assets/Light/icon-commands.png b/EllieHub/Assets/Light/icon-commands.png new file mode 100644 index 0000000..0d9b7ad Binary files /dev/null and b/EllieHub/Assets/Light/icon-commands.png differ diff --git a/EllieHub/Assets/Light/icon-embeds.png b/EllieHub/Assets/Light/icon-embeds.png new file mode 100644 index 0000000..b7f16fe Binary files /dev/null and b/EllieHub/Assets/Light/icon-embeds.png differ diff --git a/EllieHub/Assets/Light/icon-link.png b/EllieHub/Assets/Light/icon-link.png new file mode 100644 index 0000000..f65d578 Binary files /dev/null and b/EllieHub/Assets/Light/icon-link.png differ diff --git a/EllieHub/Assets/Light/icon-suggest.png b/EllieHub/Assets/Light/icon-suggest.png new file mode 100644 index 0000000..8c80f1c Binary files /dev/null and b/EllieHub/Assets/Light/icon-suggest.png differ diff --git a/EllieHub/Assets/Light/icon-support.png b/EllieHub/Assets/Light/icon-support.png new file mode 100644 index 0000000..29e47ce Binary files /dev/null and b/EllieHub/Assets/Light/icon-support.png differ diff --git a/EllieHub/Assets/Light/terminal.png b/EllieHub/Assets/Light/terminal.png new file mode 100644 index 0000000..9865ea5 Binary files /dev/null and b/EllieHub/Assets/Light/terminal.png differ diff --git a/EllieHub/Assets/Unused/addbot.png b/EllieHub/Assets/Unused/addbot.png new file mode 100644 index 0000000..fd90daa Binary files /dev/null and b/EllieHub/Assets/Unused/addbot.png differ diff --git a/EllieHub/Assets/Unused/avalonia-logo.ico b/EllieHub/Assets/Unused/avalonia-logo.ico new file mode 100644 index 0000000..da8d49f Binary files /dev/null and b/EllieHub/Assets/Unused/avalonia-logo.ico differ diff --git a/EllieHub/Assets/Unused/black_home.png b/EllieHub/Assets/Unused/black_home.png new file mode 100644 index 0000000..0a561cb Binary files /dev/null and b/EllieHub/Assets/Unused/black_home.png differ diff --git a/EllieHub/Assets/Unused/config2.png b/EllieHub/Assets/Unused/config2.png new file mode 100644 index 0000000..de715c9 Binary files /dev/null and b/EllieHub/Assets/Unused/config2.png differ diff --git a/EllieHub/Assets/Unused/config_active.png b/EllieHub/Assets/Unused/config_active.png new file mode 100644 index 0000000..87a17f4 Binary files /dev/null and b/EllieHub/Assets/Unused/config_active.png differ diff --git a/EllieHub/Assets/Unused/delete.png b/EllieHub/Assets/Unused/delete.png new file mode 100644 index 0000000..d20b20d Binary files /dev/null and b/EllieHub/Assets/Unused/delete.png differ diff --git a/EllieHub/Assets/Unused/install_bg.png b/EllieHub/Assets/Unused/install_bg.png new file mode 100644 index 0000000..fd872fd Binary files /dev/null and b/EllieHub/Assets/Unused/install_bg.png differ diff --git a/EllieHub/Assets/Unused/setup.png b/EllieHub/Assets/Unused/setup.png new file mode 100644 index 0000000..cbb71ef Binary files /dev/null and b/EllieHub/Assets/Unused/setup.png differ diff --git a/EllieHub/Assets/Unused/setup_active.png b/EllieHub/Assets/Unused/setup_active.png new file mode 100644 index 0000000..601c926 Binary files /dev/null and b/EllieHub/Assets/Unused/setup_active.png differ diff --git a/EllieHub/Assets/Unused/terminal_active.png b/EllieHub/Assets/Unused/terminal_active.png new file mode 100644 index 0000000..346c3a1 Binary files /dev/null and b/EllieHub/Assets/Unused/terminal_active.png differ diff --git a/EllieHub/Assets/Unused/unknown.png b/EllieHub/Assets/Unused/unknown.png new file mode 100644 index 0000000..1a38b72 Binary files /dev/null and b/EllieHub/Assets/Unused/unknown.png differ diff --git a/EllieHub/Assets/Unused/white_home.png b/EllieHub/Assets/Unused/white_home.png new file mode 100644 index 0000000..853b761 Binary files /dev/null and b/EllieHub/Assets/Unused/white_home.png differ diff --git a/EllieHub/Assets/ellie.png b/EllieHub/Assets/ellie.png new file mode 100644 index 0000000..c338b09 Binary files /dev/null and b/EllieHub/Assets/ellie.png differ diff --git a/EllieHub/Assets/ko-fi.webp b/EllieHub/Assets/ko-fi.webp new file mode 100644 index 0000000..0a12895 Binary files /dev/null and b/EllieHub/Assets/ko-fi.webp differ diff --git a/EllieHub/Assets/patreon.png b/EllieHub/Assets/patreon.png new file mode 100644 index 0000000..c6da3af Binary files /dev/null and b/EllieHub/Assets/patreon.png differ diff --git a/EllieHub/Assets/paypal.png b/EllieHub/Assets/paypal.png new file mode 100644 index 0000000..52af164 Binary files /dev/null and b/EllieHub/Assets/paypal.png differ diff --git a/EllieHub/Common/AppConstants.cs b/EllieHub/Common/AppConstants.cs new file mode 100644 index 0000000..044f70d --- /dev/null +++ b/EllieHub/Common/AppConstants.cs @@ -0,0 +1,17 @@ +namespace EllieHub.Common; + +/// +/// Defines the constants used throughout the whole application. +/// +public static class AppConstants +{ + /// + /// Defines the location of the default image for the bot avatar. + /// + public const string BotAvatarUri = "avares://EllieHub/Assets/ellie.png"; + + /// + /// The name for an that does not automatically follow redirect responses. + /// + public const string NoRedirectClient = "NoRedirect"; +} \ No newline at end of file diff --git a/EllieHub/Common/AppResources.cs b/EllieHub/Common/AppResources.cs new file mode 100644 index 0000000..fe8b56a --- /dev/null +++ b/EllieHub/Common/AppResources.cs @@ -0,0 +1,127 @@ +using Avalonia.Controls; +using Avalonia.Media.Imaging; +using Avalonia.Media.Immutable; + +namespace EllieHub.Common; + +/// +/// Defines names of resources for the application. +/// +public static class AppResources +{ + #region Images + + /// + /// Resource type: + /// + public const string EllieAvatar = "EllieAvatar"; + + /// + /// Resource type: + /// + public const string PaypalIcon = "PaypalIcon"; + + /// + /// Resource type: + /// + public const string PatreonIcon = "PatreonIcon"; + + /// + /// Resource type: + /// + public const string CheckForUpdateIcon = "CheckForUpdateIcon"; + + /// + /// Resource type: + /// + public const string ConfigIcon = "ConfigIcon"; + + /// + /// Resource type: + /// + public const string DependenciesIcon = "DependenciesIcon"; + + /// + /// Resource type: + /// + public const string DocumentationIcon = "DocumentationIcon"; + + /// + /// Resource type: + /// + public const string HomeIcon = "HomeIcon"; + + /// + /// Resource type: + /// + public const string EmbedsIcon = "EmbedsIcon"; + + /// + /// Resource type: + /// + public const string UrlIcon = "UrlIcon"; + + /// + /// Resource type: + /// + public const string SuggestionIcon = "SuggestionIcon"; + + /// + /// Resource type: + /// + public const string DiscordIcon = "DiscordIcon"; + + /// + /// Resource type: + /// + public const string EllieHubIcon = "EllieHubIcon"; + + /// + /// Resource type: + /// + public const string EllieHubImage = "EllieHubImage"; + + /// + /// Resource type: + /// + public const string TerminalIcon = "TerminalIcon"; + + #endregion + + #region Color Brushes + + /// + /// Resource type: + /// + public const string LightBackground = "LightBackground"; + + + /// + /// Resource type: + /// + public const string MediumBackground = "MediumBackground"; + + + /// + /// Resource type: + /// + public const string HeavyBackground = "HeavyBackground"; + + + /// + /// Resource type: + /// + public const string BotSelectionColor = "BotSelectionColor"; + + /// + /// Resource type: + /// + public const string DependencyInstall = "DependencyInstall"; + + /// + /// Resource type: + /// + public const string DependencyUpdate = "DependencyUpdate"; + + #endregion +} \ No newline at end of file diff --git a/EllieHub/Common/AppStatics.cs b/EllieHub/Common/AppStatics.cs new file mode 100644 index 0000000..1a9dfa1 --- /dev/null +++ b/EllieHub/Common/AppStatics.cs @@ -0,0 +1,73 @@ +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Platform.Storage; +using System.Text.RegularExpressions; + +namespace EllieHub.Common; + +/// +/// Defines the application's environment data. +/// +public static partial class AppStatics +{ + /// + /// Defines the default location where the updater configuration and bot instances are stored. + /// +#if DEBUG + public static string AppDefaultConfigDirectoryUri { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "EllieHubDebug"); +#else + public static string AppDefaultConfigDirectoryUri { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "EllieHub"); +#endif + + /// + /// Defines the default location where the bot instances are stored. + /// + public static string AppDefaultBotDirectoryUri { get; } = Path.Combine(AppDefaultConfigDirectoryUri, "Bots"); + + /// + /// Defines the default location where the backups of bot instances are stored. + /// + public static string AppDefaultBotBackupDirectoryUri { get; } = Path.Combine(AppDefaultConfigDirectoryUri, "Backups"); + + /// + /// Defines the default location where the logs of bot instances are stored. + /// + public static string AppDefaultLogDirectoryUri { get; } = Path.Combine(AppDefaultConfigDirectoryUri, "Logs"); + + /// + /// Defines the location of the application's configuration file. + /// + public static string AppConfigUri { get; } = Path.Combine(AppDefaultConfigDirectoryUri, "config.json"); + + /// + /// Defines the location of the application's dependencies. + /// + public static string AppDepsUri { get; } = Path.Combine(AppDefaultConfigDirectoryUri, "Dependencies"); + + /// + /// Defines a transparent color brush. + /// + public static ImmutableSolidColorBrush TransparentColorBrush { get; } = new(Colors.Transparent); + + /// + /// Represents the image formats supported by the views of this application. + /// + public static FilePickerOpenOptions ImageFilePickerOptions { get; } = new() + { + AllowMultiple = false, + FileTypeFilter = new FilePickerFileType[] + { + new("Image") { Patterns = new[] { "*.png", "*.jpg", "*.jpeg", "*.gif", "*.webp" } }, + new("All") { Patterns = new[] { "*.*" } } + } + }; + + /// + /// Matches the version of Ffmpeg from its CLI output. + /// + /// Pattern: ^(?:\S+\s+\D*?){2}(git\S+|[\d\.]+) + public static Regex FfmpegVersionRegex { get; } = FfmpegVersionRegexGenerator(); + + [GeneratedRegex(@"^(?:\S+\s+\D*?){2}(git\S+|[\d\.]+)", RegexOptions.Compiled)] + private static partial Regex FfmpegVersionRegexGenerator(); +} \ No newline at end of file diff --git a/EllieHub/Common/DialogType.cs b/EllieHub/Common/DialogType.cs new file mode 100644 index 0000000..1207ec3 --- /dev/null +++ b/EllieHub/Common/DialogType.cs @@ -0,0 +1,22 @@ +namespace EllieHub.Common; + +/// +/// Defines the available types of dialog windows. +/// +public enum DialogType +{ + /// + /// The dialog box notifies the user about something. + /// + Notification, + + /// + /// The dialog box notifies the user of a non-fatal error. + /// + Warning, + + /// + /// The dialog box notifies the user of a fatal error. + /// + Error +} \ No newline at end of file diff --git a/EllieHub/Common/Utilities.cs b/EllieHub/Common/Utilities.cs new file mode 100644 index 0000000..c6c7cf9 --- /dev/null +++ b/EllieHub/Common/Utilities.cs @@ -0,0 +1,215 @@ +using Avalonia.Platform; +using SkiaSharp; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace EllieHub.Common; + +/// +/// Miscellaneous utility methods. +/// +internal static class Utilities +{ + private static readonly string _programVerifier = (Environment.OSVersion.Platform is PlatformID.Win32NT) ? "where" : "which"; + private static readonly string _envPathSeparator = (Environment.OSVersion.Platform is PlatformID.Win32NT) ? ";" : ":"; + private static readonly EnvironmentVariableTarget _envTarget = (Environment.OSVersion.Platform is PlatformID.Win32NT) + ? EnvironmentVariableTarget.User + : EnvironmentVariableTarget.Process; + + /// + /// Loads an image embeded with this application. + /// + /// An uri that starts with "avares://" + /// Valid uris must start with "avares://". + /// The embeded image or the default bot avatar placeholder. + /// Occurs when the embeded resource does not exist. + public static SKBitmap LoadEmbededImage(string? uri = default) + { + return (string.IsNullOrWhiteSpace(uri) || !uri.StartsWith("avares://")) + ? SKBitmap.Decode(AssetLoader.Open(new Uri(AppConstants.BotAvatarUri))) + : SKBitmap.Decode(AssetLoader.Open(new Uri(uri))); + } + + /// + /// Loads the image at the specified location or the bot avatar placeholder if it was not found. + /// + /// The absolute path to the image file or to get the avatar placeholder. + /// This fallsback to if doesn't point to a valid image file. + /// The requested image or the default bot avatar placeholder. + public static SKBitmap LoadLocalImage(string? uri = default) + { + return (File.Exists(uri)) + ? SKBitmap.Decode(uri) + : LoadEmbededImage(uri); + } + + /// + /// Safely casts an to a . + /// + /// The type to cast to. + /// The object to be cast. + /// The cast object, or is casting failed. + /// if the object was successfully cast, otherwise. + public static bool TryCastTo(object? obj, [MaybeNullWhen(false)] out T castObject) + { + if (obj is T result) + { + castObject = result; + return true; + } + + castObject = default; + return false; + } + + /// + /// Starts the specified program in the background. + /// + /// + /// The name of the program in the PATH environment variable, + /// or the absolute path to its executable. + /// + /// The arguments to the program. + /// The process of the specified program. + /// + /// + /// Occurs when does not exist. + /// Occurs when the process fails to execute. + public static Process StartProcess(string program, string arguments = "") + { + ArgumentException.ThrowIfNullOrEmpty(program, nameof(program)); + ArgumentNullException.ThrowIfNull(arguments, nameof(arguments)); + + return Process.Start(new ProcessStartInfo() + { + FileName = program, + Arguments = arguments, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + }) ?? throw new InvalidOperationException($"Failed spawing process for: {program} {arguments}"); + } + + /// + /// Checks if a program exists. + /// + /// The name of the program. + /// The cancellation token. + /// if the program exists, otherwise. + /// + /// + public static async ValueTask ProgramExistsAsync(string programName, CancellationToken cToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(programName, nameof(programName)); + + using var process = StartProcess(_programVerifier, programName); + return !string.IsNullOrWhiteSpace(await process.StandardOutput.ReadToEndAsync(cToken)); + } + + /// + /// Safely deletes a file. + /// + /// The absolute path to the file. + /// if the file was deleted, otherwise. + /// + /// + /// + /// + /// + /// + public static bool TryDeleteFile(string fileUri) + { + ArgumentException.ThrowIfNullOrEmpty(fileUri, nameof(fileUri)); + + if (!File.Exists(fileUri)) + return false; + + File.Delete(fileUri); + return true; + } + + /// + /// Safely deletes a directory. + /// + /// The absolute path to the directory. + /// if the directory was deleted, otherwise. + /// + /// + /// + /// + /// + /// + public static bool TryDeleteDirectory(string directoryUri) + { + ArgumentException.ThrowIfNullOrEmpty(directoryUri, nameof(directoryUri)); + + if (!Directory.Exists(directoryUri)) + return false; + + Directory.Delete(directoryUri, true); + return true; + } + + /// + /// Checks if this application can write to . + /// + /// The absolute path to a directory. + /// if writing is allowed, otherwise. + /// + /// + public static bool CanWriteTo(string directoryUri) + { + var tempFileUri = Path.Combine(directoryUri, $"{Guid.NewGuid()}.tmp"); + + try + { + using var fileStream = File.Create(tempFileUri); + return true; + } + catch (UnauthorizedAccessException) + { + return false; + } + finally + { + TryDeleteFile(tempFileUri); + } + } + + /// + /// Adds a directory path to the PATH environment variable. + /// + /// The absolute path to a directory. + /// + /// On Windows, this needs to be called once and the dependencies will be available for the user forever.
+ /// On Unix systems, we can only add to the PATH on a process basis, so this needs to be called at least once everytime the application is opened. + ///
+ /// if got successfully added to the PATH envar, otherwise. + /// + /// + public static bool AddPathToPATHEnvar(string directoryUri) + { + ArgumentException.ThrowIfNullOrEmpty(directoryUri, nameof(directoryUri)); + + if (File.Exists(directoryUri)) + throw new ArgumentException("Parameter must point to a directory, not a file.", nameof(directoryUri)); + + var envPathValue = Environment.GetEnvironmentVariable("PATH", _envTarget) ?? string.Empty; + + // If directoryPath is already in the PATH envar, don't add it again. + if (envPathValue.Contains(directoryUri, StringComparison.Ordinal)) + return false; + + var newPathEnvValue = envPathValue + _envPathSeparator + directoryUri; + + // Add path to Windows' user envar, so it persists across reboots. + if (Environment.OSVersion.Platform is PlatformID.Win32NT) + Environment.SetEnvironmentVariable("PATH", newPathEnvValue, EnvironmentVariableTarget.User); + + // Add path to the current process' envar, so the updater can see the dependencies. + Environment.SetEnvironmentVariable("PATH", newPathEnvValue, EnvironmentVariableTarget.Process); + + return true; + } +} \ No newline at end of file diff --git a/EllieHub/Common/WindowConstants.cs b/EllieHub/Common/WindowConstants.cs new file mode 100644 index 0000000..770c1c1 --- /dev/null +++ b/EllieHub/Common/WindowConstants.cs @@ -0,0 +1,37 @@ +namespace EllieHub.Common; + +/// +/// Defines constants to be used inside views. +/// +public static class WindowConstants +{ + /// + /// Defines the default width of the window. + /// + public const string DefaultWindowWidth = "885"; + + /// + /// Defines the default height of the window. + /// + public const string DefaultWindowHeight = "570"; + + /// + /// Defines the minimum height of the window. + /// + public const string MinWindowWidth = "550"; + + /// + /// Defines the minimum height of the window. + /// + public const string MinWindowHeight = "500"; + + /// + /// Defines the default window title. + /// + public const string WindowTitle = "Ellie Hub"; + + /// + /// Defines the message that should be shown when a view's parameterless constructor should not be used. + /// + public const string DesignerCtorWarning = "This constructor exists to satisfy Avalonia's designer. Please, use the parameterized constructor instead."; +} \ No newline at end of file diff --git a/EllieHub/DesignData/Common/DesignStatics.cs b/EllieHub/DesignData/Common/DesignStatics.cs new file mode 100644 index 0000000..b307c18 --- /dev/null +++ b/EllieHub/DesignData/Common/DesignStatics.cs @@ -0,0 +1,18 @@ +using Avalonia; +using Avalonia.Controls; + +namespace EllieHub.DesignData.Common; + +/// +/// Defines objects useful at design-time. +/// +internal static class DesignStatics +{ + /// + /// Provides the services necessary for design-time rendering of views. + /// + /// This property is when the application is not in design mode. + internal static IServiceProvider Services { get; } = (Design.IsDesignMode) + ? (Application.Current as App)!.Services + : null!; +} \ No newline at end of file diff --git a/EllieHub/DesignData/Controls/DesignBotConfigViewModel.cs b/EllieHub/DesignData/Controls/DesignBotConfigViewModel.cs new file mode 100644 index 0000000..1af830b --- /dev/null +++ b/EllieHub/DesignData/Controls/DesignBotConfigViewModel.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.DependencyInjection; +using EllieHub.DesignData.Common; +using EllieHub.Services.Abstractions; +using EllieHub.Services.Mocks; +using EllieHub.ViewModels.Controls; +using EllieHub.Views.Windows; + +namespace EllieHub.DesignData.Controls; + +/// +/// Mock view-model for . +/// +public sealed class DesignBotConfigViewModel : BotConfigViewModel +{ + /// + /// Creates a mock to be used at design-time. + /// + public DesignBotConfigViewModel() : base( + DesignStatics.Services.GetRequiredService(), + DesignStatics.Services.GetRequiredService(), + DesignStatics.Services.GetRequiredService(), + DesignStatics.Services.GetRequiredService(), + DesignStatics.Services.GetRequiredService(), + DesignStatics.Services.GetRequiredService(), + DesignStatics.Services.GetRequiredService(), + DesignStatics.Services.GetRequiredService() + ) + { + } +} \ No newline at end of file diff --git a/EllieHub/DesignData/Controls/DesignConfigViewModel.cs b/EllieHub/DesignData/Controls/DesignConfigViewModel.cs new file mode 100644 index 0000000..ddee175 --- /dev/null +++ b/EllieHub/DesignData/Controls/DesignConfigViewModel.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.DependencyInjection; +using EllieHub.DesignData.Common; +using EllieHub.Services.Abstractions; +using EllieHub.Services.Mocks; +using EllieHub.ViewModels.Controls; +using EllieHub.ViewModels.Windows; +using EllieHub.Views.Windows; + +namespace EllieHub.DesignData.Controls; + +/// +/// Mock view-model for . +/// +public sealed class DesignConfigViewModel : ConfigViewModel +{ + /// + /// Creates a mock to be used at design-time. + /// + public DesignConfigViewModel() : base( + DesignStatics.Services.GetRequiredService(), + DesignStatics.Services.GetRequiredService(), + DesignStatics.Services.GetRequiredService(), + DesignStatics.Services.GetRequiredService(), + DesignStatics.Services.GetRequiredService(), + DesignStatics.Services.GetRequiredService(), + DesignStatics.Services.GetRequiredService(), + DesignStatics.Services.GetRequiredService() + ) + { + } +} \ No newline at end of file diff --git a/EllieHub/DesignData/Controls/DesignDependencyButtonViewModel.cs b/EllieHub/DesignData/Controls/DesignDependencyButtonViewModel.cs new file mode 100644 index 0000000..04e3507 --- /dev/null +++ b/EllieHub/DesignData/Controls/DesignDependencyButtonViewModel.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using EllieHub.DesignData.Common; +using EllieHub.ViewModels.Controls; +using EllieHub.Views.Windows; + +namespace EllieHub.DesignData.Controls; + +/// +/// Mock view-model for . +/// +public sealed class DesignDependencyButtonViewModel : DependencyButtonViewModel +{ + /// + /// Creates a mock to be used at design-time. + /// + public DesignDependencyButtonViewModel() : base(DesignStatics.Services.GetRequiredService()) + { + } +} \ No newline at end of file diff --git a/EllieHub/DesignData/Controls/DesignFakeConsoleViewModel.cs b/EllieHub/DesignData/Controls/DesignFakeConsoleViewModel.cs new file mode 100644 index 0000000..fd73a8b --- /dev/null +++ b/EllieHub/DesignData/Controls/DesignFakeConsoleViewModel.cs @@ -0,0 +1,15 @@ +using EllieHub.ViewModels.Controls; + +namespace EllieHub.DesignData.Controls; + +/// +/// Mock view-model for . +/// +public sealed class DesignFakeConsoleViewModel : FakeConsoleViewModel +{ + /// + /// Creates a mock to be used at design-time. + /// + public DesignFakeConsoleViewModel() : base() + => Watermark = "Sample watermark."; +} \ No newline at end of file diff --git a/EllieHub/DesignData/Controls/DesignHomeViewModel.cs b/EllieHub/DesignData/Controls/DesignHomeViewModel.cs new file mode 100644 index 0000000..4743724 --- /dev/null +++ b/EllieHub/DesignData/Controls/DesignHomeViewModel.cs @@ -0,0 +1,10 @@ +using EllieHub.ViewModels.Controls; + +namespace EllieHub.DesignData.Controls; + +/// +/// Mock view-model for . +/// +public sealed class DesignHomeViewModel : HomeViewModel +{ +} \ No newline at end of file diff --git a/EllieHub/DesignData/Controls/DesignLateralBarViewModel.cs b/EllieHub/DesignData/Controls/DesignLateralBarViewModel.cs new file mode 100644 index 0000000..c03049f --- /dev/null +++ b/EllieHub/DesignData/Controls/DesignLateralBarViewModel.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using EllieHub.DesignData.Common; +using EllieHub.Services.Mocks; +using EllieHub.ViewModels.Controls; + +namespace EllieHub.DesignData.Controls; + +/// +/// Mock view-model for . +/// +public sealed class DesignLateralBarViewModel : LateralBarViewModel +{ + /// + /// Creates a mock to be used at design-time. + /// + public DesignLateralBarViewModel() : base(DesignStatics.Services.GetRequiredService()) + { + } +} \ No newline at end of file diff --git a/EllieHub/DesignData/Controls/DesignUriInputBarViewModel.cs b/EllieHub/DesignData/Controls/DesignUriInputBarViewModel.cs new file mode 100644 index 0000000..8343c4e --- /dev/null +++ b/EllieHub/DesignData/Controls/DesignUriInputBarViewModel.cs @@ -0,0 +1,19 @@ +using Avalonia.Platform.Storage; +using Microsoft.Extensions.DependencyInjection; +using EllieHub.DesignData.Common; +using EllieHub.ViewModels.Controls; + +namespace EllieHub.DesignData.Controls; + +/// +/// Mock view-model for . +/// +public sealed class DesignUriInputBarViewModel : UriInputBarViewModel +{ + /// + /// Creates a mock to be used at design-time. + /// + public DesignUriInputBarViewModel() : base(DesignStatics.Services.GetRequiredService()) + { + } +} \ No newline at end of file diff --git a/EllieHub/DesignData/Windows/DesignAboutMeViewModel.cs b/EllieHub/DesignData/Windows/DesignAboutMeViewModel.cs new file mode 100644 index 0000000..6535212 --- /dev/null +++ b/EllieHub/DesignData/Windows/DesignAboutMeViewModel.cs @@ -0,0 +1,10 @@ +using EllieHub.ViewModels.Windows; + +namespace EllieHub.DesignData.Windows; + +/// +/// Mock view-model for . +/// +public sealed class DesignAboutMeViewModel : AboutMeViewModel +{ +} \ No newline at end of file diff --git a/EllieHub/DesignData/Windows/DesignAppViewModel.cs b/EllieHub/DesignData/Windows/DesignAppViewModel.cs new file mode 100644 index 0000000..a48a0fe --- /dev/null +++ b/EllieHub/DesignData/Windows/DesignAppViewModel.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.DependencyInjection; +using EllieHub.DesignData.Common; +using EllieHub.ViewModels.Controls; +using EllieHub.ViewModels.Windows; + +namespace EllieHub.DesignData.Windows; + +/// +/// Mock view-model for . +/// +public sealed class DesignAppViewModel : AppViewModel +{ + /// + /// Creates a mock to be used at design-time. + /// + public DesignAppViewModel() : base( + DesignStatics.Services.GetRequiredService(), + DesignStatics.Services.GetRequiredService() + ) + { + } +} \ No newline at end of file diff --git a/EllieHub/DesignData/Windows/DesignUpdateViewModel.cs b/EllieHub/DesignData/Windows/DesignUpdateViewModel.cs new file mode 100644 index 0000000..2abed0b --- /dev/null +++ b/EllieHub/DesignData/Windows/DesignUpdateViewModel.cs @@ -0,0 +1,10 @@ +using EllieHub.ViewModels.Windows; + +namespace EllieHub.DesignData.Windows; + +/// +/// Mock view-model for . +/// +public sealed class DesignUpdateViewModel : UpdateViewModel +{ +} \ No newline at end of file diff --git a/EllieHub/EllieHub.csproj b/EllieHub/EllieHub.csproj new file mode 100644 index 0000000..9811eb9 --- /dev/null +++ b/EllieHub/EllieHub.csproj @@ -0,0 +1,62 @@ + + + + WinExe + net7.0 + latest + latest + enable + enable + Nullable + True + True + True + True + Assets/Light/ellieupdatericon.ico + + + true + embedded + + + 1.0.0.0 + + + app.manifest + true + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AppView.axaml + + + \ No newline at end of file diff --git a/EllieHub/Enums/DependencyStatus.cs b/EllieHub/Enums/DependencyStatus.cs new file mode 100644 index 0000000..9b809ad --- /dev/null +++ b/EllieHub/Enums/DependencyStatus.cs @@ -0,0 +1,32 @@ +namespace EllieHub.Enums; + +/// +/// Defines the possible status for a dependency. +/// +public enum DependencyStatus +{ + /// + /// The dependency is available for installation. + /// + Install, + + /// + /// The dependency is installed and up-to-date. + /// + Installed, + + /// + /// The dependency is in the process of being updated. + /// + Updating, + + /// + /// The dependency has an update available. + /// + Update, + + /// + /// The dependency is currently being checked for updates. + /// + Checking +} \ No newline at end of file diff --git a/EllieHub/Enums/DependencyType.cs b/EllieHub/Enums/DependencyType.cs new file mode 100644 index 0000000..2789fde --- /dev/null +++ b/EllieHub/Enums/DependencyType.cs @@ -0,0 +1,17 @@ +namespace EllieHub.Enums; + +/// +/// Defines the types of dependencies available to install. +/// +public enum DependencyType +{ + /// + /// Ffmpeg. + /// + Ffmpeg, + + /// + /// Yt-dlp. + /// + Ytdlp +} \ No newline at end of file diff --git a/EllieHub/Enums/ThemeType.cs b/EllieHub/Enums/ThemeType.cs new file mode 100644 index 0000000..96a7097 --- /dev/null +++ b/EllieHub/Enums/ThemeType.cs @@ -0,0 +1,22 @@ +namespace EllieHub.Enums; + +/// +/// The types of themes available. +/// +public enum ThemeType +{ + /// + /// Either Light or Dark, according to the OS preferences. + /// + Auto, + + /// + /// Light theme. + /// + Light, + + /// + /// Dark theme. + /// + Dark +} \ No newline at end of file diff --git a/EllieHub/Extensions/HttpClientExt.cs b/EllieHub/Extensions/HttpClientExt.cs new file mode 100644 index 0000000..5263f1e --- /dev/null +++ b/EllieHub/Extensions/HttpClientExt.cs @@ -0,0 +1,41 @@ +using System.Text.Json; + +namespace EllieHub.Extensions; + +/// +/// Defines extension methods for . +/// +public static class HttpClientExt +{ + /// + /// Sends a GET request to an API at the specified and returns a Json deserialized response. + /// + /// The type to be returned. + /// This http client. + /// The API endpoint to be called. + /// The cancellation token. + /// A response. + /// Occurs when the deserialization fails. + public static async Task CallApiAsync(this HttpClient http, string endpoint, CancellationToken cToken = default) + { + var responseString = await http.GetStringAsync(endpoint, cToken); + + return JsonSerializer.Deserialize(responseString) + ?? throw new InvalidOperationException($"Could not deserialize response to {nameof(T)}."); + } + + /// + /// Checks if a request to the specified returns a successful HTTP response. + /// + /// This http client. + /// The url to check. + /// The cancellation token. + /// if the is valid, otherwise. + public static async Task IsUrlValidAsync(this HttpClient http, string url, CancellationToken cToken = default) + { + var request = new HttpRequestMessage(HttpMethod.Head, url); + var response = await http.SendAsync(request, cToken); + + return response.IsSuccessStatusCode; + } +} \ No newline at end of file diff --git a/EllieHub/Extensions/IServiceCollectionExt.cs b/EllieHub/Extensions/IServiceCollectionExt.cs new file mode 100644 index 0000000..785105d --- /dev/null +++ b/EllieHub/Extensions/IServiceCollectionExt.cs @@ -0,0 +1,91 @@ +using Avalonia.Controls; +using Microsoft.Extensions.DependencyInjection; +using EllieHub.Models.Config; +using EllieHub.Services; +using EllieHub.Services.Abstractions; +using EllieHub.Services.Mocks; +using EllieHub.Views.Windows; +using ReactiveUI; +using System.Reflection; +using System.Text.Json; + +namespace EllieHub.Extensions; + +/// +/// Defines extension methods for . +/// +public static class IServiceCollectionExt +{ + /// + /// Registers all views and view-models in the provided to this service collection. + /// + /// This service collection. + /// The assembly to get the views and view-models from. + /// This service collection with the views and view-models added. + public static IServiceCollection RegisterViewsAndViewModels(this IServiceCollection serviceCollection, Assembly assembly) + { + var viewModelPairs = assembly.GetTypes() + .Where(x => !x.IsAbstract && x.IsAssignableTo(typeof(IViewFor))) + .Select(x => (ViewType: x, ViewModelType: x.GetInterface(typeof(IViewFor<>).Name)!.GenericTypeArguments[0])); + + foreach (var (viewType, viewModelType) in viewModelPairs) + { + serviceCollection.AddSingleton(viewType); + serviceCollection.AddTransient(viewModelType); + } + + return serviceCollection; + } + + /// + /// Registers the application's services. + /// + /// This service collection. + /// This service collection with the services added. + public static IServiceCollection RegisterServices(this IServiceCollection serviceCollection) + { + // Design-time + if (Design.IsDesignMode) + { + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + } + + // Internal + serviceCollection.AddMemoryCache(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(x => x.GetRequiredService().StorageProvider); + + // Web requests + serviceCollection.AddHttpClient(); + serviceCollection.AddHttpClient(AppConstants.NoRedirectClient) // Client that doesn't allow automatic reditections + .ConfigureHttpMessageHandlerBuilder(builder => builder.PrimaryHandler = new HttpClientHandler() { AllowAutoRedirect = false }); + + // App settings + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(_ => + (File.Exists(AppStatics.AppConfigUri)) + ? JsonSerializer.Deserialize(File.ReadAllText(AppStatics.AppConfigUri)) ?? new() + : new() + ); + + // Dependency resolvers + serviceCollection.AddSingleton(); + serviceCollection.AddTransient(); + + // Platform-dependent services + if (OperatingSystem.IsWindows()) + serviceCollection.AddSingleton(); + else if (OperatingSystem.IsLinux()) + serviceCollection.AddSingleton(); + else if (OperatingSystem.IsMacOS()) + serviceCollection.AddSingleton(); + else + serviceCollection.AddSingleton(); + + return serviceCollection; + } +} \ No newline at end of file diff --git a/EllieHub/Extensions/IServiceProviderExt.cs b/EllieHub/Extensions/IServiceProviderExt.cs new file mode 100644 index 0000000..f488bd5 --- /dev/null +++ b/EllieHub/Extensions/IServiceProviderExt.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace EllieHub.Extensions; + +/// +/// Defines extension methods for . +/// +public static class IServiceProviderExt +{ + /// + /// Gets service of type from the . + /// + /// The type of service object to get. + /// The to retrieve the service object from. + /// + /// Do not use abstract types in the type argument! + /// A service object of type . + /// Occurs when or are . + /// Occurs when there is no concrete service of type or when the arguments are wrong. + public static T GetParameterizedService(this IServiceProvider serviceProvider, params object[] arguments) + { + ArgumentNullException.ThrowIfNull(serviceProvider, nameof(serviceProvider)); + ArgumentNullException.ThrowIfNull(arguments, nameof(arguments)); + + var result = ActivatorUtilities.CreateInstance(serviceProvider, arguments); + + return (result is null) + ? throw new InvalidOperationException($"There is no service of type {nameof(T)} or the arguments were incorrect.") + : result; + } +} \ No newline at end of file diff --git a/EllieHub/Extensions/WindowExt.cs b/EllieHub/Extensions/WindowExt.cs new file mode 100644 index 0000000..02de951 --- /dev/null +++ b/EllieHub/Extensions/WindowExt.cs @@ -0,0 +1,90 @@ +using Avalonia.Controls; +using Avalonia.Styling; +using MsBox.Avalonia; +using MsBox.Avalonia.Dto; +using MsBox.Avalonia.Enums; + +namespace EllieHub.Extensions; + +/// +/// Provides extension methods for . +/// +public static class WindowExt +{ + /// + /// Shows a dialog window that blocks the main window. + /// + /// The active window. + /// The message to be displayed. + /// The type of dialog window to display. + /// The icon to be displayed. + /// The button type that was pressed. + public static Task ShowDialogWindowAsync(this Window activeView, string message, DialogType dialogType = DialogType.Notification, Icon iconType = Icon.None) + => ShowDialogWindowAsync(activeView, message, dialogType.ToString(), iconType); + + /// + /// Shows a dialog window that blocks the main window. + /// + /// The active window. + /// The message to be displayed. + /// The title of the dialog box. + /// The icon to be displayed. + /// The button type that was pressed. + public static Task ShowDialogWindowAsync(this Window activeView, string message, string title, Icon iconType = Icon.None) + { + var messageparameters = new MessageBoxStandardParams() + { + ButtonDefinitions = ButtonEnum.Ok, + ContentMessage = message, + ContentTitle = title, + Icon = iconType, + WindowIcon = activeView.GetResource(AppResources.EllieHubIcon), + MaxWidth = int.Parse(WindowConstants.DefaultWindowWidth) / 1.7, + SizeToContent = SizeToContent.WidthAndHeight, + ShowInCenter = true, + WindowStartupLocation = WindowStartupLocation.CenterOwner + }; + + return ShowDialogWindowAsync(activeView, messageparameters); + } + + /// + /// Shows a dialog window that blocks the main window. + /// + /// The active window. + /// The parameters of the message dialog box. + /// The button type that was pressed. + public static Task ShowDialogWindowAsync(this Window activeView, MessageBoxStandardParams messageParameters) + => MessageBoxManager.GetMessageBoxStandard(messageParameters).ShowWindowDialogAsync(activeView); + + /// + /// Finds the specified resource by searching up the logical tree and then global styles. + /// + /// The type of the resource. + /// The active window. + /// The name of the resource. + /// The requested resource. + /// Occurs when the resource is not found. + /// Occurs when the resource is not of type . + public static T GetResource(this Window activeView, string resourceName) + => GetResource(activeView, resourceName, activeView.ActualThemeVariant); + + /// + /// Finds the specified resource by searching up the logical tree and then global styles. + /// + /// The type of the resource. + /// The active window. + /// The name of the resource. + /// The UI theme variant the resource belongs to. + /// The requested resource. + /// Occurs when the resource is not found. + /// Occurs when the resource is not of type . + public static T GetResource(this Window activeView, string resourceName, ThemeVariant theme) + { + return (!activeView.TryFindResource(resourceName, theme, out var resource)) + ? throw new InvalidOperationException($"Resource '{resourceName}' was not found.") + : (!Utilities.TryCastTo(resource, out var result)) + ? throw new InvalidCastException($"Could not convert resource of type '{resource?.GetType()?.FullName}' to '{nameof(T)}'.") + : result; + } +} \ No newline at end of file diff --git a/EllieHub/Models/Api/EvermeetDownloadInfo.cs b/EllieHub/Models/Api/EvermeetDownloadInfo.cs new file mode 100644 index 0000000..c07844c --- /dev/null +++ b/EllieHub/Models/Api/EvermeetDownloadInfo.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace EllieHub.Models.Api; + +/// +/// Represents download information for a component. +/// +/// The url to download the component. +/// The size of the package to download, in bytes. +/// The url to download the cryptographic signature of the component. +public sealed record EvermeetDownloadInfo( + [property: JsonPropertyName("url")] string Url, + [property: JsonPropertyName("size")] uint Size, + [property: JsonPropertyName("sig")] string SignatureUrl +); \ No newline at end of file diff --git a/EllieHub/Models/Api/EvermeetInfo.cs b/EllieHub/Models/Api/EvermeetInfo.cs new file mode 100644 index 0000000..ac91065 --- /dev/null +++ b/EllieHub/Models/Api/EvermeetInfo.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace EllieHub.Models.Api; + +/// +/// Represents a response from the "https://evermeet.cx/ffmpeg/info" endpoint. +/// +/// The name of the component. +/// The type of the component (snapshot or release). +/// The version of the component. +/// The size of the component, in bytes. +/// The download links to the component, where the key is the desired file format. +public sealed record EvermeetInfo( + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("type")] string Type, + [property: JsonPropertyName("version")] string Version, + [property: JsonPropertyName("size")] uint Size, + [property: JsonPropertyName("download")] IReadOnlyDictionary Download +); \ No newline at end of file diff --git a/EllieHub/Models/BotEntry.cs b/EllieHub/Models/BotEntry.cs new file mode 100644 index 0000000..51b9e47 --- /dev/null +++ b/EllieHub/Models/BotEntry.cs @@ -0,0 +1,10 @@ +using EllieHub.ViewModels.Controls; + +namespace EllieHub.Models; + +/// +/// Represents a bot entry in the . +/// +/// The Id of the bot. +/// The information about the bot instance. +public sealed record BotEntry(Guid Id, BotInstanceInfo BotInfo); \ No newline at end of file diff --git a/EllieHub/Models/BotInstanceInfo.cs b/EllieHub/Models/BotInstanceInfo.cs new file mode 100644 index 0000000..021f294 --- /dev/null +++ b/EllieHub/Models/BotInstanceInfo.cs @@ -0,0 +1,11 @@ +namespace EllieHub.Models; + +/// +/// Represents the information of a bot instance. +/// +/// The name of the bot. +/// The path to the directory where the bot instance is located at. +/// The position of the bot in the lateral bar. +/// The version of the bot or if the bot hasn't been downloaded yet. +/// The path to the bot's avatar image file or is there is none. +public sealed record BotInstanceInfo(string Name, string InstanceDirectoryUri, uint Position, string? Version = default, string? AvatarUri = default); \ No newline at end of file diff --git a/EllieHub/Models/Config/AppConfig.cs b/EllieHub/Models/Config/AppConfig.cs new file mode 100644 index 0000000..949a972 --- /dev/null +++ b/EllieHub/Models/Config/AppConfig.cs @@ -0,0 +1,56 @@ +using EllieHub.Enums; +using System.Collections.Concurrent; + +namespace EllieHub.Models.Config; + +/// +/// Represents the settings of the application. +/// +/// Prefer using in dependency injection, if possible. +public sealed class AppConfig +{ + /// + /// The absolute path to the directory where the bot instances are stored. + /// + public string BotsDirectoryUri { get; set; } = AppStatics.AppDefaultBotDirectoryUri; + + /// + /// The absolute path to the directory where the bot instances are backed up. + /// + public string BotsBackupDirectoryUri { get; set; } = AppStatics.AppDefaultBotBackupDirectoryUri; + + /// + /// The absolute path to the directory where the bot logs are stored. + /// + public string LogsDirectoryUri { get; set; } = AppStatics.AppDefaultLogDirectoryUri; + + /// + /// The theme to be used. + /// + public ThemeType Theme { get; set; } = ThemeType.Auto; + + /// + /// Determines whether the application should update itself. + /// + public bool AutomaticUpdates { get; set; } = true; + + /// + /// Determines whether the application should be minimized to the system tray when closed. + /// + public bool MinimizeToTray { get; set; } = true; + + /// + /// Determines the maximum size a log file can have, in Mb. + /// + public double LogMaxSizeMb { get; set; } = 0.5; + + /// + /// Determines the size the application window should be set on startup. + /// + public WindowSize WindowSize { get; set; } = new(double.Parse(WindowConstants.DefaultWindowWidth), double.Parse(WindowConstants.DefaultWindowHeight)); + + /// + /// A collection of metadata about the bot instances. + /// + public ConcurrentDictionary BotEntries { get; init; } = new(); +} \ No newline at end of file diff --git a/EllieHub/Models/Config/ReadOnlyAppConfig.cs b/EllieHub/Models/Config/ReadOnlyAppConfig.cs new file mode 100644 index 0000000..0c5b2d5 --- /dev/null +++ b/EllieHub/Models/Config/ReadOnlyAppConfig.cs @@ -0,0 +1,72 @@ +using EllieHub.Enums; + +namespace EllieHub.Models.Config; + +/// +/// Represents a read-only version of . +/// +public sealed class ReadOnlyAppConfig +{ + private readonly AppConfig _appConfig; + + /// + /// The absolute path to the directory where the bot instances are stored. + /// + public string BotsDirectoryUri + => _appConfig.BotsDirectoryUri; + + /// + /// The absolute path to the directory where the bot instances are backed up. + /// + public string BotsBackupDirectoryUri + => _appConfig.BotsBackupDirectoryUri; + + /// + /// The absolute path to the directory where the bot logs are stored. + /// + public string LogsDirectoryUri + => _appConfig.LogsDirectoryUri; + + /// + /// The theme to be used. + /// + public ThemeType Theme + => _appConfig.Theme; + + /// + /// Determines whether the application should update itself. + /// + public bool AutomaticUpdates + => _appConfig.AutomaticUpdates; + + /// + /// Determines whether the application should be minimized to the system tray when closed. + /// + public bool MinimizeToTray + => _appConfig.MinimizeToTray; + + /// + /// Determines the maximum size a log file can have, in Mb. + /// + public double LogMaxSizeMb + => _appConfig.LogMaxSizeMb; + + /// + /// Determines the size the application window should be set on startup. + /// + public WindowSize WindowSize + => _appConfig.WindowSize; + + /// + /// A collection of metadata about the bot instances. + /// + public IReadOnlyDictionary BotEntries + => _appConfig.BotEntries; + + /// + /// Initializes a read-only version of . + /// + /// The application settings to read from. + public ReadOnlyAppConfig(AppConfig appConfig) + => _appConfig = appConfig; +} \ No newline at end of file diff --git a/EllieHub/Models/EventArguments/AvatarChangedEventArgs.cs b/EllieHub/Models/EventArguments/AvatarChangedEventArgs.cs new file mode 100644 index 0000000..59b8267 --- /dev/null +++ b/EllieHub/Models/EventArguments/AvatarChangedEventArgs.cs @@ -0,0 +1,37 @@ +using SkiaSharp; + +namespace EllieHub.Models.EventArguments; + +/// +/// Defines the event arguments for when the user sets a new avatar for a bot instance. +/// +public sealed class AvatarChangedEventArgs : EventArgs +{ + /// + /// The Id of the bot. + /// + public Guid BotId { get; } + + /// + /// The new avatar. + /// + public SKBitmap Avatar { get; } + + /// + /// The absolute path to the avatar's file. + /// + public string AvatarUri { get; } + + /// + /// Creates the event arguments for when the user sets a new avatar for a bot instance. + /// + /// The Id of the bot. + /// The new avatar. + /// The absolute path to the avatar's file. + public AvatarChangedEventArgs(Guid botId, SKBitmap avatar, string avatarUri) + { + BotId = botId; + Avatar = avatar; + AvatarUri = avatarUri; + } +} \ No newline at end of file diff --git a/EllieHub/Models/EventArguments/BotExitEventArgs.cs b/EllieHub/Models/EventArguments/BotExitEventArgs.cs new file mode 100644 index 0000000..d2160eb --- /dev/null +++ b/EllieHub/Models/EventArguments/BotExitEventArgs.cs @@ -0,0 +1,28 @@ +namespace EllieHub.Models.EventArguments; + +/// +/// Defines the event arguments when a bot process exits. +/// +public sealed class BotExitEventArgs : EventArgs +{ + /// + /// The bot's Id. + /// + public Guid Id { get; } + + /// + /// The exit code. + /// + public int ExitCode { get; } + + /// + /// Creates the event arguments when a bot process exits. + /// + /// The bot's Id. + /// The exit code. + public BotExitEventArgs(Guid botId, int exitCode) + { + Id = botId; + ExitCode = exitCode; + } +} \ No newline at end of file diff --git a/EllieHub/Models/EventArguments/LogFlushEventArgs.cs b/EllieHub/Models/EventArguments/LogFlushEventArgs.cs new file mode 100644 index 0000000..4cb8572 --- /dev/null +++ b/EllieHub/Models/EventArguments/LogFlushEventArgs.cs @@ -0,0 +1,35 @@ +namespace EllieHub.Models.EventArguments; + +/// +/// Defines the event arguments when a log is written to disk. +/// +public sealed class LogFlushEventArgs : EventArgs +{ + /// + /// The absolute path to the recently created log file. + /// + public string FileUri { get; } + + /// + /// The size of the log file, in bytes. + /// + public int Size { get; } + + /// + /// The date the log file was created. + /// + public DateTimeOffset CreatedAt { get; } + + /// + /// Creates the event arguments when a log is written to disk. + /// + /// The absolute path to the recently created log file. + /// The size of the log file, in bytes. + /// The date the log file was created. + public LogFlushEventArgs(string fileUri, int size, DateTimeOffset createdAt) + { + FileUri = fileUri; + Size = size; + CreatedAt = createdAt; + } +} \ No newline at end of file diff --git a/EllieHub/Models/EventArguments/ProcessStdWriteEventArgs.cs b/EllieHub/Models/EventArguments/ProcessStdWriteEventArgs.cs new file mode 100644 index 0000000..d061195 --- /dev/null +++ b/EllieHub/Models/EventArguments/ProcessStdWriteEventArgs.cs @@ -0,0 +1,28 @@ +namespace EllieHub.Models.EventArguments; + +/// +/// Defines the event arguments when a bot process writes to stdout or stderr. +/// +public sealed class ProcessStdWriteEventArgs : EventArgs +{ + /// + /// The Id of the bot. + /// + public Guid Id { get; } + + /// + /// The value that was just written to std. + /// + public string Output { get; } + + /// + /// Creates the event arguments when a bot process writes to stdout or stderr. + /// + /// The Id of the bot. + /// The value that was just written to std. + public ProcessStdWriteEventArgs(Guid botId, string output) + { + Id = botId; + Output = output; + } +} \ No newline at end of file diff --git a/EllieHub/Models/EventArguments/UriInputBarEventArgs.cs b/EllieHub/Models/EventArguments/UriInputBarEventArgs.cs new file mode 100644 index 0000000..9faa5c9 --- /dev/null +++ b/EllieHub/Models/EventArguments/UriInputBarEventArgs.cs @@ -0,0 +1,30 @@ +using EllieHub.ViewModels.Controls; + +namespace EllieHub.Models.EventArguments; + +/// +/// Defines the event arguments for when a valid uri is set to a . +/// +public sealed class UriInputBarEventArgs : EventArgs +{ + /// + /// The old valid uri. + /// + public string OldUri { get; } + + /// + /// The new valid uri. + /// + public string NewUri { get; } + + /// + /// Creates the event arguments for when a valid uri is set to a . + /// + /// The old valid uri. + /// The new valid uri. + public UriInputBarEventArgs(string oldUri, string newUri) + { + OldUri = oldUri; + NewUri = newUri; + } +} \ No newline at end of file diff --git a/EllieHub/Models/WindowSize.cs b/EllieHub/Models/WindowSize.cs new file mode 100644 index 0000000..3f7b23b --- /dev/null +++ b/EllieHub/Models/WindowSize.cs @@ -0,0 +1,8 @@ +namespace EllieHub.Models; + +/// +/// Represents the dimensions of the application's window. +/// +/// The width of the application window. +/// The height of the application window. +public sealed record WindowSize(double Width, double Height); \ No newline at end of file diff --git a/EllieHub/Program.cs b/EllieHub/Program.cs new file mode 100644 index 0000000..8a970c5 --- /dev/null +++ b/EllieHub/Program.cs @@ -0,0 +1,23 @@ +using Avalonia; +using Avalonia.ReactiveUI; + +namespace EllieHub; + +internal sealed class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace() + .UseReactiveUI(); +} \ No newline at end of file diff --git a/EllieHub/Resources/Colors.axaml b/EllieHub/Resources/Colors.axaml new file mode 100644 index 0000000..a378581 --- /dev/null +++ b/EllieHub/Resources/Colors.axaml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + #24A148 + #D6D600 + + + + #FAF5F8 + #FCFAFC + #FFFAFD + #FF0067 + Blue + + + #252525 + #202020 + #181818 + #D90058 + #5090BB + + + \ No newline at end of file diff --git a/EllieHub/Resources/Fonts.axaml b/EllieHub/Resources/Fonts.axaml new file mode 100644 index 0000000..7b6d0a9 --- /dev/null +++ b/EllieHub/Resources/Fonts.axaml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + avares://EllieHub/Assets/Fonts/NotoSans-Regular.ttf + avares://EllieHub/Assets/Fonts/NotoSans-Bold.ttf + \ No newline at end of file diff --git a/EllieHub/Resources/Images.axaml b/EllieHub/Resources/Images.axaml new file mode 100644 index 0000000..9e52a0d --- /dev/null +++ b/EllieHub/Resources/Images.axaml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + avares://EllieHub/Assets/ellie.png + avares://EllieHub/Assets/paypal.png + avares://EllieHub/Assets/patreon.png + avares://EllieHub/Assets/ko-fi.webp + + + + + avares://EllieHub/Assets/Light/check_for_updates.png + avares://EllieHub/Assets/Light/config.png + avares://EllieHub/Assets/Light/deps.png + avares://EllieHub/Assets/Light/docs.png + avares://EllieHub/Assets/Light/home.png + avares://EllieHub/Assets/Light/icon-commands.png + avares://EllieHub/Assets/Light/icon-embeds.png + avares://EllieHub/Assets/Light/icon-link.png + avares://EllieHub/Assets/Light/icon-suggest.png + avares://EllieHub/Assets/Light/icon-support.png + avares://EllieHub/Assets/Light/ellieupdatericon.ico + avares://EllieHub/Assets/Light/ellieupdatericon.ico + avares://EllieHub/Assets/Light/terminal.png + + + avares://EllieHub/Assets/Dark/check_for_updates.png + avares://EllieHub/Assets/Dark/config.png + avares://EllieHub/Assets/Dark/deps.png + avares://EllieHub/Assets/Dark/docs.png + avares://EllieHub/Assets/Dark/home.png + avares://EllieHub/Assets/Dark/icon-commands.png + avares://EllieHub/Assets/Dark/icon-embeds.png + avares://EllieHub/Assets/Dark/icon-link.png + avares://EllieHub/Assets/Dark/icon-suggest.png + avares://EllieHub/Assets/Dark/icon-support.png + avares://EllieHub/Assets/Dark/ellieupdatericon.ico + avares://EllieHub/Assets/Dark/ellieupdatericon.ico + avares://EllieHub/Assets/Dark/terminal.png + + + \ No newline at end of file diff --git a/EllieHub/Services/Abstractions/FfmpregResolver.cs b/EllieHub/Services/Abstractions/FfmpregResolver.cs new file mode 100644 index 0000000..4ffd60a --- /dev/null +++ b/EllieHub/Services/Abstractions/FfmpregResolver.cs @@ -0,0 +1,71 @@ +namespace EllieHub.Services.Abstractions; + +/// +/// Base class for a service that checks, downloads, installs, and updates ffmpeg. +/// +public abstract class FfmpegResolver : IFfmpegResolver +{ + private readonly string _programVerifier = (Environment.OSVersion.Platform is PlatformID.Win32NT) ? "where" : "which"; + + /// + /// The name of the Ffmpeg process. + /// + protected const string FfmpegProcessName = "ffmpeg"; + + /// + public string DependencyName { get; } = "FFMPEG"; + + /// + public abstract string FileName { get; } + + /// + public virtual async ValueTask CanUpdateAsync(CancellationToken cToken = default) + { + // Check where ffmpeg is referenced. + using var whereProcess = Utilities.StartProcess(_programVerifier, FfmpegProcessName); + var installationPath = await whereProcess.StandardOutput.ReadToEndAsync(cToken); + + // If ffmpeg is present but not managed by us, just report it is installed. + if (!string.IsNullOrWhiteSpace(installationPath) && !installationPath.Contains(AppStatics.AppDepsUri, StringComparison.Ordinal)) + return false; + + var currentVer = await GetCurrentVersionAsync(cToken); + + // If ffmpeg or ffprobe are absent, a reinstall needs to be performed. + if (currentVer is null || !await Utilities.ProgramExistsAsync("ffprobe", cToken)) + return null; + + var latestVer = await GetLatestVersionAsync(cToken); + + return !latestVer.Equals(currentVer, StringComparison.Ordinal); + } + + /// + public virtual async ValueTask GetCurrentVersionAsync(CancellationToken cToken = default) + { + // If ffmpeg is not accessible from the shell... + if (!await Utilities.ProgramExistsAsync(FfmpegProcessName, cToken)) + { + // And doesn't exist in the dependencies folder, + // report that ffmpeg is not installed. + if (!File.Exists(Path.Combine(AppStatics.AppDepsUri, FileName))) + return null; + + // Else, add the dependencies directory to the PATH envar, + // then try again. + Utilities.AddPathToPATHEnvar(AppStatics.AppDepsUri); + return await GetCurrentVersionAsync(cToken); + } + + using var ffmpeg = Utilities.StartProcess(FfmpegProcessName, "-version"); + var match = AppStatics.FfmpegVersionRegex.Match(await ffmpeg.StandardOutput.ReadLineAsync(cToken) ?? string.Empty); + + return match.Groups[1].Value; + } + + /// + public abstract ValueTask GetLatestVersionAsync(CancellationToken cToken = default); + + /// + public abstract ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string dependenciesUri, CancellationToken cToken = default); +} \ No newline at end of file diff --git a/EllieHub/Services/Abstractions/IAppConfigManager.cs b/EllieHub/Services/Abstractions/IAppConfigManager.cs new file mode 100644 index 0000000..231fba9 --- /dev/null +++ b/EllieHub/Services/Abstractions/IAppConfigManager.cs @@ -0,0 +1,56 @@ +using EllieHub.Models; +using EllieHub.Models.Config; + +namespace EllieHub.Services.Abstractions; + +/// +/// Represents a service that manages the application's settings. +/// +public interface IAppConfigManager +{ + /// + /// The application settings. + /// + ReadOnlyAppConfig AppConfig { get; } + + /// + /// Creates a bot entry. + /// + /// The cancellation token. + /// The bot entry that got created. + /// Occurs when the bot entry is not successfully created. + ValueTask CreateBotEntryAsync(CancellationToken cToken = default); + + /// + /// Deletes a bot entry at the specified . + /// + /// The Id of the bot. + /// The cancellation token. + /// The bot entry that got deleted, otherwise. + ValueTask DeleteBotEntryAsync(Guid id, CancellationToken cToken = default); + + /// + /// Moves a bot entry in the list. + /// + /// The bot being swapped. + /// The bot to swap with. + /// The cancellation token. + /// if the entry got moved, otherwise. + ValueTask SwapBotEntryAsync(Guid firstBotId, Guid secondBotId, CancellationToken cToken = default); + + /// + /// Changes the bot entry with the specified . + /// + /// The Id of the bot. + /// The changes that should be performed on the entry. + /// The cancellation token. + /// if changes were made on the entry, otherwise. + ValueTask UpdateBotEntryAsync(Guid id, Func selector, CancellationToken cToken = default); + + /// + /// Changes the application's settings file according to the . + /// + /// The action to be performed on the configuration file. + /// The cancellation token. + ValueTask UpdateConfigAsync(Action action, CancellationToken cToken = default); +} \ No newline at end of file diff --git a/EllieHub/Services/Abstractions/IAppResolver.cs b/EllieHub/Services/Abstractions/IAppResolver.cs new file mode 100644 index 0000000..cfcfe71 --- /dev/null +++ b/EllieHub/Services/Abstractions/IAppResolver.cs @@ -0,0 +1,28 @@ +namespace EllieHub.Services.Abstractions; + +/// +/// Represents a service that updates this application. +/// +public interface IAppResolver : IDependencyResolver +{ + /// + /// The absolute path to the binary file of this application. + /// + string BinaryUri { get; } + + /// + /// The suffix appended to the name of old files. + /// + string OldFileSuffix { get; } + + /// + /// Removes the files from the old installation. + /// + /// if old files were removed, otherwise. + bool RemoveOldFiles(); + + /// + /// Starts the recently updated version of this application. + /// + void LaunchNewVersion(); +} \ No newline at end of file diff --git a/EllieHub/Services/Abstractions/IBotOrchestrator.cs b/EllieHub/Services/Abstractions/IBotOrchestrator.cs new file mode 100644 index 0000000..689f422 --- /dev/null +++ b/EllieHub/Services/Abstractions/IBotOrchestrator.cs @@ -0,0 +1,51 @@ +using EllieHub.Models.EventArguments; + +namespace EllieHub.Services.Abstractions; + +/// +/// Represents an object that coordinates multiple running processes of Ellie. +/// +public interface IBotOrchestrator +{ + /// + /// Raised when a bot process exits. + /// + event EventHandler? OnBotExit; + + /// + /// Raised when a bot process prints data to stderr. + /// + event EventHandler? OnStderr; + + /// + /// Raised when a bot process prints data to stdout. + /// + event EventHandler? OnStdout; + + /// + /// Determines whether the bot with the specified . + /// + /// The bot's Id. + /// if the bot is running, otherwise. + bool IsBotRunning(Guid botId); + + /// + /// Starts the bot with the specified . + /// + /// The bot's Id. + /// if the bot successfully started, otherwise. + bool Start(Guid botId); + + /// + /// Stops the bot with the specified . + /// + /// The bot's Id. + /// if the bot successfully stopped, otherwise. + bool Stop(Guid botId); + + /// + /// Stops all bot instances. + /// + /// if at least one bot instance was stopped, otherwise. + bool StopAll(); +} \ No newline at end of file diff --git a/EllieHub/Services/Abstractions/IBotResolver.cs b/EllieHub/Services/Abstractions/IBotResolver.cs new file mode 100644 index 0000000..0ae96a7 --- /dev/null +++ b/EllieHub/Services/Abstractions/IBotResolver.cs @@ -0,0 +1,23 @@ +namespace EllieHub.Services.Abstractions; + +/// +/// Represents a service that checks, downloads, installs, and updates a bot instance. +/// +public interface IBotResolver : IDependencyResolver +{ + /// + /// The name of the bot instance. + /// + string BotName { get; } + + /// + /// The Id of the bot. + /// + Guid Id { get; } + + /// + /// Creates a backup of the bot instance associated with this resolver. + /// + /// The absolute path to the backup file or if the backup failed. + ValueTask CreateBackupAsync(); +} \ No newline at end of file diff --git a/EllieHub/Services/Abstractions/IDependencyResolver.cs b/EllieHub/Services/Abstractions/IDependencyResolver.cs new file mode 100644 index 0000000..8c002f3 --- /dev/null +++ b/EllieHub/Services/Abstractions/IDependencyResolver.cs @@ -0,0 +1,59 @@ +namespace EllieHub.Services.Abstractions; + +/// +/// Represents a service that checks, downloads, installs, and updates a dependency. +/// +public interface IDependencyResolver +{ + /// + /// The name of this dependency. + /// + string DependencyName { get; } + + /// + /// The name of the dependency binary file. + /// + string FileName { get; } + + /// + /// Checks if the dependency can be updated. + /// + /// The cancellation token. + /// + /// if the dependency can be updated, + /// if the dependency is up-to-date, + /// if the dependency is not installed. + /// + ValueTask CanUpdateAsync(CancellationToken cToken = default); + + /// + /// Gets the version of the dependency currently installed on this system. + /// + /// The cancellation token. + /// The version of the dependency on this system or if the dependency is not installed. + ValueTask GetCurrentVersionAsync(CancellationToken cToken = default); + + /// + /// Gets the latest version of the dependency. + /// + /// The cancellation token. + /// The latest version of the dependency. + /// + /// Occurs when there is an issue with the redirection of GitHub's latest release link. + /// + ValueTask GetLatestVersionAsync(CancellationToken cToken = default); + + /// + /// Installs or updates the dependency on this system. + /// + /// The absolute path to the directory where the dependency should be installed to. + /// The cancellation token. + /// + /// A tuple that may or may not contain the old and new versions of the dependency.
+ /// (, ): the dependency is being updated by another thread, so no operation was performed.
+ /// (, ): the dependency is already up-to-date, so no operation was performed.
+ /// (, ): the dependency got installed.
+ /// (, ): the dependency got updated. + ///
+ ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string installationUri, CancellationToken cToken = default); +} \ No newline at end of file diff --git a/EllieHub/Services/Abstractions/IFfmpegResolver.cs b/EllieHub/Services/Abstractions/IFfmpegResolver.cs new file mode 100644 index 0000000..a627e9f --- /dev/null +++ b/EllieHub/Services/Abstractions/IFfmpegResolver.cs @@ -0,0 +1,9 @@ +namespace EllieHub.Services.Abstractions; + +/// +/// Represents a service that checks, downloads, installs, and updates ffmpeg. +/// +/// This interface exists mainly for DI registration. +public interface IFfmpegResolver : IDependencyResolver +{ +} \ No newline at end of file diff --git a/EllieHub/Services/Abstractions/ILogWriter.cs b/EllieHub/Services/Abstractions/ILogWriter.cs new file mode 100644 index 0000000..bb2a2e3 --- /dev/null +++ b/EllieHub/Services/Abstractions/ILogWriter.cs @@ -0,0 +1,52 @@ +using EllieHub.Models.EventArguments; +using System.Diagnostics.CodeAnalysis; + +namespace EllieHub.Services.Abstractions; + +/// +/// Represents a service that writes logs of bot instances to the disk. +/// +public interface ILogWriter +{ + /// + /// Raised when a log file is created. + /// + event EventHandler? OnLogCreated; + + /// + /// Writes the logs of all bots to a log file. + /// + /// + /// if the backing storage for the bot's logs should be removed from memory, otherwise. + /// + /// The cancellation token. + /// if at least one log file was created, otherwise. + Task FlushAllAsync(bool removeFromMemory = false, CancellationToken cToken = default); + + /// + /// Writes the logs of the specified bot to a log file. + /// + /// The Id of the bot. + /// + /// if the backing storage for the bot's logs should be removed from memory, otherwise. + /// + /// The cancellation token. + /// if the log file was successfully created, otherwise. + Task FlushAsync(Guid botId, bool removeFromMemory = false, CancellationToken cToken = default); + + /// + /// Safely adds a to the log of the bot with the specified . + /// + /// The Id of the bot. + /// The message to be appended to the log. + /// if the was successfully added to the log, otherwise. + bool TryAdd(Guid botId, string message); + + /// + /// Safely gets the logs of the bot with the specified . + /// + /// The Id of the bot. + /// The log of the bot. + /// if the was successfully retrieved, otherwise. + bool TryRead(Guid botId, [MaybeNullWhen(false)] out string log); +} \ No newline at end of file diff --git a/EllieHub/Services/Abstractions/IYtdlpResolver.cs b/EllieHub/Services/Abstractions/IYtdlpResolver.cs new file mode 100644 index 0000000..a19834f --- /dev/null +++ b/EllieHub/Services/Abstractions/IYtdlpResolver.cs @@ -0,0 +1,9 @@ +namespace EllieHub.Services.Abstractions; + +/// +/// Represents a service that checks, downloads, installs, and updates yt-dlp. +/// +/// This interface exists mainly for DI registration. +public interface IYtdlpResolver : IDependencyResolver +{ +} \ No newline at end of file diff --git a/EllieHub/Services/AppConfigManager.cs b/EllieHub/Services/AppConfigManager.cs new file mode 100644 index 0000000..1c8f66d --- /dev/null +++ b/EllieHub/Services/AppConfigManager.cs @@ -0,0 +1,126 @@ +using EllieHub.Models; +using EllieHub.Models.Config; +using EllieHub.Services.Abstractions; +using System.Text.Json; + +namespace EllieHub.Services; + +/// +/// Defines a service that manages the application's settings. +/// +public sealed class AppConfigManager : IAppConfigManager +{ + private readonly JsonSerializerOptions _jsonSerializerOptions = new() { WriteIndented = true }; + private readonly AppConfig _appConfig; + + /// + public ReadOnlyAppConfig AppConfig { get; } + + /// + /// Creates a service that manages the application's settings. + /// + public AppConfigManager(AppConfig appConfig, ReadOnlyAppConfig readOnlyAppConfig) + { + _appConfig = appConfig; + AppConfig = readOnlyAppConfig; + + Directory.CreateDirectory(AppStatics.AppDefaultConfigDirectoryUri); // Create the directory where the app settings will be stored. + } + + /// + public async ValueTask CreateBotEntryAsync(CancellationToken cToken = default) + { + var newId = CreateNewId(); + var newPosition = (_appConfig.BotEntries.Count is 0) ? 0 : _appConfig.BotEntries.Values.Max(x => x.Position) + 1; + var newBotName = "NewBot_" + newPosition; + var newEntry = new BotInstanceInfo(newBotName, Path.Combine(_appConfig.BotsDirectoryUri, newBotName), newPosition); + + if (!_appConfig.BotEntries.TryAdd(newId, newEntry)) + throw new InvalidOperationException($"Could not create a new bot entry with Id {newId}."); + + await SaveAsync(cToken); + + return new(newId, newEntry); + } + + /// + public async ValueTask DeleteBotEntryAsync(Guid id, CancellationToken cToken = default) + { + if (!_appConfig.BotEntries.TryRemove(id, out var removedEntry)) + return null; + + Utilities.TryDeleteDirectory(removedEntry.InstanceDirectoryUri); + + await SaveAsync(cToken); + + return new(id, removedEntry); + } + + /// + public async ValueTask SwapBotEntryAsync(Guid firstBotId, Guid secondBotId, CancellationToken cToken = default) + { + if (firstBotId == secondBotId + || !_appConfig.BotEntries.TryGetValue(firstBotId, out var firstBotEntry) + || !_appConfig.BotEntries.TryGetValue(secondBotId, out var secondBotEntry)) + return false; + + var tempFirstPosition = firstBotEntry.Position; + + _appConfig.BotEntries[firstBotId] = _appConfig.BotEntries[firstBotId] with { Position = secondBotEntry.Position }; + _appConfig.BotEntries[secondBotId] = _appConfig.BotEntries[secondBotId] with { Position = tempFirstPosition }; + + await SaveAsync(cToken); + + return true; + } + + /// + public async ValueTask UpdateBotEntryAsync(Guid id, Func selector, CancellationToken cToken = default) + { + if (!_appConfig.BotEntries.TryRemove(id, out var entry)) + return false; + + var updatedEntry = selector(entry); + + _appConfig.BotEntries.TryAdd(id, updatedEntry); + + await SaveAsync(cToken); + + return true; + } + + /// + public ValueTask UpdateConfigAsync(Action action, CancellationToken cToken = default) + { + action(_appConfig); + return SaveAsync(cToken); + } + + /// + /// Creates a new Id that is known to be unique in the configuration file. + /// + /// An unique Id. + private Guid CreateNewId() + { + var id = Guid.NewGuid(); + + while (_appConfig.BotEntries.ContainsKey(id)) + id = Guid.NewGuid(); + + return id; + } + + /// + /// Saves the bot entries to a configuration file. + /// + /// The cancellation token. + private async ValueTask SaveAsync(CancellationToken cToken = default) + { + // Create the directory where the config file will be stored, if it doesn't exist. + Directory.CreateDirectory(AppStatics.AppDefaultConfigDirectoryUri); + + // Create the configuration file. + var json = JsonSerializer.Serialize(_appConfig, _jsonSerializerOptions); + await File.WriteAllTextAsync(AppStatics.AppConfigUri, json, cToken); + } +} \ No newline at end of file diff --git a/EllieHub/Services/AppResolver.cs b/EllieHub/Services/AppResolver.cs new file mode 100644 index 0000000..7ad4454 --- /dev/null +++ b/EllieHub/Services/AppResolver.cs @@ -0,0 +1,185 @@ +using EllieHub.Services.Abstractions; +using System.IO.Compression; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace EllieHub.Services; + +/// +/// Defines a service that updates this application. +/// +public sealed class AppResolver : IAppResolver +{ + private static readonly string _tempDirectory = Path.GetTempPath(); + private static readonly string _downloadedFileName = GetDownloadFileName(); + private static readonly string? _currentUpdaterVersion = Assembly.GetEntryAssembly()?.GetName().Version?.ToString(); + private readonly IHttpClientFactory _httpClientFactory; + + /// + public string DependencyName { get; } = "EllieHub"; + + /// + public string OldFileSuffix { get; } = "_old"; + + /// + public string FileName { get; } + + /// + public string BinaryUri { get; } + + /// + /// Creates a service that updates this application. + /// + /// The Http client factory. + public AppResolver(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + FileName = (OperatingSystem.IsWindows()) ? "EllieHub.exe" : "EllieHub"; + BinaryUri = Path.Combine(AppContext.BaseDirectory, FileName); + } + + /// + public ValueTask GetCurrentVersionAsync(CancellationToken cToken = default) + => ValueTask.FromResult(_currentUpdaterVersion); + + /// + public void LaunchNewVersion() + => Utilities.StartProcess(BinaryUri); + + /// + /// if the updater can be updated, + /// if the updater is up-to-date, + /// if the updater cannot update itself. + /// + /// + public async ValueTask CanUpdateAsync(CancellationToken cToken = default) + { + if (!Utilities.CanWriteTo(AppContext.BaseDirectory)) + return null; + + var currentVersion = await GetCurrentVersionAsync(cToken); + + if (currentVersion is null) + return null; + + var latestVersion = await GetLatestVersionAsync(cToken); + + if (latestVersion.Equals(currentVersion, StringComparison.Ordinal)) + return false; + + var http = _httpClientFactory.CreateClient(); + + return await http.IsUrlValidAsync($"https://toastielab.dev/ToastieSharp/EllieHub/releases/download/{latestVersion}/{_downloadedFileName}", cToken); + } + + /// + public bool RemoveOldFiles() + { + var result = false; + + foreach (var file in Directory.GetFiles(AppContext.BaseDirectory).Where(x => x.EndsWith(OldFileSuffix))) + result |= Utilities.TryDeleteFile(file); + + return result; + } + + /// + public async ValueTask GetLatestVersionAsync(CancellationToken cToken = default) + { + var http = _httpClientFactory.CreateClient(AppConstants.NoRedirectClient); + + var response = await http.GetAsync("https://toastielab.dev/ToastieSharp/EllieHub/releases/latest", cToken); + + var lastSlashIndex = response.Headers.Location?.OriginalString.LastIndexOf('/') + ?? throw new InvalidOperationException("Failed to get the latest EllieBotUpdater version."); + + return response.Headers.Location.OriginalString[(lastSlashIndex + 1)..]; + } + + /// + public async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string installationUri, CancellationToken cToken = default) + { + var currentVersion = await GetCurrentVersionAsync(cToken); + var latestVersion = await GetLatestVersionAsync(cToken); + + if (latestVersion.Equals(currentVersion, StringComparison.Ordinal)) + return (currentVersion, null); + + var http = _httpClientFactory.CreateClient(); + var appTempLocation = Path.Combine(_tempDirectory, _downloadedFileName[..(_downloadedFileName.LastIndexOf('.'))]); + var zipTempLocation = Path.Combine(_tempDirectory, _downloadedFileName); + + try + { + using var downloadStream = await http.GetStreamAsync($"https://toastielab.dev/ToastieSharp/EllieHub/releases/download/{latestVersion}/{_downloadedFileName}", cToken); + + // Save the zip file + using (var fileStream = new FileStream(zipTempLocation, FileMode.Create)) + await downloadStream.CopyToAsync(fileStream, cToken); + + // Extract the zip file + await Task.Run(() => ZipFile.ExtractToDirectory(zipTempLocation, _tempDirectory), cToken); + + // Move the new binary and its dependencies + var newFilesUris = Directory.EnumerateFiles(appTempLocation); + + foreach (var newFileUri in newFilesUris) + { + var destinationUri = Path.Combine(AppContext.BaseDirectory, newFileUri[(newFileUri.LastIndexOf(Path.DirectorySeparatorChar) + 1)..]); + + // Rename the original file from "file" to "file_old". + if (File.Exists(destinationUri)) + File.Move(destinationUri, destinationUri + OldFileSuffix); + + // Move the new file to the application's directory. + if (Environment.OSVersion.Platform is not PlatformID.Unix) + File.Move(newFileUri, destinationUri, true); + else + { + // Circumvent this issue on Unix systems: https://github.com/dotnet/runtime/issues/31149 + using var moveProcess = Utilities.StartProcess("mv", $"\"{newFileUri}\" \"{destinationUri}\""); + await moveProcess.WaitForExitAsync(cToken); + } + } + + // Mark the new binary file as executable. + if (Environment.OSVersion.Platform is PlatformID.Unix) + { + using var chmod = Utilities.StartProcess("chmod", $"+x \"{BinaryUri}\""); + await chmod.WaitForExitAsync(cToken); + } + + return (currentVersion, latestVersion); + } + finally + { + // Cleanup + Utilities.TryDeleteFile(zipTempLocation); + Utilities.TryDeleteDirectory(appTempLocation); + } + } + + /// + /// Gets the name of the file to be downloaded. + /// + /// The name of the file to be downloaded. + /// Occurs when this method is used in an unsupported system. + private static string GetDownloadFileName() + { + return RuntimeInformation.OSArchitecture switch + { + // Windows + Architecture.X64 when OperatingSystem.IsWindows() => "EllieHub_win-x64.zip", + Architecture.Arm64 when OperatingSystem.IsWindows() => "EllieHub_win-arm64.zip", + + // Linux + Architecture.X64 when OperatingSystem.IsLinux() => "EllieHub_linux-x64.zip", + Architecture.Arm64 when OperatingSystem.IsLinux() => "EllieHub_linux-arm64.zip", + + // MacOS + Architecture.X64 when OperatingSystem.IsMacOS() => "EllieHub_osx-x64.zip", + Architecture.Arm64 when OperatingSystem.IsMacOS() => "EllieHub_osx-arm64.zip", + _ => throw new NotSupportedException($"Architecture of type {RuntimeInformation.OSArchitecture} is not supported by EllieHub on this OS.") + }; + } +} \ No newline at end of file diff --git a/EllieHub/Services/EllieOrchestrator.cs b/EllieHub/Services/EllieOrchestrator.cs new file mode 100644 index 0000000..2b05bd3 --- /dev/null +++ b/EllieHub/Services/EllieOrchestrator.cs @@ -0,0 +1,139 @@ +using EllieHub.Models.Config; +using EllieHub.Models.EventArguments; +using EllieHub.Services.Abstractions; +using System.Diagnostics; + +namespace EllieHub.Services; + +/// +/// Defines an object that coordinates multiple running processes of EllieBot. +/// +public sealed class EllieOrchestrator : IBotOrchestrator +{ + private readonly Dictionary _runningBots = new(); + private readonly ReadOnlyAppConfig _appConfig; + private readonly string _fileName = (OperatingSystem.IsWindows()) ? "NadekoBot.exe" : "NadekoBot"; + + /// + public event EventHandler? OnBotExit; + + /// + public event EventHandler? OnStderr; + + /// + public event EventHandler? OnStdout; + + /// + /// Creates an object that coordinates multiple running processes of EllieBot. + /// + /// The application settings. + public EllieOrchestrator(ReadOnlyAppConfig appConfig) + => _appConfig = appConfig; + + + /// + public bool IsBotRunning(Guid botId) + => _runningBots.ContainsKey(botId); + + + /// + public bool Start(Guid botId) + { + if (_runningBots.ContainsKey(botId) + || !_appConfig.BotEntries.TryGetValue(botId, out var botEntry) + || !File.Exists(Path.Combine(botEntry.InstanceDirectoryUri, _fileName))) + return false; + + var botProcess = Process.Start(new ProcessStartInfo() + { + FileName = Path.Combine(botEntry.InstanceDirectoryUri, _fileName), + WorkingDirectory = botEntry.InstanceDirectoryUri, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + }); + + if (botProcess is null) + return false; + + botProcess.EnableRaisingEvents = true; + botProcess.OutputDataReceived += EmitStdout; + botProcess.ErrorDataReceived += EmitStderr; + botProcess.Exited += OnExit; + botProcess.BeginOutputReadLine(); + botProcess.BeginErrorReadLine(); + + return _runningBots.TryAdd(botId, botProcess); + } + + /// + public bool Stop(Guid botId) + { + if (!_runningBots.TryGetValue(botId, out var botProcess)) + return false; + + botProcess.Kill(true); + return true; + } + + /// + public bool StopAll() + { + var amount = _runningBots.Count; + + foreach (var process in _runningBots.Values) + try { process.Kill(true); } catch { } + + return amount is not 0; + } + + /// + /// Finalizes a process when it stops running. + /// + /// The . + /// The event arguments. + /// Occurs when is not of type . + private void OnExit(object? sender, EventArgs eventArgs) + { + var (id, process) = _runningBots.First(x => x.Value.Equals(sender)); + OnBotExit?.Invoke(this, new(id, process.ExitCode)); + + _runningBots.Remove(id); + process.CancelOutputRead(); + process.CancelErrorRead(); + process.Dispose(); + } + + /// + /// Raises with its appropriate event arguments. + /// + /// The . + /// The event arguments. + private void EmitStdout(object sender, DataReceivedEventArgs eventArgs) + { + if (string.IsNullOrWhiteSpace(eventArgs.Data)) + return; + + var (id, _) = _runningBots.First(x => x.Value.Equals(sender)); + var newEventArgs = new ProcessStdWriteEventArgs(id, eventArgs.Data); + + OnStdout?.Invoke(this, newEventArgs); + } + + /// + /// Raises with its appropriate event arguments. + /// + /// The . + /// The event arguments. + private void EmitStderr(object sender, DataReceivedEventArgs eventArgs) + { + if (string.IsNullOrWhiteSpace(eventArgs.Data)) + return; + + var (id, _) = _runningBots.First(x => x.Value.Equals(sender)); + var newEventArgs = new ProcessStdWriteEventArgs(id, eventArgs.Data); + + OnStderr?.Invoke(this, newEventArgs); + } +} \ No newline at end of file diff --git a/EllieHub/Services/EllieResolver.cs b/EllieHub/Services/EllieResolver.cs new file mode 100644 index 0000000..6e2f6cf --- /dev/null +++ b/EllieHub/Services/EllieResolver.cs @@ -0,0 +1,274 @@ +using EllieHub.Services.Abstractions; +using System.Formats.Tar; +using System.IO.Compression; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; + +namespace EllieHub.Services; + +/// +/// Service that checks, downloads, installs, and updates a NadekoBot instance. +/// +/// Source: https://gitlab.com/Kwoth/nadekobot/-/releases/permalink/latest +public sealed partial class EllieResolver : IBotResolver +{ + private static readonly HashSet _updateIdOngoing = new(); + private static readonly string _tempDirectory = Path.GetTempPath(); + private static readonly Regex _unzipedDirRegex = GenerateUnzipedDirRegex(); + private readonly IHttpClientFactory _httpClientFactory; + private readonly IAppConfigManager _appConfigManager; + + /// + public string DependencyName { get; } = "NadekoBot"; + + /// + public string FileName { get; } = (OperatingSystem.IsWindows()) ? "NadekoBot.exe" : "NadekoBot"; + + /// + public Guid Id { get; } + + /// + public string BotName { get; } + + /// + /// Creates a service that checks, downloads, installs, and updates a NadekoBot instance. + /// + /// The HTTP client factory. + /// The application's settings. + /// The Id of the bot. + public EllieResolver(IHttpClientFactory httpClientFactory, IAppConfigManager appConfigManager, Guid botId) + { + _httpClientFactory = httpClientFactory; + _appConfigManager = appConfigManager; + Id = botId; + BotName = _appConfigManager.AppConfig.BotEntries[Id].Name; + } + + /// + public async ValueTask CanUpdateAsync(CancellationToken cToken = default) + { + var currentVersion = await GetCurrentVersionAsync(cToken); + + if (currentVersion is null) + return null; + + var latestVersion = await GetLatestVersionAsync(cToken); + + if (latestVersion.Equals(currentVersion, StringComparison.Ordinal)) + return false; + + var http = _httpClientFactory.CreateClient(); + + return await http.IsUrlValidAsync( + $"https://gitlab.com/api/v4/projects/9321079/packages/generic/NadekoBot-build/{latestVersion}/{GetDownloadFileName(latestVersion)}", + cToken + ); + } + + /// + public async ValueTask CreateBackupAsync() + { + var botInstance = _appConfigManager.AppConfig.BotEntries[Id]; + + if (!Directory.Exists(botInstance.InstanceDirectoryUri)) + return null; + + Directory.CreateDirectory(_appConfigManager.AppConfig.BotsBackupDirectoryUri); + + var now = DateTimeOffset.Now; + var date = new DateOnly(now.Year, now.Month, now.Day).ToShortDateString().Replace('/', '-'); + var backupZipName = $"{botInstance.Name}_{date}-{now.ToUnixTimeMilliseconds()}.zip"; + var destinationUri = Path.Combine(_appConfigManager.AppConfig.BotsBackupDirectoryUri, backupZipName); + + // ZipFile does not provide asynchronous implementations, so we have to schedule its + // execution to be run in the thread-pool due to how long it takes to finish execution. + await Task.Run(() => ZipFile.CreateFromDirectory(botInstance.InstanceDirectoryUri, destinationUri, CompressionLevel.SmallestSize, true)); + + return destinationUri; + } + + /// + public async ValueTask GetCurrentVersionAsync(CancellationToken cToken = default) + { + var botEntry = _appConfigManager.AppConfig.BotEntries[Id]; + + if (!string.IsNullOrWhiteSpace(botEntry.Version)) + return botEntry.Version; + + var assemblyUri = Path.Combine(botEntry.InstanceDirectoryUri, "NadekoBot.dll"); + + if (!File.Exists(assemblyUri)) + return null; + var nadekoAssembly = Assembly.LoadFile(assemblyUri); + 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}"; + + await _appConfigManager.UpdateBotEntryAsync(Id, x => x with { Version = currentVersion }, cToken); + + return currentVersion; + } + + /// + public async ValueTask GetLatestVersionAsync(CancellationToken cToken = default) + { + var http = _httpClientFactory.CreateClient(AppConstants.NoRedirectClient); + + var response = await http.GetAsync("https://gitlab.com/Kwoth/nadekobot/-/releases/permalink/latest", cToken); + + var lastSlashIndex = response.Headers.Location?.OriginalString.LastIndexOf('/') + ?? throw new InvalidOperationException("Failed to get the latest NadekoBot version."); + + return response.Headers.Location.OriginalString[(lastSlashIndex + 1)..]; + } + + /// + public async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string installationUri, CancellationToken cToken = default) + { + if (_updateIdOngoing.Contains(Id)) + return (null, null); + + _updateIdOngoing.Add(Id); + + var currentVersion = await GetCurrentVersionAsync(cToken); + var latestVersion = await GetLatestVersionAsync(cToken); + + // Update + if (latestVersion == currentVersion) + { + _updateIdOngoing.Remove(Id); + return (currentVersion, null); + } + + var backupFileUri = await CreateBackupAsync(); + + if (currentVersion is not null) + Directory.Delete(installationUri, true); + + // Install + Directory.CreateDirectory(_appConfigManager.AppConfig.BotsDirectoryUri); + + var http = _httpClientFactory.CreateClient(); + var downloadFileName = GetDownloadFileName(latestVersion); + var botTempLocation = Path.Combine(_tempDirectory, "nadekobot-" + _unzipedDirRegex.Match(downloadFileName).Groups[1].Value); + var zipTempLocation = Path.Combine(_tempDirectory, downloadFileName); + + try + { + using var downloadStream = await http.GetStreamAsync( + $"https://gitlab.com/api/v4/projects/9321079/packages/generic/NadekoBot-build/{latestVersion}/{downloadFileName}", + cToken + ); + + // Move the bot root directory while renaming it + if (Environment.OSVersion.Platform is not PlatformID.Unix) + { + // Save the zip file + using (var fileStream = new FileStream(zipTempLocation, FileMode.Create)) + await downloadStream.CopyToAsync(fileStream, cToken); + + // Extract the zip file + await Task.Run(() => ZipFile.ExtractToDirectory(zipTempLocation, _tempDirectory), cToken); + + // Move the bot root directory while renaming it + Directory.Move(botTempLocation, installationUri); + } + else + { + // Extract the tar ball + await TarFile.ExtractToDirectoryAsync(downloadStream, _tempDirectory, true, cToken); + + // Move the bot root directory with "mv" to circumvent this issue on Unix systems: https://github.com/dotnet/runtime/issues/31149 + using var moveProcess = Utilities.StartProcess("mv", $"\"{botTempLocation}\" \"{installationUri}\""); + await moveProcess.WaitForExitAsync(cToken); + + // Set executable permission + using var chmod = Utilities.StartProcess("chmod", $"+x \"{Path.Combine(installationUri, FileName)}\""); + await chmod.WaitForExitAsync(cToken); + } + + // Reapply bot settings + if (File.Exists(backupFileUri)) + { + using var zipFile = ZipFile.OpenRead(backupFileUri); + var zippedFiles = zipFile.Entries + .Where(x => + x.Name is "creds.yml" or "creds_example.yml" + || (!string.IsNullOrWhiteSpace(x.Name) && x.FullName.Contains("data/")) + ); + + foreach (var zippedFile in zippedFiles) + { + var fileDestinationPath = zippedFile.FullName.Split('/') + .Prepend(Directory.GetParent(installationUri)?.FullName ?? string.Empty) + .ToArray(); + + await RestoreFileAsync(zippedFile, Path.Combine(fileDestinationPath), cToken); + } + } + + // Update settings + await _appConfigManager.UpdateBotEntryAsync(Id, x => x with { Version = latestVersion }, cToken); + + // Create creds.yml + var credsUri = Path.Combine(installationUri, "creds.yml"); + + if (!File.Exists(credsUri)) + File.Copy(Path.Combine(installationUri, "creds_example.yml"), credsUri); + + return (currentVersion, latestVersion); + } + finally + { + _updateIdOngoing.Remove(Id); + + // Cleanup + Utilities.TryDeleteFile(zipTempLocation); + Utilities.TryDeleteDirectory(botTempLocation); + } + } + + /// + /// Extracts the specified to the . + /// + /// The file to be extracted. + /// The final location of the extracted file. + /// The cancellation token. + private async static ValueTask RestoreFileAsync(ZipArchiveEntry zippedFile, string destinationPath, CancellationToken cToken = default) + { + using var zipStream = zippedFile.Open(); + using var fileStream = new FileStream(destinationPath, FileMode.Create); + + await zipStream.CopyToAsync(fileStream, cToken); + } + + /// + /// Gets the name of the file to be downloaded. + /// + /// The version of NadekoBot. + /// The name of the file to download. + /// Occurs when this method is executed in an unsupported platform. + private static string GetDownloadFileName(string version) + { + return version + RuntimeInformation.OSArchitecture switch + { + // Windows + Architecture.X64 when OperatingSystem.IsWindows() => "-windows-x64-build.zip", + Architecture.Arm64 when OperatingSystem.IsWindows() => "-windows-arm64-build.zip", + + // Linux + Architecture.X64 when OperatingSystem.IsLinux() => "-linux-x64-build.tar", + Architecture.Arm64 when OperatingSystem.IsLinux() => "-linux-arm64-build.tar", + + // MacOS + Architecture.X64 when OperatingSystem.IsMacOS() => "-osx-x64-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.") + }; + } + + [GeneratedRegex(@"^(?:\S+\-)(\S+\-\S+)\-", RegexOptions.Compiled)] + private static partial Regex GenerateUnzipedDirRegex(); +} \ No newline at end of file diff --git a/EllieHub/Services/FfmpegLinuxResolver.cs b/EllieHub/Services/FfmpegLinuxResolver.cs new file mode 100644 index 0000000..b81a174 --- /dev/null +++ b/EllieHub/Services/FfmpegLinuxResolver.cs @@ -0,0 +1,110 @@ +using EllieHub.Services.Abstractions; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Text.RegularExpressions; + +namespace EllieHub.Services; + +/// +/// Service that checks, downloads, installs, and updates ffmpeg on Linux. +/// +/// Source: https://johnvansickle.com/ffmpeg +[SupportedOSPlatform("linux")] +public sealed partial class FfmpegLinuxResolver : FfmpegResolver +{ + private readonly Regex _ffmpegLatestVersionRegex = FfmpegLatestVersionRegexGenerator(); + private readonly string _tempDirectory = Path.GetTempPath(); + private bool _isUpdating = false; + private readonly IHttpClientFactory _httpClientFactory; + + /// + public override string FileName { get; } = "ffmpeg"; + + /// + /// Creates a service that checks, downloads, installs, and updates ffmpeg on Linux. + /// + /// The HTTP client factory. + public FfmpegLinuxResolver(IHttpClientFactory httpClientFactory) + => _httpClientFactory = httpClientFactory; + + /// + public override ValueTask CanUpdateAsync(CancellationToken cToken = default) + { + return (RuntimeInformation.OSArchitecture is Architecture.X64 or Architecture.Arm64) + ? base.CanUpdateAsync(cToken) + : ValueTask.FromResult(false); + } + + /// + public override async ValueTask GetLatestVersionAsync(CancellationToken cToken = default) + { + var http = _httpClientFactory.CreateClient(); + var pageContent = await http.GetStringAsync("https://johnvansickle.com/ffmpeg", cToken); + var match = _ffmpegLatestVersionRegex.Match(pageContent); + + return (match.Success) + ? match.Groups[1].Value + : throw new InvalidOperationException("Regex did not match the web page content."); + } + + /// + public override async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string dependenciesUri, CancellationToken cToken = default) + { + if (_isUpdating) + return (null, null); + + _isUpdating = true; + var currentVersion = await GetCurrentVersionAsync(cToken); + var newVersion = await GetLatestVersionAsync(cToken); + + // Update + if (currentVersion is not null) + { + // If the versions are the same, exit. + if (currentVersion == newVersion) + return (currentVersion, null); + + Utilities.TryDeleteFile(Path.Combine(dependenciesUri, FileName)); + Utilities.TryDeleteFile(Path.Combine(dependenciesUri, "ffprobe")); + } + + // Install + Directory.CreateDirectory(dependenciesUri); + + var architecture = (RuntimeInformation.OSArchitecture is Architecture.X64) ? "amd" : "arm"; + var tarFileName = $"ffmpeg-release-{architecture}64-static.tar.xz"; + var http = _httpClientFactory.CreateClient(); + using var downloadStream = await http.GetStreamAsync($"https://johnvansickle.com/ffmpeg/releases/{tarFileName}", cToken); + + // Save tar file to the temporary directory. + var tarFilePath = Path.Combine(_tempDirectory, tarFileName); + var tarExtractDir = Path.Combine(_tempDirectory, $"ffmpeg-{newVersion}-{architecture}64-static"); + using (var fileStream = new FileStream(tarFilePath, FileMode.Create)) + await downloadStream.CopyToAsync(fileStream, cToken); + + // Extract the tar file. + using var extractProcess = Utilities.StartProcess("tar", $"xf \"{tarFilePath}\" --directory=\"{_tempDirectory}\""); + await extractProcess.WaitForExitAsync(cToken); + + // Move ffmpeg to the dependencies directory. + File.Move(Path.Combine(tarExtractDir, FileName), Path.Combine(dependenciesUri, FileName), true); + File.Move(Path.Combine(tarExtractDir, "ffprobe"), Path.Combine(dependenciesUri, "ffprobe"), true); + + // Mark the files as executable. + using var chmod = Utilities.StartProcess("chmod", $"+x \"{Path.Combine(dependenciesUri, FileName)}\" \"{Path.Combine(dependenciesUri, "ffprobe")}\""); + await chmod.WaitForExitAsync(cToken); + + // Cleanup + File.Delete(tarFilePath); + Directory.Delete(tarExtractDir, true); + + // Update environment variable + Utilities.AddPathToPATHEnvar(dependenciesUri); + + _isUpdating = false; + return (currentVersion, newVersion); + } + + [GeneratedRegex(@"release:\s?([\d\.]+)", RegexOptions.Compiled)] + private static partial Regex FfmpegLatestVersionRegexGenerator(); +} \ No newline at end of file diff --git a/EllieHub/Services/FfmpegMacResolver.cs b/EllieHub/Services/FfmpegMacResolver.cs new file mode 100644 index 0000000..3250fba --- /dev/null +++ b/EllieHub/Services/FfmpegMacResolver.cs @@ -0,0 +1,116 @@ +using EllieHub.Models.Api; +using EllieHub.Services.Abstractions; +using System.IO.Compression; +using System.Runtime.Versioning; + +namespace EllieHub.Services; + +/// +/// Service that checks, downloads, installs, and updates ffmpeg on MacOS. +/// +/// Source: https://evermeet.cx/ffmpeg +[SupportedOSPlatform("osx")] +public sealed class FfmpegMacResolver : FfmpegResolver +{ + private const string _apiFfmpegInfoEndpoint = "https://evermeet.cx/ffmpeg/info/ffmpeg/release"; + private const string _apiFfprobeInfoEndpoint = "https://evermeet.cx/ffmpeg/info/ffprobe/release"; + private readonly string _tempDirectory = Path.GetTempPath(); + private bool _isUpdating = false; + private readonly IHttpClientFactory _httpClientFactory; + + /// + public override string FileName { get; } = "ffmpeg"; + + /// + /// Creates a service that checks, downloads, installs, and updates ffmpeg on MacOS. + /// + /// The HTTP client factory. + public FfmpegMacResolver(IHttpClientFactory httpClientFactory) + => _httpClientFactory = httpClientFactory; + + /// + public override async ValueTask GetLatestVersionAsync(CancellationToken cToken = default) + { + var http = _httpClientFactory.CreateClient(); + var response = await http.CallApiAsync(_apiFfmpegInfoEndpoint, cToken); + + return response.Version; + } + + /// + public override async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string dependenciesUri, CancellationToken cToken = default) + { + if (_isUpdating) + return (null, null); + + _isUpdating = true; + var currentVersion = await GetCurrentVersionAsync(cToken); + var newVersion = await GetLatestVersionAsync(cToken); + + // Update + if (currentVersion is not null) + { + // If the versions are the same, exit. + if (currentVersion == newVersion) + { + _isUpdating = false; + return (currentVersion, null); + } + + Utilities.TryDeleteFile(Path.Combine(dependenciesUri, FileName)); + Utilities.TryDeleteFile(Path.Combine(dependenciesUri, "ffprobe")); + } + + // Install + Directory.CreateDirectory(dependenciesUri); + + var http = _httpClientFactory.CreateClient(); + var ffmpegResponse = await http.CallApiAsync(_apiFfmpegInfoEndpoint, cToken); + var ffprobeResponse = await http.CallApiAsync(_apiFfprobeInfoEndpoint, cToken); + + await Task.WhenAll( + InstallDependencyAsync(ffmpegResponse, dependenciesUri, cToken), + InstallDependencyAsync(ffprobeResponse, dependenciesUri, cToken) + ); + + // Update environment variable + Utilities.AddPathToPATHEnvar(dependenciesUri); + + _isUpdating = false; + return (currentVersion, newVersion); + } + + /// + /// Install the dependency provided by . + /// + /// The dependency to be installed. + /// The absolute path to the directory where the dependency should be installed to. + /// The cancellation token. + private async Task InstallDependencyAsync(EvermeetInfo downloadInfo, string dependenciesUri, CancellationToken cToken = default) + { + var http = _httpClientFactory.CreateClient(); + var downloadUrl = downloadInfo.Download["zip"].Url; + var zipFileName = downloadUrl[(downloadUrl.LastIndexOf('/') + 1)..]; + var zipFilePath = Path.Combine(_tempDirectory, zipFileName); + + // Download the zip file and save it to the temporary directory. + using var zipStream = await http.GetStreamAsync(downloadUrl, cToken); + + using (var fileStream = new FileStream(zipFilePath, FileMode.Create)) + await zipStream.CopyToAsync(fileStream, cToken); + + // Extract the zip file. + ZipFile.ExtractToDirectory(zipFileName, _tempDirectory); + + // Move the dependency binary. + var finalFileUri = Path.Combine(dependenciesUri, downloadInfo.Name); + File.Move(Path.Combine(_tempDirectory, downloadInfo.Name), finalFileUri, true); + + // Mark binary as executable. + using var chmod = Utilities.StartProcess("chmod", $"+x \"{finalFileUri}\""); + await chmod.WaitForExitAsync(cToken); + + // Cleanup. + File.Delete(zipFilePath); + } +} \ No newline at end of file diff --git a/EllieHub/Services/FfmpegWindowsResolver.cs b/EllieHub/Services/FfmpegWindowsResolver.cs new file mode 100644 index 0000000..9857788 --- /dev/null +++ b/EllieHub/Services/FfmpegWindowsResolver.cs @@ -0,0 +1,112 @@ +using EllieHub.Services.Abstractions; +using System.IO.Compression; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace EllieHub.Services; + +/// +/// Service that checks, downloads, installs, and updates ffmpeg on Windows. +/// +/// Source: https://github.com/GyanD/codexffmpeg/releases/latest +[SupportedOSPlatform("windows")] +public sealed class FfmpegWindowsResolver : FfmpegResolver +{ + private readonly string _tempDirectory = Path.GetTempPath(); + private bool _isUpdating = false; + private readonly IHttpClientFactory _httpClientFactory; + + /// + public override string FileName { get; } = "ffmpeg.exe"; + + /// + /// Creates a service that checks, downloads, installs, and updates ffmpeg on Windows. + /// + /// The HTTP client factory. + public FfmpegWindowsResolver(IHttpClientFactory httpClientFactory) + => _httpClientFactory = httpClientFactory; + + /// + public override ValueTask CanUpdateAsync(CancellationToken cToken = default) + { + // I could not find any ARM build of ffmpeg for Windows. + return (RuntimeInformation.OSArchitecture is Architecture.X64) + ? base.CanUpdateAsync(cToken) + : ValueTask.FromResult(false); + } + + /// + public override async ValueTask GetLatestVersionAsync(CancellationToken cToken = default) + { + var http = _httpClientFactory.CreateClient(AppConstants.NoRedirectClient); + + var response = await http.GetAsync("https://github.com/GyanD/codexffmpeg/releases/latest", cToken); + + var lastSlashIndex = response.Headers.Location?.OriginalString.LastIndexOf('/') + ?? throw new InvalidOperationException("Failed to get the latest yt-dlp version."); + + return response.Headers.Location.OriginalString[(lastSlashIndex + 1)..]; + } + + /// + public override async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string dependenciesUri, CancellationToken cToken = default) + { + if (_isUpdating) + return (null, null); + + _isUpdating = true; + var currentVersion = await GetCurrentVersionAsync(cToken); + var newVersion = await GetLatestVersionAsync(cToken); + + // Update + if (currentVersion is not null) + { + // If the versions are the same, exit. + if (currentVersion == newVersion) + { + _isUpdating = false; + return (currentVersion, null); + } + + Utilities.TryDeleteFile(Path.Combine(dependenciesUri, FileName)); + Utilities.TryDeleteFile(Path.Combine(dependenciesUri, "ffprobe.exe")); + //Utilities.TryDeleteFile(Path.Combine(dependenciesUri, "ffplay.exe")); + } + + // Install + Directory.CreateDirectory(dependenciesUri); + + var zipFileName = $"ffmpeg-{newVersion}-full_build.zip"; + var http = _httpClientFactory.CreateClient(); + using var downloadStream = await http.GetStreamAsync($"https://github.com/GyanD/codexffmpeg/releases/download/{newVersion}/{zipFileName}", cToken); + + // Save zip file to the temporary directory. + var zipFilePath = Path.Combine(_tempDirectory, zipFileName); + var zipExtractDir = Path.Combine(_tempDirectory, zipFileName[..^4]); + using (var fileStream = new FileStream(zipFilePath, FileMode.Create)) + await downloadStream.CopyToAsync(fileStream, cToken); + + // Schedule installation to the thread-pool because ffmpeg is pretty + // large and doing I/O with it can potentially block the UI thread. + await Task.Run(() => + { + // Extract the zip file. + ZipFile.ExtractToDirectory(zipFilePath, _tempDirectory); + + // Move ffmpeg to the dependencies directory. + File.Move(Path.Combine(zipExtractDir, "bin", FileName), Path.Combine(dependenciesUri, FileName), true); + File.Move(Path.Combine(zipExtractDir, "bin", "ffprobe.exe"), Path.Combine(dependenciesUri, "ffprobe.exe"), true); + //File.Move(Path.Combine(zipExtractDir, "bin", "ffplay.exe"), Path.Combine(dependenciesUri, "ffplay.exe")); + + // Cleanup + File.Delete(zipFilePath); + Directory.Delete(zipExtractDir, true); + }, cToken); + + // Update environment variable + Utilities.AddPathToPATHEnvar(dependenciesUri); + + _isUpdating = false; + return (currentVersion, newVersion); + } +} \ No newline at end of file diff --git a/EllieHub/Services/LogWriter.cs b/EllieHub/Services/LogWriter.cs new file mode 100644 index 0000000..60f519c --- /dev/null +++ b/EllieHub/Services/LogWriter.cs @@ -0,0 +1,94 @@ +using EllieHub.Models.Config; +using EllieHub.Models.EventArguments; +using EllieHub.Services.Abstractions; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace EllieHub.Services; + +/// +/// Defines a service that writes logs of bot instances to the disk. +/// +public sealed class LogWriter : ILogWriter +{ + private readonly Dictionary _botLogs = new(); + private readonly ReadOnlyAppConfig _appConfig; + + /// + public event EventHandler? OnLogCreated; + + /// + /// Creates a service that writes logs of bot instances to the disk. + /// + /// The application settings. + public LogWriter(ReadOnlyAppConfig appConfig) + => _appConfig = appConfig; + + /// + public async Task FlushAllAsync(bool removeFromMemory = false, CancellationToken cToken = default) + { + var result = await Task.WhenAll(_botLogs.Keys.Select(x => FlushAsync(x, removeFromMemory, cToken))); + return result.Any(x => x); + } + + /// + public async Task FlushAsync(Guid botId, bool removeFromMemory = false, CancellationToken cToken = default) + { + if (!_botLogs.TryGetValue(botId, out var logStringBuilder)) + return false; + + if (removeFromMemory) + _botLogs.Remove(botId); + + if (logStringBuilder.Length is 0) + return false; + + Directory.CreateDirectory(_appConfig.LogsDirectoryUri); + var logByteSize = (logStringBuilder.Length * 2) + 1; + var botEntry = _appConfig.BotEntries[botId]; + var now = DateTimeOffset.Now; + var date = new DateOnly(now.Year, now.Month, now.Day).ToShortDateString().Replace('/', '-'); + var fileUri = Path.Combine(_appConfig.LogsDirectoryUri, $"{botEntry.Name}_{date}-{now.ToUnixTimeSeconds()}.txt"); + + await File.WriteAllTextAsync(fileUri, logStringBuilder.ToString(), cToken); + + logStringBuilder.Clear(); + + OnLogCreated?.Invoke(this, new(fileUri, logByteSize, now)); + + return true; + } + + /// + public bool TryAdd(Guid botId, string message) + { + if (string.IsNullOrWhiteSpace(message) || _appConfig.LogMaxSizeMb <= 0.0) + return false; + + if (!_botLogs.TryGetValue(botId, out var logStringBuilder)) + { + logStringBuilder = new(); + _botLogs.TryAdd(botId, logStringBuilder); + } + + logStringBuilder.AppendLine(message); + + if ((logStringBuilder.Length > _appConfig.LogMaxSizeMb * 1_000_000)) + _ = FlushAsync(botId); + + return true; + } + + /// + public bool TryRead(Guid botId, [MaybeNullWhen(false)] out string log) + { + if (_botLogs.TryGetValue(botId, out var logStringBuilder)) + { + log = logStringBuilder.ToString(); + return true; + } + + log = null; + return false; + } +} \ No newline at end of file diff --git a/EllieHub/Services/Mocks/FfmpegMockResolver.cs b/EllieHub/Services/Mocks/FfmpegMockResolver.cs new file mode 100644 index 0000000..1d179a6 --- /dev/null +++ b/EllieHub/Services/Mocks/FfmpegMockResolver.cs @@ -0,0 +1,30 @@ +using EllieHub.Services.Abstractions; + +namespace EllieHub.Services.Mocks; + +/// +/// Service that pretends to check, download, install, and update ffmpeg. +/// +internal sealed class FfmpegMockResolver : FfmpegResolver +{ + private const string _currentVersion = "6.0"; + + /// + public override string FileName { get; } = "ffmpeg"; + + /// + public override ValueTask CanUpdateAsync(CancellationToken cToken = default) + => ValueTask.FromResult(false); + + /// + public override ValueTask GetCurrentVersionAsync(CancellationToken cToken = default) + => ValueTask.FromResult(_currentVersion); + + /// + public override ValueTask GetLatestVersionAsync(CancellationToken cToken = default) + => ValueTask.FromResult(_currentVersion); + + /// + public override ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string dependenciesUri, CancellationToken cToken = default) + => ValueTask.FromResult<(string?, string?)>((_currentVersion, null)); +} \ No newline at end of file diff --git a/EllieHub/Services/Mocks/MockAppConfigManager.cs b/EllieHub/Services/Mocks/MockAppConfigManager.cs new file mode 100644 index 0000000..ff254c2 --- /dev/null +++ b/EllieHub/Services/Mocks/MockAppConfigManager.cs @@ -0,0 +1,34 @@ +using EllieHub.Models; +using EllieHub.Models.Config; +using EllieHub.Services.Abstractions; + +namespace EllieHub.Services.Mocks; + +/// +/// Represents a service that pretends to manage the application's settings. +/// +internal sealed class MockAppConfigManager : IAppConfigManager +{ + /// + public ReadOnlyAppConfig AppConfig { get; } = new(new() { BotEntries = new() { [Guid.Empty] = new("MockBot", Path.Combine(AppStatics.AppDefaultBotDirectoryUri, "MockBot"), 0) } }); + + /// + public ValueTask CreateBotEntryAsync(CancellationToken cToken = default) + => ValueTask.FromResult(new BotEntry(Guid.Empty, AppConfig.BotEntries[Guid.Empty])); + + /// + public ValueTask DeleteBotEntryAsync(Guid id, CancellationToken cToken = default) + => ValueTask.FromResult(null); + + /// + public ValueTask SwapBotEntryAsync(Guid firstBotId, Guid secondBotId, CancellationToken cToken = default) + => ValueTask.FromResult(false); + + /// + public ValueTask UpdateBotEntryAsync(Guid id, Func selector, CancellationToken cToken = default) + => ValueTask.FromResult(false); + + /// + public ValueTask UpdateConfigAsync(Action action, CancellationToken cToken = default) + => ValueTask.CompletedTask; +} \ No newline at end of file diff --git a/EllieHub/Services/Mocks/MockEllieResolver.cs b/EllieHub/Services/Mocks/MockEllieResolver.cs new file mode 100644 index 0000000..304edfd --- /dev/null +++ b/EllieHub/Services/Mocks/MockEllieResolver.cs @@ -0,0 +1,41 @@ +using EllieHub.Services.Abstractions; + +namespace EllieHub.Services.Mocks; + +/// +/// Defines a service that pretends to check, download, install, and update a bot instance. +/// +internal sealed class MockEllieResolver : IBotResolver +{ + /// + public string BotName { get; } = "MockBot"; + + /// + public Guid Id { get; } = Guid.Empty; + + /// + public string DependencyName { get; } = "NadekoBot"; + + /// + public string FileName { get; } = "NadekoBot"; + + /// + public ValueTask CanUpdateAsync(CancellationToken cToken = default) + => ValueTask.FromResult(false); + + /// + public ValueTask CreateBackupAsync() + => ValueTask.FromResult(null); + + /// + public ValueTask GetCurrentVersionAsync(CancellationToken cToken = default) + => ValueTask.FromResult("4.4.4"); + + /// + public ValueTask GetLatestVersionAsync(CancellationToken cToken = default) + => ValueTask.FromResult("4.4.4"); + + /// + public ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string dependenciesUri, CancellationToken cToken = default) + => ValueTask.FromResult<(string?, string?)>(("4.4.4", null)); +} \ No newline at end of file diff --git a/EllieHub/Services/YtdlpResolver.cs b/EllieHub/Services/YtdlpResolver.cs new file mode 100644 index 0000000..54421f8 --- /dev/null +++ b/EllieHub/Services/YtdlpResolver.cs @@ -0,0 +1,171 @@ +using Microsoft.Extensions.Caching.Memory; +using EllieHub.Services.Abstractions; +using System.Runtime.InteropServices; + +namespace EllieHub.Services; + +/// +/// Service that checks, downloads, installs, and updates yt-dlp. +/// +/// Source: https://github.com/yt-dlp/yt-dlp/releases/latest +public sealed class YtdlpResolver : IYtdlpResolver +{ + private const string _cachedCurrentVersionKey = "currentVersion:yt-dlp"; + private const string _ytdlpProcessName = "yt-dlp"; + private static readonly string _downloadedFileName = GetDownloadFileName(); + private bool _isUpdating = false; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IMemoryCache _memoryCache; + + /// + public string DependencyName { get; } = "Youtube-dlp"; + + /// + public string FileName { get; } = (OperatingSystem.IsWindows()) ? "yt-dlp.exe" : "yt-dlp"; + + /// + /// Creates a service that checks, downloads, installs, and updates yt-dlp. + /// + /// The HTTP client factory. + /// The memory cache. + public YtdlpResolver(IHttpClientFactory httpClientFactory, IMemoryCache memoryCache) + { + _httpClientFactory = httpClientFactory; + _memoryCache = memoryCache; + } + + /// + public async ValueTask CanUpdateAsync(CancellationToken cToken = default) + { + var currentVersion = await GetCurrentVersionAsync(cToken); + + if (currentVersion is null) + return null; + + var latestVersion = await GetLatestVersionAsync(cToken); + + if (latestVersion.Equals(currentVersion, StringComparison.Ordinal)) + return false; + + var http = _httpClientFactory.CreateClient(); + + return await http.IsUrlValidAsync($"https://github.com/yt-dlp/yt-dlp/releases/download/{latestVersion}/{_downloadedFileName}", cToken); + } + + /// + public async ValueTask GetCurrentVersionAsync(CancellationToken cToken = default) + { + // If yt-dlp is not accessible from the shell... + if (!await Utilities.ProgramExistsAsync(_ytdlpProcessName, cToken)) + { + // And doesn't exist in the dependencies folder, + // report that yt-dlp is not installed. + if (!File.Exists(Path.Combine(AppStatics.AppDepsUri, FileName))) + return null; + + // Else, add the dependencies directory to the PATH envar, + // then try again. + Utilities.AddPathToPATHEnvar(AppStatics.AppDepsUri); + return await GetCurrentVersionAsync(cToken); + } + + // "yt-dlp --version" takes a very long time to return, so we cache the result for 90 seconds. + if (_memoryCache.TryGetValue(_cachedCurrentVersionKey, out var currentVersion) && currentVersion is not null) + return currentVersion; + + using var ytdlp = Utilities.StartProcess(_ytdlpProcessName, "--version"); + + var currentProcessVersion = (await ytdlp.StandardOutput.ReadToEndAsync(cToken)).Trim(); + _memoryCache.Set(_cachedCurrentVersionKey, currentProcessVersion, TimeSpan.FromMinutes(1.5)); + + return currentProcessVersion; + } + + /// + public async ValueTask GetLatestVersionAsync(CancellationToken cToken = default) + { + var http = _httpClientFactory.CreateClient(AppConstants.NoRedirectClient); + + var response = await http.GetAsync("https://github.com/yt-dlp/yt-dlp/releases/latest", cToken); + + var lastSlashIndex = response.Headers.Location?.OriginalString.LastIndexOf('/') + ?? throw new InvalidOperationException("Failed to get the latest yt-dlp version."); + + return response.Headers.Location.OriginalString[(lastSlashIndex + 1)..]; + } + + /// + public async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string dependenciesUri, CancellationToken cToken = default) + { + if (_isUpdating) + return (null, null); + + _isUpdating = true; + var currentVersion = await GetCurrentVersionAsync(cToken); + var newVersion = await GetLatestVersionAsync(cToken); + + _memoryCache.Remove(_cachedCurrentVersionKey); + + // Update + if (currentVersion is not null) + { + // If the versions are the same, exit. + if (currentVersion == newVersion) + { + _isUpdating = false; + return (currentVersion, null); + } + + using var ytdlp = Utilities.StartProcess(_ytdlpProcessName, "-U"); + await ytdlp.WaitForExitAsync(cToken); + + _isUpdating = false; + return (currentVersion, newVersion); + } + + // Install + Directory.CreateDirectory(dependenciesUri); + + var finalFilePath = Path.Combine(dependenciesUri, FileName); + var http = _httpClientFactory.CreateClient(); + using var downloadStream = await http.GetStreamAsync($"https://github.com/yt-dlp/yt-dlp/releases/download/{newVersion}/{_downloadedFileName}", cToken); + using (var fileStream = new FileStream(finalFilePath, FileMode.Create)) + await downloadStream.CopyToAsync(fileStream, cToken); + + // Update environment variable + Utilities.AddPathToPATHEnvar(dependenciesUri); + + // On Linux and MacOS, we need to mark the file as executable. + if (Environment.OSVersion.Platform is PlatformID.Unix) + { + using var chmod = Utilities.StartProcess("chmod", $"+x \"{finalFilePath}\""); + await chmod.WaitForExitAsync(cToken); + } + + _isUpdating = false; + return (null, newVersion); + } + + /// + /// Gets the name of the yt-dlp binary file to be downloaded. + /// + /// The name of the yt-dlp binary file. + /// Occurs when this method is executed in an unsupported platform. + private static string GetDownloadFileName() + { + return RuntimeInformation.OSArchitecture switch + { + // Windows + Architecture.X64 when OperatingSystem.IsWindows() => "yt-dlp.exe", + + // Linux + Architecture.X64 when OperatingSystem.IsLinux() => "yt-dlp_linux", + Architecture.Arm64 when OperatingSystem.IsLinux() => "yt-dlp_linux_aarch64", + + // MacOS + Architecture.X64 when OperatingSystem.IsMacOS() => "yt-dlp_macos_legacy", + Architecture.Arm64 when OperatingSystem.IsMacOS() => "yt-dlp_macos", + _ => throw new NotSupportedException($"Architecture of type {RuntimeInformation.OSArchitecture} is not supported by yt-dlp on this OS.") + }; + } +} \ No newline at end of file diff --git a/EllieHub/Styles/EllieStyles.axaml b/EllieHub/Styles/EllieStyles.axaml new file mode 100644 index 0000000..b831792 --- /dev/null +++ b/EllieHub/Styles/EllieStyles.axaml @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/EllieHub/Views/Controls/LateralBarView.axaml.cs b/EllieHub/Views/Controls/LateralBarView.axaml.cs new file mode 100644 index 0000000..9bdc860 --- /dev/null +++ b/EllieHub/Views/Controls/LateralBarView.axaml.cs @@ -0,0 +1,190 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media.Immutable; +using Avalonia.ReactiveUI; +using Microsoft.Extensions.DependencyInjection; +using EllieHub.DesignData.Common; +using EllieHub.Models.Config; +using EllieHub.Models.EventArguments; +using EllieHub.Services.Mocks; +using EllieHub.ViewModels.Controls; +using SkiaImageView; +using SkiaSharp; + +namespace EllieHub.Views.Controls; + +/// +/// View for the lateral bar with home, bot, and configuration buttons. +/// +public partial class LateralBarView : ReactiveUserControl +{ + private static readonly Cursor _pointingHandCursor = new(StandardCursorType.Hand); + private static readonly Cursor _arrow = new(StandardCursorType.Arrow); + private readonly ReadOnlyAppConfig _appConfig; + + /// + /// Raised when the user clicks a bot button. + /// + public event EventHandler? BotButtonClick; + + /// + /// Designer's constructor. Use the parameterized constructor instead. + /// + [Obsolete(WindowConstants.DesignerCtorWarning, true)] + public LateralBarView() : this(DesignStatics.Services.GetRequiredService().AppConfig) + { + } + + /// + /// Creates the lateral bar of the application. + /// + /// The application settings. + public LateralBarView(ReadOnlyAppConfig appConfig) + { + _appConfig = appConfig; + InitializeComponent(); + } + + /// + /// Changes the avatar of a bot button in the lateral bar. + /// + /// The event arguments emited when a bot avatar is changed. + public Task UpdateBotButtonAvatarAsync(AvatarChangedEventArgs eventArgs) + { + var buttonAvatar = FindAvatarComponent(eventArgs.BotId); + var oldAvatar = buttonAvatar.Source as SKBitmap; + + buttonAvatar.Source = eventArgs.Avatar; + oldAvatar?.Dispose(); // Dispose the old avatar + + return Task.CompletedTask; + } + + /// + /// Applies the selection outline to the specified bot button. + /// + /// The bot button. + /// Occurs when the visual tree has an unexpected structure. + public void ApplyBotButtonBorder(Button button) + { + if (!Utilities.TryCastTo(button.Parent?.Parent, out var border)) + throw new InvalidOperationException("Visual tree has an unexpected structure."); + + if (!Utilities.TryCastTo(this.FindResource(base.ActualThemeVariant, "BotSelectionColor"), out var resourceColor)) + return; + + border.BorderBrush = resourceColor; + } + + /// + /// Applies a transparent outline to all bot buttons. + /// + public void ResetBotButtonBorders() + { + foreach (var border in ButtonList.Children.Cast()) + border.BorderBrush = AppStatics.TransparentColorBrush; + } + + /// + /// Routes a button click from the view to . + /// + /// The bot button that was clicked. + /// The event arguments. + private void LoadBotViewModel(object sender, RoutedEventArgs eventArgs) + => BotButtonClick?.Invoke((Button)sender, eventArgs); + + /// + /// Loads the bot avatar when the buttons on the lateral bar are rendered. + /// + /// The that contains the bot buttons and avatars. + /// The event arguments. + /// This is executed each time one of the buttons is rendered. + /// Occurs when the visual tree has an unexpected structure. + private void OnBotButtonLoad(object? sender, VisualTreeAttachmentEventArgs eventArgs) + { + if (!Utilities.TryCastTo(sender, out var panel) + || !Utilities.TryCastTo(((Border)panel.Children[0]).Child, out var botAvatar) + || !Utilities.TryCastTo + + + + + + + + + + + + + + + + +