Compare commits
36 commits
Author | SHA1 | Date | |
---|---|---|---|
b1514c208e | |||
f046ef5d3e | |||
761aab3e9e | |||
b0654474c4 | |||
8e7c2a7374 | |||
daf6b89056 | |||
516e768aba | |||
f0030af0ae | |||
6fc3eba498 | |||
2a8cd606c5 | |||
7fa866369c | |||
4deaa828c5 | |||
f5c48cd8c4 | |||
d210f77ecb | |||
d997fa5952 | |||
87e197aebf | |||
235d787a6e | |||
9fb1c69a5c | |||
c2ff1e857d | |||
5cab4c2c80 | |||
cd06397e64 | |||
468728c4ef | |||
e270daeeb1 | |||
9e00d65418 | |||
15c6e06475 | |||
da72525cd3 | |||
3d6b44530a | |||
e0c276f8e5 | |||
7427592de9 | |||
1ecd07f906 | |||
027ee8d80f | |||
74642d0873 | |||
9180716702 | |||
2c37112b4a | |||
ff2957f935 | |||
7429e0298a |
107 changed files with 2257 additions and 1125 deletions
253
.editorconfig
253
.editorconfig
|
@ -1,3 +1,4 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
|
@ -8,14 +9,66 @@ 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
|
||||
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
|
||||
dotnet_style_prefer_compound_assignment = true:suggestion
|
||||
dotnet_style_prefer_simplified_interpolation = true:suggestion
|
||||
dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
|
||||
dotnet_style_namespace_match_folder = true:suggestion
|
||||
dotnet_style_readonly_field = true:warning
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:warning
|
||||
dotnet_style_predefined_type_for_member_access = true:warning
|
||||
dotnet_style_allow_multiple_blank_lines_experimental = false:silent
|
||||
dotnet_style_allow_statement_immediately_after_block_experimental = false:silent
|
||||
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
|
||||
dotnet_style_qualification_for_field = false:silent
|
||||
dotnet_style_qualification_for_property = false:silent
|
||||
dotnet_style_qualification_for_method = false:silent
|
||||
dotnet_style_qualification_for_event = false:silent
|
||||
dotnet_diagnostic.CA1310.severity = warning
|
||||
dotnet_diagnostic.CA1509.severity = error
|
||||
dotnet_diagnostic.CA1000.severity = suggestion
|
||||
dotnet_diagnostic.CA1001.severity = error
|
||||
dotnet_diagnostic.CA1005.severity = none
|
||||
dotnet_diagnostic.CA1008.severity = suggestion
|
||||
dotnet_diagnostic.CA1010.severity = suggestion
|
||||
dotnet_diagnostic.CA1031.severity = silent
|
||||
dotnet_diagnostic.CA1050.severity = error
|
||||
dotnet_diagnostic.CA1051.severity = error
|
||||
dotnet_diagnostic.CA1069.severity = suggestion
|
||||
dotnet_diagnostic.CA1724.severity = error
|
||||
dotnet_diagnostic.CA1826.severity = warning
|
||||
dotnet_diagnostic.CA1827.severity = warning
|
||||
dotnet_diagnostic.CA1828.severity = warning
|
||||
dotnet_diagnostic.CA1829.severity = warning
|
||||
dotnet_diagnostic.CA1830.severity = warning
|
||||
dotnet_diagnostic.CA1832.severity = warning
|
||||
dotnet_diagnostic.CA1833.severity = warning
|
||||
dotnet_diagnostic.CA1842.severity = warning
|
||||
dotnet_diagnostic.CA1843.severity = warning
|
||||
dotnet_diagnostic.CA1836.severity = none
|
||||
dotnet_diagnostic.CA1839.severity = warning
|
||||
dotnet_diagnostic.CA1840.severity = warning
|
||||
dotnet_diagnostic.CA1846.severity = warning
|
||||
dotnet_diagnostic.CA1848.severity = suggestion
|
||||
dotnet_diagnostic.CA1852.severity = suggestion
|
||||
dotnet_diagnostic.CA2012.severity = warning
|
||||
dotnet_diagnostic.CA2019.severity = warning
|
||||
dotnet_diagnostic.CA2211.severity = warning
|
||||
dotnet_diagnostic.CA1822.severity = suggestion
|
||||
dotnet_diagnostic.CA1725.severity = suggestion
|
||||
|
||||
[*.cs]
|
||||
|
||||
#### .NET Coding Conventions ####
|
||||
|
@ -79,49 +132,49 @@ 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
|
||||
csharp_style_var_elsewhere = true:warning
|
||||
csharp_style_var_for_built_in_types = true:warning
|
||||
csharp_style_var_when_type_is_apparent = true:warning
|
||||
|
||||
# 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_lambdas = when_on_single_line: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_pattern_matching_over_as_with_null_check = true:warning
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true:warning
|
||||
csharp_style_prefer_not_pattern = true:warning
|
||||
csharp_style_prefer_pattern_matching = true:suggestion
|
||||
csharp_style_prefer_switch_expression = true
|
||||
csharp_style_prefer_switch_expression = true:suggestion
|
||||
|
||||
# Null-checking preferences
|
||||
csharp_style_conditional_delegate_call = true:error
|
||||
csharp_style_conditional_delegate_call = true:warning
|
||||
|
||||
# Modifier preferences
|
||||
csharp_prefer_static_local_function = true
|
||||
csharp_prefer_static_local_function = true:suggestion
|
||||
csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async
|
||||
|
||||
# Code-block preferences
|
||||
csharp_prefer_braces = when_multiline:suggestion
|
||||
csharp_prefer_simple_using_statement = true
|
||||
csharp_prefer_simple_using_statement = true:suggestion
|
||||
|
||||
# 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_prefer_simple_default_expression = true:suggestion
|
||||
csharp_style_deconstructed_variable_declaration = true:suggestion
|
||||
csharp_style_implicit_object_creation_when_type_is_apparent = true:warning
|
||||
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_prefer_index_operator = true:suggestion
|
||||
csharp_style_prefer_range_operator = true:suggestion
|
||||
csharp_style_throw_expression = true:error
|
||||
csharp_style_unused_value_assignment_preference = discard_variable:warning
|
||||
csharp_style_unused_value_expression_statement_preference = discard_variable
|
||||
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
|
||||
|
||||
# 'using' directive preferences
|
||||
csharp_using_directive_placement = outside_namespace:error
|
||||
|
@ -130,9 +183,9 @@ csharp_using_directive_placement = outside_namespace:error
|
|||
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
|
||||
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
|
||||
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:silent
|
||||
csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
|
||||
|
||||
|
||||
#### C# Formatting Rules ####
|
||||
|
@ -189,40 +242,40 @@ csharp_preserve_single_line_statements = true
|
|||
dotnet_naming_rule.class_should_be_pascal_case.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.class_should_be_pascal_case.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.class_should_be_pascal_case.style = upper_camel_case_style
|
||||
|
||||
dotnet_naming_rule.struct_should_be_pascal_case.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.struct_should_be_pascal_case.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.struct_should_be_pascal_case.style = upper_camel_case_style
|
||||
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.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.interface_should_be_begins_with_i.style = i_upper_camel_case_style
|
||||
|
||||
dotnet_naming_rule.types_should_be_pascal_case.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.types_should_be_pascal_case.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.types_should_be_pascal_case.style = upper_camel_case_style
|
||||
|
||||
dotnet_naming_rule.enum_should_be_pascal_case.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.enum_should_be_pascal_case.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.enum_should_be_pascal_case.style = upper_camel_case_style
|
||||
|
||||
dotnet_naming_rule.property_should_be_pascal_case.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.property_should_be_pascal_case.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.property_should_be_pascal_case.style = upper_camel_case_style
|
||||
|
||||
dotnet_naming_rule.method_should_be_pascal_case.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.method_should_be_pascal_case.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.method_should_be_pascal_case.style = upper_camel_case_style
|
||||
|
||||
dotnet_naming_rule.async_method_should_be_ends_with_async.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.async_method_should_be_ends_with_async.severity = error
|
||||
dotnet_naming_rule.async_method_should_be_ends_with_async.severity = warning
|
||||
dotnet_naming_rule.async_method_should_be_ends_with_async.symbols = async_method
|
||||
dotnet_naming_rule.async_method_should_be_ends_with_async.style = ends_with_async
|
||||
|
||||
|
@ -234,17 +287,17 @@ dotnet_naming_rule.private_field_should_be_begins_with_underscore.style = begins
|
|||
dotnet_naming_rule.non_field_members_should_be_pascal_case.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.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.non_field_members_should_be_pascal_case.style = upper_camel_case_style
|
||||
|
||||
dotnet_naming_rule.local_variable_should_be_camel_case.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.local_variable_should_be_camel_case.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.local_variable_should_be_camel_case.style = lower_camel_case_style_1
|
||||
|
||||
dotnet_naming_rule.public_anything_should_be_pascal_case.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.public_anything_should_be_pascal_case.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.public_anything_should_be_pascal_case.style = upper_camel_case_style
|
||||
|
||||
dotnet_naming_rule.constants_rule.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.constants_rule.severity = error
|
||||
|
@ -288,7 +341,7 @@ dotnet_naming_rule.parameters_rule.symbols = parameters_symbols
|
|||
|
||||
dotnet_naming_rule.private_constants_rule.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.private_constants_rule.severity = error
|
||||
dotnet_naming_rule.private_constants_rule.style = lower_camel_case_style
|
||||
dotnet_naming_rule.private_constants_rule.style = begins_with_underscore
|
||||
dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols
|
||||
|
||||
dotnet_naming_rule.private_instance_fields_rule.import_to_resharper = as_predefined
|
||||
|
@ -298,12 +351,12 @@ dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_field
|
|||
|
||||
dotnet_naming_rule.private_static_fields_rule.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.private_static_fields_rule.severity = error
|
||||
dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style
|
||||
dotnet_naming_rule.private_static_fields_rule.style = begins_with_underscore
|
||||
dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols
|
||||
|
||||
dotnet_naming_rule.private_static_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.style = begins_with_underscore
|
||||
dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols
|
||||
|
||||
dotnet_naming_rule.property_rule.import_to_resharper = as_predefined
|
||||
|
@ -318,7 +371,7 @@ dotnet_naming_rule.public_fields_rule.symbols = public_fields_symbols
|
|||
|
||||
dotnet_naming_rule.public_fields_rule.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.public_fields_rule.severity = error
|
||||
dotnet_naming_rule.public_fields_rule.style = camel_case
|
||||
dotnet_naming_rule.public_fields_rule.style = lower_camel_case_style_1
|
||||
dotnet_naming_rule.public_fields_rule.symbols = protected_fields_symbols
|
||||
|
||||
dotnet_naming_rule.static_readonly_rule.import_to_resharper = as_predefined
|
||||
|
@ -482,23 +535,92 @@ dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities = *
|
|||
dotnet_naming_symbols.type_parameters_symbols.applicable_kinds = type_parameter
|
||||
|
||||
|
||||
#### Visual Studio Specific Rules ####
|
||||
#### Style Rules ####
|
||||
|
||||
# CA1822: Mark members as static
|
||||
dotnet_diagnostic.CA1822.severity = none
|
||||
csharp_style_prefer_method_group_conversion = true:silent
|
||||
csharp_style_prefer_top_level_statements = false:suggestion
|
||||
csharp_style_prefer_primary_constructors = false:warning
|
||||
csharp_style_prefer_null_check_over_type_check = true:suggestion
|
||||
csharp_style_prefer_local_over_anonymous_function = true:suggestion
|
||||
csharp_style_prefer_tuple_swap = true:suggestion
|
||||
csharp_style_prefer_utf8_string_literals = true:suggestion
|
||||
csharp_style_prefer_readonly_struct = true:suggestion
|
||||
csharp_style_prefer_readonly_struct_member = true:suggestion
|
||||
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
|
||||
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
|
||||
csharp_style_prefer_extended_property_pattern = true:warning
|
||||
dotnet_diagnostic.CA1311.severity = suggestion
|
||||
dotnet_diagnostic.CA1507.severity = warning
|
||||
dotnet_diagnostic.IDE0047.severity = none
|
||||
dotnet_diagnostic.IDE0052.severity = warning
|
||||
dotnet_diagnostic.IDE0051.severity = warning
|
||||
dotnet_diagnostic.IDE0076.severity = error
|
||||
dotnet_diagnostic.IDE0077.severity = error
|
||||
dotnet_diagnostic.IDE0043.severity = warning
|
||||
dotnet_diagnostic.CA1070.severity = error
|
||||
dotnet_diagnostic.CA1200.severity = suggestion
|
||||
dotnet_diagnostic.CA1805.severity = suggestion
|
||||
dotnet_diagnostic.CA1825.severity = warning
|
||||
dotnet_diagnostic.IDE0009.severity = suggestion
|
||||
dotnet_diagnostic.IDE0016.severity = suggestion
|
||||
dotnet_diagnostic.IDE0017.severity = suggestion
|
||||
dotnet_diagnostic.IDE0018.severity = suggestion
|
||||
dotnet_diagnostic.IDE0019.severity = suggestion
|
||||
dotnet_diagnostic.IDE0020.severity = suggestion
|
||||
dotnet_diagnostic.IDE0028.severity = suggestion
|
||||
dotnet_diagnostic.IDE0029.severity = suggestion
|
||||
dotnet_diagnostic.IDE0030.severity = suggestion
|
||||
dotnet_diagnostic.IDE0031.severity = suggestion
|
||||
dotnet_diagnostic.IDE0032.severity = suggestion
|
||||
dotnet_diagnostic.IDE0034.severity = suggestion
|
||||
dotnet_diagnostic.IDE0039.severity = suggestion
|
||||
dotnet_diagnostic.IDE0040.severity = warning
|
||||
dotnet_diagnostic.IDE0041.severity = suggestion
|
||||
dotnet_diagnostic.IDE0044.severity = suggestion
|
||||
dotnet_diagnostic.IDE0045.severity = suggestion
|
||||
dotnet_diagnostic.IDE0046.severity = suggestion
|
||||
dotnet_diagnostic.IDE0048.severity = suggestion
|
||||
dotnet_diagnostic.IDE0053.severity = suggestion
|
||||
dotnet_diagnostic.IDE0059.severity = warning
|
||||
dotnet_diagnostic.IDE0060.severity = warning
|
||||
dotnet_diagnostic.IDE0062.severity = suggestion
|
||||
dotnet_diagnostic.IDE0063.severity = suggestion
|
||||
dotnet_diagnostic.IDE0071.severity = suggestion
|
||||
dotnet_diagnostic.IDE0082.severity = warning
|
||||
dotnet_diagnostic.IDE0080.severity = suggestion
|
||||
dotnet_diagnostic.IDE0130.severity = warning
|
||||
dotnet_diagnostic.IDE0120.severity = suggestion
|
||||
dotnet_diagnostic.IDE0100.severity = warning
|
||||
dotnet_diagnostic.IDE0150.severity = suggestion
|
||||
dotnet_diagnostic.IDE0161.severity = warning
|
||||
dotnet_diagnostic.IDE0170.severity = suggestion
|
||||
dotnet_diagnostic.IDE0180.severity = suggestion
|
||||
dotnet_diagnostic.IDE0200.severity = suggestion
|
||||
dotnet_diagnostic.IDE0211.severity = suggestion
|
||||
dotnet_diagnostic.IDE0230.severity = suggestion
|
||||
dotnet_diagnostic.IDE0240.severity = suggestion
|
||||
dotnet_diagnostic.IDE0241.severity = suggestion
|
||||
dotnet_diagnostic.IDE0251.severity = suggestion
|
||||
dotnet_diagnostic.IDE0250.severity = suggestion
|
||||
dotnet_diagnostic.IDE0260.severity = suggestion
|
||||
dotnet_diagnostic.IDE0270.severity = suggestion
|
||||
dotnet_diagnostic.IDE0280.severity = warning
|
||||
dotnet_diagnostic.IDE1005.severity = suggestion
|
||||
dotnet_diagnostic.IDE1006.severity = warning
|
||||
|
||||
# IDE0004: Cast is redundant
|
||||
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
|
||||
# IDE0058: Expression value is never used
|
||||
dotnet_diagnostic.IDE0058.severity = none
|
||||
|
||||
# Use primary constructor
|
||||
dotnet_diagnostic.IDE0290.severity = none
|
||||
|
||||
|
||||
#### ReSharper Properties ####
|
||||
|
||||
resharper_accessor_owner_body = expression_body
|
||||
|
@ -581,14 +703,16 @@ 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_fixed = required_for_multiline
|
||||
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_lock = required_for_multiline
|
||||
resharper_braces_for_using = required_for_multiline
|
||||
resharper_braces_for_while = required_for_multiline
|
||||
resharper_braces_redundant = true
|
||||
resharper_builtin_type_apply_to_native_integer = true
|
||||
resharper_csharp_empty_block_style = together_same_line
|
||||
resharper_break_template_declaration = line_break
|
||||
resharper_can_use_global_alias = false
|
||||
resharper_configure_await_analysis_mode = disabled
|
||||
|
@ -686,7 +810,7 @@ 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_instance_members_qualify_declared_in =
|
||||
resharper_int_align_assignments = false
|
||||
resharper_int_align_binary_expressions = false
|
||||
resharper_int_align_declaration_names = false
|
||||
|
@ -787,7 +911,7 @@ 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_prefer_separate_deconstructed_variables_declaration = false
|
||||
resharper_preserve_spaces_inside_tags = pre,textarea
|
||||
resharper_qualified_using_at_nested_scope = false
|
||||
resharper_quote_style = doublequoted
|
||||
|
@ -938,6 +1062,7 @@ 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_csharp_allow_alias = false
|
||||
resharper_special_else_if_treatment = true
|
||||
resharper_static_members_qualify_members = none
|
||||
resharper_static_members_qualify_with = declared_type
|
||||
|
@ -1043,7 +1168,7 @@ 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_missing_parentheses_highlighting = none
|
||||
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
|
||||
|
@ -1158,7 +1283,7 @@ 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_pattern_to_null_check_highlighting = none
|
||||
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
|
||||
|
@ -1277,8 +1402,8 @@ 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_private_global_highlighting = hint
|
||||
resharper_member_can_be_private_local_highlighting = hint
|
||||
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
|
||||
|
@ -1406,7 +1531,7 @@ 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_base_qualifier_highlighting = none
|
||||
resharper_redundant_blank_lines_highlighting = none
|
||||
resharper_redundant_bool_compare_highlighting = warning
|
||||
resharper_redundant_case_label_highlighting = warning
|
||||
|
@ -1416,7 +1541,7 @@ 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_default_member_initializer_highlighting = hint
|
||||
resharper_redundant_delegate_creation_highlighting = warning
|
||||
resharper_redundant_disable_warning_comment_highlighting = warning
|
||||
resharper_redundant_discard_designation_highlighting = suggestion
|
||||
|
@ -1741,4 +1866,18 @@ 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
|
||||
resharper_xunit_xunit_test_with_console_output_highlighting = warning
|
||||
resharper_arrange_constructor_or_destructor_body_highlighting = hint
|
||||
resharper_arrange_local_function_body_highlighting = hint
|
||||
resharper_arrange_method_or_operator_body_highlighting = hint
|
||||
resharper_arrange_null_checking_pattern_highlighting = suggestion
|
||||
resharper_enforce_do_while_statement_braces_highlighting = warning
|
||||
resharper_enforce_fixed_statement_braces_highlighting = warning
|
||||
resharper_enforce_foreach_statement_braces_highlighting = none
|
||||
resharper_enforce_for_statement_braces_highlighting = none
|
||||
resharper_enforce_if_statement_braces_highlighting = none
|
||||
resharper_enforce_lock_statement_braces_highlighting = warning
|
||||
resharper_enforce_while_statement_braces_highlighting = none
|
||||
resharper_remove_redundant_braces_highlighting = hint
|
||||
resharper_suggest_discard_declaration_var_style_highlighting = warning
|
||||
resharper_prefer_concrete_value_over_default_highlighting = hint
|
56
.forgejo/issue_template/bug_report.yml
Normal file
56
.forgejo/issue_template/bug_report.yml
Normal file
|
@ -0,0 +1,56 @@
|
|||
name: Bug Report
|
||||
description: File a bug report.
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Which version of EllieHub are you using?
|
||||
multiple: false
|
||||
options:
|
||||
- "Windows x64"
|
||||
- "Windows arm64"
|
||||
- "Linux x64"
|
||||
- "Linux arm64"
|
||||
- "MacOS x64"
|
||||
- "MacOS arm64"
|
||||
- "I compiled from source"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Description"
|
||||
description: "Give a concise description of the problem"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Reproduction Steps"
|
||||
description: "Enumerate the steps needed to reproduce the behavior"
|
||||
value: "1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Expected Behavior"
|
||||
description: "Describe what you expected to happen"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Actual Behavior"
|
||||
description: "Describe what actually happened"
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Screenshots"
|
||||
description: "If applicable, add screenshots to help explain your problem"
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Notes"
|
||||
description: "Add any other context about the problem here"
|
||||
placeholder: "eg. OS version, etc"
|
||||
validations:
|
||||
required: false
|
30
.forgejo/issue_template/feature_request.yml
Normal file
30
.forgejo/issue_template/feature_request.yml
Normal file
|
@ -0,0 +1,30 @@
|
|||
name: "Feature Request"
|
||||
description: Suggest an idea for this project.
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: "textarea"
|
||||
attributes:
|
||||
label: "Is your feature request related to a problem?"
|
||||
description: "If so, describe what the problem is"
|
||||
placeholder: "eg. I'm always frustrated when [...]"
|
||||
validations:
|
||||
required: false
|
||||
- type: "textarea"
|
||||
attributes:
|
||||
label: "Describe the feature you'd like"
|
||||
description: "Describe what you want to happen"
|
||||
placeholder: "eg. The application should do [...] then [...]"
|
||||
validations:
|
||||
required: true
|
||||
- type: "textarea"
|
||||
attributes:
|
||||
label: "What alternatives have you considered?"
|
||||
description: "Feel free to mention similar applications if you feel this will get your point across better"
|
||||
validations:
|
||||
required: false
|
||||
- type: "textarea"
|
||||
attributes:
|
||||
label: "Additional context"
|
||||
description: "Add any other context or screenshots about the feature request here"
|
||||
validations:
|
||||
required: false
|
0
.forgejo/readme_assets/.gitkeep
Normal file
0
.forgejo/readme_assets/.gitkeep
Normal file
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -480,4 +480,7 @@ $RECYCLE.BIN/
|
|||
|
||||
# Built files
|
||||
build/*
|
||||
zips/*
|
||||
zips/*
|
||||
|
||||
# Scripts
|
||||
release.ps1
|
|
@ -5,6 +5,11 @@ 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
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2A553F1B-5A2C-43D6-A145-F219530326D3}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
release.ps1 = release.ps1
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
xmlns:local="using:EllieHub"
|
||||
RequestedThemeVariant="Default"
|
||||
x:Class="EllieHub.App">
|
||||
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||
|
||||
<Application.DataTemplates>
|
||||
<local:ViewLocator/>
|
||||
|
@ -14,9 +14,9 @@
|
|||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceInclude Source="/Resources/Colors.axaml" />
|
||||
<ResourceInclude Source="/Resources/Fonts.axaml" />
|
||||
<ResourceInclude Source="/Resources/Images.axaml" />
|
||||
<ResourceInclude Source="/Avalonia/Resources/Colors.axaml" />
|
||||
<ResourceInclude Source="/Avalonia/Resources/Fonts.axaml" />
|
||||
<ResourceInclude Source="/Avalonia/Resources/Images.axaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
|
@ -24,27 +24,27 @@
|
|||
<!--Styles-->
|
||||
<!--Think of them like CSS classes-->
|
||||
<Application.Styles>
|
||||
<StyleInclude Source="/Styles/EllieStyles.axaml" />
|
||||
<StyleInclude Source="/Avalonia/Styles/EllieStyles.axaml" />
|
||||
<!--Generated Here: https://theme.xaml.live/-->
|
||||
<FluentTheme>
|
||||
<FluentTheme.Palettes>
|
||||
<ColorPaletteResources x:Key="Light" Accent="#ff0073cf" AltHigh="White" AltLow="White" AltMedium="White" AltMediumHigh="White" AltMediumLow="White" BaseHigh="Black" BaseLow="#6fcccccc" BaseMedium="#c5898989" BaseMediumHigh="#ff5d5d5d" BaseMediumLow="#e2737373" ChromeAltLow="#ff5d5d5d" ChromeBlackHigh="Black" ChromeBlackLow="#6fcccccc" ChromeBlackMedium="#ff5d5d5d" ChromeBlackMediumLow="#c5898989" ChromeDisabledHigh="#6fcccccc" ChromeDisabledLow="#c5898989" ChromeGray="#e2737373" ChromeHigh="#6fcccccc" ChromeLow="#ffececec" ChromeMedium="#e2e6e6e6" ChromeMediumLow="#ffececec" ChromeWhite="White" ListLow="#e2e6e6e6" ListMedium="#6fcccccc" RegionColor="White" />
|
||||
<ColorPaletteResources x:Key="Dark" Accent="#ff0073cf" AltHigh="Black" AltLow="Black" AltMedium="Black" AltMediumHigh="Black" AltMediumLow="Black" BaseHigh="White" BaseLow="#ff333333" BaseMedium="#ff9a9a9a" BaseMediumHigh="#ffb4b4b4" BaseMediumLow="#ff676767" ChromeAltLow="#ffb4b4b4" ChromeBlackHigh="Black" ChromeBlackLow="#ffb4b4b4" ChromeBlackMedium="Black" ChromeBlackMediumLow="Black" ChromeDisabledHigh="#ff333333" ChromeDisabledLow="#ff9a9a9a" ChromeGray="Gray" ChromeHigh="Gray" ChromeLow="#ff151515" ChromeMedium="#ff1d1d1d" ChromeMediumLow="#ff2c2c2c" ChromeWhite="White" ListLow="#ff1d1d1d" ListMedium="#ff333333" RegionColor="#ff181818" />
|
||||
</FluentTheme.Palettes>
|
||||
<FluentTheme.Palettes>
|
||||
<ColorPaletteResources x:Key="Light" Accent="#ff0066ff" AltHigh="White" AltLow="White" AltMedium="White" AltMediumHigh="White" AltMediumLow="White" BaseHigh="Black" BaseLow="#ffcccccc" BaseMedium="#ff898989" BaseMediumHigh="#ff5d5d5d" BaseMediumLow="#ff737373" ChromeAltLow="#ff5d5d5d" ChromeBlackHigh="Black" ChromeBlackLow="#ffcccccc" ChromeBlackMedium="#ff5d5d5d" ChromeBlackMediumLow="#ff898989" ChromeDisabledHigh="#ffcccccc" ChromeDisabledLow="#ff898989" ChromeGray="#ff737373" ChromeHigh="#ffcccccc" ChromeLow="#ffececec" ChromeMedium="#ffe6e6e6" ChromeMediumLow="#ffececec" ChromeWhite="White" ListLow="#ffe6e6e6" ListMedium="#ffcccccc" RegionColor="White" />
|
||||
<ColorPaletteResources x:Key="Dark" Accent="#ff005dd9" AltHigh="Black" AltLow="Black" AltMedium="Black" AltMediumHigh="Black" AltMediumLow="Black" BaseHigh="White" BaseLow="#ff333333" BaseMedium="#ff9a9a9a" BaseMediumHigh="#ffb4b4b4" BaseMediumLow="#ff676767" ChromeAltLow="#ffb4b4b4" ChromeBlackHigh="Black" ChromeBlackLow="#ffb4b4b4" ChromeBlackMedium="Black" ChromeBlackMediumLow="Black" ChromeDisabledHigh="#ff333333" ChromeDisabledLow="#ff9a9a9a" ChromeGray="Gray" ChromeHigh="Gray" ChromeLow="#ff151515" ChromeMedium="#ff1d1d1d" ChromeMediumLow="#ff2c2c2c" ChromeWhite="White" ListLow="#ff1d1d1d" ListMedium="#ff333333" RegionColor="Black" />
|
||||
</FluentTheme.Palettes>
|
||||
</FluentTheme>
|
||||
</Application.Styles>
|
||||
|
||||
<TrayIcon.Icons>
|
||||
<TrayIcons>
|
||||
<TrayIcon Clicked="TrayDoubleClick" ToolTipText="Ellie Updater" Icon="{DynamicResource EllieHubIcon}">
|
||||
<TrayIcon.Menu>
|
||||
<NativeMenu>
|
||||
<NativeMenuItem Header="Open" Click="ShowApp" />
|
||||
<NativeMenuItemSeparator />
|
||||
<NativeMenuItem Header="Close" Click="CloseApp" />
|
||||
</NativeMenu>
|
||||
</TrayIcon.Menu>
|
||||
</TrayIcon>
|
||||
</TrayIcons>
|
||||
<TrayIcons>
|
||||
<TrayIcon Clicked="TrayDoubleClick" ToolTipText="Ellie Updater" Icon="{DynamicResource EllieHubIcon}">
|
||||
<TrayIcon.Menu>
|
||||
<NativeMenu>
|
||||
<NativeMenuItem Header="Open" Click="ShowApp" />
|
||||
<NativeMenuItemSeparator />
|
||||
<NativeMenuItem Header="Close" Click="CloseApp" />
|
||||
</NativeMenu>
|
||||
</TrayIcon.Menu>
|
||||
</TrayIcon>
|
||||
</TrayIcons>
|
||||
</TrayIcon.Icons>
|
||||
</Application>
|
|
@ -3,7 +3,7 @@ using Avalonia.Controls;
|
|||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using EllieHub.Views.Windows;
|
||||
using EllieHub.Features.AppWindow.Views.Windows;
|
||||
using System.Reflection;
|
||||
|
||||
namespace EllieHub;
|
||||
|
@ -20,7 +20,7 @@ public partial class App : Application
|
|||
/// </summary>
|
||||
public IServiceProvider Services { get; } = new ServiceCollection()
|
||||
.RegisterViewsAndViewModels(Assembly.GetExecutingAssembly())
|
||||
.RegisterServices()
|
||||
.RegisterAppServices()
|
||||
.BuildServiceProvider(true);
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace EllieHub.DesignData.Common;
|
||||
namespace EllieHub.Avalonia.DesignData.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Defines objects useful at design-time.
|
|
@ -1,11 +1,14 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using EllieHub.DesignData.Common;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using EllieHub.Services.Mocks;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
using EllieHub.Views.Windows;
|
||||
using EllieHub.Avalonia.DesignData.Common;
|
||||
using EllieHub.Features.AppConfig.Services.Mocks;
|
||||
using EllieHub.Features.AppWindow.ViewModels;
|
||||
using EllieHub.Features.AppWindow.Views.Windows;
|
||||
using EllieHub.Features.BotConfig.Services.Abstractions;
|
||||
using EllieHub.Features.BotConfig.Services.Mocks;
|
||||
using EllieHub.Features.BotConfig.ViewModels;
|
||||
using EllieHub.Features.Common.ViewModels;
|
||||
|
||||
namespace EllieHub.DesignData.Controls;
|
||||
namespace EllieHub.Avalonia.DesignData.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Mock view-model for <see cref="BotConfigViewModel"/>.
|
|
@ -1,12 +1,12 @@
|
|||
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;
|
||||
using EllieHub.Avalonia.DesignData.Common;
|
||||
using EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
using EllieHub.Features.AppConfig.Services.Mocks;
|
||||
using EllieHub.Features.AppConfig.ViewModels;
|
||||
using EllieHub.Features.AppWindow.Views.Windows;
|
||||
using EllieHub.Features.Common.ViewModels;
|
||||
|
||||
namespace EllieHub.DesignData.Controls;
|
||||
namespace EllieHub.Avalonia.DesignData.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Mock view-model for <see cref="ConfigViewModel"/>.
|
|
@ -1,9 +1,9 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using EllieHub.DesignData.Common;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
using EllieHub.Views.Windows;
|
||||
using EllieHub.Avalonia.DesignData.Common;
|
||||
using EllieHub.Features.AppWindow.Views.Windows;
|
||||
using EllieHub.Features.Common.ViewModels;
|
||||
|
||||
namespace EllieHub.DesignData.Controls;
|
||||
namespace EllieHub.Avalonia.DesignData.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Mock view-model for <see cref="DependencyButtonViewModel"/>.
|
|
@ -1,6 +1,6 @@
|
|||
using EllieHub.ViewModels.Controls;
|
||||
using EllieHub.Features.BotConfig.ViewModels;
|
||||
|
||||
namespace EllieHub.DesignData.Controls;
|
||||
namespace EllieHub.Avalonia.DesignData.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Mock view-model for <see cref="FakeConsoleViewModel"/>.
|
||||
|
@ -10,6 +10,6 @@ public sealed class DesignFakeConsoleViewModel : FakeConsoleViewModel
|
|||
/// <summary>
|
||||
/// Creates a mock <see cref="FakeConsoleViewModel"/> to be used at design-time.
|
||||
/// </summary>
|
||||
public DesignFakeConsoleViewModel() : base()
|
||||
public DesignFakeConsoleViewModel()
|
||||
=> Watermark = "Sample watermark.";
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
using EllieHub.ViewModels.Controls;
|
||||
using EllieHub.Features.Home.ViewModels;
|
||||
|
||||
namespace EllieHub.DesignData.Controls;
|
||||
namespace EllieHub.Avalonia.DesignData.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Mock view-model for <see cref="HomeViewModel"/>.
|
|
@ -1,9 +1,9 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using EllieHub.DesignData.Common;
|
||||
using EllieHub.Services.Mocks;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
using EllieHub.Avalonia.DesignData.Common;
|
||||
using EllieHub.Features.AppConfig.Services.Mocks;
|
||||
using EllieHub.Features.AppWindow.ViewModels;
|
||||
|
||||
namespace EllieHub.DesignData.Controls;
|
||||
namespace EllieHub.Avalonia.DesignData.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Mock view-model for <see cref="LateralBarViewModel"/>.
|
|
@ -1,9 +1,9 @@
|
|||
using Avalonia.Platform.Storage;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using EllieHub.DesignData.Common;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
using EllieHub.Avalonia.DesignData.Common;
|
||||
using EllieHub.Features.Common.ViewModels;
|
||||
|
||||
namespace EllieHub.DesignData.Controls;
|
||||
namespace EllieHub.Avalonia.DesignData.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Mock view-model for <see cref="UriInputBarViewModel"/>.
|
|
@ -1,6 +1,6 @@
|
|||
using EllieHub.ViewModels.Windows;
|
||||
using EllieHub.Features.AppConfig.ViewModels;
|
||||
|
||||
namespace EllieHub.DesignData.Windows;
|
||||
namespace EllieHub.Avalonia.DesignData.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Mock view-model for <see cref="AboutMeViewModel"/>.
|
|
@ -1,9 +1,9 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using EllieHub.DesignData.Common;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
using EllieHub.ViewModels.Windows;
|
||||
using EllieHub.Avalonia.DesignData.Common;
|
||||
using EllieHub.Features.AppWindow.ViewModels;
|
||||
using EllieHub.Features.Home.ViewModels;
|
||||
|
||||
namespace EllieHub.DesignData.Windows;
|
||||
namespace EllieHub.Avalonia.DesignData.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Mock view-model for <see cref="AppViewModel"/>.
|
|
@ -1,6 +1,6 @@
|
|||
using EllieHub.ViewModels.Windows;
|
||||
using EllieHub.Features.Home.ViewModels;
|
||||
|
||||
namespace EllieHub.DesignData.Windows;
|
||||
namespace EllieHub.Avalonia.DesignData.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Mock view-model for <see cref="UpdateViewModel"/>.
|
|
@ -21,14 +21,14 @@
|
|||
<SolidColorBrush x:Key='LightBackground'>#FAF5F8</SolidColorBrush>
|
||||
<SolidColorBrush x:Key='MediumBackground'>#FCFAFC</SolidColorBrush>
|
||||
<SolidColorBrush x:Key='HeavyBackground'>#FFFAFD</SolidColorBrush>
|
||||
<SolidColorBrush x:Key='BotSelectionColor'>#FF0067</SolidColorBrush>
|
||||
<SolidColorBrush x:Key='BotSelectionColor'>#3498DB</SolidColorBrush>
|
||||
<SolidColorBrush x:Key='HyperlinkColor'>Blue</SolidColorBrush>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key='Dark'>
|
||||
<SolidColorBrush x:Key='LightBackground'>#252525</SolidColorBrush>
|
||||
<SolidColorBrush x:Key='MediumBackground'>#202020</SolidColorBrush>
|
||||
<SolidColorBrush x:Key='HeavyBackground'>#181818</SolidColorBrush>
|
||||
<SolidColorBrush x:Key='BotSelectionColor'>#D90058</SolidColorBrush>
|
||||
<SolidColorBrush x:Key='BotSelectionColor'>#206694</SolidColorBrush>
|
||||
<SolidColorBrush x:Key='HyperlinkColor'>#5090BB</SolidColorBrush>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
|
@ -1,5 +1,9 @@
|
|||
<ResourceDictionary xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<FontFamily x:Key="NotoSansFont">avares://EllieHub/Assets/Fonts/NotoSans-Regular.ttf</FontFamily>
|
||||
<FontFamily x:Key="NotoSansBoldFont">avares://EllieHub/Assets/Fonts/NotoSans-Bold.ttf</FontFamily>
|
||||
|
||||
<!--Preview-->
|
||||
<Design.PreviewWith>
|
||||
<Border Padding="20">
|
||||
|
@ -13,7 +17,4 @@
|
|||
</StackPanel>
|
||||
</Border>
|
||||
</Design.PreviewWith>
|
||||
|
||||
<FontFamily x:Key="NotoSansFont">avares://EllieHub/Assets/Fonts/NotoSans-Regular.ttf</FontFamily>
|
||||
<FontFamily x:Key="NotoSansBoldFont">avares://EllieHub/Assets/Fonts/NotoSans-Bold.ttf</FontFamily>
|
||||
</ResourceDictionary>
|
|
@ -36,7 +36,7 @@
|
|||
<Bitmap x:Key="PaypalIcon">avares://EllieHub/Assets/paypal.png</Bitmap>
|
||||
<Bitmap x:Key="PatreonIcon">avares://EllieHub/Assets/patreon.png</Bitmap>
|
||||
<Bitmap x:Key="KofiIcon">avares://EllieHub/Assets/ko-fi.webp</Bitmap>
|
||||
|
||||
|
||||
<!--Themed Images-->
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key='Light'>
|
||||
|
@ -50,7 +50,7 @@
|
|||
<Bitmap x:Key="UrlIcon">avares://EllieHub/Assets/Light/icon-link.png</Bitmap>
|
||||
<Bitmap x:Key="SuggestionIcon">avares://EllieHub/Assets/Light/icon-suggest.png</Bitmap>
|
||||
<Bitmap x:Key="DiscordIcon">avares://EllieHub/Assets/Light/icon-support.png</Bitmap>
|
||||
<WindowIcon x:Key="EllieHubIcon">avares://EllieHub/Assets/Light/ellieupdatericon.ico</WindowIcon>
|
||||
<WindowIcon x:Key="EllieHubIcon"><![CDATA[avares://EllieHub/Assets/Light/ellieupdatericon.ico]]></WindowIcon>
|
||||
<Bitmap x:Key="EllieHubImage">avares://EllieHub/Assets/Light/ellieupdatericon.ico</Bitmap>
|
||||
<Bitmap x:Key="TerminalIcon">avares://EllieHub/Assets/Light/terminal.png</Bitmap>
|
||||
</ResourceDictionary>
|
||||
|
@ -65,7 +65,7 @@
|
|||
<Bitmap x:Key="UrlIcon">avares://EllieHub/Assets/Dark/icon-link.png</Bitmap>
|
||||
<Bitmap x:Key="SuggestionIcon">avares://EllieHub/Assets/Dark/icon-suggest.png</Bitmap>
|
||||
<Bitmap x:Key="DiscordIcon">avares://EllieHub/Assets/Dark/icon-support.png</Bitmap>
|
||||
<WindowIcon x:Key="EllieHubIcon">avares://EllieHub/Assets/Dark/ellieupdatericon.ico</WindowIcon>
|
||||
<WindowIcon x:Key="EllieHubIcon"><![CDATA[avares://EllieHub/Assets/Dark/ellieupdatericon.ico]]></WindowIcon>
|
||||
<Bitmap x:Key="EllieHubImage">avares://EllieHub/Assets/Dark/ellieupdatericon.ico</Bitmap>
|
||||
<Bitmap x:Key="TerminalIcon">avares://EllieHub/Assets/Dark/terminal.png</Bitmap>
|
||||
</ResourceDictionary>
|
|
@ -16,7 +16,6 @@
|
|||
<Border Classes="circular">
|
||||
<Button Classes="accent" Content="+" />
|
||||
</Border>
|
||||
<Button Classes="link-11" Content="HyperlinkButton" />
|
||||
<TextBlock Text="Text Block" />
|
||||
<TextBox Classes="console" Watermark="Fake Console" />
|
||||
<TextBox Watermark="Regular Text Box" Text="Sample Text" />
|
||||
|
@ -63,7 +62,7 @@
|
|||
<Style Selector="Button.accent">
|
||||
<Setter Property="FontSize" Value="28" />
|
||||
<Setter Property="FontWeight" Value="Bold" />
|
||||
<Setter Property="Padding" Value="12 2 12 2" />
|
||||
<Setter Property="Padding" Value="13 2 13 2" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource NotoSansBoldFont}" />
|
||||
|
@ -86,31 +85,6 @@
|
|||
</Style>
|
||||
</Style>
|
||||
|
||||
<!--Button styled like a hyperlink text-->
|
||||
<Style Selector="Button.link-11">
|
||||
<Setter Property="Foreground" Value="{DynamicResource HyperlinkColor}" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<ContentPresenter Content="{TemplateBinding Content}">
|
||||
<ContentPresenter.Styles>
|
||||
<Style Selector="TextBlock">
|
||||
<Setter Property="Foreground" Value="{TemplateBinding Foreground}"/>
|
||||
<!--This should be binding to the template's FontSize, but since I'm overriding-->
|
||||
<!--the main TextBlock class, this doesn't seem to work properly, so I have to set-->
|
||||
<!--the value manually-->
|
||||
<Setter Property="FontSize" Value="11"/>
|
||||
<Setter Property="TextDecorations" Value="Underline"/>
|
||||
</Style>
|
||||
</ContentPresenter.Styles>
|
||||
</ContentPresenter>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!--Circular Border-->
|
||||
<Style Selector="Border.circular">
|
||||
<Setter Property="ClipToBounds" Value="True" />
|
||||
|
@ -129,7 +103,7 @@
|
|||
<Setter Property="Width" Value="12" />
|
||||
<Setter Property="Height" Value="12" />
|
||||
</Style>
|
||||
|
||||
|
||||
<!--Fake Console-->
|
||||
<Style Selector="TextBox.console">
|
||||
<Setter Property="Background" Value="Black" />
|
|
@ -3,7 +3,7 @@ namespace EllieHub.Common;
|
|||
/// <summary>
|
||||
/// Defines the constants used throughout the whole application.
|
||||
/// </summary>
|
||||
public static class AppConstants
|
||||
public static class AppConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the location of the default image for the bot avatar.
|
||||
|
@ -14,4 +14,9 @@ public static class AppConstants
|
|||
/// The name for an <see cref="HttpClient"/> that does not automatically follow redirect responses.
|
||||
/// </summary>
|
||||
public const string NoRedirectClient = "NoRedirect";
|
||||
|
||||
/// <summary>
|
||||
/// The name for an <see cref="HttpClient"/> that makes calls to the Toastielab API.
|
||||
/// </summary>
|
||||
public const string ToastielabClient = "NoRedirect";
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
using Avalonia.Media;
|
||||
using Avalonia.Media.Immutable;
|
||||
using Avalonia.Platform.Storage;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace EllieHub.Common;
|
||||
|
@ -14,35 +15,35 @@ public static partial class AppStatics
|
|||
/// Defines the default location where the updater configuration and bot instances are stored.
|
||||
/// </summary>
|
||||
#if DEBUG
|
||||
public static string AppDefaultConfigDirectoryUri { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "EllieHubDebug");
|
||||
public static string AppDefaultConfigDirectoryUri { get; } = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "EllieHubDebug");
|
||||
#else
|
||||
public static string AppDefaultConfigDirectoryUri { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "EllieHub");
|
||||
public static string AppDefaultConfigDirectoryUri { get; } = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "EllieHub");
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Defines the default location where the bot instances are stored.
|
||||
/// </summary>
|
||||
public static string AppDefaultBotDirectoryUri { get; } = Path.Combine(AppDefaultConfigDirectoryUri, "Bots");
|
||||
public static string AppDefaultBotDirectoryUri { get; } = Path.Join(AppDefaultConfigDirectoryUri, "Bots");
|
||||
|
||||
/// <summary>
|
||||
/// Defines the default location where the backups of bot instances are stored.
|
||||
/// </summary>
|
||||
public static string AppDefaultBotBackupDirectoryUri { get; } = Path.Combine(AppDefaultConfigDirectoryUri, "Backups");
|
||||
public static string AppDefaultBotBackupDirectoryUri { get; } = Path.Join(AppDefaultConfigDirectoryUri, "Backups");
|
||||
|
||||
/// <summary>
|
||||
/// Defines the default location where the logs of bot instances are stored.
|
||||
/// </summary>
|
||||
public static string AppDefaultLogDirectoryUri { get; } = Path.Combine(AppDefaultConfigDirectoryUri, "Logs");
|
||||
public static string AppDefaultLogDirectoryUri { get; } = Path.Join(AppDefaultConfigDirectoryUri, "Logs");
|
||||
|
||||
/// <summary>
|
||||
/// Defines the location of the application's configuration file.
|
||||
/// </summary>
|
||||
public static string AppConfigUri { get; } = Path.Combine(AppDefaultConfigDirectoryUri, "config.json");
|
||||
public static string AppConfigUri { get; } = Path.Join(AppDefaultConfigDirectoryUri, "config.json");
|
||||
|
||||
/// <summary>
|
||||
/// Defines the location of the application's dependencies.
|
||||
/// </summary>
|
||||
public static string AppDepsUri { get; } = Path.Combine(AppDefaultConfigDirectoryUri, "Dependencies");
|
||||
public static string AppDepsUri { get; } = Path.Join(AppDefaultConfigDirectoryUri, "Dependencies");
|
||||
|
||||
/// <summary>
|
||||
/// Defines a transparent color brush.
|
||||
|
@ -55,13 +56,19 @@ public static partial class AppStatics
|
|||
public static FilePickerOpenOptions ImageFilePickerOptions { get; } = new()
|
||||
{
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter = new FilePickerFileType[]
|
||||
{
|
||||
new("Image") { Patterns = new[] { "*.png", "*.jpg", "*.jpeg", "*.gif", "*.webp" } },
|
||||
new("All") { Patterns = new[] { "*.*" } }
|
||||
}
|
||||
FileTypeFilter =
|
||||
[
|
||||
new("Image") { Patterns = ["*.png", "*.jpg", "*.jpeg", "*.gif", "*.webp"]},
|
||||
new("All") { Patterns = ["*.*"]}
|
||||
]
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The version of this application.
|
||||
/// </summary>
|
||||
public static string AppVersion { get; } = Assembly.GetExecutingAssembly().GetName().Version?.ToString()
|
||||
?? throw new InvalidOperationException("Version is missing from application assembly.");
|
||||
|
||||
/// <summary>
|
||||
/// Matches the version of Ffmpeg from its CLI output.
|
||||
/// </summary>
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
using Avalonia.Platform;
|
||||
using SkiaSharp;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace EllieHub.Common;
|
||||
|
||||
|
@ -11,205 +8,30 @@ namespace EllieHub.Common;
|
|||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Loads an image embeded with this application.
|
||||
/// Loads an image embedded with this application.
|
||||
/// </summary>
|
||||
/// <param name="uri">An uri that starts with "avares://"</param>
|
||||
/// <remarks>Valid uris must start with "avares://".</remarks>
|
||||
/// <returns>The embeded image or the default bot avatar placeholder.</returns>
|
||||
/// <exception cref="FileNotFoundException">Occurs when the embeded resource does not exist.</exception>
|
||||
public static SKBitmap LoadEmbededImage(string? uri = default)
|
||||
/// <returns>The embedded image or the default bot avatar placeholder.</returns>
|
||||
/// <exception cref="FileNotFoundException">Occurs when the embedded resource does not exist.</exception>
|
||||
public static SKBitmap LoadEmbeddedImage(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)));
|
||||
return (string.IsNullOrWhiteSpace(uri) || !uri.StartsWith("avares://", StringComparison.Ordinal))
|
||||
? SKBitmap.Decode(AssetLoader.Open(new(AppConstants.BotAvatarUri)))
|
||||
: SKBitmap.Decode(AssetLoader.Open(new(uri)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the image at the specified location or the bot avatar placeholder if it was not found.
|
||||
/// </summary>
|
||||
/// <param name="uri">The absolute path to the image file or <see langword="null"/> to get the avatar placeholder.</param>
|
||||
/// <remarks>This fallsback to <see cref="LoadEmbededImage(string?)"/> if <paramref name="uri"/> doesn't point to a valid image file.</remarks>
|
||||
/// <param name="imagePath">The absolute path to the image file or <see langword="null"/> to get the avatar placeholder.</param>
|
||||
/// <remarks>This fallsback to <see cref="LoadEmbeddedImage(string?)"/> if <paramref name="imagePath"/> doesn't point to a valid image file.</remarks>
|
||||
/// <returns>The requested image or the default bot avatar placeholder.</returns>
|
||||
public static SKBitmap LoadLocalImage(string? uri = default)
|
||||
public static SKBitmap LoadLocalImage(string? imagePath)
|
||||
{
|
||||
return (File.Exists(uri))
|
||||
? SKBitmap.Decode(uri)
|
||||
: LoadEmbededImage(uri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safely casts an <see cref="object"/> to a <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to cast to.</typeparam>
|
||||
/// <param name="obj">The object to be cast.</param>
|
||||
/// <param name="castObject">The cast object, or <see langword="null"/> is casting failed.</param>
|
||||
/// <returns><see langword="true"/> if the object was successfully cast, <see langword="false"/> otherwise.</returns>
|
||||
public static bool TryCastTo<T>(object? obj, [MaybeNullWhen(false)] out T castObject)
|
||||
{
|
||||
if (obj is T result)
|
||||
{
|
||||
castObject = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
castObject = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the specified program in the background.
|
||||
/// </summary>
|
||||
/// <param name="program">
|
||||
/// The name of the program in the PATH environment variable,
|
||||
/// or the absolute path to its executable.
|
||||
/// </param>
|
||||
/// <param name="arguments">The arguments to the program.</param>
|
||||
/// <returns>The process of the specified program.</returns>
|
||||
/// <exception cref="ArgumentException" />
|
||||
/// <exception cref="ArgumentNullException" />
|
||||
/// <exception cref="Win32Exception">Occurs when <paramref name="program"/> does not exist.</exception>
|
||||
/// <exception cref="InvalidOperationException">Occurs when the process fails to execute.</exception>
|
||||
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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a program exists.
|
||||
/// </summary>
|
||||
/// <param name="programName">The name of the program.</param>
|
||||
/// <param name="cToken">The cancellation token.</param>
|
||||
/// <returns><see langword="true"/> if the program exists, <see langword="false"/> otherwise.</returns>
|
||||
/// <exception cref="ArgumentException" />
|
||||
/// <exception cref="ArgumentNullException" />
|
||||
public static async ValueTask<bool> 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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safely deletes a file.
|
||||
/// </summary>
|
||||
/// <param name="fileUri">The absolute path to the file.</param>
|
||||
/// <returns><see langword="true"/> if the file was deleted, <see langword="false"/> otherwise.</returns>
|
||||
/// <exception cref="ArgumentException" />
|
||||
/// <exception cref="ArgumentNullException" />
|
||||
/// <exception cref="IOException" />
|
||||
/// <exception cref="NotSupportedException" />
|
||||
/// <exception cref="PathTooLongException" />
|
||||
/// <exception cref="UnauthorizedAccessException" />
|
||||
public static bool TryDeleteFile(string fileUri)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(fileUri, nameof(fileUri));
|
||||
|
||||
if (!File.Exists(fileUri))
|
||||
return false;
|
||||
|
||||
File.Delete(fileUri);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safely deletes a directory.
|
||||
/// </summary>
|
||||
/// <param name="directoryUri">The absolute path to the directory.</param>
|
||||
/// <returns><see langword="true"/> if the directory was deleted, <see langword="false"/> otherwise.</returns>
|
||||
/// <exception cref="ArgumentException" />
|
||||
/// <exception cref="ArgumentNullException" />
|
||||
/// <exception cref="IOException" />
|
||||
/// <exception cref="DirectoryNotFoundException" />
|
||||
/// <exception cref="PathTooLongException" />
|
||||
/// <exception cref="UnauthorizedAccessException" />
|
||||
public static bool TryDeleteDirectory(string directoryUri)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(directoryUri, nameof(directoryUri));
|
||||
|
||||
if (!Directory.Exists(directoryUri))
|
||||
return false;
|
||||
|
||||
Directory.Delete(directoryUri, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this application can write to <paramref name="directoryUri"/>.
|
||||
/// </summary>
|
||||
/// <param name="directoryUri">The absolute path to a directory.</param>
|
||||
/// <returns><see langword="true"/> if writing is allowed, <see langword="false"/> otherwise.</returns>
|
||||
/// <exception cref="PathTooLongException" />
|
||||
/// <exception cref="DirectoryNotFoundException" />
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a directory path to the PATH environment variable.
|
||||
/// </summary>
|
||||
/// <param name="directoryUri">The absolute path to a directory.</param>
|
||||
/// <remarks>
|
||||
/// On Windows, this needs to be called once and the dependencies will be available for the user forever. <br />
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <returns><see langword="true"/> if <paramref name="directoryUri"/> got successfully added to the PATH envar, <see langword="false"/> otherwise.</returns>
|
||||
/// <exception cref="ArgumentException" />
|
||||
/// <exception cref="ArgumentNullException" />
|
||||
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;
|
||||
return (File.Exists(imagePath))
|
||||
? SKBitmap.Decode(imagePath)
|
||||
: LoadEmbeddedImage(imagePath);
|
||||
}
|
||||
}
|
|
@ -8,12 +8,12 @@ public static class WindowConstants
|
|||
/// <summary>
|
||||
/// Defines the default width of the window.
|
||||
/// </summary>
|
||||
public const string DefaultWindowWidth = "885";
|
||||
public const string DefaultWindowWidth = "900";
|
||||
|
||||
/// <summary>
|
||||
/// Defines the default height of the window.
|
||||
/// </summary>
|
||||
public const string DefaultWindowHeight = "570";
|
||||
public const string DefaultWindowHeight = "575";
|
||||
|
||||
/// <summary>
|
||||
/// Defines the minimum height of the window.
|
||||
|
@ -33,5 +33,5 @@ public static class WindowConstants
|
|||
/// <summary>
|
||||
/// Defines the message that should be shown when a view's parameterless constructor should not be used.
|
||||
/// </summary>
|
||||
public const string DesignerCtorWarning = "This constructor exists to satisfy Avalonia's designer. Please, use the parameterized constructor instead.";
|
||||
public const string DesignerCtorWarning = "This constructor exists to satisfy Avalonia's previewer. Please, use the parameterized constructor instead.";
|
||||
}
|
|
@ -2,13 +2,12 @@
|
|||
<PropertyGroup>
|
||||
<!--Project Settings-->
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<AnalysisLevel>latest</AnalysisLevel>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>Nullable</WarningsAsErrors>
|
||||
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
|
||||
<EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
<BuiltInComInteropSupport>True</BuiltInComInteropSupport>
|
||||
|
@ -16,11 +15,12 @@
|
|||
|
||||
<!--Publishing-->
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||
<SelfContained>true</SelfContained>
|
||||
<DebugType>embedded</DebugType>
|
||||
|
||||
<!--Version-->
|
||||
<VersionPrefix>1.0.1.0</VersionPrefix>
|
||||
<VersionPrefix>1.0.4.0</VersionPrefix>
|
||||
|
||||
<!--Avalonia Settings-->
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
|
@ -30,6 +30,13 @@
|
|||
<UseAppHost>true</UseAppHost>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<TreatWarningsAsErrors>False</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
|
||||
|
@ -39,20 +46,23 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.0.4" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.4" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.4" />
|
||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.4" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.4" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.4" />
|
||||
<PackageReference Include="Toastie.Events" Version="2.1.0" />
|
||||
<PackageReference Include="MessageBox.Avalonia" Version="3.1.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||
<PackageReference Include="Avalonia" Version="11.1.5" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.1.5" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.1.5" />
|
||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.5" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.5" />
|
||||
<PackageReference Include="Toastie.DependencyInjection" Version="3.0.0" />
|
||||
<PackageReference Include="Toastie.Events" Version="3.0.0" />
|
||||
<PackageReference Include="MessageBox.Avalonia" Version="3.1.5.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
||||
<PackageReference Include="SingleFileExtractor.Core" Version="2.2.0" />
|
||||
<PackageReference Include="SkiaImageView.Avalonia11" Version="1.5.0" />
|
||||
<PackageReference Include="Toastie.Utilities" Version="3.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
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 EllieHub.Features.AppConfig.Models;
|
||||
using EllieHub.Features.AppConfig.Services;
|
||||
using EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
using EllieHub.Features.AppConfig.Services.Mocks;
|
||||
using EllieHub.Features.AppWindow.Views.Windows;
|
||||
using EllieHub.Features.BotConfig.Services;
|
||||
using EllieHub.Features.BotConfig.Services.Abstractions;
|
||||
using EllieHub.Features.BotConfig.Services.Mocks;
|
||||
using EllieHub.Features.Home.Services;
|
||||
using EllieHub.Features.Home.Services.Abstractions;
|
||||
using ReactiveUI;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
|
@ -42,7 +47,7 @@ public static class IServiceCollectionExt
|
|||
/// </summary>
|
||||
/// <param name="serviceCollection">This service collection.</param>
|
||||
/// <returns>This service collection with the services added.</returns>
|
||||
public static IServiceCollection RegisterServices(this IServiceCollection serviceCollection)
|
||||
public static IServiceCollection RegisterAppServices(this IServiceCollection serviceCollection)
|
||||
{
|
||||
// Design-time
|
||||
if (Design.IsDesignMode)
|
||||
|
@ -60,15 +65,26 @@ public static class IServiceCollectionExt
|
|||
|
||||
// Web requests
|
||||
serviceCollection.AddHttpClient();
|
||||
serviceCollection.AddHttpClient(AppConstants.NoRedirectClient) // Client that doesn't allow automatic reditections
|
||||
.ConfigureHttpMessageHandlerBuilder(builder => builder.PrimaryHandler = new HttpClientHandler() { AllowAutoRedirect = false });
|
||||
serviceCollection.AddHttpClient(AppConstants.NoRedirectClient) // Client that doesn't allow automatic redirections
|
||||
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { AllowAutoRedirect = false });
|
||||
serviceCollection.AddHttpClient(AppConstants.ToastielabClient)
|
||||
.ConfigureHttpClient(x =>
|
||||
{
|
||||
x.DefaultRequestHeaders.Add("Accept", "application/vnd.toastielab+json");
|
||||
x.DefaultRequestHeaders.Add("X-Toastielab-Api-Version", "2022-11-28");
|
||||
#if DEBUG
|
||||
x.DefaultRequestHeaders.UserAgent.TryParseAdd($"EllieHub v{AppStatics.AppVersion}-Debug");
|
||||
#else
|
||||
x.DefaultRequestHeaders.UserAgent.TryParseAdd($"EllieHub v{AppStatics.AppVersion}");
|
||||
#endif
|
||||
});
|
||||
|
||||
// App settings
|
||||
serviceCollection.AddSingleton<IAppConfigManager, AppConfigManager>();
|
||||
serviceCollection.AddSingleton<ReadOnlyAppConfig>();
|
||||
serviceCollection.AddSingleton<ReadOnlyAppSettings>();
|
||||
serviceCollection.AddSingleton(_ =>
|
||||
(File.Exists(AppStatics.AppConfigUri))
|
||||
? JsonSerializer.Deserialize<AppConfig>(File.ReadAllText(AppStatics.AppConfigUri)) ?? new()
|
||||
? JsonSerializer.Deserialize<AppSettings>(File.ReadAllText(AppStatics.AppConfigUri)) ?? new()
|
||||
: new()
|
||||
);
|
||||
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace EllieHub.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Defines extension methods for <see cref="IServiceProvider"/>.
|
||||
/// </summary>
|
||||
public static class IServiceProviderExt
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets service of type <typeparamref name="T"/> from the <see cref="IServiceProvider"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of service object to get.</typeparam>
|
||||
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> to retrieve the service object from.</param>
|
||||
/// <param name="arguments"></param>
|
||||
/// <remarks>Do not use abstract types in the type argument!</remarks>
|
||||
/// <returns>A service object of type <typeparamref name="T"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException">Occurs when <paramref name="serviceProvider"/> or <paramref name="arguments"/> are <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">Occurs when there is no concrete service of type <typeparamref name="T"/> or when the arguments are wrong.</exception>
|
||||
public static T GetParameterizedService<T>(this IServiceProvider serviceProvider, params object[] arguments)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(serviceProvider, nameof(serviceProvider));
|
||||
ArgumentNullException.ThrowIfNull(arguments, nameof(arguments));
|
||||
|
||||
var result = ActivatorUtilities.CreateInstance<T>(serviceProvider, arguments);
|
||||
|
||||
return (result is null)
|
||||
? throw new InvalidOperationException($"There is no service of type {nameof(T)} or the arguments were incorrect.")
|
||||
: result;
|
||||
}
|
||||
}
|
|
@ -83,8 +83,8 @@ public static class WindowExt
|
|||
{
|
||||
return (!activeView.TryFindResource(resourceName, theme, out var resource))
|
||||
? throw new InvalidOperationException($"Resource '{resourceName}' was not found.")
|
||||
: (!Utilities.TryCastTo<T>(resource, out var result))
|
||||
? throw new InvalidCastException($"Could not convert resource of type '{resource?.GetType()?.FullName}' to '{nameof(T)}'.")
|
||||
: result;
|
||||
: (resource is T castResource)
|
||||
? castResource
|
||||
: throw new InvalidCastException($"Could not convert resource of type '{resource?.GetType().FullName}' to '{nameof(T)}'.");
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
using ReactiveUI;
|
||||
|
||||
namespace EllieHub.ViewModels.Abstractions;
|
||||
namespace EllieHub.Features.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// The base view-model.
|
|
@ -1,6 +1,6 @@
|
|||
using ReactiveUI;
|
||||
|
||||
namespace EllieHub.ViewModels.Abstractions;
|
||||
namespace EllieHub.Features.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// The base view-model.
|
|
@ -1,6 +1,6 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EllieHub.Models.Api;
|
||||
namespace EllieHub.Features.AppConfig.Models.Api.Evermeet;
|
||||
|
||||
/// <summary>
|
||||
/// Represents download information for a <see cref="EvermeetInfo"/> component.
|
|
@ -1,6 +1,6 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EllieHub.Models.Api;
|
||||
namespace EllieHub.Features.AppConfig.Models.Api.Evermeet;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a response from the "https://evermeet.cx/ffmpeg/info" endpoint.
|
|
@ -1,13 +1,14 @@
|
|||
using EllieHub.Enums;
|
||||
using EllieHub.Features.AppWindow.Models;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace EllieHub.Models.Config;
|
||||
namespace EllieHub.Features.AppConfig.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the settings of the application.
|
||||
/// </summary>
|
||||
/// <remarks>Prefer using <see cref="ReadOnlyAppConfig"/> in dependency injection, if possible.</remarks>
|
||||
public sealed class AppConfig
|
||||
/// <remarks>Prefer using <see cref="ReadOnlyAppSettings"/> in dependency injection, if possible.</remarks>
|
||||
public sealed class AppSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// The absolute path to the directory where the bot instances are stored.
|
|
@ -1,13 +1,14 @@
|
|||
using EllieHub.Enums;
|
||||
using EllieHub.Features.AppWindow.Models;
|
||||
|
||||
namespace EllieHub.Models.Config;
|
||||
namespace EllieHub.Features.AppConfig.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a read-only version of <see cref="AppConfig"/>.
|
||||
/// Represents a read-only version of <see cref="AppSettings"/>.
|
||||
/// </summary>
|
||||
public sealed class ReadOnlyAppConfig
|
||||
public sealed class ReadOnlyAppSettings
|
||||
{
|
||||
private readonly AppConfig _appConfig;
|
||||
private readonly AppSettings _appConfig;
|
||||
|
||||
/// <summary>
|
||||
/// The absolute path to the directory where the bot instances are stored.
|
||||
|
@ -64,9 +65,9 @@ public sealed class ReadOnlyAppConfig
|
|||
=> _appConfig.BotEntries;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a read-only version of <see cref="AppConfig"/>.
|
||||
/// Initializes a read-only version of <see cref="AppSettings"/>.
|
||||
/// </summary>
|
||||
/// <param name="appConfig">The application settings to read from.</param>
|
||||
public ReadOnlyAppConfig(AppConfig appConfig)
|
||||
public ReadOnlyAppSettings(AppSettings appConfig)
|
||||
=> _appConfig = appConfig;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
namespace EllieHub.Models;
|
||||
namespace EllieHub.Features.AppConfig.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the dimensions of the application's window.
|
|
@ -1,11 +1,13 @@
|
|||
namespace EllieHub.Services.Abstractions;
|
||||
using Toastie.Utilities;
|
||||
|
||||
namespace EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for a service that checks, downloads, installs, and updates ffmpeg.
|
||||
/// </summary>
|
||||
public abstract class FfmpegResolver : IFfmpegResolver
|
||||
{
|
||||
private readonly string _programVerifier = (Environment.OSVersion.Platform is PlatformID.Win32NT) ? "where" : "which";
|
||||
private readonly string _programVerifier = Environment.OSVersion.Platform is PlatformID.Win32NT ? "where" : "which";
|
||||
|
||||
/// <summary>
|
||||
/// The name of the Ffmpeg process.
|
||||
|
@ -13,7 +15,7 @@ public abstract class FfmpegResolver : IFfmpegResolver
|
|||
protected const string FfmpegProcessName = "ffmpeg";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DependencyName { get; } = "FFMPEG";
|
||||
public string DependencyName { get; } = "Ffmpeg";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract string FileName { get; }
|
||||
|
@ -22,7 +24,7 @@ public abstract class FfmpegResolver : IFfmpegResolver
|
|||
public virtual async ValueTask<bool?> CanUpdateAsync(CancellationToken cToken = default)
|
||||
{
|
||||
// Check where ffmpeg is referenced.
|
||||
using var whereProcess = Utilities.StartProcess(_programVerifier, FfmpegProcessName);
|
||||
using var whereProcess = ToastieUtilities.StartProcess(_programVerifier, FfmpegProcessName, true);
|
||||
var installationPath = await whereProcess.StandardOutput.ReadToEndAsync(cToken);
|
||||
|
||||
// If ffmpeg is present but not managed by us, just report it is installed.
|
||||
|
@ -32,7 +34,7 @@ public abstract class FfmpegResolver : IFfmpegResolver
|
|||
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))
|
||||
if (currentVer is null || !ToastieUtilities.ProgramExists("ffprobe"))
|
||||
return null;
|
||||
|
||||
var latestVer = await GetLatestVersionAsync(cToken);
|
||||
|
@ -44,20 +46,20 @@ public abstract class FfmpegResolver : IFfmpegResolver
|
|||
public virtual async ValueTask<string?> GetCurrentVersionAsync(CancellationToken cToken = default)
|
||||
{
|
||||
// If ffmpeg is not accessible from the shell...
|
||||
if (!await Utilities.ProgramExistsAsync(FfmpegProcessName, cToken))
|
||||
if (!ToastieUtilities.ProgramExists(FfmpegProcessName))
|
||||
{
|
||||
// And doesn't exist in the dependencies folder,
|
||||
// report that ffmpeg is not installed.
|
||||
if (!File.Exists(Path.Combine(AppStatics.AppDepsUri, FileName)))
|
||||
if (!File.Exists(Path.Join(AppStatics.AppDepsUri, FileName)))
|
||||
return null;
|
||||
|
||||
// Else, add the dependencies directory to the PATH envar,
|
||||
// then try again.
|
||||
Utilities.AddPathToPATHEnvar(AppStatics.AppDepsUri);
|
||||
ToastieUtilities.AddPathToPATHEnvar(AppStatics.AppDepsUri);
|
||||
return await GetCurrentVersionAsync(cToken);
|
||||
}
|
||||
|
||||
using var ffmpeg = Utilities.StartProcess(FfmpegProcessName, "-version");
|
||||
using var ffmpeg = ToastieUtilities.StartProcess(FfmpegProcessName, "-version", true);
|
||||
var match = AppStatics.FfmpegVersionRegex.Match(await ffmpeg.StandardOutput.ReadLineAsync(cToken) ?? string.Empty);
|
||||
|
||||
return match.Groups[1].Value;
|
|
@ -1,7 +1,7 @@
|
|||
using EllieHub.Models;
|
||||
using EllieHub.Models.Config;
|
||||
using EllieHub.Features.AppConfig.Models;
|
||||
using EllieHub.Features.AppWindow.Models;
|
||||
|
||||
namespace EllieHub.Services.Abstractions;
|
||||
namespace EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a service that manages the application's settings.
|
||||
|
@ -11,7 +11,7 @@ public interface IAppConfigManager
|
|||
/// <summary>
|
||||
/// The application settings.
|
||||
/// </summary>
|
||||
ReadOnlyAppConfig AppConfig { get; }
|
||||
ReadOnlyAppSettings AppConfig { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a bot entry.
|
||||
|
@ -52,5 +52,5 @@ public interface IAppConfigManager
|
|||
/// </summary>
|
||||
/// <param name="action">The action to be performed on the configuration file.</param>
|
||||
/// <param name="cToken">The cancellation token.</param>
|
||||
ValueTask UpdateConfigAsync(Action<AppConfig> action, CancellationToken cToken = default);
|
||||
ValueTask UpdateConfigAsync(Action<AppSettings> action, CancellationToken cToken = default);
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
namespace EllieHub.Services.Abstractions;
|
||||
using EllieHub.Features.Common.Services.Abstractions;
|
||||
|
||||
namespace EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a service that checks, downloads, installs, and updates ffmpeg.
|
|
@ -1,4 +1,6 @@
|
|||
namespace EllieHub.Services.Abstractions;
|
||||
using EllieHub.Features.Common.Services.Abstractions;
|
||||
|
||||
namespace EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a service that checks, downloads, installs, and updates yt-dlp.
|
|
@ -1,9 +1,10 @@
|
|||
using EllieHub.Models;
|
||||
using EllieHub.Models.Config;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using Toastie.Utilities;
|
||||
using EllieHub.Features.AppConfig.Models;
|
||||
using EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
using EllieHub.Features.AppWindow.Models;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace EllieHub.Services;
|
||||
namespace EllieHub.Features.AppConfig.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a service that manages the application's settings.
|
||||
|
@ -11,15 +12,15 @@ namespace EllieHub.Services;
|
|||
public sealed class AppConfigManager : IAppConfigManager
|
||||
{
|
||||
private readonly JsonSerializerOptions _jsonSerializerOptions = new() { WriteIndented = true };
|
||||
private readonly AppConfig _appConfig;
|
||||
private readonly AppSettings _appConfig;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ReadOnlyAppConfig AppConfig { get; }
|
||||
public ReadOnlyAppSettings AppConfig { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a service that manages the application's settings.
|
||||
/// </summary>
|
||||
public AppConfigManager(AppConfig appConfig, ReadOnlyAppConfig readOnlyAppConfig)
|
||||
public AppConfigManager(AppSettings appConfig, ReadOnlyAppSettings readOnlyAppConfig)
|
||||
{
|
||||
_appConfig = appConfig;
|
||||
AppConfig = readOnlyAppConfig;
|
||||
|
@ -31,9 +32,9 @@ public sealed class AppConfigManager : IAppConfigManager
|
|||
public async ValueTask<BotEntry> CreateBotEntryAsync(CancellationToken cToken = default)
|
||||
{
|
||||
var newId = CreateNewId();
|
||||
var newPosition = (_appConfig.BotEntries.Count is 0) ? 0 : _appConfig.BotEntries.Values.Max(x => x.Position) + 1;
|
||||
var newPosition = _appConfig.BotEntries.Count is 0 ? 0 : _appConfig.BotEntries.Values.Max(x => x.Position) + 1;
|
||||
var newBotName = "NewBot_" + newPosition;
|
||||
var newEntry = new BotInstanceInfo(newBotName, Path.Combine(_appConfig.BotsDirectoryUri, newBotName), newPosition);
|
||||
var newEntry = new BotInstanceInfo(newBotName, Path.Join(_appConfig.BotsDirectoryUri, newBotName), newPosition);
|
||||
|
||||
if (!_appConfig.BotEntries.TryAdd(newId, newEntry))
|
||||
throw new InvalidOperationException($"Could not create a new bot entry with Id {newId}.");
|
||||
|
@ -49,7 +50,7 @@ public sealed class AppConfigManager : IAppConfigManager
|
|||
if (!_appConfig.BotEntries.TryRemove(id, out var removedEntry))
|
||||
return null;
|
||||
|
||||
Utilities.TryDeleteDirectory(removedEntry.InstanceDirectoryUri);
|
||||
ToastieUtilities.TryDeleteDirectory(removedEntry.InstanceDirectoryUri);
|
||||
|
||||
await SaveAsync(cToken);
|
||||
|
||||
|
@ -90,7 +91,7 @@ public sealed class AppConfigManager : IAppConfigManager
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask UpdateConfigAsync(Action<AppConfig> action, CancellationToken cToken = default)
|
||||
public ValueTask UpdateConfigAsync(Action<AppSettings> action, CancellationToken cToken = default)
|
||||
{
|
||||
action(_appConfig);
|
||||
return SaveAsync(cToken);
|
|
@ -1,9 +1,10 @@
|
|||
using EllieHub.Services.Abstractions;
|
||||
using Toastie.Utilities;
|
||||
using EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace EllieHub.Services;
|
||||
namespace EllieHub.Features.AppConfig.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service that checks, downloads, installs, and updates ffmpeg on Linux.
|
||||
|
@ -14,7 +15,7 @@ public sealed partial class FfmpegLinuxResolver : FfmpegResolver
|
|||
{
|
||||
private readonly Regex _ffmpegLatestVersionRegex = FfmpegLatestVersionRegexGenerator();
|
||||
private readonly string _tempDirectory = Path.GetTempPath();
|
||||
private bool _isUpdating = false;
|
||||
private bool _isUpdating;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
@ -48,7 +49,7 @@ public sealed partial class FfmpegLinuxResolver : FfmpegResolver
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string dependenciesUri, CancellationToken cToken = default)
|
||||
public override async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string installationUri, CancellationToken cToken = default)
|
||||
{
|
||||
if (_isUpdating)
|
||||
return (null, null);
|
||||
|
@ -64,42 +65,42 @@ public sealed partial class FfmpegLinuxResolver : FfmpegResolver
|
|||
if (currentVersion == newVersion)
|
||||
return (currentVersion, null);
|
||||
|
||||
Utilities.TryDeleteFile(Path.Combine(dependenciesUri, FileName));
|
||||
Utilities.TryDeleteFile(Path.Combine(dependenciesUri, "ffprobe"));
|
||||
ToastieUtilities.TryDeleteFile(Path.Join(installationUri, FileName));
|
||||
ToastieUtilities.TryDeleteFile(Path.Join(installationUri, "ffprobe"));
|
||||
}
|
||||
|
||||
// Install
|
||||
Directory.CreateDirectory(dependenciesUri);
|
||||
Directory.CreateDirectory(installationUri);
|
||||
|
||||
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);
|
||||
await 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))
|
||||
var tarFilePath = Path.Join(_tempDirectory, tarFileName);
|
||||
var tarExtractDir = Path.Join(_tempDirectory, $"ffmpeg-{newVersion}-{architecture}64-static");
|
||||
await 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}\"");
|
||||
using var extractProcess = ToastieUtilities.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);
|
||||
ToastieUtilities.TryMoveFile(Path.Join(tarExtractDir, FileName), Path.Join(installationUri, FileName), true);
|
||||
ToastieUtilities.TryMoveFile(Path.Join(tarExtractDir, "ffprobe"), Path.Join(installationUri, "ffprobe"), true);
|
||||
|
||||
// Mark the files as executable.
|
||||
using var chmod = Utilities.StartProcess("chmod", $"+x \"{Path.Combine(dependenciesUri, FileName)}\" \"{Path.Combine(dependenciesUri, "ffprobe")}\"");
|
||||
using var chmod = ToastieUtilities.StartProcess("chmod", ["+x", Path.Join(installationUri, FileName), Path.Join(installationUri, "ffprobe")]);
|
||||
await chmod.WaitForExitAsync(cToken);
|
||||
|
||||
// Cleanup
|
||||
File.Delete(tarFilePath);
|
||||
Directory.Delete(tarExtractDir, true);
|
||||
ToastieUtilities.TryDeleteFile(tarFilePath);
|
||||
ToastieUtilities.TryDeleteDirectory(tarExtractDir, true);
|
||||
|
||||
// Update environment variable
|
||||
Utilities.AddPathToPATHEnvar(dependenciesUri);
|
||||
ToastieUtilities.AddPathToPATHEnvar(installationUri);
|
||||
|
||||
_isUpdating = false;
|
||||
return (currentVersion, newVersion);
|
|
@ -1,9 +1,10 @@
|
|||
using EllieHub.Models.Api;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using Toastie.Utilities;
|
||||
using EllieHub.Features.AppConfig.Models.Api.Evermeet;
|
||||
using EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
using System.IO.Compression;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace EllieHub.Services;
|
||||
namespace EllieHub.Features.AppConfig.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service that checks, downloads, installs, and updates ffmpeg on MacOS.
|
||||
|
@ -15,7 +16,7 @@ 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 bool _isUpdating;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
@ -38,7 +39,7 @@ public sealed class FfmpegMacResolver : FfmpegResolver
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string dependenciesUri, CancellationToken cToken = default)
|
||||
public override async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string installationUri, CancellationToken cToken = default)
|
||||
{
|
||||
if (_isUpdating)
|
||||
return (null, null);
|
||||
|
@ -57,24 +58,24 @@ public sealed class FfmpegMacResolver : FfmpegResolver
|
|||
return (currentVersion, null);
|
||||
}
|
||||
|
||||
Utilities.TryDeleteFile(Path.Combine(dependenciesUri, FileName));
|
||||
Utilities.TryDeleteFile(Path.Combine(dependenciesUri, "ffprobe"));
|
||||
ToastieUtilities.TryDeleteFile(Path.Join(installationUri, FileName));
|
||||
ToastieUtilities.TryDeleteFile(Path.Join(installationUri, "ffprobe"));
|
||||
}
|
||||
|
||||
// Install
|
||||
Directory.CreateDirectory(dependenciesUri);
|
||||
Directory.CreateDirectory(installationUri);
|
||||
|
||||
var http = _httpClientFactory.CreateClient();
|
||||
var ffmpegResponse = await http.CallApiAsync<EvermeetInfo>(_apiFfmpegInfoEndpoint, cToken);
|
||||
var ffprobeResponse = await http.CallApiAsync<EvermeetInfo>(_apiFfprobeInfoEndpoint, cToken);
|
||||
|
||||
await Task.WhenAll(
|
||||
InstallDependencyAsync(ffmpegResponse, dependenciesUri, cToken),
|
||||
InstallDependencyAsync(ffprobeResponse, dependenciesUri, cToken)
|
||||
InstallDependencyAsync(ffmpegResponse, installationUri, cToken),
|
||||
InstallDependencyAsync(ffprobeResponse, installationUri, cToken)
|
||||
);
|
||||
|
||||
// Update environment variable
|
||||
Utilities.AddPathToPATHEnvar(dependenciesUri);
|
||||
ToastieUtilities.AddPathToPATHEnvar(installationUri);
|
||||
|
||||
_isUpdating = false;
|
||||
return (currentVersion, newVersion);
|
||||
|
@ -91,26 +92,26 @@ public sealed class FfmpegMacResolver : FfmpegResolver
|
|||
var http = _httpClientFactory.CreateClient();
|
||||
var downloadUrl = downloadInfo.Download["zip"].Url;
|
||||
var zipFileName = downloadUrl[(downloadUrl.LastIndexOf('/') + 1)..];
|
||||
var zipFilePath = Path.Combine(_tempDirectory, zipFileName);
|
||||
var zipFilePath = Path.Join(_tempDirectory, zipFileName);
|
||||
|
||||
// Download the zip file and save it to the temporary directory.
|
||||
using var zipStream = await http.GetStreamAsync(downloadUrl, cToken);
|
||||
await using var zipStream = await http.GetStreamAsync(downloadUrl, cToken);
|
||||
|
||||
using (var fileStream = new FileStream(zipFilePath, FileMode.Create))
|
||||
await 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);
|
||||
var finalFileUri = Path.Join(dependenciesUri, downloadInfo.Name);
|
||||
ToastieUtilities.TryMoveFile(Path.Join(_tempDirectory, downloadInfo.Name), finalFileUri, true);
|
||||
|
||||
// Mark binary as executable.
|
||||
using var chmod = Utilities.StartProcess("chmod", $"+x \"{finalFileUri}\"");
|
||||
using var chmod = ToastieUtilities.StartProcess("chmod", $"+x \"{finalFileUri}\"");
|
||||
await chmod.WaitForExitAsync(cToken);
|
||||
|
||||
// Cleanup.
|
||||
File.Delete(zipFilePath);
|
||||
ToastieUtilities.TryDeleteFile(zipFilePath);
|
||||
}
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
using EllieHub.Services.Abstractions;
|
||||
using Toastie.Utilities;
|
||||
using EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
using System.IO.Compression;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace EllieHub.Services;
|
||||
namespace EllieHub.Features.AppConfig.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service that checks, downloads, installs, and updates ffmpeg on Windows.
|
||||
|
@ -13,7 +14,7 @@ namespace EllieHub.Services;
|
|||
public sealed class FfmpegWindowsResolver : FfmpegResolver
|
||||
{
|
||||
private readonly string _tempDirectory = Path.GetTempPath();
|
||||
private bool _isUpdating = false;
|
||||
private bool _isUpdating;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -30,7 +31,7 @@ public sealed class FfmpegWindowsResolver : FfmpegResolver
|
|||
public override ValueTask<bool?> CanUpdateAsync(CancellationToken cToken = default)
|
||||
{
|
||||
// I could not find any ARM build of ffmpeg for Windows.
|
||||
return (RuntimeInformation.OSArchitecture is Architecture.X64)
|
||||
return RuntimeInformation.OSArchitecture is Architecture.X64
|
||||
? base.CanUpdateAsync(cToken)
|
||||
: ValueTask.FromResult<bool?>(false);
|
||||
}
|
||||
|
@ -49,7 +50,7 @@ public sealed class FfmpegWindowsResolver : FfmpegResolver
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string dependenciesUri, CancellationToken cToken = default)
|
||||
public override async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string installationUri, CancellationToken cToken = default)
|
||||
{
|
||||
if (_isUpdating)
|
||||
return (null, null);
|
||||
|
@ -68,22 +69,22 @@ public sealed class FfmpegWindowsResolver : FfmpegResolver
|
|||
return (currentVersion, null);
|
||||
}
|
||||
|
||||
Utilities.TryDeleteFile(Path.Combine(dependenciesUri, FileName));
|
||||
Utilities.TryDeleteFile(Path.Combine(dependenciesUri, "ffprobe.exe"));
|
||||
//Utilities.TryDeleteFile(Path.Combine(dependenciesUri, "ffplay.exe"));
|
||||
ToastieUtilities.TryDeleteFile(Path.Join(installationUri, FileName));
|
||||
ToastieUtilities.TryDeleteFile(Path.Join(installationUri, "ffprobe.exe"));
|
||||
//ToastieUtilities.TryDeleteFile(Path.Join(dependenciesUri, "ffplay.exe"));
|
||||
}
|
||||
|
||||
// Install
|
||||
Directory.CreateDirectory(dependenciesUri);
|
||||
Directory.CreateDirectory(installationUri);
|
||||
|
||||
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);
|
||||
await 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))
|
||||
var zipFilePath = Path.Join(_tempDirectory, zipFileName);
|
||||
var zipExtractDir = Path.Join(_tempDirectory, zipFileName[..^4]);
|
||||
await using (var fileStream = new FileStream(zipFilePath, FileMode.Create))
|
||||
await downloadStream.CopyToAsync(fileStream, cToken);
|
||||
|
||||
// Schedule installation to the thread-pool because ffmpeg is pretty
|
||||
|
@ -94,17 +95,17 @@ public sealed class FfmpegWindowsResolver : FfmpegResolver
|
|||
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"));
|
||||
ToastieUtilities.TryMoveFile(Path.Join(zipExtractDir, "bin", FileName), Path.Join(installationUri, FileName), true);
|
||||
ToastieUtilities.TryMoveFile(Path.Join(zipExtractDir, "bin", "ffprobe.exe"), Path.Join(installationUri, "ffprobe.exe"), true);
|
||||
//ToastieUtilities.TryMoveFile(Path.Join(zipExtractDir, "bin", "ffplay.exe"), Path.Join(dependenciesUri, "ffplay.exe"));
|
||||
|
||||
// Cleanup
|
||||
File.Delete(zipFilePath);
|
||||
Directory.Delete(zipExtractDir, true);
|
||||
ToastieUtilities.TryDeleteFile(zipFilePath);
|
||||
ToastieUtilities.TryDeleteDirectory(zipExtractDir);
|
||||
}, cToken);
|
||||
|
||||
// Update environment variable
|
||||
Utilities.AddPathToPATHEnvar(dependenciesUri);
|
||||
ToastieUtilities.AddPathToPATHEnvar(installationUri);
|
||||
|
||||
_isUpdating = false;
|
||||
return (currentVersion, newVersion);
|
|
@ -1,6 +1,6 @@
|
|||
using EllieHub.Services.Abstractions;
|
||||
using EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
|
||||
namespace EllieHub.Services.Mocks;
|
||||
namespace EllieHub.Features.AppConfig.Services.Mocks;
|
||||
|
||||
/// <summary>
|
||||
/// Service that pretends to check, download, install, and update ffmpeg.
|
|
@ -1,8 +1,8 @@
|
|||
using EllieHub.Models;
|
||||
using EllieHub.Models.Config;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using EllieHub.Features.AppConfig.Models;
|
||||
using EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
using EllieHub.Features.AppWindow.Models;
|
||||
|
||||
namespace EllieHub.Services.Mocks;
|
||||
namespace EllieHub.Features.AppConfig.Services.Mocks;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a service that pretends to manage the application's settings.
|
||||
|
@ -10,7 +10,7 @@ namespace EllieHub.Services.Mocks;
|
|||
internal sealed class MockAppConfigManager : IAppConfigManager
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public ReadOnlyAppConfig AppConfig { get; } = new(new() { BotEntries = new() { [Guid.Empty] = new("MockBot", Path.Combine(AppStatics.AppDefaultBotDirectoryUri, "MockBot"), 0) } });
|
||||
public ReadOnlyAppSettings AppConfig { get; } = new(new() { BotEntries = new() { [Guid.Empty] = new("MockBot", Path.Join(AppStatics.AppDefaultBotDirectoryUri, "MockBot"), 0) } });
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<BotEntry> CreateBotEntryAsync(CancellationToken cToken = default)
|
||||
|
@ -29,6 +29,6 @@ internal sealed class MockAppConfigManager : IAppConfigManager
|
|||
=> ValueTask.FromResult(false);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask UpdateConfigAsync(Action<AppConfig> action, CancellationToken cToken = default)
|
||||
public ValueTask UpdateConfigAsync(Action<AppSettings> action, CancellationToken cToken = default)
|
||||
=> ValueTask.CompletedTask;
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
using Toastie.Utilities;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace EllieHub.Services;
|
||||
namespace EllieHub.Features.AppConfig.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service that checks, downloads, installs, and updates yt-dlp.
|
||||
|
@ -13,15 +14,15 @@ 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 bool _isUpdating;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DependencyName { get; } = "Youtube-dlp";
|
||||
public string DependencyName { get; } = "Yt-dlp";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string FileName { get; } = (OperatingSystem.IsWindows()) ? "yt-dlp.exe" : "yt-dlp";
|
||||
public string FileName { get; } = OperatingSystem.IsWindows() ? "yt-dlp.exe" : "yt-dlp";
|
||||
|
||||
/// <summary>
|
||||
/// Creates a service that checks, downloads, installs, and updates yt-dlp.
|
||||
|
@ -56,16 +57,16 @@ public sealed class YtdlpResolver : IYtdlpResolver
|
|||
public async ValueTask<string?> GetCurrentVersionAsync(CancellationToken cToken = default)
|
||||
{
|
||||
// If yt-dlp is not accessible from the shell...
|
||||
if (!await Utilities.ProgramExistsAsync(_ytdlpProcessName, cToken))
|
||||
if (!ToastieUtilities.ProgramExists(_ytdlpProcessName))
|
||||
{
|
||||
// And doesn't exist in the dependencies folder,
|
||||
// report that yt-dlp is not installed.
|
||||
if (!File.Exists(Path.Combine(AppStatics.AppDepsUri, FileName)))
|
||||
if (!File.Exists(Path.Join(AppStatics.AppDepsUri, FileName)))
|
||||
return null;
|
||||
|
||||
// Else, add the dependencies directory to the PATH envar,
|
||||
// then try again.
|
||||
Utilities.AddPathToPATHEnvar(AppStatics.AppDepsUri);
|
||||
ToastieUtilities.AddPathToPATHEnvar(AppStatics.AppDepsUri);
|
||||
return await GetCurrentVersionAsync(cToken);
|
||||
}
|
||||
|
||||
|
@ -73,7 +74,7 @@ public sealed class YtdlpResolver : IYtdlpResolver
|
|||
if (_memoryCache.TryGetValue<string>(_cachedCurrentVersionKey, out var currentVersion) && currentVersion is not null)
|
||||
return currentVersion;
|
||||
|
||||
using var ytdlp = Utilities.StartProcess(_ytdlpProcessName, "--version");
|
||||
using var ytdlp = ToastieUtilities.StartProcess(_ytdlpProcessName, "--version", true);
|
||||
|
||||
var currentProcessVersion = (await ytdlp.StandardOutput.ReadToEndAsync(cToken)).Trim();
|
||||
_memoryCache.Set(_cachedCurrentVersionKey, currentProcessVersion, TimeSpan.FromMinutes(1.5));
|
||||
|
@ -95,7 +96,7 @@ public sealed class YtdlpResolver : IYtdlpResolver
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string dependenciesUri, CancellationToken cToken = default)
|
||||
public async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string installationUri, CancellationToken cToken = default)
|
||||
{
|
||||
if (_isUpdating)
|
||||
return (null, null);
|
||||
|
@ -116,7 +117,7 @@ public sealed class YtdlpResolver : IYtdlpResolver
|
|||
return (currentVersion, null);
|
||||
}
|
||||
|
||||
using var ytdlp = Utilities.StartProcess(_ytdlpProcessName, "-U");
|
||||
using var ytdlp = ToastieUtilities.StartProcess(_ytdlpProcessName, "-U");
|
||||
await ytdlp.WaitForExitAsync(cToken);
|
||||
|
||||
_isUpdating = false;
|
||||
|
@ -124,21 +125,21 @@ public sealed class YtdlpResolver : IYtdlpResolver
|
|||
}
|
||||
|
||||
// Install
|
||||
Directory.CreateDirectory(dependenciesUri);
|
||||
Directory.CreateDirectory(installationUri);
|
||||
|
||||
var finalFilePath = Path.Combine(dependenciesUri, FileName);
|
||||
var finalFilePath = Path.Join(installationUri, 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 using var downloadStream = await http.GetStreamAsync($"https://github.com/yt-dlp/yt-dlp/releases/download/{newVersion}/{_downloadedFileName}", cToken);
|
||||
await using (var fileStream = new FileStream(finalFilePath, FileMode.Create))
|
||||
await downloadStream.CopyToAsync(fileStream, cToken);
|
||||
|
||||
// Update environment variable
|
||||
Utilities.AddPathToPATHEnvar(dependenciesUri);
|
||||
ToastieUtilities.AddPathToPATHEnvar(installationUri);
|
||||
|
||||
// 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}\"");
|
||||
using var chmod = ToastieUtilities.StartProcess("chmod", ["+x", finalFilePath]);
|
||||
await chmod.WaitForExitAsync(cToken);
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
using EllieHub.ViewModels.Abstractions;
|
||||
using EllieHub.Views.Windows;
|
||||
using EllieHub.Features.Abstractions;
|
||||
using EllieHub.Features.AppConfig.Views.Windows;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace EllieHub.ViewModels.Windows;
|
||||
namespace EllieHub.Features.AppConfig.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// View-model for the about me dialog window.
|
|
@ -2,15 +2,18 @@ using Avalonia.Controls;
|
|||
using Avalonia.Styling;
|
||||
using MsBox.Avalonia.Enums;
|
||||
using EllieHub.Enums;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using EllieHub.ViewModels.Abstractions;
|
||||
using EllieHub.ViewModels.Windows;
|
||||
using EllieHub.Views.Controls;
|
||||
using EllieHub.Views.Windows;
|
||||
using EllieHub.Features.Abstractions;
|
||||
using EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
using EllieHub.Features.AppConfig.Views.Controls;
|
||||
using EllieHub.Features.AppConfig.Views.Windows;
|
||||
using EllieHub.Features.AppWindow.ViewModels;
|
||||
using EllieHub.Features.AppWindow.Views.Windows;
|
||||
using EllieHub.Features.Common.Services.Abstractions;
|
||||
using EllieHub.Features.Common.ViewModels;
|
||||
using ReactiveUI;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace EllieHub.ViewModels.Controls;
|
||||
namespace EllieHub.Features.AppConfig.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// The view-model for the application's settings.
|
||||
|
@ -25,6 +28,7 @@ public class ConfigViewModel : ViewModelBase<ConfigView>
|
|||
private readonly IAppConfigManager _appConfigManager;
|
||||
private readonly AboutMeViewModel _aboutMeViewModel;
|
||||
private readonly AppView _mainWindow;
|
||||
private readonly LateralBarViewModel _lateralBarViewModel;
|
||||
private double _maxLogSize;
|
||||
private int _selectedThemeIndex;
|
||||
|
||||
|
@ -95,6 +99,7 @@ public class ConfigViewModel : ViewModelBase<ConfigView>
|
|||
_maxLogSize = _appConfigManager.AppConfig.LogMaxSizeMb;
|
||||
_selectedThemeIndex = (int)_appConfigManager.AppConfig.Theme;
|
||||
_aboutMeViewModel = aboutMeViewModel;
|
||||
_lateralBarViewModel = mainWindow.ViewModel!.LateralBarInstance;
|
||||
|
||||
BotsUriBar = botsUriBar;
|
||||
BotsUriBar.CurrentUri = appConfigManager.AppConfig.BotsDirectoryUri;
|
||||
|
@ -148,7 +153,7 @@ public class ConfigViewModel : ViewModelBase<ConfigView>
|
|||
if (MaxLogSize is 0.0 && spinDirection is SpinDirection.Decrease)
|
||||
return;
|
||||
|
||||
MaxLogSize = (spinDirection is SpinDirection.Increase)
|
||||
MaxLogSize = spinDirection is SpinDirection.Increase
|
||||
? Math.Round(Math.Max(0.0, MaxLogSize + 0.1), 2)
|
||||
: Math.Round(Math.Max(0.0, MaxLogSize - 0.1), 2);
|
||||
|
||||
|
@ -196,11 +201,12 @@ public class ConfigViewModel : ViewModelBase<ConfigView>
|
|||
dependencyButton.Click += async (buttonViewModel, _) => await HandleDependencyAsync(buttonViewModel, dependencyResolver);
|
||||
|
||||
var canUpdate = await dependencyResolver.CanUpdateAsync();
|
||||
dependencyButton.Status = (canUpdate is null)
|
||||
? DependencyStatus.Install
|
||||
: (canUpdate is true)
|
||||
? DependencyStatus.Update
|
||||
: DependencyStatus.Installed;
|
||||
dependencyButton.Status = canUpdate switch
|
||||
{
|
||||
true => DependencyStatus.Update,
|
||||
false => DependencyStatus.Installed,
|
||||
null => DependencyStatus.Install
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -213,6 +219,7 @@ public class ConfigViewModel : ViewModelBase<ConfigView>
|
|||
/// </exception>
|
||||
private async ValueTask HandleDependencyAsync(DependencyButtonViewModel buttonViewModel, IDependencyResolver dependencyResolver)
|
||||
{
|
||||
_lateralBarViewModel.ToggleEnable(false);
|
||||
var originalStatus = buttonViewModel.Status;
|
||||
buttonViewModel.Status = DependencyStatus.Updating;
|
||||
|
||||
|
@ -234,5 +241,9 @@ public class ConfigViewModel : ViewModelBase<ConfigView>
|
|||
await _mainWindow.ShowDialogWindowAsync($"An error occurred while updating {dependencyResolver.DependencyName}:\n{ex.Message}", DialogType.Error, Icon.Error);
|
||||
buttonViewModel.Status = originalStatus;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lateralBarViewModel.ToggleEnable(true);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,14 +2,14 @@
|
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:EllieHub.ViewModels.Controls"
|
||||
xmlns:views="using:EllieHub.Views.Controls"
|
||||
xmlns:vm="using:EllieHub.Features.AppConfig.ViewModels"
|
||||
xmlns:views="using:EllieHub.Features.Common.Views.Controls"
|
||||
xmlns:const="using:EllieHub.Common"
|
||||
xmlns:dd="using:EllieHub.DesignData.Controls"
|
||||
xmlns:dd="using:EllieHub.Avalonia.DesignData.Controls"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="{x:Static const:WindowConstants.MinWindowWidth}"
|
||||
d:DesignHeight="{x:Static const:WindowConstants.DefaultWindowHeight}"
|
||||
x:Class="EllieHub.Views.Controls.ConfigView"
|
||||
x:Class="EllieHub.Features.AppConfig.Views.Controls.ConfigView"
|
||||
x:DataType="vm:ConfigViewModel">
|
||||
|
||||
<Design.DataContext>
|
||||
|
@ -23,7 +23,6 @@
|
|||
Fill="{DynamicResource HeavyBackground}" />
|
||||
|
||||
<Rectangle Grid.Row="1"
|
||||
Grid.RowSpan="2"
|
||||
Fill="{DynamicResource MediumBackground}"/>
|
||||
|
||||
<!--Settings Title-->
|
||||
|
@ -39,7 +38,7 @@
|
|||
<TextBlock Text="Dependencies"
|
||||
FontSize="22"/>
|
||||
|
||||
<TextBlock Text="Make sure the dependencies below are installed if you want to use Ellie to play music."/>
|
||||
<TextBlock Text="Make sure the dependencies below are installed if you want to use EllieBot to play music."/>
|
||||
|
||||
<!--Dependency Buttons-->
|
||||
<ItemsRepeater ItemsSource="{Binding DependencyButtons}">
|
||||
|
@ -87,9 +86,10 @@
|
|||
|
||||
<!--Backup Directory Bar-->
|
||||
<TextBlock Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Margin="0 8 0 0"
|
||||
Text="Backup Directory:"
|
||||
ToolTip.Tip="Defines the directory the updater is going to store the backup of the bot instances."/>
|
||||
ToolTip.Tip="Defines the directory the updater is going to store the backups of the bot instances."/>
|
||||
|
||||
<TextBox Grid.Row="3"
|
||||
Grid.Column="0"
|
||||
|
@ -113,6 +113,7 @@
|
|||
|
||||
<!--Logs directory Bar-->
|
||||
<TextBlock Grid.Row="4"
|
||||
Grid.Column="0"
|
||||
Margin="0 8 0 0"
|
||||
Text="Logs Directory:"
|
||||
ToolTip.Tip="Defines the directory the updater is going to store the logs of the bot instances."/>
|
|
@ -1,8 +1,8 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.ReactiveUI;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
using EllieHub.Features.AppConfig.ViewModels;
|
||||
|
||||
namespace EllieHub.Views.Controls;
|
||||
namespace EllieHub.Features.AppConfig.Views.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// The view for the application's settings.
|
|
@ -2,12 +2,11 @@
|
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:EllieHub.ViewModels.Windows"
|
||||
xmlns:const="using:EllieHub.Common"
|
||||
xmlns:dd="using:EllieHub.DesignData.Windows"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="325"
|
||||
Width="400" Height="325"
|
||||
x:Class="EllieHub.Views.Windows.AboutMeView"
|
||||
xmlns:vm="using:EllieHub.Features.AppConfig.ViewModels"
|
||||
xmlns:dd="using:EllieHub.Avalonia.DesignData.Windows"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="305"
|
||||
Width="400" Height="305"
|
||||
x:Class="EllieHub.Features.AppConfig.Views.Windows.AboutMeView"
|
||||
x:DataType="vm:AboutMeViewModel"
|
||||
Title="About EllieHub"
|
||||
Icon="{DynamicResource EllieHubIcon}"
|
||||
|
@ -18,12 +17,12 @@
|
|||
</Design.DataContext>
|
||||
|
||||
<StackPanel Margin="10 20 10 20">
|
||||
<TextBlock Text="Ellie is a general purpose open-source Discord bot created by Toastie. Support the project!"
|
||||
<TextBlock Text="EllieBot is a general purpose open-source Discord bot created by Toastie. Support the project!"
|
||||
TextAlignment="Center"
|
||||
Margin="0 0 0 10"/>
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<Button CommandParameter="https://www.paypal.com/paypalme/toastiet0ast"
|
||||
<Button CommandParameter="https://paypal.me/toastiet0ast"
|
||||
Command="{Binding OpenUrl}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="Paypal " />
|
||||
|
@ -42,7 +41,7 @@
|
|||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button CommandParameter="https://www.patreon.com/emotionchild"
|
||||
<Button CommandParameter="https://www.patreon.com/toastiet0ast"
|
||||
Command="{Binding OpenUrl}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="Patreon " />
|
||||
|
@ -55,33 +54,41 @@
|
|||
<Separator Margin="20"
|
||||
HorizontalAlignment="Stretch" />
|
||||
|
||||
<TextBlock Text="This tool was made by Toastie. If it has been useful to you, consider showing your support by buying me a coffee."
|
||||
<TextBlock Text="This tool was made by Toastie. If it has been useful to you, consider showing your support."
|
||||
TextAlignment="Center"
|
||||
Margin="0 0 0 10" />
|
||||
|
||||
<Button HorizontalAlignment="Center"
|
||||
CommandParameter="https://ko-fi.com/toastie_t0ast"
|
||||
Command="{Binding OpenUrl}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="Buy me a Ko-fi " />
|
||||
<Image Classes="icon-url"
|
||||
Source="{DynamicResource UrlIcon}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<Button Margin="0 0 5 0"
|
||||
HorizontalAlignment="Center"
|
||||
CommandParameter="https://ko-fi.com/toastie_t0ast"
|
||||
Command="{Binding OpenUrl}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="Buy me a Ko-fi " />
|
||||
<Image Classes="icon-url"
|
||||
Source="{DynamicResource UrlIcon}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Margin="5 0 0 0"
|
||||
HorizontalAlignment="Center"
|
||||
CommandParameter="https://toastielab.dev/EllieBotDevs/EllieHub/issues/new"
|
||||
Command="{Binding OpenUrl}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="Report a bug " />
|
||||
<Image Classes="icon-url"
|
||||
Source="{DynamicResource UrlIcon}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="© 2023 Toastie"
|
||||
<TextBlock Text="© 2025 Toastie_t0ast"
|
||||
FontSize="11"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0 30 0 0"/>
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Center">
|
||||
<TextBlock Text="License: "
|
||||
FontSize="11" />
|
||||
<Button Classes="link-11"
|
||||
FontSize="11"
|
||||
Content="Apache License Version 2"
|
||||
CommandParameter="https://toastielab.dev/ToastieSharp/EllieHub/src/branch/main/LICENSE"
|
||||
Command="{Binding OpenUrl}" />
|
||||
</StackPanel>
|
||||
<HyperlinkButton FontSize="11"
|
||||
HorizontalAlignment="Center"
|
||||
Content="GNU General Public License Version 3"
|
||||
CommandParameter="https://toastielab.dev/EllieBotDevs/EllieHub/src/branch/main/LICENSE"
|
||||
Command="{Binding OpenUrl}" />
|
||||
</StackPanel>
|
||||
</Window>
|
||||
</Window>
|
|
@ -1,7 +1,7 @@
|
|||
using Avalonia.ReactiveUI;
|
||||
using EllieHub.ViewModels.Windows;
|
||||
using EllieHub.Features.AppConfig.ViewModels;
|
||||
|
||||
namespace EllieHub.Views.Windows;
|
||||
namespace EllieHub.Features.AppConfig.Views.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the about me dialog window.
|
|
@ -1,6 +1,6 @@
|
|||
using EllieHub.ViewModels.Controls;
|
||||
using EllieHub.Features.AppWindow.ViewModels;
|
||||
|
||||
namespace EllieHub.Models;
|
||||
namespace EllieHub.Features.AppWindow.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a bot entry in the <see cref="LateralBarViewModel"/>.
|
|
@ -1,4 +1,4 @@
|
|||
namespace EllieHub.Models;
|
||||
namespace EllieHub.Features.AppWindow.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the information of a bot instance.
|
11
EllieHub/Features/AppWindow/Models/OldUpdaterBotEntry.cs
Normal file
11
EllieHub/Features/AppWindow/Models/OldUpdaterBotEntry.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
namespace EllieHub.Features.AppWindow.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a bot entry from EllieUpdater.
|
||||
/// </summary>
|
||||
/// <param name="Guid">The Id of the bot.</param>
|
||||
/// <param name="Name">The name of the bot.</param>
|
||||
/// <param name="IconUri">The path to the avatar image.</param>
|
||||
/// <param name="Version">The version of the bot.</param>
|
||||
/// <param name="PathUri">The path to the bot files.</param>
|
||||
internal sealed record OldUpdaterBotEntry(Guid Guid, string Name, string? IconUri, string? Version, string? PathUri);
|
|
@ -1,10 +1,10 @@
|
|||
using EllieHub.ViewModels.Abstractions;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
using EllieHub.Views.Controls;
|
||||
using EllieHub.Views.Windows;
|
||||
using EllieHub.Features.Abstractions;
|
||||
using EllieHub.Features.AppWindow.Views.Controls;
|
||||
using EllieHub.Features.AppWindow.Views.Windows;
|
||||
using EllieHub.Features.Home.ViewModels;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace EllieHub.ViewModels.Windows;
|
||||
namespace EllieHub.Features.AppWindow.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// View-model for the main window.
|
|
@ -1,25 +1,35 @@
|
|||
using Avalonia.Controls;
|
||||
using EllieHub.Models;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using EllieHub.ViewModels.Abstractions;
|
||||
using EllieHub.Views.Controls;
|
||||
using EllieHub.Features.Abstractions;
|
||||
using EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
using EllieHub.Features.AppWindow.Models;
|
||||
using EllieHub.Features.AppWindow.Views.Controls;
|
||||
using ReactiveUI;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Reactive.Disposables;
|
||||
|
||||
namespace EllieHub.ViewModels.Controls;
|
||||
namespace EllieHub.Features.AppWindow.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// View-model for <see cref="LateralBarView"/>, the lateral bar with home, bot, and configuration buttons.
|
||||
/// </summary>
|
||||
public class LateralBarViewModel : ViewModelBase<LateralBarView>
|
||||
{
|
||||
private bool _isLateralBarEnabled = true;
|
||||
private readonly IAppConfigManager _appConfigManager;
|
||||
|
||||
/// <summary>
|
||||
/// Collection of buttons for bot instances.
|
||||
/// </summary>
|
||||
public ObservableCollection<Button> BotButtonList { get; } = new();
|
||||
public ObservableCollection<Button> BotButtonList { get; } = [];
|
||||
|
||||
private readonly IAppConfigManager _appConfigManager;
|
||||
/// <summary>
|
||||
/// Determines whether the buttons on the lateral bar are enabled or not.
|
||||
/// </summary>
|
||||
public bool IsLateralBarEnabled
|
||||
{
|
||||
get => _isLateralBarEnabled;
|
||||
private set => this.RaiseAndSetIfChanged(ref _isLateralBarEnabled, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the view-model for the <see cref="LateralBarView"/>.
|
||||
|
@ -67,11 +77,23 @@ public class LateralBarViewModel : ViewModelBase<LateralBarView>
|
|||
this.RaisePropertyChanged(nameof(BotButtonList));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables the buttons on the lateral bar.
|
||||
/// </summary>
|
||||
/// <param name="enable"><see langword="true"/> to enable the buttons, <see langword="false"/> otherwise.</param>
|
||||
public void ToggleEnable(bool enable)
|
||||
{
|
||||
IsLateralBarEnabled = enable;
|
||||
|
||||
foreach (var button in BotButtonList)
|
||||
button.IsEnabled = enable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the bot buttons to the lateral bar.
|
||||
/// </summary>
|
||||
/// <param name="botEntires">The bot entries.</param>
|
||||
private void ReloadBotButtons(IReadOnlyDictionary<Guid, BotInstanceInfo> botEntires)
|
||||
public void ReloadBotButtons(IReadOnlyDictionary<Guid, BotInstanceInfo> botEntires)
|
||||
{
|
||||
BotButtonList.Clear();
|
||||
|
|
@ -3,11 +3,10 @@
|
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:siv="https://github.com/kekyo/SkiaImageView"
|
||||
xmlns:vm="using:EllieHub.ViewModels.Controls"
|
||||
xmlns:dd="using:EllieHub.DesignData.Controls"
|
||||
xmlns:const="using:EllieHub.Common"
|
||||
xmlns:vm="using:EllieHub.Features.AppWindow.ViewModels"
|
||||
xmlns:dd="using:EllieHub.Avalonia.DesignData.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="70" d:DesignHeight="450"
|
||||
x:Class="EllieHub.Views.Controls.LateralBarView"
|
||||
x:Class="EllieHub.Features.AppWindow.Views.Controls.LateralBarView"
|
||||
x:DataType="vm:LateralBarViewModel">
|
||||
|
||||
<Design.DataContext>
|
||||
|
@ -20,21 +19,23 @@
|
|||
<Button Classes="transparent"
|
||||
Name="HomeButton"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center">
|
||||
HorizontalContentAlignment="Center"
|
||||
IsEnabled="{Binding IsLateralBarEnabled}">
|
||||
<Image Classes="icon"
|
||||
Source="{DynamicResource HomeIcon}"/>
|
||||
</Button>
|
||||
|
||||
|
||||
<!--Visual separator-->
|
||||
<Separator HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center" />
|
||||
|
||||
|
||||
<!--Button that adds new bot buttons-->
|
||||
<Border Classes="circular">
|
||||
<Button Classes="accent"
|
||||
Content="+"
|
||||
Cursor="Hand"
|
||||
Command="{Binding AddBotButtonAsync}"/>
|
||||
Command="{Binding AddBotButtonAsync}"
|
||||
IsEnabled="{Binding IsLateralBarEnabled}"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
|
@ -58,7 +59,7 @@
|
|||
MinHeight="52"
|
||||
MinWidth="52"/>
|
||||
</Border>
|
||||
|
||||
|
||||
<Button Opacity="0"
|
||||
Cursor="Hand"
|
||||
HorizontalAlignment="Stretch"
|
||||
|
@ -71,13 +72,14 @@
|
|||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
</ScrollViewer>
|
||||
|
||||
|
||||
<!--Configurations button-->
|
||||
<Button Grid.Row="2"
|
||||
Classes="transparent"
|
||||
Name="ConfigButton"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center">
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{Binding IsLateralBarEnabled}">
|
||||
<Image MaxHeight="17"
|
||||
Source="{DynamicResource ConfigIcon}"/>
|
||||
</Button>
|
|
@ -5,24 +5,22 @@ 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 EllieHub.Avalonia.DesignData.Common;
|
||||
using EllieHub.Features.AppConfig.Models;
|
||||
using EllieHub.Features.AppConfig.Services.Mocks;
|
||||
using EllieHub.Features.AppWindow.ViewModels;
|
||||
using EllieHub.Features.BotConfig.Models;
|
||||
using SkiaImageView;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace EllieHub.Views.Controls;
|
||||
namespace EllieHub.Features.AppWindow.Views.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// View for the lateral bar with home, bot, and configuration buttons.
|
||||
/// </summary>
|
||||
public partial class LateralBarView : ReactiveUserControl<LateralBarViewModel>
|
||||
{
|
||||
private static readonly Cursor _pointingHandCursor = new(StandardCursorType.Hand);
|
||||
private static readonly Cursor _arrow = new(StandardCursorType.Arrow);
|
||||
private readonly ReadOnlyAppConfig _appConfig;
|
||||
private readonly ReadOnlyAppSettings _appConfig;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the user clicks a bot button.
|
||||
|
@ -41,7 +39,7 @@ public partial class LateralBarView : ReactiveUserControl<LateralBarViewModel>
|
|||
/// Creates the lateral bar of the application.
|
||||
/// </summary>
|
||||
/// <param name="appConfig">The application settings.</param>
|
||||
public LateralBarView(ReadOnlyAppConfig appConfig)
|
||||
public LateralBarView(ReadOnlyAppSettings appConfig)
|
||||
{
|
||||
_appConfig = appConfig;
|
||||
InitializeComponent();
|
||||
|
@ -69,10 +67,10 @@ public partial class LateralBarView : ReactiveUserControl<LateralBarViewModel>
|
|||
/// <exception cref="InvalidOperationException">Occurs when the visual tree has an unexpected structure.</exception>
|
||||
public void ApplyBotButtonBorder(Button button)
|
||||
{
|
||||
if (!Utilities.TryCastTo<Border>(button.Parent?.Parent, out var border))
|
||||
if (button.Parent?.Parent is not Border border)
|
||||
throw new InvalidOperationException("Visual tree has an unexpected structure.");
|
||||
|
||||
if (!Utilities.TryCastTo<ImmutableSolidColorBrush>(this.FindResource(base.ActualThemeVariant, "BotSelectionColor"), out var resourceColor))
|
||||
if (this.FindResource(base.ActualThemeVariant, "BotSelectionColor") is not ImmutableSolidColorBrush resourceColor)
|
||||
return;
|
||||
|
||||
border.BorderBrush = resourceColor;
|
||||
|
@ -93,7 +91,11 @@ public partial class LateralBarView : ReactiveUserControl<LateralBarViewModel>
|
|||
/// <param name="sender">The bot button that was clicked.</param>
|
||||
/// <param name="eventArgs">The event arguments.</param>
|
||||
private void LoadBotViewModel(object sender, RoutedEventArgs eventArgs)
|
||||
=> BotButtonClick?.Invoke((Button)sender, eventArgs);
|
||||
{
|
||||
// "sender", for some reason, is not one of the buttons stored in the lateral bar's view-model.
|
||||
if (sender is Button button && this.ViewModel!.BotButtonList.First(x => x.Content == button.Content).IsEnabled)
|
||||
BotButtonClick?.Invoke(button, eventArgs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the bot avatar when the buttons on the lateral bar are rendered.
|
||||
|
@ -104,10 +106,11 @@ public partial class LateralBarView : ReactiveUserControl<LateralBarViewModel>
|
|||
/// <exception cref="InvalidOperationException">Occurs when the visual tree has an unexpected structure.</exception>
|
||||
private void OnBotButtonLoad(object? sender, VisualTreeAttachmentEventArgs eventArgs)
|
||||
{
|
||||
if (!Utilities.TryCastTo<Panel>(sender, out var panel)
|
||||
|| !Utilities.TryCastTo<SKImageView>(((Border)panel.Children[0]).Child, out var botAvatar)
|
||||
|| !Utilities.TryCastTo<Button>(panel.Children[1], out var button)
|
||||
|| !Utilities.TryCastTo<Guid>(button.Content, out var botId))
|
||||
if (sender is not Panel panel
|
||||
|| panel.Children[0] is not Border border
|
||||
|| border.Child is not SKImageView botAvatar
|
||||
|| panel.Children[1] is not Button button
|
||||
|| button.Content is not Guid botId)
|
||||
throw new InvalidOperationException("Visual tree has an unexpected structure.");
|
||||
|
||||
// Set the avatar
|
||||
|
@ -128,7 +131,7 @@ public partial class LateralBarView : ReactiveUserControl<LateralBarViewModel>
|
|||
/// <exception cref="InvalidOperationException">Occurs when <paramref name="sender"/> is not a <see cref="Button"/>.</exception>
|
||||
private void DownsizeBotAvatar(object? sender, PointerPressedEventArgs eventArgs)
|
||||
{
|
||||
if (!Utilities.TryCastTo<Button>(sender, out var button))
|
||||
if (sender is not Button button)
|
||||
throw new InvalidOperationException($"Sender is not a {nameof(Button)}.");
|
||||
|
||||
var botAvatar = FindAvatarComponent(button);
|
||||
|
@ -145,7 +148,7 @@ public partial class LateralBarView : ReactiveUserControl<LateralBarViewModel>
|
|||
/// <exception cref="InvalidOperationException">Occurs when <paramref name="sender"/> is not a <see cref="Button"/>.</exception>
|
||||
private void UpsizeBotAvatar(object? sender, PointerReleasedEventArgs eventArgs)
|
||||
{
|
||||
if (!Utilities.TryCastTo<Button>(sender, out var button))
|
||||
if (sender is not Button button)
|
||||
throw new InvalidOperationException($"Sender is not a {nameof(Button)}.");
|
||||
|
||||
var botAvatar = FindAvatarComponent(button);
|
||||
|
@ -163,7 +166,7 @@ public partial class LateralBarView : ReactiveUserControl<LateralBarViewModel>
|
|||
/// <exception cref="InvalidOperationException">Occurs when the component's content is not a Guid.</exception>
|
||||
private SKImageView FindAvatarComponent<T>(T component) where T : ContentControl
|
||||
{
|
||||
return (!Utilities.TryCastTo<Guid>(component.Content, out var botId))
|
||||
return (component.Content is not Guid botId)
|
||||
? throw new InvalidOperationException($"{nameof(T)} does not contain a bot Id.")
|
||||
: FindAvatarComponent(botId);
|
||||
}
|
|
@ -2,13 +2,13 @@
|
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:EllieHub.ViewModels.Windows"
|
||||
xmlns:vm="using:EllieHub.Features.AppWindow.ViewModels"
|
||||
xmlns:const="using:EllieHub.Common"
|
||||
xmlns:dd="using:EllieHub.DesignData.Windows"
|
||||
xmlns:dd="using:EllieHub.Avalonia.DesignData.Windows"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="{x:Static const:WindowConstants.MinWindowWidth}"
|
||||
d:DesignHeight="{x:Static const:WindowConstants.MinWindowHeight}"
|
||||
x:Class="EllieHub.Views.Windows.AppView"
|
||||
x:Class="EllieHub.Features.AppWindow.Views.Windows.AppView"
|
||||
x:DataType="vm:AppViewModel"
|
||||
Icon="{DynamicResource EllieHubIcon}"
|
||||
WindowStartupLocation="CenterScreen"
|
|
@ -1,19 +1,29 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.ReactiveUI;
|
||||
using Avalonia.Styling;
|
||||
using Toastie.DependencyInjection.Extensions;
|
||||
using Toastie.Utilities;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using EllieHub.DesignData.Common;
|
||||
using EllieHub.Avalonia.DesignData.Common;
|
||||
using EllieHub.Enums;
|
||||
using EllieHub.Services;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using EllieHub.ViewModels.Abstractions;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
using EllieHub.ViewModels.Windows;
|
||||
using EllieHub.Views.Controls;
|
||||
using EllieHub.Features.Abstractions;
|
||||
using EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
using EllieHub.Features.AppConfig.ViewModels;
|
||||
using EllieHub.Features.AppWindow.Models;
|
||||
using EllieHub.Features.AppWindow.ViewModels;
|
||||
using EllieHub.Features.AppWindow.Views.Controls;
|
||||
using EllieHub.Features.BotConfig.Services.Abstractions;
|
||||
using EllieHub.Features.BotConfig.ViewModels;
|
||||
using EllieHub.Features.Home.Services.Abstractions;
|
||||
using EllieHub.Features.Home.ViewModels;
|
||||
using EllieHub.Features.Home.Views.Windows;
|
||||
using EllieHub.Features.BotConfig.Services;
|
||||
using ReactiveUI;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace EllieHub.Views.Windows;
|
||||
namespace EllieHub.Features.AppWindow.Views.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the main window of the application.
|
||||
|
@ -103,19 +113,19 @@ public partial class AppView : ReactiveWindow<AppViewModel>
|
|||
base.OnResized(eventArgs);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <inheritdoc />
|
||||
/// <exception cref="UnreachableException">Occurs when <see cref="ThemeType"/> has an unimplemented value.</exception>
|
||||
protected override void OnOpened(EventArgs eventArgs)
|
||||
{
|
||||
// Ensure that bots on Unix system have access to the dependencies.
|
||||
if (Environment.OSVersion.Platform is PlatformID.Unix)
|
||||
Utilities.AddPathToPATHEnvar(AppStatics.AppDepsUri);
|
||||
ToastieUtilities.AddPathToPATHEnvar(AppStatics.AppDepsUri);
|
||||
|
||||
// Set the window size from the last session
|
||||
base.Height = _appConfigManager.AppConfig.WindowSize.Height;
|
||||
base.Width = _appConfigManager.AppConfig.WindowSize.Width;
|
||||
|
||||
// Set the user prefered theme
|
||||
// Set the user preferred theme
|
||||
base.RequestedThemeVariant = _appConfigManager.AppConfig.Theme switch
|
||||
{
|
||||
ThemeType.Auto => ThemeVariant.Default,
|
||||
|
@ -127,6 +137,10 @@ public partial class AppView : ReactiveWindow<AppViewModel>
|
|||
// Update the application, if one is available
|
||||
_ = UpdateAndCloseAsync();
|
||||
|
||||
// Import bots from the old updater, if available
|
||||
if (OperatingSystem.IsWindows())
|
||||
_ = MigrateOldBotsAsync();
|
||||
|
||||
base.OnOpened(eventArgs);
|
||||
}
|
||||
|
||||
|
@ -146,11 +160,20 @@ public partial class AppView : ReactiveWindow<AppViewModel>
|
|||
/// <inheritdoc/>
|
||||
protected override async void OnClosed(EventArgs eventArgs)
|
||||
{
|
||||
// When the updater is closed, kill all bots and write their logs.
|
||||
_botOrchestrator.StopAll();
|
||||
await _logWriter.FlushAllAsync(true);
|
||||
|
||||
base.OnClosed(eventArgs);
|
||||
try
|
||||
{
|
||||
// When the updater is closed, kill all bots and write their logs.
|
||||
_botOrchestrator.StopAllBots();
|
||||
await _logWriter.FlushAllAsync(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
base.OnClosed(eventArgs);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -242,12 +265,40 @@ public partial class AppView : ReactiveWindow<AppViewModel>
|
|||
|
||||
_ = new UpdateView().ShowDialog(this);
|
||||
|
||||
await _appResolver.InstallOrUpdateAsync(AppContext.BaseDirectory);
|
||||
_appResolver.LaunchNewVersion();
|
||||
try
|
||||
{
|
||||
await _appResolver.InstallOrUpdateAsync(AppContext.BaseDirectory);
|
||||
_appResolver.LaunchNewVersion();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
|
||||
base.Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Migrates bots created by the EllieUpdater when EllieHub is run for the first time.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
private async Task MigrateOldBotsAsync()
|
||||
{
|
||||
var configFileUri = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "EllieBotUpdater", "bots.json");
|
||||
|
||||
if (File.Exists(AppStatics.AppConfigUri) || !File.Exists(configFileUri))
|
||||
return;
|
||||
|
||||
var bots = (JsonSerializer.Deserialize<OldUpdaterBotEntry[]>(await File.ReadAllTextAsync(configFileUri)) ?? [])
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.PathUri) && File.Exists(Path.Join(x.PathUri, "EllieBot.exe")))
|
||||
.Select((x, y) => new BotEntry(x.Guid, new(x.Name, x.PathUri!, (uint)y, x.Version, x.IconUri)));
|
||||
|
||||
foreach (var botEntry in bots)
|
||||
await _appConfigManager.UpdateConfigAsync(x => x.BotEntries.TryAdd(botEntry.Id, botEntry.BotInfo));
|
||||
|
||||
_lateralBarView.ViewModel?.ReloadBotButtons(_appConfigManager.AppConfig.BotEntries);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="BotConfigViewModel"/> from the <paramref name="scopeFactory"/> and initializes
|
||||
/// its properties with user data.
|
||||
|
@ -260,7 +311,7 @@ public partial class AppView : ReactiveWindow<AppViewModel>
|
|||
private static BotConfigViewModel GetBotConfigViewModel(Button button, IServiceScopeFactory scopeFactory)
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var botId = (Guid)(button.Content ?? throw new InvalidOperationException("Bot button has no valid Id."));
|
||||
var botId = button.Content ?? throw new InvalidOperationException("Bot button has no valid Id.");
|
||||
var botResolver = scope.ServiceProvider.GetParameterizedService<EllieResolver>(botId);
|
||||
|
||||
return scope.ServiceProvider.GetParameterizedService<BotConfigViewModel>(botResolver);
|
13
EllieHub/Features/BotConfig/Models/Api/Toastielab/Assets.cs
Normal file
13
EllieHub/Features/BotConfig/Models/Api/Toastielab/Assets.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EllieHub.Features.BotConfig.Models.Api.Toastielab;
|
||||
|
||||
/// <summary>
|
||||
/// The assets of a Toastielab release.
|
||||
/// </summary>
|
||||
/// <param name="Name">The name of the release file.</param>
|
||||
/// <param name="Url">The url to the release file.</param>
|
||||
public sealed record Assets(
|
||||
[property: JsonPropertyName("name")] string Name,
|
||||
[property: JsonPropertyName("browser_download_url")] string Url
|
||||
);
|
|
@ -0,0 +1,13 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EllieHub.Features.BotConfig.Models.Api.Toastielab;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a release from the Toastielab API.
|
||||
/// </summary>
|
||||
/// <param name="Tag">The tag of the release.</param>
|
||||
/// <param name="Assets">The assets of the release.</param>
|
||||
public sealed record ToastielabRelease(
|
||||
[property: JsonPropertyName("tag_name")] string Tag,
|
||||
[property: JsonPropertyName("assets")] Assets[] Assets
|
||||
);
|
|
@ -1,6 +1,6 @@
|
|||
using SkiaSharp;
|
||||
|
||||
namespace EllieHub.Models.EventArguments;
|
||||
namespace EllieHub.Features.BotConfig.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the event arguments for when the user sets a new avatar for a bot instance.
|
|
@ -1,4 +1,4 @@
|
|||
namespace EllieHub.Models.EventArguments;
|
||||
namespace EllieHub.Features.BotConfig.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the event arguments when a bot process exits.
|
||||
|
@ -15,14 +15,21 @@ public sealed class BotExitEventArgs : EventArgs
|
|||
/// </summary>
|
||||
public int ExitCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The exit message.
|
||||
/// </summary>
|
||||
public string Message { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates the event arguments when a bot process exits.
|
||||
/// </summary>
|
||||
/// <param name="botId">The bot's Id.</param>
|
||||
/// <param name="exitCode">The exit code.</param>
|
||||
public BotExitEventArgs(Guid botId, int exitCode)
|
||||
/// <param name="message">The message for the bot process that just exited.</param>
|
||||
public BotExitEventArgs(Guid botId, int exitCode, string message)
|
||||
{
|
||||
Id = botId;
|
||||
ExitCode = exitCode;
|
||||
Message = message;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
namespace EllieHub.Models.EventArguments;
|
||||
namespace EllieHub.Features.BotConfig.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the event arguments when a log is written to disk.
|
|
@ -1,4 +1,4 @@
|
|||
namespace EllieHub.Models.EventArguments;
|
||||
namespace EllieHub.Features.BotConfig.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the event arguments when a bot process writes to stdout or stderr.
|
|
@ -1,9 +1,9 @@
|
|||
using EllieHub.Models.EventArguments;
|
||||
using EllieHub.Features.BotConfig.Models;
|
||||
|
||||
namespace EllieHub.Services.Abstractions;
|
||||
namespace EllieHub.Features.BotConfig.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an object that coordinates multiple running processes of Ellie.
|
||||
/// Represents an object that coordinates multiple running processes of EllieBot.
|
||||
/// </summary>
|
||||
public interface IBotOrchestrator
|
||||
{
|
||||
|
@ -34,18 +34,18 @@ public interface IBotOrchestrator
|
|||
/// </summary>
|
||||
/// <param name="botId">The bot's Id.</param>
|
||||
/// <returns><see langword="true"/> if the bot successfully started, <see langword="false"/> otherwise.</returns>
|
||||
bool Start(Guid botId);
|
||||
bool StartBot(Guid botId);
|
||||
|
||||
/// <summary>
|
||||
/// Stops the bot with the specified <paramref name="botId"/>.
|
||||
/// </summary>
|
||||
/// <param name="botId">The bot's Id.</param>
|
||||
/// <returns><see langword="true"/> if the bot successfully stopped, <see langword="false"/> otherwise.</returns>
|
||||
bool Stop(Guid botId);
|
||||
bool StopBot(Guid botId);
|
||||
|
||||
/// <summary>
|
||||
/// Stops all bot instances.
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> if at least one bot instance was stopped, <see langword="false"/> otherwise.</returns>
|
||||
bool StopAll();
|
||||
bool StopAllBots();
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
namespace EllieHub.Services.Abstractions;
|
||||
using EllieHub.Features.Common.Services.Abstractions;
|
||||
|
||||
namespace EllieHub.Features.BotConfig.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a service that checks, downloads, installs, and updates a bot instance.
|
||||
|
@ -10,6 +12,11 @@ public interface IBotResolver : IDependencyResolver
|
|||
/// </summary>
|
||||
string BotName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Defines whether there is an ongoing update.
|
||||
/// </summary>
|
||||
bool IsUpdateInProgress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The Id of the bot.
|
||||
/// </summary>
|
|
@ -1,7 +1,7 @@
|
|||
using EllieHub.Models.EventArguments;
|
||||
using EllieHub.Features.BotConfig.Models;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace EllieHub.Services.Abstractions;
|
||||
namespace EllieHub.Features.BotConfig.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a service that writes logs of bot instances to the disk.
|
|
@ -1,18 +1,19 @@
|
|||
using EllieHub.Models.Config;
|
||||
using EllieHub.Models.EventArguments;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using EllieHub.Features.AppConfig.Models;
|
||||
using EllieHub.Features.BotConfig.Models;
|
||||
using EllieHub.Features.BotConfig.Services.Abstractions;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace EllieHub.Services;
|
||||
namespace EllieHub.Features.BotConfig.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Defines an object that coordinates multiple running processes of EllieBot.
|
||||
/// </summary>
|
||||
public sealed class EllieOrchestrator : IBotOrchestrator
|
||||
{
|
||||
private readonly ReadOnlyAppSettings _appConfig;
|
||||
private readonly ILogWriter _logWriter;
|
||||
private readonly Dictionary<Guid, Process> _runningBots = new();
|
||||
private readonly ReadOnlyAppConfig _appConfig;
|
||||
private readonly string _fileName = (OperatingSystem.IsWindows()) ? "NadekoBot.exe" : "NadekoBot";
|
||||
private readonly string _fileName = OperatingSystem.IsWindows() ? "EllieBot.exe" : "EllieBot";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<IBotOrchestrator, BotExitEventArgs>? OnBotExit;
|
||||
|
@ -27,24 +28,28 @@ public sealed class EllieOrchestrator : IBotOrchestrator
|
|||
/// Creates an object that coordinates multiple running processes of EllieBot.
|
||||
/// </summary>
|
||||
/// <param name="appConfig">The application settings.</param>
|
||||
public EllieOrchestrator(ReadOnlyAppConfig appConfig)
|
||||
=> _appConfig = appConfig;
|
||||
/// <param name="logWriter">The service that writes bot logs to disk.</param>
|
||||
public EllieOrchestrator(ReadOnlyAppSettings appConfig, ILogWriter logWriter)
|
||||
{
|
||||
_appConfig = appConfig;
|
||||
_logWriter = logWriter;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsBotRunning(Guid botId)
|
||||
=> _runningBots.ContainsKey(botId);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Start(Guid botId)
|
||||
public bool StartBot(Guid botId)
|
||||
{
|
||||
if (_runningBots.ContainsKey(botId)
|
||||
|| !_appConfig.BotEntries.TryGetValue(botId, out var botEntry)
|
||||
|| !File.Exists(Path.Combine(botEntry.InstanceDirectoryUri, _fileName)))
|
||||
|| !File.Exists(Path.Join(botEntry.InstanceDirectoryUri, _fileName)))
|
||||
return false;
|
||||
|
||||
var botProcess = Process.Start(new ProcessStartInfo()
|
||||
{
|
||||
FileName = Path.Combine(botEntry.InstanceDirectoryUri, _fileName),
|
||||
FileName = Path.Join(botEntry.InstanceDirectoryUri, _fileName),
|
||||
WorkingDirectory = botEntry.InstanceDirectoryUri,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
|
@ -66,7 +71,7 @@ public sealed class EllieOrchestrator : IBotOrchestrator
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Stop(Guid botId)
|
||||
public bool StopBot(Guid botId)
|
||||
{
|
||||
if (!_runningBots.TryGetValue(botId, out var botProcess))
|
||||
return false;
|
||||
|
@ -76,10 +81,11 @@ public sealed class EllieOrchestrator : IBotOrchestrator
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool StopAll()
|
||||
public bool StopAllBots()
|
||||
{
|
||||
var amount = _runningBots.Count;
|
||||
|
||||
// ReSharper disable once EmptyGeneralCatchClause
|
||||
foreach (var process in _runningBots.Values)
|
||||
try { process.Kill(true); } catch { }
|
||||
|
||||
|
@ -95,7 +101,12 @@ public sealed class EllieOrchestrator : IBotOrchestrator
|
|||
private void OnExit(object? sender, EventArgs eventArgs)
|
||||
{
|
||||
var (id, process) = _runningBots.First(x => x.Value.Equals(sender));
|
||||
OnBotExit?.Invoke(this, new(id, process.ExitCode));
|
||||
var message = Environment.NewLine
|
||||
+ $"{_appConfig.BotEntries[id].Name} stopped. Status code: {process.ExitCode}"
|
||||
+ Environment.NewLine;
|
||||
|
||||
_logWriter.TryAdd(id, message);
|
||||
OnBotExit?.Invoke(this, new(id, process.ExitCode, message));
|
||||
|
||||
_runningBots.Remove(id);
|
||||
process.CancelOutputRead();
|
||||
|
@ -116,6 +127,7 @@ public sealed class EllieOrchestrator : IBotOrchestrator
|
|||
var (id, _) = _runningBots.First(x => x.Value.Equals(sender));
|
||||
var newEventArgs = new ProcessStdWriteEventArgs(id, eventArgs.Data);
|
||||
|
||||
_logWriter.TryAdd(id, eventArgs.Data);
|
||||
OnStdout?.Invoke(this, newEventArgs);
|
||||
}
|
||||
|
||||
|
@ -132,6 +144,7 @@ public sealed class EllieOrchestrator : IBotOrchestrator
|
|||
var (id, _) = _runningBots.First(x => x.Value.Equals(sender));
|
||||
var newEventArgs = new ProcessStdWriteEventArgs(id, eventArgs.Data);
|
||||
|
||||
_logWriter.TryAdd(id, eventArgs.Data);
|
||||
OnStderr?.Invoke(this, newEventArgs);
|
||||
}
|
||||
}
|
|
@ -1,29 +1,43 @@
|
|||
using EllieHub.Services.Abstractions;
|
||||
using Toastie.Utilities;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
using EllieHub.Features.BotConfig.Models.Api.Toastielab;
|
||||
using EllieHub.Features.BotConfig.Services.Abstractions;
|
||||
using SingleFileExtractor.Core;
|
||||
using System.Formats.Tar;
|
||||
using System.IO.Compression;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace EllieHub.Services;
|
||||
namespace EllieHub.Features.BotConfig.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service that checks, downloads, installs, and updates a NadekoBot instance.
|
||||
/// Service that checks, downloads, installs, and updates a EllieBot instance.
|
||||
/// </summary>
|
||||
/// <remarks>Source: https://gitlab.com/Kwoth/nadekobot/-/releases/permalink/latest</remarks>
|
||||
/// <remarks>Source: https://toastielab.dev/EllieBotDevs/elliebot/releases/latest</remarks>
|
||||
public sealed partial class EllieResolver : IBotResolver
|
||||
{
|
||||
private static readonly HashSet<Guid> _updateIdOngoing = new();
|
||||
private const string _cachedCurrentVersionKey = "currentVersion:EllieBot";
|
||||
private const string _toastielabReleasesEndpointUrl = "https://toastielab.dev/api/v1/repos/EllieBotDevs/elliebot/releases/latest";
|
||||
private const string _toastielabReleasesRepoUrl = "https://toastielab.dev/EllieBotDevs/elliebot/releases/latest";
|
||||
private static readonly HashSet<Guid> _updateIdOngoing = [];
|
||||
private static readonly string _tempDirectory = Path.GetTempPath();
|
||||
private static readonly Regex _unzipedDirRegex = GenerateUnzipedDirRegex();
|
||||
private static readonly Regex _unzippedDirRegex = GenerateUnzipedDirRegex();
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly IAppConfigManager _appConfigManager;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DependencyName { get; } = "NadekoBot";
|
||||
public string DependencyName { get; } = "EllieBot";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string FileName { get; } = (OperatingSystem.IsWindows()) ? "NadekoBot.exe" : "NadekoBot";
|
||||
public string FileName { get; } = (OperatingSystem.IsWindows()) ? "EllieBot.exe" : "EllieBot";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsUpdateInProgress
|
||||
=> _updateIdOngoing.Contains(Id);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Guid Id { get; }
|
||||
|
@ -32,14 +46,16 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
public string BotName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a service that checks, downloads, installs, and updates a NadekoBot instance.
|
||||
/// Creates a service that checks, downloads, installs, and updates a EllieBot instance.
|
||||
/// </summary>
|
||||
/// <param name="httpClientFactory">The HTTP client factory.</param>
|
||||
/// <param name="memoryCache">The memory cache.</param>
|
||||
/// <param name="appConfigManager">The application's settings.</param>
|
||||
/// <param name="botId">The Id of the bot.</param>
|
||||
public EllieResolver(IHttpClientFactory httpClientFactory, IAppConfigManager appConfigManager, Guid botId)
|
||||
public EllieResolver(IHttpClientFactory httpClientFactory, IMemoryCache memoryCache, IAppConfigManager appConfigManager, Guid botId)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_memoryCache = memoryCache;
|
||||
_appConfigManager = appConfigManager;
|
||||
Id = botId;
|
||||
BotName = _appConfigManager.AppConfig.BotEntries[Id].Name;
|
||||
|
@ -55,13 +71,13 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
|
||||
var latestVersion = await GetLatestVersionAsync(cToken);
|
||||
|
||||
if (latestVersion.Equals(currentVersion, StringComparison.Ordinal))
|
||||
if (Version.Parse(latestVersion) <= Version.Parse(currentVersion))
|
||||
return false;
|
||||
|
||||
var http = _httpClientFactory.CreateClient();
|
||||
|
||||
return await http.IsUrlValidAsync(
|
||||
$"https://gitlab.com/api/v4/projects/9321079/packages/generic/NadekoBot-build/{latestVersion}/{GetDownloadFileName(latestVersion)}",
|
||||
await GetDownloadUrlAsync(latestVersion, cToken),
|
||||
cToken
|
||||
);
|
||||
}
|
||||
|
@ -78,8 +94,8 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
|
||||
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);
|
||||
var backupZipName = $"{botInstance.Name}_v{botInstance.Version}_{date}-{now.ToUnixTimeMilliseconds()}.zip";
|
||||
var destinationUri = Path.Join(_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.
|
||||
|
@ -92,42 +108,36 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
public async ValueTask<string?> GetCurrentVersionAsync(CancellationToken cToken = default)
|
||||
{
|
||||
var botEntry = _appConfigManager.AppConfig.BotEntries[Id];
|
||||
var executableUri = Path.Join(botEntry.InstanceDirectoryUri, FileName);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(botEntry.Version))
|
||||
return botEntry.Version;
|
||||
|
||||
var assemblyUri = Path.Combine(botEntry.InstanceDirectoryUri, "NadekoBot.dll");
|
||||
|
||||
if (!File.Exists(assemblyUri))
|
||||
if (!File.Exists(executableUri))
|
||||
{
|
||||
await _appConfigManager.UpdateBotEntryAsync(Id, x => x with { Version = null }, cToken);
|
||||
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;
|
||||
return (string.IsNullOrWhiteSpace(botEntry.Version))
|
||||
? await GetBotVersionFromAssemblyAsync(executableUri, cToken)
|
||||
: botEntry.Version;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<string> 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)..];
|
||||
try
|
||||
{
|
||||
return (await GetLatestVersionFromApiAsync(cToken)).Tag;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return await GetLatestVersionFromUrlAsync(cToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string installationUri, CancellationToken cToken = default)
|
||||
{
|
||||
if (_updateIdOngoing.Contains(Id))
|
||||
if (IsUpdateInProgress)
|
||||
return (null, null);
|
||||
|
||||
_updateIdOngoing.Add(Id);
|
||||
|
@ -135,8 +145,8 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
var currentVersion = await GetCurrentVersionAsync(cToken);
|
||||
var latestVersion = await GetLatestVersionAsync(cToken);
|
||||
|
||||
// Update
|
||||
if (latestVersion == currentVersion)
|
||||
// Already up-to-date, quit
|
||||
if (currentVersion is not null && Version.Parse(latestVersion) <= Version.Parse(currentVersion))
|
||||
{
|
||||
_updateIdOngoing.Remove(Id);
|
||||
return (currentVersion, null);
|
||||
|
@ -152,13 +162,13 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
|
||||
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);
|
||||
var botTempLocation = Path.Join(_tempDirectory, "elliebot-" + _unzippedDirRegex.Match(downloadFileName).Groups[1].Value);
|
||||
var zipTempLocation = Path.Join(_tempDirectory, downloadFileName);
|
||||
|
||||
try
|
||||
{
|
||||
using var downloadStream = await http.GetStreamAsync(
|
||||
$"https://gitlab.com/api/v4/projects/9321079/packages/generic/NadekoBot-build/{latestVersion}/{downloadFileName}",
|
||||
await GetDownloadUrlAsync(latestVersion, cToken),
|
||||
cToken
|
||||
);
|
||||
|
||||
|
@ -176,10 +186,10 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
await _appConfigManager.UpdateBotEntryAsync(Id, x => x with { Version = latestVersion }, cToken);
|
||||
|
||||
// Create creds.yml
|
||||
var credsUri = Path.Combine(installationUri, "creds.yml");
|
||||
var credsUri = Path.Join(installationUri, "creds.yml");
|
||||
|
||||
if (!File.Exists(credsUri))
|
||||
File.Copy(Path.Combine(installationUri, "creds_example.yml"), credsUri);
|
||||
File.Copy(Path.Join(installationUri, "creds_example.yml"), credsUri);
|
||||
|
||||
return (currentVersion, latestVersion);
|
||||
}
|
||||
|
@ -188,13 +198,13 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
_updateIdOngoing.Remove(Id);
|
||||
|
||||
// Cleanup
|
||||
Utilities.TryDeleteFile(zipTempLocation);
|
||||
Utilities.TryDeleteDirectory(botTempLocation);
|
||||
ToastieUtilities.TryDeleteFile(zipTempLocation);
|
||||
ToastieUtilities.TryDeleteDirectory(botTempLocation);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs the Nadeko instance on a Unix system.
|
||||
/// Installs the Ellie instance on a Unix system.
|
||||
/// </summary>
|
||||
/// <param name="downloadStream">The stream of data downloaded from the source.</param>
|
||||
/// <param name="installationUri">The absolute path to the directory the bot got installed to.</param>
|
||||
|
@ -205,27 +215,25 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
// 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);
|
||||
ToastieUtilities.TryMoveDirectory(botTempLocation, installationUri);
|
||||
|
||||
// Set executable permission
|
||||
using var chmod = Utilities.StartProcess("chmod", $"+x \"{Path.Combine(installationUri, FileName)}\"");
|
||||
using var chmod = ToastieUtilities.StartProcess("chmod", ["+x", Path.Join(installationUri, FileName)]);
|
||||
await chmod.WaitForExitAsync(cToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs the Nadeko instance on a non-Unix system.
|
||||
/// Installs the Ellie instance on a non-Unix system.
|
||||
/// </summary>
|
||||
/// <param name="downloadStream">The stream of data downloaded from the source.</param>
|
||||
/// <param name="installationUri">The absolute path to the directory the bot got installed to.</param>
|
||||
/// <param name="zipTempLocation">The absolute path to the zip file the bot is initially on.</param>
|
||||
/// <param name="botTempLocation">The absolute path to the temporary directory the bot is extracted to.</param>
|
||||
/// <param name="cToken">The cancellation token.</param>
|
||||
private async static ValueTask InstallToWindowsAsync(Stream downloadStream, string installationUri, string zipTempLocation, string botTempLocation, CancellationToken cToken = default)
|
||||
private static async ValueTask InstallToWindowsAsync(Stream downloadStream, string installationUri, string zipTempLocation, string botTempLocation, CancellationToken cToken = default)
|
||||
{
|
||||
// Save the zip file
|
||||
using (var fileStream = new FileStream(zipTempLocation, FileMode.Create))
|
||||
await using (var fileStream = new FileStream(zipTempLocation, FileMode.Create))
|
||||
await downloadStream.CopyToAsync(fileStream, cToken);
|
||||
|
||||
// Extract the zip file
|
||||
|
@ -241,13 +249,13 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
/// <param name="installationUri">The absolute path to the directory the bot got installed to.</param>
|
||||
/// <param name="backupFileUri">The absolute path to the backup zip file.</param>
|
||||
/// <param name="cToken">The cancellation token.</param>
|
||||
private async static ValueTask ReaplyBotSettingsAsync(string installationUri, string backupFileUri, CancellationToken cToken = default)
|
||||
private static async ValueTask ReaplyBotSettingsAsync(string installationUri, string backupFileUri, CancellationToken cToken = default)
|
||||
{
|
||||
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/"))
|
||||
x.Name is "creds.yml" // Restore creds.yml and everything in the "data" folder, except the stuff in the "strings" folder.
|
||||
|| (!string.IsNullOrWhiteSpace(x.Name) && x.FullName.Contains("data/") && !x.FullName.Contains("strings/"))
|
||||
);
|
||||
|
||||
foreach (var zippedFile in zippedFiles)
|
||||
|
@ -256,7 +264,7 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
.Prepend(Directory.GetParent(installationUri)?.FullName ?? string.Empty)
|
||||
.ToArray();
|
||||
|
||||
await RestoreFileAsync(zippedFile, Path.Combine(fileDestinationPath), cToken);
|
||||
await RestoreFileAsync(zippedFile, Path.Join(fileDestinationPath), cToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -266,10 +274,10 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
/// <param name="zippedFile">The file to be extracted.</param>
|
||||
/// <param name="destinationPath">The final location of the extracted file.</param>
|
||||
/// <param name="cToken">The cancellation token.</param>
|
||||
private async static ValueTask RestoreFileAsync(ZipArchiveEntry zippedFile, string destinationPath, CancellationToken cToken = default)
|
||||
private static async ValueTask RestoreFileAsync(ZipArchiveEntry zippedFile, string destinationPath, CancellationToken cToken = default)
|
||||
{
|
||||
using var zipStream = zippedFile.Open();
|
||||
using var fileStream = new FileStream(destinationPath, FileMode.Create);
|
||||
await using var zipStream = zippedFile.Open();
|
||||
await using var fileStream = new FileStream(destinationPath, FileMode.Create);
|
||||
|
||||
await zipStream.CopyToAsync(fileStream, cToken);
|
||||
}
|
||||
|
@ -277,7 +285,7 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
/// <summary>
|
||||
/// Gets the name of the file to be downloaded.
|
||||
/// </summary>
|
||||
/// <param name="version">The version of NadekoBot.</param>
|
||||
/// <param name="version">The version of EllieBot.</param>
|
||||
/// <returns>The name of the file to download.</returns>
|
||||
/// <exception cref="NotSupportedException">Occurs when this method is executed in an unsupported platform.</exception>
|
||||
private static string GetDownloadFileName(string version)
|
||||
|
@ -295,10 +303,120 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
// 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.")
|
||||
_ => throw new NotSupportedException($"Architecture of type {RuntimeInformation.OSArchitecture} is not supported by EllieBot on this OS.")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the download url to the latest bot release.
|
||||
/// </summary>
|
||||
/// <param name="latestVersion">The latest version of the bot.</param>
|
||||
/// <param name="cToken">The cancellation token.</param>
|
||||
/// <returns>The url to the latest bot release.</returns>
|
||||
private async ValueTask<string> GetDownloadUrlAsync(string latestVersion, CancellationToken cToken = default)
|
||||
{
|
||||
var downloadFileName = GetDownloadFileName(latestVersion);
|
||||
|
||||
try
|
||||
{
|
||||
// The first release is the most recent one.
|
||||
return (await GetLatestVersionFromApiAsync(cToken)).Assets
|
||||
.First(x => x.Name.Equals(downloadFileName, StringComparison.Ordinal))
|
||||
.Url;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return $"https://toastielab.dev/EllieBotDevs/elliebot/releases/download/{latestVersion}/{downloadFileName}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the latest bot version from the Toastielab latest release URL.
|
||||
/// </summary>
|
||||
/// <param name="cToken">The cancellation token.</param>
|
||||
/// <returns>The latest version of the bot.</returns>
|
||||
/// <exception cref="InvalidOperationException">Occurs when parsing of the response fails.</exception>
|
||||
private async ValueTask<string> GetLatestVersionFromUrlAsync(CancellationToken cToken = default)
|
||||
{
|
||||
var http = _httpClientFactory.CreateClient(AppConstants.NoRedirectClient);
|
||||
var response = await http.GetAsync(_toastielabReleasesRepoUrl, cToken);
|
||||
|
||||
var lastSlashIndex = response.Headers.Location?.OriginalString.LastIndexOf('/')
|
||||
?? throw new InvalidOperationException("Failed to get the latest EllieBot version.");
|
||||
|
||||
return response.Headers.Location.OriginalString[(lastSlashIndex + 1)..];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the latest bot version from the Toastielab API.
|
||||
/// </summary>
|
||||
/// <param name="cToken">The cancellation token.</param>
|
||||
/// <returns>The latest version of the bot.</returns>
|
||||
/// <exception cref="InvalidOperationException">Occurs when the API call fails.</exception>
|
||||
private async ValueTask<ToastielabRelease> GetLatestVersionFromApiAsync(CancellationToken cToken = default)
|
||||
{
|
||||
if (_memoryCache.TryGetValue(_cachedCurrentVersionKey, out var cachedObject) && cachedObject is ToastielabRelease cachedResponse)
|
||||
return cachedResponse;
|
||||
|
||||
var http = _httpClientFactory.CreateClient();
|
||||
var httpResponse = await http.GetAsync(_toastielabReleasesEndpointUrl, cToken);
|
||||
|
||||
if (!httpResponse.IsSuccessStatusCode)
|
||||
throw new InvalidOperationException("The call to the Toastielab API failed.");
|
||||
|
||||
var response = JsonSerializer.Deserialize<ToastielabRelease>(await httpResponse.Content.ReadAsStringAsync(cToken))
|
||||
?? throw new InvalidOperationException("Failed deserializing Toastielab's response.");
|
||||
|
||||
_memoryCache.Set(_cachedCurrentVersionKey, response, TimeSpan.FromMinutes(1));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bot version from the bot's assembly.
|
||||
/// </summary>
|
||||
/// <param name="executableUri">The path to the bot's executable file.</param>
|
||||
/// <param name="cToken">The cancellation token.</param>
|
||||
/// <returns>The version of the bot or <see langword="null"/> if the executable file is not found.</returns>
|
||||
/// <exception cref="InvalidOperationException">Occurs when the assembly file is not found.</exception>
|
||||
private async ValueTask<string?> GetBotVersionFromAssemblyAsync(string executableUri, CancellationToken cToken)
|
||||
{
|
||||
if (!File.Exists(executableUri))
|
||||
return null;
|
||||
|
||||
var directoryUri = Directory.GetParent(executableUri)?.FullName ?? Path.GetPathRoot(executableUri)!;
|
||||
var assemblyUri = Path.Join(directoryUri, "EllieBot.dll");
|
||||
var isSingleFile = !File.Exists(assemblyUri);
|
||||
|
||||
// If Ellie is published as a single-file binary, we have to extract
|
||||
// its contents first in order to read the assembly for its version.
|
||||
if (isSingleFile)
|
||||
{
|
||||
directoryUri = Path.Join(_tempDirectory, "EllieBotExtract_" + DateTimeOffset.Now.Ticks);
|
||||
assemblyUri = Path.Join(directoryUri, "EllieBot.dll");
|
||||
using var executableReader = new ExecutableReader(executableUri);
|
||||
await executableReader.ExtractToDirectoryAsync(directoryUri, cToken);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var ellieAssembly = Assembly.LoadFile(assemblyUri);
|
||||
var version = ellieAssembly.GetName().Version
|
||||
?? throw new InvalidOperationException($"Could not find version for the assembly at {assemblyUri}.");
|
||||
|
||||
var currentVersion = $"{version.Major}.{version.Minor}.{version.Build}";
|
||||
|
||||
await _appConfigManager.UpdateBotEntryAsync(Id, x => x with { Version = currentVersion }, cToken);
|
||||
|
||||
return currentVersion;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (isSingleFile)
|
||||
ToastieUtilities.TryDeleteDirectory(directoryUri);
|
||||
}
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"^(?:\S+\-)(\S+\-\S+)\-", RegexOptions.Compiled)]
|
||||
private static partial Regex GenerateUnzipedDirRegex();
|
||||
}
|
|
@ -1,18 +1,19 @@
|
|||
using EllieHub.Models.Config;
|
||||
using EllieHub.Models.EventArguments;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using EllieHub.Features.AppConfig.Models;
|
||||
using EllieHub.Features.BotConfig.Models;
|
||||
using EllieHub.Features.BotConfig.Services.Abstractions;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
|
||||
namespace EllieHub.Services;
|
||||
namespace EllieHub.Features.BotConfig.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a service that writes logs of bot instances to the disk.
|
||||
/// </summary>
|
||||
public sealed class LogWriter : ILogWriter
|
||||
{
|
||||
private readonly Dictionary<Guid, StringBuilder> _botLogs = new();
|
||||
private readonly ReadOnlyAppConfig _appConfig;
|
||||
private readonly ConcurrentDictionary<Guid, StringBuilder> _botLogs = [];
|
||||
private readonly ReadOnlyAppSettings _appConfig;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<ILogWriter, LogFlushEventArgs>? OnLogCreated;
|
||||
|
@ -21,13 +22,17 @@ public sealed class LogWriter : ILogWriter
|
|||
/// Creates a service that writes logs of bot instances to the disk.
|
||||
/// </summary>
|
||||
/// <param name="appConfig">The application settings.</param>
|
||||
public LogWriter(ReadOnlyAppConfig appConfig)
|
||||
public LogWriter(ReadOnlyAppSettings appConfig)
|
||||
=> _appConfig = appConfig;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<bool> FlushAllAsync(bool removeFromMemory = false, CancellationToken cToken = default)
|
||||
{
|
||||
var result = await Task.WhenAll(_botLogs.Keys.Select(x => FlushAsync(x, removeFromMemory, cToken)));
|
||||
var result = await Task.WhenAll(_botLogs.Keys.Select(x => FlushAsync(x, false, cToken)));
|
||||
|
||||
if (removeFromMemory)
|
||||
_botLogs.Clear();
|
||||
|
||||
return result.Any(x => x);
|
||||
}
|
||||
|
||||
|
@ -38,7 +43,7 @@ public sealed class LogWriter : ILogWriter
|
|||
return false;
|
||||
|
||||
if (removeFromMemory)
|
||||
_botLogs.Remove(botId);
|
||||
_botLogs.Remove(botId, out _);
|
||||
|
||||
if (logStringBuilder.Length is 0)
|
||||
return false;
|
||||
|
@ -48,7 +53,7 @@ public sealed class LogWriter : ILogWriter
|
|||
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}_v{botEntry.Version}_{date}-{now.ToUnixTimeSeconds()}.txt");
|
||||
var fileUri = Path.Join(_appConfig.LogsDirectoryUri, $"{botEntry.Name}_v{botEntry.Version}_{date}-{now.ToUnixTimeSeconds()}.txt");
|
||||
|
||||
await File.WriteAllTextAsync(fileUri, logStringBuilder.ToString(), cToken);
|
||||
|
||||
|
@ -73,7 +78,7 @@ public sealed class LogWriter : ILogWriter
|
|||
|
||||
logStringBuilder.AppendLine(message);
|
||||
|
||||
if ((logStringBuilder.Length > _appConfig.LogMaxSizeMb * 1_000_000))
|
||||
if (logStringBuilder.Length > _appConfig.LogMaxSizeMb * 1_000_000)
|
||||
_ = FlushAsync(botId);
|
||||
|
||||
return true;
|
|
@ -1,6 +1,6 @@
|
|||
using EllieHub.Services.Abstractions;
|
||||
using EllieHub.Features.BotConfig.Services.Abstractions;
|
||||
|
||||
namespace EllieHub.Services.Mocks;
|
||||
namespace EllieHub.Features.BotConfig.Services.Mocks;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a service that pretends to check, download, install, and update a bot instance.
|
||||
|
@ -14,10 +14,13 @@ internal sealed class MockEllieResolver : IBotResolver
|
|||
public Guid Id { get; } = Guid.Empty;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DependencyName { get; } = "NadekoBot";
|
||||
public string DependencyName { get; } = "EllieBot";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string FileName { get; } = "NadekoBot";
|
||||
public string FileName { get; } = "EllieBot";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsUpdateInProgress { get; } = false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<bool?> CanUpdateAsync(CancellationToken cToken = default)
|
|
@ -1,17 +1,21 @@
|
|||
using Avalonia.Controls;
|
||||
using Toastie.Utilities;
|
||||
using MsBox.Avalonia.Enums;
|
||||
using EllieHub.Enums;
|
||||
using EllieHub.Models.EventArguments;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using EllieHub.ViewModels.Abstractions;
|
||||
using EllieHub.Views.Controls;
|
||||
using EllieHub.Views.Windows;
|
||||
using EllieHub.Features.Abstractions;
|
||||
using EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
using EllieHub.Features.AppWindow.ViewModels;
|
||||
using EllieHub.Features.AppWindow.Views.Windows;
|
||||
using EllieHub.Features.BotConfig.Models;
|
||||
using EllieHub.Features.BotConfig.Services.Abstractions;
|
||||
using EllieHub.Features.BotConfig.Views.Controls;
|
||||
using EllieHub.Features.Common.ViewModels;
|
||||
using ReactiveUI;
|
||||
using SkiaSharp;
|
||||
using System.Diagnostics;
|
||||
using System.Reactive.Disposables;
|
||||
|
||||
namespace EllieHub.ViewModels.Controls;
|
||||
namespace EllieHub.Features.BotConfig.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// View-model for <see cref="BotConfigView"/>, the window with settings and controls for a specific bot instance.
|
||||
|
@ -28,6 +32,7 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
private readonly AppView _mainWindow;
|
||||
private readonly IBotOrchestrator _botOrchestrator;
|
||||
private readonly ILogWriter _logWriter;
|
||||
private readonly LateralBarViewModel _lateralBarViewModel;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the user deletes the bot instance associated with this view-model.
|
||||
|
@ -39,8 +44,6 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
/// </summary>
|
||||
public event AsyncEventHandler<BotConfigViewModel, AvatarChangedEventArgs>? AvatarChanged;
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The name of the bot as defined in the settings file.
|
||||
/// </summary>
|
||||
|
@ -100,7 +103,7 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
{
|
||||
var sanitizedValue = value.ReplaceLineEndings(string.Empty);
|
||||
|
||||
DirectoryHint = $"Select the absolute path to the bot directory. For example: {Path.Combine(_appConfigManager.AppConfig.BotsDirectoryUri, sanitizedValue)}";
|
||||
DirectoryHint = $"Select the absolute path to the bot directory. For example: {Path.Join(_appConfigManager.AppConfig.BotsDirectoryUri, sanitizedValue)}";
|
||||
this.RaiseAndSetIfChanged(ref _botName, sanitizedValue);
|
||||
}
|
||||
}
|
||||
|
@ -150,6 +153,7 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
_mainWindow = mainWindow;
|
||||
_botOrchestrator = botOrchestrator;
|
||||
_logWriter = logWriter;
|
||||
_lateralBarViewModel = mainWindow.ViewModel!.LateralBarInstance;
|
||||
BotDirectoryUriBar = botDirectoryUriBar;
|
||||
UpdateBar = updateBotBar;
|
||||
FakeConsole = fakeConsole;
|
||||
|
@ -162,6 +166,7 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
|
||||
var botEntry = _appConfigManager.AppConfig.BotEntries[botResolver.Id];
|
||||
|
||||
_ = LoadUpdateBarAsync(botResolver, updateBotBar);
|
||||
_logWriter.TryRead(botResolver.Id, out var logContent);
|
||||
FakeConsole.Content = logContent ?? string.Empty;
|
||||
FakeConsole.Watermark = "Waiting for the bot to start...";
|
||||
|
@ -170,18 +175,15 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
_botAvatar = Utilities.LoadLocalImage(botEntry.AvatarUri);
|
||||
BotName = botResolver.BotName;
|
||||
Id = botResolver.Id;
|
||||
UpdateBar.DependencyName = "Checking...";
|
||||
IsBotRunning = botOrchestrator.IsBotRunning(botResolver.Id);
|
||||
|
||||
if (IsBotRunning)
|
||||
EnableButtons(true, false);
|
||||
else
|
||||
EnableButtons(!Directory.Exists(botEntry.InstanceDirectoryUri), true);
|
||||
|
||||
_ = LoadUpdateBarAsync(botResolver, updateBotBar);
|
||||
EnableButtons(!File.Exists(Path.Join(botEntry.InstanceDirectoryUri, Resolver.FileName)), true);
|
||||
|
||||
// Dispose when the view is deactivated
|
||||
this.WhenActivated(disposables => Disposable.Create(() => Dispose()).DisposeWith(disposables));
|
||||
this.WhenActivated(disposables => Disposable.Create(Dispose).DisposeWith(disposables));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -214,14 +216,12 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
Directory.Delete(BotDirectoryUriBar.CurrentUri);
|
||||
}
|
||||
|
||||
if (Environment.OSVersion.Platform is not PlatformID.Unix)
|
||||
Directory.Move(oldUri, BotDirectoryUriBar.CurrentUri);
|
||||
else
|
||||
{
|
||||
// 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", $"\"{oldUri}\" \"{BotDirectoryUriBar.CurrentUri}\"");
|
||||
await moveProcess.WaitForExitAsync();
|
||||
}
|
||||
if (!ToastieUtilities.TryMoveDirectory(oldUri, BotDirectoryUriBar.CurrentUri))
|
||||
throw new InvalidOperationException(
|
||||
$"Could not move \"{oldUri}\" to \"{BotDirectoryUriBar.CurrentUri}\"." +
|
||||
Environment.NewLine + Environment.NewLine +
|
||||
"Make sure you have permission to write to the target directory."
|
||||
);
|
||||
|
||||
BotDirectoryUriBar.RecheckCurrentUri();
|
||||
}
|
||||
|
@ -243,7 +243,12 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
}
|
||||
finally
|
||||
{
|
||||
EnableButtons(!wereButtonsUnlocked, true);
|
||||
_ = LoadUpdateBarAsync(Resolver, UpdateBar);
|
||||
|
||||
if (!wereButtonsUnlocked && File.Exists(Path.Join(BotDirectoryUriBar.CurrentUri, Resolver.FileName)))
|
||||
EnableButtons(false, true);
|
||||
else
|
||||
EnableButtons(!wereButtonsUnlocked, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -306,7 +311,7 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
|
||||
var backupUri = await Resolver.CreateBackupAsync();
|
||||
|
||||
await ((string.IsNullOrWhiteSpace(backupUri))
|
||||
await (string.IsNullOrWhiteSpace(backupUri)
|
||||
? _mainWindow.ShowDialogWindowAsync($"Bot {ActualBotName} not found.", DialogType.Error, Icon.Error)
|
||||
: _mainWindow.ShowDialogWindowAsync($"Successfully backed up {ActualBotName} to:{Environment.NewLine}{backupUri}", iconType: Icon.Success));
|
||||
|
||||
|
@ -322,7 +327,7 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
{
|
||||
ButtonDefinitions = ButtonEnum.OkCancel,
|
||||
ContentTitle = "Are you sure?",
|
||||
ContentMessage = $"Are you sure you want to delete {ActualBotName}?{Environment.NewLine}This action cannot be undone.",
|
||||
ContentMessage = $"Are you sure you want to delete {ActualBotName}?{Environment.NewLine}This action cannot be reversed.",
|
||||
MaxWidth = int.Parse(WindowConstants.DefaultWindowWidth) / 2.0,
|
||||
SizeToContent = SizeToContent.WidthAndHeight,
|
||||
ShowInCenter = true,
|
||||
|
@ -336,7 +341,7 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
EnableButtons(true, false);
|
||||
|
||||
// Stop the bot instance
|
||||
_botOrchestrator.Stop(Resolver.Id);
|
||||
_botOrchestrator.StopBot(Resolver.Id);
|
||||
|
||||
// Cleanup
|
||||
FakeConsole.Content = string.Empty;
|
||||
|
@ -366,6 +371,7 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
}
|
||||
|
||||
EnableButtons(true, false);
|
||||
_lateralBarViewModel.ToggleEnable(false);
|
||||
|
||||
var originalStatus = UpdateBar.Status;
|
||||
UpdateBar.Status = DependencyStatus.Updating;
|
||||
|
@ -391,6 +397,10 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
await _mainWindow.ShowDialogWindowAsync($"An error occurred while updating {Resolver.DependencyName}:\n{ex.Message}", DialogType.Error, Icon.Error);
|
||||
UpdateBar.Status = originalStatus;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lateralBarViewModel.ToggleEnable(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -398,7 +408,7 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
/// </summary>
|
||||
public void StartBot()
|
||||
{
|
||||
IsBotRunning = _botOrchestrator.Start(Id);
|
||||
IsBotRunning = _botOrchestrator.StartBot(Id);
|
||||
|
||||
if (IsBotRunning)
|
||||
EnableButtons(true, false);
|
||||
|
@ -408,32 +418,37 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
/// Stops the bot instance associated with this view-model.
|
||||
/// </summary>
|
||||
public void StopBot()
|
||||
=> _botOrchestrator.Stop(Id);
|
||||
=> _botOrchestrator.StopBot(Id);
|
||||
|
||||
/// <summary>
|
||||
/// Loads the bot update bar.
|
||||
/// </summary>
|
||||
/// <param name="botResolver">The bot resolver.</param>
|
||||
/// <param name="updateBotBar">The update bar.</param>
|
||||
private async static Task LoadUpdateBarAsync(IBotResolver botResolver, DependencyButtonViewModel updateBotBar)
|
||||
private static async Task LoadUpdateBarAsync(IBotResolver botResolver, DependencyButtonViewModel updateBotBar)
|
||||
{
|
||||
updateBotBar.DependencyName = "Checking...";
|
||||
updateBotBar.Status = DependencyStatus.Checking;
|
||||
|
||||
var currentVersion = await botResolver.GetCurrentVersionAsync();
|
||||
updateBotBar.DependencyName = (string.IsNullOrWhiteSpace(currentVersion))
|
||||
updateBotBar.DependencyName = string.IsNullOrWhiteSpace(currentVersion)
|
||||
? "Not Installed"
|
||||
: "EllieBot v" + currentVersion;
|
||||
|
||||
var canUpdate = await botResolver.CanUpdateAsync();
|
||||
updateBotBar.Status = (canUpdate is true)
|
||||
? DependencyStatus.Update
|
||||
: (canUpdate is null)
|
||||
? DependencyStatus.Install
|
||||
: DependencyStatus.Installed;
|
||||
updateBotBar.Status = canUpdate switch
|
||||
{
|
||||
true => DependencyStatus.Update,
|
||||
false => DependencyStatus.Installed,
|
||||
null when botResolver.IsUpdateInProgress => DependencyStatus.Updating,
|
||||
null => DependencyStatus.Install
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Locks or unlocks the settings buttons of this view-model.
|
||||
/// </summary>
|
||||
/// <param name="lockButtons">Whether the settings buttons should be locked.</param>
|
||||
/// <param name="lockButtons">Whether the setting buttons should be locked.</param>
|
||||
/// <param name="isIdle">Whether this view-model is currently undergoing an operation.</param>
|
||||
private void EnableButtons(bool lockButtons, bool isIdle)
|
||||
{
|
||||
|
@ -451,10 +466,8 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
if (eventArgs.Id != Resolver.Id)
|
||||
return;
|
||||
|
||||
_logWriter.TryAdd(eventArgs.Id, eventArgs.Output);
|
||||
|
||||
FakeConsole.Content = (FakeConsole.Content.Length > 100_000)
|
||||
? FakeConsole.Content[FakeConsole.Content.IndexOf(Environment.NewLine, 60_000)..] + eventArgs.Output + Environment.NewLine
|
||||
FakeConsole.Content = FakeConsole.Content.Length > 100_000
|
||||
? FakeConsole.Content[FakeConsole.Content.IndexOf(Environment.NewLine, 60_000, StringComparison.Ordinal)..] + eventArgs.Output + Environment.NewLine
|
||||
: FakeConsole.Content + eventArgs.Output + Environment.NewLine;
|
||||
}
|
||||
|
||||
|
@ -468,14 +481,11 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
if (eventArgs.Id != Resolver.Id)
|
||||
return;
|
||||
|
||||
var message = Environment.NewLine + ActualBotName + " stopped." + Environment.NewLine;
|
||||
|
||||
_logWriter.TryAdd(Resolver.Id, message);
|
||||
FakeConsole.Content += message;
|
||||
FakeConsole.Content += eventArgs.Message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reenables the buttons when the bot instance associated with this view-model exits.
|
||||
/// Re-enables the buttons when the bot instance associated with this view-model exits.
|
||||
/// </summary>
|
||||
/// <param name="botOrchestrator">The bot orchestrator.</param>
|
||||
/// <param name="eventArgs">The event arguments.</param>
|
||||
|
@ -491,7 +501,13 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
BotAvatar?.Dispose();
|
||||
UpdateBar.Click -= InstallOrUpdateAsync;
|
||||
_botOrchestrator.OnStdout -= WriteLog;
|
||||
_botOrchestrator.OnStderr -= WriteLog;
|
||||
_botOrchestrator.OnBotExit -= LogBotExit;
|
||||
_botOrchestrator.OnBotExit -= ReenableButtonsOnBotExit;
|
||||
|
||||
BotAvatar.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
using EllieHub.ViewModels.Abstractions;
|
||||
using EllieHub.Views.Controls;
|
||||
using EllieHub.Features.Abstractions;
|
||||
using EllieHub.Features.BotConfig.Views.Controls;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace EllieHub.ViewModels.Controls;
|
||||
namespace EllieHub.Features.BotConfig.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// View-model for <see cref="FakeConsole"/>, the fake console that displays text.
|
|
@ -3,13 +3,13 @@
|
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:siv="https://github.com/kekyo/SkiaImageView"
|
||||
xmlns:vm="using:EllieHub.ViewModels.Controls"
|
||||
xmlns:dd="using:EllieHub.DesignData.Controls"
|
||||
xmlns:vm="using:EllieHub.Features.BotConfig.ViewModels"
|
||||
xmlns:dd="using:EllieHub.Avalonia.DesignData.Controls"
|
||||
xmlns:const="using:EllieHub.Common"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="{x:Static const:WindowConstants.MinWindowWidth}"
|
||||
d:DesignHeight="{x:Static const:WindowConstants.MinWindowHeight}"
|
||||
x:Class="EllieHub.Views.Controls.BotConfigView"
|
||||
x:Class="EllieHub.Features.BotConfig.Views.Controls.BotConfigView"
|
||||
x:DataType="vm:BotConfigViewModel">
|
||||
|
||||
<Design.DataContext>
|
||||
|
@ -22,7 +22,7 @@
|
|||
Fill="{DynamicResource HeavyBackground}"/>
|
||||
|
||||
<Rectangle Grid.Row="1"
|
||||
Grid.RowSpan="3"
|
||||
Grid.RowSpan="2"
|
||||
Fill="{DynamicResource MediumBackground}"/>
|
||||
|
||||
<!--Bot Settings Title-->
|
||||
|
@ -146,4 +146,4 @@
|
|||
<ContentControl Content="{Binding FakeConsole}" />
|
||||
</Expander>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
</UserControl>
|
|
@ -1,9 +1,9 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.ReactiveUI;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
using EllieHub.Features.BotConfig.ViewModels;
|
||||
|
||||
namespace EllieHub.Views.Controls;
|
||||
namespace EllieHub.Features.BotConfig.Views.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the view with settings and controls for a specific bot instance.
|
||||
|
@ -26,7 +26,7 @@ public partial class BotConfigView : ReactiveUserControl<BotConfigViewModel>
|
|||
/// <param name="eventArgs">The event arguments.</param>
|
||||
private void AvatarButtonHover(object? sender, PointerEventArgs eventArgs)
|
||||
{
|
||||
if (!Utilities.TryCastTo<Button>(sender, out var button))
|
||||
if (sender is not Button button)
|
||||
return;
|
||||
|
||||
button.Opacity = 3.0;
|
||||
|
@ -40,7 +40,7 @@ public partial class BotConfigView : ReactiveUserControl<BotConfigViewModel>
|
|||
/// <param name="eventArgs">The event arguments.</param>
|
||||
private void AvatarButtonUnhover(object? sender, PointerEventArgs eventArgs)
|
||||
{
|
||||
if (!Utilities.TryCastTo<Button>(sender, out var button))
|
||||
if (sender is not Button button)
|
||||
return;
|
||||
|
||||
button.Opacity = 0.0;
|
|
@ -2,10 +2,10 @@
|
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:EllieHub.ViewModels.Controls"
|
||||
xmlns:dd="using:EllieHub.DesignData.Controls"
|
||||
xmlns:vm="using:EllieHub.Features.BotConfig.ViewModels"
|
||||
xmlns:dd="using:EllieHub.Avalonia.DesignData.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="250"
|
||||
x:Class="EllieHub.Views.Controls.FakeConsole"
|
||||
x:Class="EllieHub.Features.BotConfig.Views.Controls.FakeConsole"
|
||||
x:DataType="vm:FakeConsoleViewModel">
|
||||
<Design.DataContext>
|
||||
<dd:DesignFakeConsoleViewModel/>
|
||||
|
@ -16,4 +16,4 @@
|
|||
Watermark="{Binding Watermark}"
|
||||
CaretIndex="{Binding Content.Length}" />
|
||||
|
||||
</UserControl>
|
||||
</UserControl>
|
|
@ -1,7 +1,7 @@
|
|||
using Avalonia.ReactiveUI;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
using EllieHub.Features.BotConfig.ViewModels;
|
||||
|
||||
namespace EllieHub.Views.Controls;
|
||||
namespace EllieHub.Features.BotConfig.Views.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a control that mimics the appearance of a terminal emulator.
|
|
@ -1,6 +1,6 @@
|
|||
using EllieHub.ViewModels.Controls;
|
||||
using EllieHub.Features.Common.ViewModels;
|
||||
|
||||
namespace EllieHub.Models.EventArguments;
|
||||
namespace EllieHub.Features.Common.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the event arguments for when a valid uri is set to a <see cref="UriInputBarViewModel"/>.
|
|
@ -1,4 +1,4 @@
|
|||
namespace EllieHub.Services.Abstractions;
|
||||
namespace EllieHub.Features.Common.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a service that checks, downloads, installs, and updates a dependency.
|
|
@ -1,12 +1,12 @@
|
|||
using Avalonia.Media.Immutable;
|
||||
using EllieHub.Enums;
|
||||
using EllieHub.ViewModels.Abstractions;
|
||||
using EllieHub.Views.Controls;
|
||||
using EllieHub.Views.Windows;
|
||||
using EllieHub.Features.Abstractions;
|
||||
using EllieHub.Features.AppWindow.Views.Windows;
|
||||
using EllieHub.Features.Common.Views.Controls;
|
||||
using ReactiveUI;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace EllieHub.ViewModels.Controls;
|
||||
namespace EllieHub.Features.Common.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the view-model for a button that installs a dependency for Ellie.
|
|
@ -1,11 +1,12 @@
|
|||
using Avalonia.Platform.Storage;
|
||||
using EllieHub.Models.EventArguments;
|
||||
using EllieHub.ViewModels.Abstractions;
|
||||
using EllieHub.Views.Controls;
|
||||
using Toastie.Utilities;
|
||||
using EllieHub.Features.Abstractions;
|
||||
using EllieHub.Features.Common.Models;
|
||||
using EllieHub.Features.Common.Views.Controls;
|
||||
using ReactiveUI;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace EllieHub.ViewModels.Controls;
|
||||
namespace EllieHub.Features.Common.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a text box for inputting the absolute path of a directory.
|
||||
|
@ -14,7 +15,7 @@ public class UriInputBarViewModel : ViewModelBase<UriInputBar>
|
|||
{
|
||||
private static readonly FolderPickerOpenOptions _folderPickerOptions = new();
|
||||
private string _lastValidUri = AppStatics.AppDefaultConfigDirectoryUri;
|
||||
private bool _isDirectoryValid = false;
|
||||
private bool _isDirectoryValid;
|
||||
private string _currentUri = string.Empty;
|
||||
private readonly IStorageProvider _storageProvider;
|
||||
|
||||
|
@ -92,5 +93,5 @@ public class UriInputBarViewModel : ViewModelBase<UriInputBar>
|
|||
/// <param name="directoryUri">The absolute path to a directory.</param>
|
||||
/// <returns><see langword="true"/> if the directory is valid, <see langword="false"/> otherwise.</returns>
|
||||
private bool IsValidDirectory(string directoryUri)
|
||||
=> Directory.Exists(directoryUri) && Utilities.CanWriteTo(directoryUri);
|
||||
=> Directory.Exists(directoryUri) && ToastieUtilities.HasWritePermissionAt(directoryUri);
|
||||
}
|
|
@ -2,10 +2,10 @@
|
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:EllieHub.ViewModels.Controls"
|
||||
xmlns:dd="using:EllieHub.DesignData.Controls"
|
||||
xmlns:vm="using:EllieHub.Features.Common.ViewModels"
|
||||
xmlns:dd="using:EllieHub.Avalonia.DesignData.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="50"
|
||||
x:Class="EllieHub.Views.Controls.DependencyButton"
|
||||
x:Class="EllieHub.Features.Common.Views.Controls.DependencyButton"
|
||||
x:DataType="vm:DependencyButtonViewModel">
|
||||
|
||||
<Design.DataContext>
|
||||
|
@ -36,4 +36,4 @@
|
|||
IsEnabled="{Binding IsEnabled}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
</UserControl>
|
|
@ -1,7 +1,7 @@
|
|||
using Avalonia.ReactiveUI;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
using EllieHub.Features.Common.ViewModels;
|
||||
|
||||
namespace EllieHub.Views.Controls;
|
||||
namespace EllieHub.Features.Common.Views.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a button that installs a dependency for Ellie.
|
|
@ -2,10 +2,10 @@
|
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:EllieHub.ViewModels.Controls"
|
||||
xmlns:dd="using:EllieHub.DesignData.Controls"
|
||||
xmlns:vm="using:EllieHub.Features.Common.ViewModels"
|
||||
xmlns:dd="using:EllieHub.Avalonia.DesignData.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="33"
|
||||
x:Class="EllieHub.Views.Controls.UriInputBar"
|
||||
x:Class="EllieHub.Features.Common.Views.Controls.UriInputBar"
|
||||
x:DataType="vm:UriInputBarViewModel">
|
||||
|
||||
<Design.DataContext>
|
||||
|
@ -33,4 +33,4 @@
|
|||
Command="{Binding OpenFolder}"
|
||||
CommandParameter="{Binding CurrentUri}"/>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
</UserControl>
|
|
@ -1,7 +1,7 @@
|
|||
using Avalonia.ReactiveUI;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
using EllieHub.Features.Common.ViewModels;
|
||||
|
||||
namespace EllieHub.Views.Controls;
|
||||
namespace EllieHub.Features.Common.Views.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a text box that receives the absolute path to a directory.
|
13
EllieHub/Features/Home/Models/Api/Toastielab/Assets.cs
Normal file
13
EllieHub/Features/Home/Models/Api/Toastielab/Assets.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EllieHub.Features.Home.Models.Api.Toastielab;
|
||||
|
||||
/// <summary>
|
||||
/// The assets of a Toastielab release.
|
||||
/// </summary>
|
||||
/// <param name="Name">The name of the release file.</param>
|
||||
/// <param name="Url">The url of the release file.</param>
|
||||
public sealed record Assets(
|
||||
[property: JsonPropertyName("name")] string Name,
|
||||
[property: JsonPropertyName("browser_download_url")] string Url
|
||||
);
|
|
@ -0,0 +1,13 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EllieHub.Features.Home.Models.Api.Toastielab;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a release from the Toastielab API.
|
||||
/// </summary>
|
||||
/// <param name="Tag">The tag of the release.</param>
|
||||
/// <param name="Assets">The assets of the release.</param>
|
||||
public sealed record ToastielabRelease(
|
||||
[property: JsonPropertyName("tag_name")] string Tag,
|
||||
[property: JsonPropertyName("assets")] Assets[] Assets
|
||||
);
|
|
@ -1,4 +1,6 @@
|
|||
namespace EllieHub.Services.Abstractions;
|
||||
using EllieHub.Features.Common.Services.Abstractions;
|
||||
|
||||
namespace EllieHub.Features.Home.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a service that updates this application.
|
265
EllieHub/Features/Home/Services/AppResolver.cs
Normal file
265
EllieHub/Features/Home/Services/AppResolver.cs
Normal file
|
@ -0,0 +1,265 @@
|
|||
using Toastie.Utilities;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using EllieHub.Features.Home.Models.Api.Toastielab;
|
||||
using EllieHub.Features.Home.Services.Abstractions;
|
||||
using System.IO.Compression;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace EllieHub.Features.Home.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a service that updates this application.
|
||||
/// </summary>
|
||||
public sealed class AppResolver : IAppResolver
|
||||
{
|
||||
private const string _cachedCurrentVersionKey = "currentVersion:EllieHub";
|
||||
private const string _toastielabReleasesEndpointUrl = "https://toastielab.dev/api/v1/repos/EllieBotDevs/EllieHub/releases/latest";
|
||||
private const string _toastielabReleasesRepoUrl = "https://toastielab.dev/EllieBotDevs/EllieHub/releases/latest";
|
||||
private static readonly string _tempDirectory = Path.GetTempPath();
|
||||
private static readonly string _downloadedFileName = GetDownloadFileName();
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DependencyName { get; } = "EllieHub";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string OldFileSuffix { get; } = "_old";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string FileName { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string BinaryUri { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a service that updates this application.
|
||||
/// </summary>
|
||||
/// <param name="httpClientFactory">The Http client factory.</param>
|
||||
/// <param name="memoryCache">The memory cache.</param>
|
||||
public AppResolver(IHttpClientFactory httpClientFactory, IMemoryCache memoryCache)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_memoryCache = memoryCache;
|
||||
FileName = OperatingSystem.IsWindows() ? "EllieHub.exe" : "EllieHub";
|
||||
BinaryUri = Path.Join(AppContext.BaseDirectory, FileName);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<string?> GetCurrentVersionAsync(CancellationToken cToken = default)
|
||||
=> ValueTask.FromResult<string?>(AppStatics.AppVersion);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void LaunchNewVersion()
|
||||
=> ToastieUtilities.StartProcess(BinaryUri);
|
||||
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the updater can be updated,
|
||||
/// <see langword="false"/> if the updater is up-to-date,
|
||||
/// <see langword="null"/> if the updater cannot update itself.
|
||||
/// </returns>
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool?> CanUpdateAsync(CancellationToken cToken = default)
|
||||
{
|
||||
if (!ToastieUtilities.HasWritePermissionAt(AppContext.BaseDirectory))
|
||||
return null;
|
||||
|
||||
var currentVersion = await GetCurrentVersionAsync(cToken);
|
||||
|
||||
if (currentVersion is null)
|
||||
return null;
|
||||
|
||||
var latestVersion = await GetLatestVersionAsync(cToken);
|
||||
|
||||
if (Version.Parse(latestVersion) <= Version.Parse(currentVersion))
|
||||
return false;
|
||||
|
||||
var http = _httpClientFactory.CreateClient();
|
||||
|
||||
return await http.IsUrlValidAsync(
|
||||
await GetDownloadUrlAsync(latestVersion, cToken),
|
||||
cToken
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool RemoveOldFiles()
|
||||
{
|
||||
var result = false;
|
||||
|
||||
foreach (var file in Directory.GetFiles(AppContext.BaseDirectory).Where(x => x.EndsWith(OldFileSuffix, StringComparison.Ordinal)))
|
||||
result |= ToastieUtilities.TryDeleteFile(file);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<string> GetLatestVersionAsync(CancellationToken cToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
return (await GetLatestVersionFromApiAsync(cToken)).Tag;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return await GetLatestVersionFromUrlAsync(cToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string installationUri, CancellationToken cToken = default)
|
||||
{
|
||||
var currentVersion = await GetCurrentVersionAsync(cToken);
|
||||
var latestVersion = await GetLatestVersionAsync(cToken);
|
||||
|
||||
if (currentVersion is not null && Version.Parse(latestVersion) <= Version.Parse(currentVersion))
|
||||
return (currentVersion, null);
|
||||
|
||||
var http = _httpClientFactory.CreateClient(); // Do not initialize a ToastielabClient here, it returns 302 with no data
|
||||
var appTempLocation = Path.Join(_tempDirectory, _downloadedFileName[.._downloadedFileName.LastIndexOf('.')]);
|
||||
var zipTempLocation = Path.Join(_tempDirectory, _downloadedFileName);
|
||||
|
||||
try
|
||||
{
|
||||
await using var downloadStream = await http.GetStreamAsync(
|
||||
await GetDownloadUrlAsync(latestVersion, cToken),
|
||||
cToken
|
||||
);
|
||||
|
||||
// Save the zip file
|
||||
await 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.Join(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, true); // This executes fine
|
||||
|
||||
// Move the new file to the application's directory.
|
||||
// ...
|
||||
// This is a workaround for a really weird bug with Unix applications published as single-file.
|
||||
// The moving operation works, but invoking any process from the shell results in:
|
||||
// FileNotFoundException: Could not load file or assembly 'System.IO.Pipes, Version=9.0.0.0 [...]
|
||||
if (Environment.OSVersion.Platform is not PlatformID.Unix)
|
||||
File.Move(newFileUri, destinationUri, true);
|
||||
else
|
||||
{
|
||||
using var moveProcess = ToastieUtilities.StartProcess("mv", [newFileUri, destinationUri]);
|
||||
await moveProcess.WaitForExitAsync(cToken);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark the new binary file as executable.c
|
||||
if (Environment.OSVersion.Platform is PlatformID.Unix)
|
||||
{
|
||||
using var chmod = ToastieUtilities.StartProcess("chmod", ["+x", BinaryUri]);
|
||||
await chmod.WaitForExitAsync(cToken);
|
||||
}
|
||||
|
||||
return (currentVersion, latestVersion);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup
|
||||
ToastieUtilities.TryDeleteFile(zipTempLocation);
|
||||
ToastieUtilities.TryDeleteDirectory(appTempLocation);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the file to be downloaded.
|
||||
/// </summary>
|
||||
/// <returns>The name of the file to be downloaded.</returns>
|
||||
/// <exception cref="NotSupportedException">Occurs when this method is used in an unsupported system.</exception>
|
||||
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.")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the download url to the latest bot release.
|
||||
/// </summary>
|
||||
/// <param name="latestVersion">The latest version of the bot.</param>
|
||||
/// <param name="cToken">The cancellation token.</param>
|
||||
/// <returns>The url to the latest bot release.</returns>
|
||||
private async ValueTask<string> GetDownloadUrlAsync(string latestVersion, CancellationToken cToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
// The first release is the most recent one.
|
||||
return (await GetLatestVersionFromApiAsync(cToken)).Assets
|
||||
.First(x => x.Name.Equals(_downloadedFileName, StringComparison.Ordinal))
|
||||
.Url;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return $"https://toastielab.dev/EllieBotDevs/EllieHub/releases/download/{latestVersion}/{_downloadedFileName}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the latest bot version from the Toastielab latest release URL.
|
||||
/// </summary>
|
||||
/// <param name="cToken">The cancellation token.</param>
|
||||
/// <returns>The latest version of the bot.</returns>
|
||||
/// <exception cref="InvalidOperationException">Occurs when parsing of the response fails.</exception>
|
||||
private async ValueTask<string> GetLatestVersionFromUrlAsync(CancellationToken cToken = default)
|
||||
{
|
||||
var http = _httpClientFactory.CreateClient(AppConstants.NoRedirectClient);
|
||||
var response = await http.GetAsync(_toastielabReleasesRepoUrl, cToken);
|
||||
|
||||
var lastSlashIndex = response.Headers.Location?.OriginalString.LastIndexOf('/')
|
||||
?? throw new InvalidOperationException("Failed to get the latest EllieBot version.");
|
||||
|
||||
return response.Headers.Location.OriginalString[(lastSlashIndex + 1)..];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the latest bot version from the Toastielab API.
|
||||
/// </summary>
|
||||
/// <param name="cToken">The cancellation token.</param>
|
||||
/// <returns>The latest version of the bot.</returns>
|
||||
/// <exception cref="InvalidOperationException">Occurs when the API call fails.</exception>
|
||||
private async ValueTask<ToastielabRelease> GetLatestVersionFromApiAsync(CancellationToken cToken = default)
|
||||
{
|
||||
if (_memoryCache.TryGetValue(_cachedCurrentVersionKey, out var cachedObject) && cachedObject is ToastielabRelease cachedResponse)
|
||||
return cachedResponse;
|
||||
|
||||
var http = _httpClientFactory.CreateClient(AppConstants.ToastielabClient);
|
||||
var httpResponse = await http.GetAsync(_toastielabReleasesEndpointUrl, cToken);
|
||||
|
||||
if (!httpResponse.IsSuccessStatusCode)
|
||||
throw new InvalidOperationException("The call to the Toastielab API failed.");
|
||||
|
||||
var response = JsonSerializer.Deserialize<ToastielabRelease>(await httpResponse.Content.ReadAsStringAsync(cToken))
|
||||
?? throw new InvalidOperationException("Failed deserializing Toastielab's response.");
|
||||
|
||||
_memoryCache.Set(_cachedCurrentVersionKey, response, TimeSpan.FromMinutes(1));
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
using EllieHub.ViewModels.Abstractions;
|
||||
using EllieHub.Views.Controls;
|
||||
using EllieHub.Features.Abstractions;
|
||||
using EllieHub.Features.Home.Views.Controls;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace EllieHub.ViewModels.Controls;
|
||||
namespace EllieHub.Features.Home.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// View-model for the home window, with links to Ellie resources.
|
|
@ -1,6 +1,7 @@
|
|||
using EllieHub.ViewModels.Abstractions;
|
||||
using EllieHub.Features.Abstractions;
|
||||
using EllieHub.Features.Home.Views.Windows;
|
||||
|
||||
namespace EllieHub.ViewModels.Windows;
|
||||
namespace EllieHub.Features.Home.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// View-model for the update dialog window.
|
|
@ -2,13 +2,13 @@
|
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:EllieHub.ViewModels.Controls"
|
||||
xmlns:vm="using:EllieHub.Features.Home.ViewModels"
|
||||
xmlns:const="using:EllieHub.Common"
|
||||
xmlns:dd="using:EllieHub.DesignData.Controls"
|
||||
xmlns:dd="using:EllieHub.Avalonia.DesignData.Controls"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="{x:Static const:WindowConstants.MinWindowWidth}"
|
||||
d:DesignHeight="{x:Static const:WindowConstants.MinWindowHeight}"
|
||||
x:Class="EllieHub.Views.Controls.HomeView"
|
||||
x:Class="EllieHub.Features.Home.Views.Controls.HomeView"
|
||||
x:DataType="vm:HomeViewModel">
|
||||
<Design.DataContext>
|
||||
<dd:DesignHomeViewModel/>
|
||||
|
@ -28,18 +28,18 @@
|
|||
Grid.ColumnSpan="5"
|
||||
Fill="{DynamicResource HeavyBackground}"/>
|
||||
|
||||
<Rectangle Grid.Row="5"
|
||||
<Rectangle Grid.Row="4"
|
||||
Grid.Column="0"
|
||||
Fill="#008DDA"/>
|
||||
|
||||
<Rectangle Grid.Row="5"
|
||||
<Rectangle Grid.Row="4"
|
||||
Grid.Column="1"
|
||||
Fill="#F86752"/>
|
||||
|
||||
<Rectangle Grid.Row="5"
|
||||
<Rectangle Grid.Row="4"
|
||||
Grid.Column="2"
|
||||
Grid.ColumnSpan="3"
|
||||
Fill="#B90060"/>
|
||||
Fill="#0e3f88"/>
|
||||
|
||||
<!--Top Banner-->
|
||||
<Border Classes="circular"
|
||||
|
@ -64,7 +64,7 @@
|
|||
Classes="transparent"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
CommandParameter="https://commands.elliebot.net"
|
||||
CommandParameter="https://commands.elliebot.net/"
|
||||
Command="{Binding OpenUrl}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Classes="icon"
|
||||
|
@ -78,7 +78,7 @@
|
|||
Classes="transparent"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
CommandParameter="https://beta.elliebot.net/"
|
||||
CommandParameter="https://app.feedbacky.net/b/ellie-feedback"
|
||||
Command="{Binding OpenUrl}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Classes="icon"
|
||||
|
@ -92,7 +92,7 @@
|
|||
Classes="transparent"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
CommandParameter="https://docs.elliebot.net"
|
||||
CommandParameter="https://docs.elliebot.net/ellie/"
|
||||
Command="{Binding OpenUrl}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Classes="icon"
|
||||
|
@ -106,7 +106,7 @@
|
|||
Classes="transparent"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
CommandParameter="https://discord.elliebot.net/"
|
||||
CommandParameter="https://discord.gg/etQdZxSyEH/"
|
||||
Command="{Binding OpenUrl}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Classes="icon"
|
||||
|
@ -147,7 +147,7 @@
|
|||
VerticalAlignment="Top"/>
|
||||
|
||||
<!--Bottom Buttons-->
|
||||
<Button Grid.Row="5"
|
||||
<Button Grid.Row="4"
|
||||
Grid.Column="0"
|
||||
Classes="transparent"
|
||||
Content="Buy me a Ko-fi"
|
||||
|
@ -158,7 +158,7 @@
|
|||
CommandParameter="https://ko-fi.com/toastie_t0ast"
|
||||
Command="{Binding OpenUrl}"/>
|
||||
|
||||
<Image Grid.Row="5"
|
||||
<Image Grid.Row="4"
|
||||
Grid.Column="0"
|
||||
Classes="icon"
|
||||
MaxHeight="30"
|
||||
|
@ -167,18 +167,18 @@
|
|||
Margin="0 0 3 0"
|
||||
Source="{DynamicResource KofiIcon}"/>
|
||||
|
||||
<Button Grid.Row="5"
|
||||
<Button Grid.Row="4"
|
||||
Grid.Column="1"
|
||||
Classes="transparent"
|
||||
Content="Support Ellie"
|
||||
Content="Support EllieBot"
|
||||
Foreground="White"
|
||||
FontSize="12"
|
||||
Cursor="Hand"
|
||||
HorizontalAlignment="Stretch"
|
||||
CommandParameter="https://patreon.com/emotionchild"
|
||||
CommandParameter="https://patreon.com/toastiet0ast"
|
||||
Command="{Binding OpenUrl}"/>
|
||||
|
||||
<Image Grid.Row="5"
|
||||
<Image Grid.Row="4"
|
||||
Grid.Column="1"
|
||||
Classes="icon"
|
||||
MaxHeight="30"
|
||||
|
@ -187,7 +187,7 @@
|
|||
Margin="0 0 3 0"
|
||||
Source="{DynamicResource PatreonIcon}"/>
|
||||
|
||||
<TextBlock Grid.Row="5"
|
||||
<TextBlock Grid.Row="4"
|
||||
Grid.Column="2"
|
||||
Grid.ColumnSpan="3"
|
||||
Text="Thank You ♡"
|
||||
|
@ -195,4 +195,4 @@
|
|||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
</UserControl>
|
|
@ -1,7 +1,7 @@
|
|||
using Avalonia.ReactiveUI;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
using EllieHub.Features.Home.ViewModels;
|
||||
|
||||
namespace EllieHub.Views.Controls;
|
||||
namespace EllieHub.Features.Home.Views.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// View for the home window, with buttons linking to official Ellie resources.
|
|
@ -2,10 +2,10 @@
|
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:EllieHub.ViewModels.Windows"
|
||||
xmlns:dd="using:EllieHub.DesignData.Windows"
|
||||
xmlns:vm="using:EllieHub.Features.Home.ViewModels"
|
||||
xmlns:dd="using:EllieHub.Avalonia.DesignData.Windows"
|
||||
mc:Ignorable="d" d:DesignWidth="420" d:DesignHeight="100"
|
||||
x:Class="EllieHub.UpdateView"
|
||||
x:Class="EllieHub.Features.Home.Views.Windows.UpdateView"
|
||||
x:DataType="vm:UpdateViewModel"
|
||||
Title="Update in Progress"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
|
@ -14,7 +14,7 @@
|
|||
SystemDecorations="None"
|
||||
Width="420"
|
||||
Height="100"
|
||||
Background="#A00041">
|
||||
Background="#003ca0">
|
||||
<Design.DataContext>
|
||||
<dd:DesignUpdateViewModel/>
|
||||
</Design.DataContext>
|
||||
|
@ -36,4 +36,4 @@
|
|||
HorizontalAlignment="Center"
|
||||
Text="This might take a few minutes."/>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
</Window>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue