Compare commits
No commits in common. "main" and "1.0.1.0" have entirely different histories.
107 changed files with 1157 additions and 2289 deletions
.editorconfig
.forgejo
.gitignoreEllieHub.slnEllieHub
App.axamlApp.axaml.csEllieHub.csproj
Common
DesignData
Common
Controls
DesignBotConfigViewModel.csDesignConfigViewModel.csDesignDependencyButtonViewModel.csDesignFakeConsoleViewModel.csDesignHomeViewModel.csDesignLateralBarViewModel.csDesignUriInputBarViewModel.cs
Windows
Extensions
Features
AppWindow/Models
BotConfig/Models/Api/Toastielab
Home
Models
Api
BotEntry.csBotInstanceInfo.csConfig
EventArguments
AvatarChangedEventArgs.csBotExitEventArgs.csLogFlushEventArgs.csProcessStdWriteEventArgs.csUriInputBarEventArgs.cs
WindowSize.csResources
Services
Abstractions
FfmpregResolver.csIAppConfigManager.csIAppResolver.csIBotOrchestrator.csIBotResolver.csIDependencyResolver.csIFfmpegResolver.csILogWriter.csIYtdlpResolver.cs
AppConfigManager.csAppResolver.csEllieOrchestrator.csEllieResolver.csFfmpegLinuxResolver.csFfmpegMacResolver.csFfmpegWindowsResolver.csLogWriter.csMocks
YtdlpResolver.csStyles
ViewLocator.csViewModels
Abstractions
Controls
BotConfigViewModel.csConfigViewModel.csDependencyButtonViewModel.csFakeConsoleViewModel.csHomeViewModel.csLateralBarViewModel.csUriInputViewModel.cs
Windows
Views
251
.editorconfig
251
.editorconfig
|
@ -1,4 +1,3 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
|
@ -9,66 +8,14 @@ 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 ####
|
||||
|
@ -132,49 +79,49 @@ dotnet_style_allow_statement_immediately_after_block_experimental = false
|
|||
#### C# Coding Conventions ####
|
||||
|
||||
# var preferences
|
||||
csharp_style_var_elsewhere = true:warning
|
||||
csharp_style_var_for_built_in_types = true:warning
|
||||
csharp_style_var_when_type_is_apparent = true:warning
|
||||
csharp_style_var_elsewhere = true:error
|
||||
csharp_style_var_for_built_in_types = true:error
|
||||
csharp_style_var_when_type_is_apparent = true:error
|
||||
|
||||
# Expression-bodied members
|
||||
csharp_style_expression_bodied_accessors = true:suggestion
|
||||
csharp_style_expression_bodied_constructors = when_on_single_line:suggestion
|
||||
csharp_style_expression_bodied_indexers = true:suggestion
|
||||
csharp_style_expression_bodied_lambdas = when_on_single_line:suggestion
|
||||
csharp_style_expression_bodied_lambdas = true:suggestion
|
||||
csharp_style_expression_bodied_local_functions = true:suggestion
|
||||
csharp_style_expression_bodied_methods = when_on_single_line:suggestion
|
||||
csharp_style_expression_bodied_operators = when_on_single_line:suggestion
|
||||
csharp_style_expression_bodied_properties = true:suggestion
|
||||
|
||||
# Pattern matching preferences
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:warning
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true:warning
|
||||
csharp_style_prefer_not_pattern = true:warning
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:error
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true:error
|
||||
csharp_style_prefer_not_pattern = true:error
|
||||
csharp_style_prefer_pattern_matching = true:suggestion
|
||||
csharp_style_prefer_switch_expression = true:suggestion
|
||||
csharp_style_prefer_switch_expression = true
|
||||
|
||||
# Null-checking preferences
|
||||
csharp_style_conditional_delegate_call = true:warning
|
||||
csharp_style_conditional_delegate_call = true:error
|
||||
|
||||
# Modifier preferences
|
||||
csharp_prefer_static_local_function = true:suggestion
|
||||
csharp_prefer_static_local_function = true
|
||||
csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async
|
||||
|
||||
# Code-block preferences
|
||||
csharp_prefer_braces = when_multiline:suggestion
|
||||
csharp_prefer_simple_using_statement = true:suggestion
|
||||
csharp_prefer_simple_using_statement = true
|
||||
|
||||
# Expression-level preferences
|
||||
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_prefer_simple_default_expression = true
|
||||
csharp_style_deconstructed_variable_declaration = true
|
||||
csharp_style_implicit_object_creation_when_type_is_apparent = true:error
|
||||
csharp_style_inlined_variable_declaration = true:warning
|
||||
csharp_style_pattern_local_over_anonymous_function = true
|
||||
csharp_style_prefer_index_operator = true:suggestion
|
||||
csharp_style_prefer_range_operator = true:suggestion
|
||||
csharp_style_prefer_index_operator = true
|
||||
csharp_style_prefer_range_operator = true
|
||||
csharp_style_throw_expression = true:error
|
||||
csharp_style_unused_value_assignment_preference = discard_variable:warning
|
||||
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
|
||||
csharp_style_unused_value_expression_statement_preference = discard_variable
|
||||
|
||||
# 'using' directive preferences
|
||||
csharp_using_directive_placement = outside_namespace:error
|
||||
|
@ -183,9 +130,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:silent
|
||||
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:silent
|
||||
csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
|
||||
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
|
||||
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false
|
||||
csharp_style_allow_embedded_statements_on_same_line_experimental = true
|
||||
|
||||
|
||||
#### C# Formatting Rules ####
|
||||
|
@ -242,40 +189,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 = upper_camel_case_style
|
||||
dotnet_naming_rule.class_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.struct_should_be_pascal_case.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.struct_should_be_pascal_case.severity = error
|
||||
dotnet_naming_rule.struct_should_be_pascal_case.symbols = struct
|
||||
dotnet_naming_rule.struct_should_be_pascal_case.style = upper_camel_case_style
|
||||
dotnet_naming_rule.struct_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.severity = error
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.style = i_upper_camel_case_style
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
||||
|
||||
dotnet_naming_rule.types_should_be_pascal_case.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.types_should_be_pascal_case.severity = error
|
||||
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
|
||||
dotnet_naming_rule.types_should_be_pascal_case.style = upper_camel_case_style
|
||||
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.enum_should_be_pascal_case.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.enum_should_be_pascal_case.severity = error
|
||||
dotnet_naming_rule.enum_should_be_pascal_case.symbols = enum
|
||||
dotnet_naming_rule.enum_should_be_pascal_case.style = upper_camel_case_style
|
||||
dotnet_naming_rule.enum_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.property_should_be_pascal_case.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.property_should_be_pascal_case.severity = error
|
||||
dotnet_naming_rule.property_should_be_pascal_case.symbols = property
|
||||
dotnet_naming_rule.property_should_be_pascal_case.style = upper_camel_case_style
|
||||
dotnet_naming_rule.property_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.method_should_be_pascal_case.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.method_should_be_pascal_case.severity = error
|
||||
dotnet_naming_rule.method_should_be_pascal_case.symbols = method
|
||||
dotnet_naming_rule.method_should_be_pascal_case.style = upper_camel_case_style
|
||||
dotnet_naming_rule.method_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.async_method_should_be_ends_with_async.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.async_method_should_be_ends_with_async.severity = warning
|
||||
dotnet_naming_rule.async_method_should_be_ends_with_async.severity = error
|
||||
dotnet_naming_rule.async_method_should_be_ends_with_async.symbols = async_method
|
||||
dotnet_naming_rule.async_method_should_be_ends_with_async.style = ends_with_async
|
||||
|
||||
|
@ -287,17 +234,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 = upper_camel_case_style
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.local_variable_should_be_camel_case.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.local_variable_should_be_camel_case.severity = error
|
||||
dotnet_naming_rule.local_variable_should_be_camel_case.symbols = local_variable
|
||||
dotnet_naming_rule.local_variable_should_be_camel_case.style = lower_camel_case_style_1
|
||||
dotnet_naming_rule.local_variable_should_be_camel_case.style = camel_case
|
||||
|
||||
dotnet_naming_rule.public_anything_should_be_pascal_case.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.public_anything_should_be_pascal_case.severity = error
|
||||
dotnet_naming_rule.public_anything_should_be_pascal_case.symbols = public_anything
|
||||
dotnet_naming_rule.public_anything_should_be_pascal_case.style = upper_camel_case_style
|
||||
dotnet_naming_rule.public_anything_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.constants_rule.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.constants_rule.severity = error
|
||||
|
@ -341,7 +288,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 = begins_with_underscore
|
||||
dotnet_naming_rule.private_constants_rule.style = lower_camel_case_style
|
||||
dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols
|
||||
|
||||
dotnet_naming_rule.private_instance_fields_rule.import_to_resharper = as_predefined
|
||||
|
@ -351,12 +298,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 = begins_with_underscore
|
||||
dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style
|
||||
dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols
|
||||
|
||||
dotnet_naming_rule.private_static_readonly_rule.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.private_static_readonly_rule.severity = error
|
||||
dotnet_naming_rule.private_static_readonly_rule.style = begins_with_underscore
|
||||
dotnet_naming_rule.private_static_readonly_rule.style = lower_camel_case_style
|
||||
dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols
|
||||
|
||||
dotnet_naming_rule.property_rule.import_to_resharper = as_predefined
|
||||
|
@ -371,7 +318,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 = lower_camel_case_style_1
|
||||
dotnet_naming_rule.public_fields_rule.style = camel_case
|
||||
dotnet_naming_rule.public_fields_rule.symbols = protected_fields_symbols
|
||||
|
||||
dotnet_naming_rule.static_readonly_rule.import_to_resharper = as_predefined
|
||||
|
@ -535,91 +482,22 @@ dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities = *
|
|||
dotnet_naming_symbols.type_parameters_symbols.applicable_kinds = type_parameter
|
||||
|
||||
|
||||
#### Style Rules ####
|
||||
#### Visual Studio Specific Rules ####
|
||||
|
||||
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
|
||||
# CA1822: Mark members as static
|
||||
dotnet_diagnostic.CA1822.severity = none
|
||||
|
||||
# IDE0004: Cast is redundant
|
||||
dotnet_diagnostic.IDE0004.severity = error
|
||||
|
||||
# IDE0011: Add braces to 'if'/'else' statement
|
||||
dotnet_diagnostic.IDE0011.severity = none
|
||||
|
||||
# IDE0058: Expression value is never used
|
||||
dotnet_diagnostic.IDE0058.severity = none
|
||||
|
||||
# Use primary constructor
|
||||
dotnet_diagnostic.IDE0290.severity = none
|
||||
# IDE0011: Add braces to 'if'/'else' statement
|
||||
dotnet_diagnostic.IDE0011.severity = none
|
||||
|
||||
# IDE0290: Use primary constructor
|
||||
dotnet_diagnostic.IDE0290.severity = none
|
||||
|
||||
#### ReSharper Properties ####
|
||||
|
||||
|
@ -703,16 +581,14 @@ 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_for_multiline
|
||||
resharper_braces_for_fixed = required
|
||||
resharper_braces_for_for = required_for_multiline
|
||||
resharper_braces_for_foreach = required_for_multiline
|
||||
resharper_braces_for_ifelse = required_for_multiline
|
||||
resharper_braces_for_lock = required_for_multiline
|
||||
resharper_braces_for_using = required_for_multiline
|
||||
resharper_braces_for_lock = required
|
||||
resharper_braces_for_using = required
|
||||
resharper_braces_for_while = required_for_multiline
|
||||
resharper_braces_redundant = true
|
||||
resharper_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
|
||||
|
@ -810,7 +686,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 =
|
||||
resharper_instance_members_qualify_declared_in = base_class
|
||||
resharper_int_align_assignments = false
|
||||
resharper_int_align_binary_expressions = false
|
||||
resharper_int_align_declaration_names = false
|
||||
|
@ -911,7 +787,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 = false
|
||||
resharper_prefer_separate_deconstructed_variables_declaration = true
|
||||
resharper_preserve_spaces_inside_tags = pre,textarea
|
||||
resharper_qualified_using_at_nested_scope = false
|
||||
resharper_quote_style = doublequoted
|
||||
|
@ -1062,7 +938,6 @@ 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
|
||||
|
@ -1168,7 +1043,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 = none
|
||||
resharper_arrange_missing_parentheses_highlighting = hint
|
||||
resharper_arrange_namespace_body_highlighting = error
|
||||
resharper_arrange_object_creation_when_type_evident_highlighting = suggestion
|
||||
resharper_arrange_object_creation_when_type_not_evident_highlighting = suggestion
|
||||
|
@ -1283,7 +1158,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 = none
|
||||
resharper_convert_type_check_pattern_to_null_check_highlighting = warning
|
||||
resharper_convert_type_check_to_null_check_highlighting = warning
|
||||
resharper_co_variant_array_conversion_highlighting = warning
|
||||
resharper_c_declaration_with_implicit_int_type_highlighting = warning
|
||||
|
@ -1402,8 +1277,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 = hint
|
||||
resharper_member_can_be_private_local_highlighting = hint
|
||||
resharper_member_can_be_private_global_highlighting = suggestion
|
||||
resharper_member_can_be_private_local_highlighting = suggestion
|
||||
resharper_member_can_be_protected_global_highlighting = suggestion
|
||||
resharper_member_can_be_protected_local_highlighting = suggestion
|
||||
resharper_member_hides_interface_member_with_default_implementation_highlighting = warning
|
||||
|
@ -1531,7 +1406,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 = none
|
||||
resharper_redundant_base_qualifier_highlighting = warning
|
||||
resharper_redundant_blank_lines_highlighting = none
|
||||
resharper_redundant_bool_compare_highlighting = warning
|
||||
resharper_redundant_case_label_highlighting = warning
|
||||
|
@ -1541,7 +1416,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 = hint
|
||||
resharper_redundant_default_member_initializer_highlighting = warning
|
||||
resharper_redundant_delegate_creation_highlighting = warning
|
||||
resharper_redundant_disable_warning_comment_highlighting = warning
|
||||
resharper_redundant_discard_designation_highlighting = suggestion
|
||||
|
@ -1867,17 +1742,3 @@ resharper_void_method_with_must_use_return_value_attribute_highlighting = warnin
|
|||
resharper_with_expression_instead_of_initializer_highlighting = suggestion
|
||||
resharper_wrong_indent_size_highlighting = none
|
||||
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
|
|
@ -1,56 +0,0 @@
|
|||
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
|
|
@ -1,30 +0,0 @@
|
|||
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
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -481,6 +481,3 @@ $RECYCLE.BIN/
|
|||
# Built files
|
||||
build/*
|
||||
zips/*
|
||||
|
||||
# Scripts
|
||||
release.ps1
|
|
@ -5,11 +5,6 @@ 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
|
||||
|
|
|
@ -14,9 +14,9 @@
|
|||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceInclude Source="/Avalonia/Resources/Colors.axaml" />
|
||||
<ResourceInclude Source="/Avalonia/Resources/Fonts.axaml" />
|
||||
<ResourceInclude Source="/Avalonia/Resources/Images.axaml" />
|
||||
<ResourceInclude Source="/Resources/Colors.axaml" />
|
||||
<ResourceInclude Source="/Resources/Fonts.axaml" />
|
||||
<ResourceInclude Source="/Resources/Images.axaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
|
@ -24,12 +24,12 @@
|
|||
<!--Styles-->
|
||||
<!--Think of them like CSS classes-->
|
||||
<Application.Styles>
|
||||
<StyleInclude Source="/Avalonia/Styles/EllieStyles.axaml" />
|
||||
<StyleInclude Source="/Styles/EllieStyles.axaml" />
|
||||
<!--Generated Here: https://theme.xaml.live/-->
|
||||
<FluentTheme>
|
||||
<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" />
|
||||
<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>
|
||||
</Application.Styles>
|
||||
|
|
|
@ -3,7 +3,7 @@ using Avalonia.Controls;
|
|||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using EllieHub.Features.AppWindow.Views.Windows;
|
||||
using EllieHub.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())
|
||||
.RegisterAppServices()
|
||||
.RegisterServices()
|
||||
.BuildServiceProvider(true);
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
@ -14,9 +14,4 @@ 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,7 +1,6 @@
|
|||
using Avalonia.Media;
|
||||
using Avalonia.Media.Immutable;
|
||||
using Avalonia.Platform.Storage;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace EllieHub.Common;
|
||||
|
@ -15,35 +14,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.Join(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "EllieHubDebug");
|
||||
public static string AppDefaultConfigDirectoryUri { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "EllieHubDebug");
|
||||
#else
|
||||
public static string AppDefaultConfigDirectoryUri { get; } = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "EllieHub");
|
||||
public static string AppDefaultConfigDirectoryUri { get; } = Path.Combine(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.Join(AppDefaultConfigDirectoryUri, "Bots");
|
||||
public static string AppDefaultBotDirectoryUri { get; } = Path.Combine(AppDefaultConfigDirectoryUri, "Bots");
|
||||
|
||||
/// <summary>
|
||||
/// Defines the default location where the backups of bot instances are stored.
|
||||
/// </summary>
|
||||
public static string AppDefaultBotBackupDirectoryUri { get; } = Path.Join(AppDefaultConfigDirectoryUri, "Backups");
|
||||
public static string AppDefaultBotBackupDirectoryUri { get; } = Path.Combine(AppDefaultConfigDirectoryUri, "Backups");
|
||||
|
||||
/// <summary>
|
||||
/// Defines the default location where the logs of bot instances are stored.
|
||||
/// </summary>
|
||||
public static string AppDefaultLogDirectoryUri { get; } = Path.Join(AppDefaultConfigDirectoryUri, "Logs");
|
||||
public static string AppDefaultLogDirectoryUri { get; } = Path.Combine(AppDefaultConfigDirectoryUri, "Logs");
|
||||
|
||||
/// <summary>
|
||||
/// Defines the location of the application's configuration file.
|
||||
/// </summary>
|
||||
public static string AppConfigUri { get; } = Path.Join(AppDefaultConfigDirectoryUri, "config.json");
|
||||
public static string AppConfigUri { get; } = Path.Combine(AppDefaultConfigDirectoryUri, "config.json");
|
||||
|
||||
/// <summary>
|
||||
/// Defines the location of the application's dependencies.
|
||||
/// </summary>
|
||||
public static string AppDepsUri { get; } = Path.Join(AppDefaultConfigDirectoryUri, "Dependencies");
|
||||
public static string AppDepsUri { get; } = Path.Combine(AppDefaultConfigDirectoryUri, "Dependencies");
|
||||
|
||||
/// <summary>
|
||||
/// Defines a transparent color brush.
|
||||
|
@ -56,19 +55,13 @@ public static partial class AppStatics
|
|||
public static FilePickerOpenOptions ImageFilePickerOptions { get; } = new()
|
||||
{
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter =
|
||||
[
|
||||
new("Image") { Patterns = ["*.png", "*.jpg", "*.jpeg", "*.gif", "*.webp"]},
|
||||
new("All") { Patterns = ["*.*"]}
|
||||
]
|
||||
FileTypeFilter = new FilePickerFileType[]
|
||||
{
|
||||
new("Image") { Patterns = new[] { "*.png", "*.jpg", "*.jpeg", "*.gif", "*.webp" } },
|
||||
new("All") { Patterns = new[] { "*.*" } }
|
||||
}
|
||||
};
|
||||
|
||||
/// <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,5 +1,8 @@
|
|||
using Avalonia.Platform;
|
||||
using SkiaSharp;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace EllieHub.Common;
|
||||
|
||||
|
@ -8,30 +11,205 @@ 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 embedded with this application.
|
||||
/// Loads an image embeded with this application.
|
||||
/// </summary>
|
||||
/// <param name="uri">An uri that starts with "avares://"</param>
|
||||
/// <remarks>Valid uris must start with "avares://".</remarks>
|
||||
/// <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)
|
||||
/// <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)
|
||||
{
|
||||
return (string.IsNullOrWhiteSpace(uri) || !uri.StartsWith("avares://", StringComparison.Ordinal))
|
||||
? SKBitmap.Decode(AssetLoader.Open(new(AppConstants.BotAvatarUri)))
|
||||
: SKBitmap.Decode(AssetLoader.Open(new(uri)));
|
||||
return (string.IsNullOrWhiteSpace(uri) || !uri.StartsWith("avares://"))
|
||||
? SKBitmap.Decode(AssetLoader.Open(new Uri(AppConstants.BotAvatarUri)))
|
||||
: SKBitmap.Decode(AssetLoader.Open(new Uri(uri)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the image at the specified location or the bot avatar placeholder if it was not found.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <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>
|
||||
/// <returns>The requested image or the default bot avatar placeholder.</returns>
|
||||
public static SKBitmap LoadLocalImage(string? imagePath)
|
||||
public static SKBitmap LoadLocalImage(string? uri = default)
|
||||
{
|
||||
return (File.Exists(imagePath))
|
||||
? SKBitmap.Decode(imagePath)
|
||||
: LoadEmbeddedImage(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;
|
||||
}
|
||||
}
|
|
@ -8,12 +8,12 @@ public static class WindowConstants
|
|||
/// <summary>
|
||||
/// Defines the default width of the window.
|
||||
/// </summary>
|
||||
public const string DefaultWindowWidth = "900";
|
||||
public const string DefaultWindowWidth = "885";
|
||||
|
||||
/// <summary>
|
||||
/// Defines the default height of the window.
|
||||
/// </summary>
|
||||
public const string DefaultWindowHeight = "575";
|
||||
public const string DefaultWindowHeight = "570";
|
||||
|
||||
/// <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 previewer. Please, use the parameterized constructor instead.";
|
||||
public const string DesignerCtorWarning = "This constructor exists to satisfy Avalonia's designer. Please, use the parameterized constructor instead.";
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace EllieHub.Avalonia.DesignData.Common;
|
||||
namespace EllieHub.DesignData.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Defines objects useful at design-time.
|
|
@ -1,14 +1,11 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
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;
|
||||
using EllieHub.DesignData.Common;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using EllieHub.Services.Mocks;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
using EllieHub.Views.Windows;
|
||||
|
||||
namespace EllieHub.Avalonia.DesignData.Controls;
|
||||
namespace EllieHub.DesignData.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Mock view-model for <see cref="BotConfigViewModel"/>.
|
|
@ -1,12 +1,12 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
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;
|
||||
using EllieHub.DesignData.Common;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using EllieHub.Services.Mocks;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
using EllieHub.ViewModels.Windows;
|
||||
using EllieHub.Views.Windows;
|
||||
|
||||
namespace EllieHub.Avalonia.DesignData.Controls;
|
||||
namespace EllieHub.DesignData.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Mock view-model for <see cref="ConfigViewModel"/>.
|
|
@ -1,9 +1,9 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using EllieHub.Avalonia.DesignData.Common;
|
||||
using EllieHub.Features.AppWindow.Views.Windows;
|
||||
using EllieHub.Features.Common.ViewModels;
|
||||
using EllieHub.DesignData.Common;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
using EllieHub.Views.Windows;
|
||||
|
||||
namespace EllieHub.Avalonia.DesignData.Controls;
|
||||
namespace EllieHub.DesignData.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Mock view-model for <see cref="DependencyButtonViewModel"/>.
|
|
@ -1,6 +1,6 @@
|
|||
using EllieHub.Features.BotConfig.ViewModels;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
|
||||
namespace EllieHub.Avalonia.DesignData.Controls;
|
||||
namespace EllieHub.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()
|
||||
public DesignFakeConsoleViewModel() : base()
|
||||
=> Watermark = "Sample watermark.";
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
using EllieHub.Features.Home.ViewModels;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
|
||||
namespace EllieHub.Avalonia.DesignData.Controls;
|
||||
namespace EllieHub.DesignData.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Mock view-model for <see cref="HomeViewModel"/>.
|
|
@ -1,9 +1,9 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using EllieHub.Avalonia.DesignData.Common;
|
||||
using EllieHub.Features.AppConfig.Services.Mocks;
|
||||
using EllieHub.Features.AppWindow.ViewModels;
|
||||
using EllieHub.DesignData.Common;
|
||||
using EllieHub.Services.Mocks;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
|
||||
namespace EllieHub.Avalonia.DesignData.Controls;
|
||||
namespace EllieHub.DesignData.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Mock view-model for <see cref="LateralBarViewModel"/>.
|
|
@ -1,9 +1,9 @@
|
|||
using Avalonia.Platform.Storage;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using EllieHub.Avalonia.DesignData.Common;
|
||||
using EllieHub.Features.Common.ViewModels;
|
||||
using EllieHub.DesignData.Common;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
|
||||
namespace EllieHub.Avalonia.DesignData.Controls;
|
||||
namespace EllieHub.DesignData.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Mock view-model for <see cref="UriInputBarViewModel"/>.
|
|
@ -1,6 +1,6 @@
|
|||
using EllieHub.Features.AppConfig.ViewModels;
|
||||
using EllieHub.ViewModels.Windows;
|
||||
|
||||
namespace EllieHub.Avalonia.DesignData.Windows;
|
||||
namespace EllieHub.DesignData.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Mock view-model for <see cref="AboutMeViewModel"/>.
|
|
@ -1,9 +1,9 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using EllieHub.Avalonia.DesignData.Common;
|
||||
using EllieHub.Features.AppWindow.ViewModels;
|
||||
using EllieHub.Features.Home.ViewModels;
|
||||
using EllieHub.DesignData.Common;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
using EllieHub.ViewModels.Windows;
|
||||
|
||||
namespace EllieHub.Avalonia.DesignData.Windows;
|
||||
namespace EllieHub.DesignData.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Mock view-model for <see cref="AppViewModel"/>.
|
|
@ -1,6 +1,6 @@
|
|||
using EllieHub.Features.Home.ViewModels;
|
||||
using EllieHub.ViewModels.Windows;
|
||||
|
||||
namespace EllieHub.Avalonia.DesignData.Windows;
|
||||
namespace EllieHub.DesignData.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Mock view-model for <see cref="UpdateViewModel"/>.
|
|
@ -2,12 +2,13 @@
|
|||
<PropertyGroup>
|
||||
<!--Project Settings-->
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net8.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>
|
||||
|
@ -15,12 +16,11 @@
|
|||
|
||||
<!--Publishing-->
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||
<SelfContained>true</SelfContained>
|
||||
<DebugType>embedded</DebugType>
|
||||
|
||||
<!--Version-->
|
||||
<VersionPrefix>1.0.4.0</VersionPrefix>
|
||||
<VersionPrefix>1.0.1.0</VersionPrefix>
|
||||
|
||||
<!--Avalonia Settings-->
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
|
@ -30,13 +30,6 @@
|
|||
<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\**" />
|
||||
|
||||
|
@ -46,23 +39,20 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<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" />
|
||||
<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" />
|
||||
<!--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.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
||||
<PackageReference Include="SingleFileExtractor.Core" Version="2.2.0" />
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
||||
<PackageReference Include="SkiaImageView.Avalonia11" Version="1.5.0" />
|
||||
<PackageReference Include="Toastie.Utilities" Version="3.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
using Avalonia.Controls;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using EllieHub.Features.AppConfig.Models;
|
||||
using EllieHub.Features.AppConfig.Services;
|
||||
using EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
using EllieHub.Features.AppConfig.Services.Mocks;
|
||||
using EllieHub.Features.AppWindow.Views.Windows;
|
||||
using EllieHub.Features.BotConfig.Services;
|
||||
using EllieHub.Features.BotConfig.Services.Abstractions;
|
||||
using EllieHub.Features.BotConfig.Services.Mocks;
|
||||
using EllieHub.Features.Home.Services;
|
||||
using EllieHub.Features.Home.Services.Abstractions;
|
||||
using EllieHub.Models.Config;
|
||||
using EllieHub.Services;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using EllieHub.Services.Mocks;
|
||||
using EllieHub.Views.Windows;
|
||||
using ReactiveUI;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
|
@ -47,7 +42,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 RegisterAppServices(this IServiceCollection serviceCollection)
|
||||
public static IServiceCollection RegisterServices(this IServiceCollection serviceCollection)
|
||||
{
|
||||
// Design-time
|
||||
if (Design.IsDesignMode)
|
||||
|
@ -65,26 +60,15 @@ public static class IServiceCollectionExt
|
|||
|
||||
// Web requests
|
||||
serviceCollection.AddHttpClient();
|
||||
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
|
||||
});
|
||||
serviceCollection.AddHttpClient(AppConstants.NoRedirectClient) // Client that doesn't allow automatic reditections
|
||||
.ConfigureHttpMessageHandlerBuilder(builder => builder.PrimaryHandler = new HttpClientHandler() { AllowAutoRedirect = false });
|
||||
|
||||
// App settings
|
||||
serviceCollection.AddSingleton<IAppConfigManager, AppConfigManager>();
|
||||
serviceCollection.AddSingleton<ReadOnlyAppSettings>();
|
||||
serviceCollection.AddSingleton<ReadOnlyAppConfig>();
|
||||
serviceCollection.AddSingleton(_ =>
|
||||
(File.Exists(AppStatics.AppConfigUri))
|
||||
? JsonSerializer.Deserialize<AppSettings>(File.ReadAllText(AppStatics.AppConfigUri)) ?? new()
|
||||
? JsonSerializer.Deserialize<AppConfig>(File.ReadAllText(AppStatics.AppConfigUri)) ?? new()
|
||||
: new()
|
||||
);
|
||||
|
||||
|
|
31
EllieHub/Extensions/IServiceProviderExt.cs
Normal file
31
EllieHub/Extensions/IServiceProviderExt.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
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.")
|
||||
: (resource is T castResource)
|
||||
? castResource
|
||||
: throw new InvalidCastException($"Could not convert resource of type '{resource?.GetType().FullName}' to '{nameof(T)}'.");
|
||||
: (!Utilities.TryCastTo<T>(resource, out var result))
|
||||
? throw new InvalidCastException($"Could not convert resource of type '{resource?.GetType()?.FullName}' to '{nameof(T)}'.")
|
||||
: result;
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
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,13 +0,0 @@
|
|||
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
|
||||
);
|
|
@ -1,13 +0,0 @@
|
|||
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,13 +0,0 @@
|
|||
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
|
||||
);
|
|
@ -1,13 +0,0 @@
|
|||
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,265 +0,0 @@
|
|||
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,6 +1,6 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EllieHub.Features.AppConfig.Models.Api.Evermeet;
|
||||
namespace EllieHub.Models.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Represents download information for a <see cref="EvermeetInfo"/> component.
|
|
@ -1,6 +1,6 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EllieHub.Features.AppConfig.Models.Api.Evermeet;
|
||||
namespace EllieHub.Models.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a response from the "https://evermeet.cx/ffmpeg/info" endpoint.
|
|
@ -1,6 +1,6 @@
|
|||
using EllieHub.Features.AppWindow.ViewModels;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
|
||||
namespace EllieHub.Features.AppWindow.Models;
|
||||
namespace EllieHub.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a bot entry in the <see cref="LateralBarViewModel"/>.
|
|
@ -1,4 +1,4 @@
|
|||
namespace EllieHub.Features.AppWindow.Models;
|
||||
namespace EllieHub.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the information of a bot instance.
|
|
@ -1,14 +1,13 @@
|
|||
using EllieHub.Enums;
|
||||
using EllieHub.Features.AppWindow.Models;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace EllieHub.Features.AppConfig.Models;
|
||||
namespace EllieHub.Models.Config;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the settings of the application.
|
||||
/// </summary>
|
||||
/// <remarks>Prefer using <see cref="ReadOnlyAppSettings"/> in dependency injection, if possible.</remarks>
|
||||
public sealed class AppSettings
|
||||
/// <remarks>Prefer using <see cref="ReadOnlyAppConfig"/> in dependency injection, if possible.</remarks>
|
||||
public sealed class AppConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// The absolute path to the directory where the bot instances are stored.
|
|
@ -1,14 +1,13 @@
|
|||
using EllieHub.Enums;
|
||||
using EllieHub.Features.AppWindow.Models;
|
||||
|
||||
namespace EllieHub.Features.AppConfig.Models;
|
||||
namespace EllieHub.Models.Config;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a read-only version of <see cref="AppSettings"/>.
|
||||
/// Represents a read-only version of <see cref="AppConfig"/>.
|
||||
/// </summary>
|
||||
public sealed class ReadOnlyAppSettings
|
||||
public sealed class ReadOnlyAppConfig
|
||||
{
|
||||
private readonly AppSettings _appConfig;
|
||||
private readonly AppConfig _appConfig;
|
||||
|
||||
/// <summary>
|
||||
/// The absolute path to the directory where the bot instances are stored.
|
||||
|
@ -65,9 +64,9 @@ public sealed class ReadOnlyAppSettings
|
|||
=> _appConfig.BotEntries;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a read-only version of <see cref="AppSettings"/>.
|
||||
/// Initializes a read-only version of <see cref="AppConfig"/>.
|
||||
/// </summary>
|
||||
/// <param name="appConfig">The application settings to read from.</param>
|
||||
public ReadOnlyAppSettings(AppSettings appConfig)
|
||||
public ReadOnlyAppConfig(AppConfig appConfig)
|
||||
=> _appConfig = appConfig;
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
using SkiaSharp;
|
||||
|
||||
namespace EllieHub.Features.BotConfig.Models;
|
||||
namespace EllieHub.Models.EventArguments;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the event arguments for when the user sets a new avatar for a bot instance.
|
|
@ -1,4 +1,4 @@
|
|||
namespace EllieHub.Features.BotConfig.Models;
|
||||
namespace EllieHub.Models.EventArguments;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the event arguments when a bot process exits.
|
||||
|
@ -15,21 +15,14 @@ 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>
|
||||
/// <param name="message">The message for the bot process that just exited.</param>
|
||||
public BotExitEventArgs(Guid botId, int exitCode, string message)
|
||||
public BotExitEventArgs(Guid botId, int exitCode)
|
||||
{
|
||||
Id = botId;
|
||||
ExitCode = exitCode;
|
||||
Message = message;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
namespace EllieHub.Features.BotConfig.Models;
|
||||
namespace EllieHub.Models.EventArguments;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the event arguments when a log is written to disk.
|
|
@ -1,4 +1,4 @@
|
|||
namespace EllieHub.Features.BotConfig.Models;
|
||||
namespace EllieHub.Models.EventArguments;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the event arguments when a bot process writes to stdout or stderr.
|
|
@ -1,6 +1,6 @@
|
|||
using EllieHub.Features.Common.ViewModels;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
|
||||
namespace EllieHub.Features.Common.Models;
|
||||
namespace EllieHub.Models.EventArguments;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the event arguments for when a valid uri is set to a <see cref="UriInputBarViewModel"/>.
|
|
@ -1,4 +1,4 @@
|
|||
namespace EllieHub.Features.AppConfig.Models;
|
||||
namespace EllieHub.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the dimensions of the application's window.
|
|
@ -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'>#3498DB</SolidColorBrush>
|
||||
<SolidColorBrush x:Key='BotSelectionColor'>#FF0067</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'>#206694</SolidColorBrush>
|
||||
<SolidColorBrush x:Key='BotSelectionColor'>#D90058</SolidColorBrush>
|
||||
<SolidColorBrush x:Key='HyperlinkColor'>#5090BB</SolidColorBrush>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
|
@ -1,9 +1,5 @@
|
|||
<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">
|
||||
|
@ -17,4 +13,7 @@
|
|||
</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>
|
|
@ -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"><![CDATA[avares://EllieHub/Assets/Light/ellieupdatericon.ico]]></WindowIcon>
|
||||
<WindowIcon x:Key="EllieHubIcon">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"><![CDATA[avares://EllieHub/Assets/Dark/ellieupdatericon.ico]]></WindowIcon>
|
||||
<WindowIcon x:Key="EllieHubIcon">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>
|
|
@ -1,13 +1,11 @@
|
|||
using Toastie.Utilities;
|
||||
|
||||
namespace EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
namespace EllieHub.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.
|
||||
|
@ -15,7 +13,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; }
|
||||
|
@ -24,7 +22,7 @@ public abstract class FfmpegResolver : IFfmpegResolver
|
|||
public virtual async ValueTask<bool?> CanUpdateAsync(CancellationToken cToken = default)
|
||||
{
|
||||
// Check where ffmpeg is referenced.
|
||||
using var whereProcess = ToastieUtilities.StartProcess(_programVerifier, FfmpegProcessName, true);
|
||||
using var whereProcess = Utilities.StartProcess(_programVerifier, FfmpegProcessName);
|
||||
var installationPath = await whereProcess.StandardOutput.ReadToEndAsync(cToken);
|
||||
|
||||
// If ffmpeg is present but not managed by us, just report it is installed.
|
||||
|
@ -34,7 +32,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 || !ToastieUtilities.ProgramExists("ffprobe"))
|
||||
if (currentVer is null || !await Utilities.ProgramExistsAsync("ffprobe", cToken))
|
||||
return null;
|
||||
|
||||
var latestVer = await GetLatestVersionAsync(cToken);
|
||||
|
@ -46,20 +44,20 @@ public abstract class FfmpegResolver : IFfmpegResolver
|
|||
public virtual async ValueTask<string?> GetCurrentVersionAsync(CancellationToken cToken = default)
|
||||
{
|
||||
// If ffmpeg is not accessible from the shell...
|
||||
if (!ToastieUtilities.ProgramExists(FfmpegProcessName))
|
||||
if (!await Utilities.ProgramExistsAsync(FfmpegProcessName, cToken))
|
||||
{
|
||||
// And doesn't exist in the dependencies folder,
|
||||
// report that ffmpeg is not installed.
|
||||
if (!File.Exists(Path.Join(AppStatics.AppDepsUri, FileName)))
|
||||
if (!File.Exists(Path.Combine(AppStatics.AppDepsUri, FileName)))
|
||||
return null;
|
||||
|
||||
// Else, add the dependencies directory to the PATH envar,
|
||||
// then try again.
|
||||
ToastieUtilities.AddPathToPATHEnvar(AppStatics.AppDepsUri);
|
||||
Utilities.AddPathToPATHEnvar(AppStatics.AppDepsUri);
|
||||
return await GetCurrentVersionAsync(cToken);
|
||||
}
|
||||
|
||||
using var ffmpeg = ToastieUtilities.StartProcess(FfmpegProcessName, "-version", true);
|
||||
using var ffmpeg = Utilities.StartProcess(FfmpegProcessName, "-version");
|
||||
var match = AppStatics.FfmpegVersionRegex.Match(await ffmpeg.StandardOutput.ReadLineAsync(cToken) ?? string.Empty);
|
||||
|
||||
return match.Groups[1].Value;
|
|
@ -1,7 +1,7 @@
|
|||
using EllieHub.Features.AppConfig.Models;
|
||||
using EllieHub.Features.AppWindow.Models;
|
||||
using EllieHub.Models;
|
||||
using EllieHub.Models.Config;
|
||||
|
||||
namespace EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
namespace EllieHub.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a service that manages the application's settings.
|
||||
|
@ -11,7 +11,7 @@ public interface IAppConfigManager
|
|||
/// <summary>
|
||||
/// The application settings.
|
||||
/// </summary>
|
||||
ReadOnlyAppSettings AppConfig { get; }
|
||||
ReadOnlyAppConfig 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<AppSettings> action, CancellationToken cToken = default);
|
||||
ValueTask UpdateConfigAsync(Action<AppConfig> action, CancellationToken cToken = default);
|
||||
}
|
|
@ -1,6 +1,4 @@
|
|||
using EllieHub.Features.Common.Services.Abstractions;
|
||||
|
||||
namespace EllieHub.Features.Home.Services.Abstractions;
|
||||
namespace EllieHub.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a service that updates this application.
|
|
@ -1,9 +1,9 @@
|
|||
using EllieHub.Features.BotConfig.Models;
|
||||
using EllieHub.Models.EventArguments;
|
||||
|
||||
namespace EllieHub.Features.BotConfig.Services.Abstractions;
|
||||
namespace EllieHub.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an object that coordinates multiple running processes of EllieBot.
|
||||
/// Represents an object that coordinates multiple running processes of Ellie.
|
||||
/// </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 StartBot(Guid botId);
|
||||
bool Start(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 StopBot(Guid botId);
|
||||
bool Stop(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 StopAllBots();
|
||||
bool StopAll();
|
||||
}
|
|
@ -1,6 +1,4 @@
|
|||
using EllieHub.Features.Common.Services.Abstractions;
|
||||
|
||||
namespace EllieHub.Features.BotConfig.Services.Abstractions;
|
||||
namespace EllieHub.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a service that checks, downloads, installs, and updates a bot instance.
|
||||
|
@ -12,11 +10,6 @@ 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,4 +1,4 @@
|
|||
namespace EllieHub.Features.Common.Services.Abstractions;
|
||||
namespace EllieHub.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a service that checks, downloads, installs, and updates a dependency.
|
|
@ -1,6 +1,4 @@
|
|||
using EllieHub.Features.Common.Services.Abstractions;
|
||||
|
||||
namespace EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
namespace EllieHub.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a service that checks, downloads, installs, and updates ffmpeg.
|
|
@ -1,7 +1,7 @@
|
|||
using EllieHub.Features.BotConfig.Models;
|
||||
using EllieHub.Models.EventArguments;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace EllieHub.Features.BotConfig.Services.Abstractions;
|
||||
namespace EllieHub.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a service that writes logs of bot instances to the disk.
|
|
@ -1,6 +1,4 @@
|
|||
using EllieHub.Features.Common.Services.Abstractions;
|
||||
|
||||
namespace EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
namespace EllieHub.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a service that checks, downloads, installs, and updates yt-dlp.
|
|
@ -1,10 +1,9 @@
|
|||
using Toastie.Utilities;
|
||||
using EllieHub.Features.AppConfig.Models;
|
||||
using EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
using EllieHub.Features.AppWindow.Models;
|
||||
using EllieHub.Models;
|
||||
using EllieHub.Models.Config;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace EllieHub.Features.AppConfig.Services;
|
||||
namespace EllieHub.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a service that manages the application's settings.
|
||||
|
@ -12,15 +11,15 @@ namespace EllieHub.Features.AppConfig.Services;
|
|||
public sealed class AppConfigManager : IAppConfigManager
|
||||
{
|
||||
private readonly JsonSerializerOptions _jsonSerializerOptions = new() { WriteIndented = true };
|
||||
private readonly AppSettings _appConfig;
|
||||
private readonly AppConfig _appConfig;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ReadOnlyAppSettings AppConfig { get; }
|
||||
public ReadOnlyAppConfig AppConfig { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a service that manages the application's settings.
|
||||
/// </summary>
|
||||
public AppConfigManager(AppSettings appConfig, ReadOnlyAppSettings readOnlyAppConfig)
|
||||
public AppConfigManager(AppConfig appConfig, ReadOnlyAppConfig readOnlyAppConfig)
|
||||
{
|
||||
_appConfig = appConfig;
|
||||
AppConfig = readOnlyAppConfig;
|
||||
|
@ -32,9 +31,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.Join(_appConfig.BotsDirectoryUri, newBotName), newPosition);
|
||||
var newEntry = new BotInstanceInfo(newBotName, Path.Combine(_appConfig.BotsDirectoryUri, newBotName), newPosition);
|
||||
|
||||
if (!_appConfig.BotEntries.TryAdd(newId, newEntry))
|
||||
throw new InvalidOperationException($"Could not create a new bot entry with Id {newId}.");
|
||||
|
@ -50,7 +49,7 @@ public sealed class AppConfigManager : IAppConfigManager
|
|||
if (!_appConfig.BotEntries.TryRemove(id, out var removedEntry))
|
||||
return null;
|
||||
|
||||
ToastieUtilities.TryDeleteDirectory(removedEntry.InstanceDirectoryUri);
|
||||
Utilities.TryDeleteDirectory(removedEntry.InstanceDirectoryUri);
|
||||
|
||||
await SaveAsync(cToken);
|
||||
|
||||
|
@ -91,7 +90,7 @@ public sealed class AppConfigManager : IAppConfigManager
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask UpdateConfigAsync(Action<AppSettings> action, CancellationToken cToken = default)
|
||||
public ValueTask UpdateConfigAsync(Action<AppConfig> action, CancellationToken cToken = default)
|
||||
{
|
||||
action(_appConfig);
|
||||
return SaveAsync(cToken);
|
185
EllieHub/Services/AppResolver.cs
Normal file
185
EllieHub/Services/AppResolver.cs
Normal file
|
@ -0,0 +1,185 @@
|
|||
using EllieHub.Services.Abstractions;
|
||||
using System.IO.Compression;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace EllieHub.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a service that updates this application.
|
||||
/// </summary>
|
||||
public sealed class AppResolver : IAppResolver
|
||||
{
|
||||
private static readonly string _tempDirectory = Path.GetTempPath();
|
||||
private static readonly string _downloadedFileName = GetDownloadFileName();
|
||||
private static readonly string? _currentUpdaterVersion = Assembly.GetEntryAssembly()?.GetName().Version?.ToString();
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
/// <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>
|
||||
public AppResolver(IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
FileName = (OperatingSystem.IsWindows()) ? "EllieHub.exe" : "EllieHub";
|
||||
BinaryUri = Path.Combine(AppContext.BaseDirectory, FileName);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<string?> GetCurrentVersionAsync(CancellationToken cToken = default)
|
||||
=> ValueTask.FromResult(_currentUpdaterVersion);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void LaunchNewVersion()
|
||||
=> Utilities.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 (!Utilities.CanWriteTo(AppContext.BaseDirectory))
|
||||
return null;
|
||||
|
||||
var currentVersion = await GetCurrentVersionAsync(cToken);
|
||||
|
||||
if (currentVersion is null)
|
||||
return null;
|
||||
|
||||
var latestVersion = await GetLatestVersionAsync(cToken);
|
||||
|
||||
if (latestVersion.Equals(currentVersion, StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
var http = _httpClientFactory.CreateClient();
|
||||
|
||||
return await http.IsUrlValidAsync($"https://toastielab.dev/ToastieSharp/EllieHub/releases/download/{latestVersion}/{_downloadedFileName}", cToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool RemoveOldFiles()
|
||||
{
|
||||
var result = false;
|
||||
|
||||
foreach (var file in Directory.GetFiles(AppContext.BaseDirectory).Where(x => x.EndsWith(OldFileSuffix)))
|
||||
result |= Utilities.TryDeleteFile(file);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<string> GetLatestVersionAsync(CancellationToken cToken = default)
|
||||
{
|
||||
var http = _httpClientFactory.CreateClient(AppConstants.NoRedirectClient);
|
||||
|
||||
var response = await http.GetAsync("https://toastielab.dev/ToastieSharp/EllieHub/releases/latest", cToken);
|
||||
|
||||
var lastSlashIndex = response.Headers.Location?.OriginalString.LastIndexOf('/')
|
||||
?? throw new InvalidOperationException("Failed to get the latest EllieBotUpdater version.");
|
||||
|
||||
return response.Headers.Location.OriginalString[(lastSlashIndex + 1)..];
|
||||
}
|
||||
|
||||
/// <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 (latestVersion.Equals(currentVersion, StringComparison.Ordinal))
|
||||
return (currentVersion, null);
|
||||
|
||||
var http = _httpClientFactory.CreateClient();
|
||||
var appTempLocation = Path.Combine(_tempDirectory, _downloadedFileName[..(_downloadedFileName.LastIndexOf('.'))]);
|
||||
var zipTempLocation = Path.Combine(_tempDirectory, _downloadedFileName);
|
||||
|
||||
try
|
||||
{
|
||||
using var downloadStream = await http.GetStreamAsync($"https://toastielab.dev/ToastieSharp/EllieHub/releases/download/{latestVersion}/{_downloadedFileName}", cToken);
|
||||
|
||||
// Save the zip file
|
||||
using (var fileStream = new FileStream(zipTempLocation, FileMode.Create))
|
||||
await downloadStream.CopyToAsync(fileStream, cToken);
|
||||
|
||||
// Extract the zip file
|
||||
await Task.Run(() => ZipFile.ExtractToDirectory(zipTempLocation, _tempDirectory), cToken);
|
||||
|
||||
// Move the new binary and its dependencies
|
||||
var newFilesUris = Directory.EnumerateFiles(appTempLocation);
|
||||
|
||||
foreach (var newFileUri in newFilesUris)
|
||||
{
|
||||
var destinationUri = Path.Combine(AppContext.BaseDirectory, newFileUri[(newFileUri.LastIndexOf(Path.DirectorySeparatorChar) + 1)..]);
|
||||
|
||||
// Rename the original file from "file" to "file_old".
|
||||
if (File.Exists(destinationUri))
|
||||
File.Move(destinationUri, destinationUri + OldFileSuffix);
|
||||
|
||||
// Move the new file to the application's directory.
|
||||
if (Environment.OSVersion.Platform is not PlatformID.Unix)
|
||||
File.Move(newFileUri, destinationUri, true);
|
||||
else
|
||||
{
|
||||
// Circumvent this issue on Unix systems: https://github.com/dotnet/runtime/issues/31149
|
||||
using var moveProcess = Utilities.StartProcess("mv", $"\"{newFileUri}\" \"{destinationUri}\"");
|
||||
await moveProcess.WaitForExitAsync(cToken);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark the new binary file as executable.
|
||||
if (Environment.OSVersion.Platform is PlatformID.Unix)
|
||||
{
|
||||
using var chmod = Utilities.StartProcess("chmod", $"+x \"{BinaryUri}\"");
|
||||
await chmod.WaitForExitAsync(cToken);
|
||||
}
|
||||
|
||||
return (currentVersion, latestVersion);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup
|
||||
Utilities.TryDeleteFile(zipTempLocation);
|
||||
Utilities.TryDeleteDirectory(appTempLocation);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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.")
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,19 +1,18 @@
|
|||
using EllieHub.Features.AppConfig.Models;
|
||||
using EllieHub.Features.BotConfig.Models;
|
||||
using EllieHub.Features.BotConfig.Services.Abstractions;
|
||||
using EllieHub.Models.Config;
|
||||
using EllieHub.Models.EventArguments;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace EllieHub.Features.BotConfig.Services;
|
||||
namespace EllieHub.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 string _fileName = OperatingSystem.IsWindows() ? "EllieBot.exe" : "EllieBot";
|
||||
private readonly ReadOnlyAppConfig _appConfig;
|
||||
private readonly string _fileName = (OperatingSystem.IsWindows()) ? "NadekoBot.exe" : "NadekoBot";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<IBotOrchestrator, BotExitEventArgs>? OnBotExit;
|
||||
|
@ -28,28 +27,24 @@ public sealed class EllieOrchestrator : IBotOrchestrator
|
|||
/// Creates an object that coordinates multiple running processes of EllieBot.
|
||||
/// </summary>
|
||||
/// <param name="appConfig">The application settings.</param>
|
||||
/// <param name="logWriter">The service that writes bot logs to disk.</param>
|
||||
public EllieOrchestrator(ReadOnlyAppSettings appConfig, ILogWriter logWriter)
|
||||
{
|
||||
_appConfig = appConfig;
|
||||
_logWriter = logWriter;
|
||||
}
|
||||
public EllieOrchestrator(ReadOnlyAppConfig appConfig)
|
||||
=> _appConfig = appConfig;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsBotRunning(Guid botId)
|
||||
=> _runningBots.ContainsKey(botId);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool StartBot(Guid botId)
|
||||
public bool Start(Guid botId)
|
||||
{
|
||||
if (_runningBots.ContainsKey(botId)
|
||||
|| !_appConfig.BotEntries.TryGetValue(botId, out var botEntry)
|
||||
|| !File.Exists(Path.Join(botEntry.InstanceDirectoryUri, _fileName)))
|
||||
|| !File.Exists(Path.Combine(botEntry.InstanceDirectoryUri, _fileName)))
|
||||
return false;
|
||||
|
||||
var botProcess = Process.Start(new ProcessStartInfo()
|
||||
{
|
||||
FileName = Path.Join(botEntry.InstanceDirectoryUri, _fileName),
|
||||
FileName = Path.Combine(botEntry.InstanceDirectoryUri, _fileName),
|
||||
WorkingDirectory = botEntry.InstanceDirectoryUri,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
|
@ -71,7 +66,7 @@ public sealed class EllieOrchestrator : IBotOrchestrator
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool StopBot(Guid botId)
|
||||
public bool Stop(Guid botId)
|
||||
{
|
||||
if (!_runningBots.TryGetValue(botId, out var botProcess))
|
||||
return false;
|
||||
|
@ -81,11 +76,10 @@ public sealed class EllieOrchestrator : IBotOrchestrator
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool StopAllBots()
|
||||
public bool StopAll()
|
||||
{
|
||||
var amount = _runningBots.Count;
|
||||
|
||||
// ReSharper disable once EmptyGeneralCatchClause
|
||||
foreach (var process in _runningBots.Values)
|
||||
try { process.Kill(true); } catch { }
|
||||
|
||||
|
@ -101,12 +95,7 @@ public sealed class EllieOrchestrator : IBotOrchestrator
|
|||
private void OnExit(object? sender, EventArgs eventArgs)
|
||||
{
|
||||
var (id, process) = _runningBots.First(x => x.Value.Equals(sender));
|
||||
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));
|
||||
OnBotExit?.Invoke(this, new(id, process.ExitCode));
|
||||
|
||||
_runningBots.Remove(id);
|
||||
process.CancelOutputRead();
|
||||
|
@ -127,7 +116,6 @@ 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);
|
||||
}
|
||||
|
||||
|
@ -144,7 +132,6 @@ 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,43 +1,29 @@
|
|||
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 EllieHub.Services.Abstractions;
|
||||
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.Features.BotConfig.Services;
|
||||
namespace EllieHub.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service that checks, downloads, installs, and updates a EllieBot instance.
|
||||
/// Service that checks, downloads, installs, and updates a NadekoBot instance.
|
||||
/// </summary>
|
||||
/// <remarks>Source: https://toastielab.dev/EllieBotDevs/elliebot/releases/latest</remarks>
|
||||
/// <remarks>Source: https://gitlab.com/Kwoth/nadekobot/-/releases/permalink/latest</remarks>
|
||||
public sealed partial class EllieResolver : IBotResolver
|
||||
{
|
||||
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 HashSet<Guid> _updateIdOngoing = new();
|
||||
private static readonly string _tempDirectory = Path.GetTempPath();
|
||||
private static readonly Regex _unzippedDirRegex = GenerateUnzipedDirRegex();
|
||||
private static readonly Regex _unzipedDirRegex = GenerateUnzipedDirRegex();
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly IAppConfigManager _appConfigManager;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DependencyName { get; } = "EllieBot";
|
||||
public string DependencyName { get; } = "NadekoBot";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string FileName { get; } = (OperatingSystem.IsWindows()) ? "EllieBot.exe" : "EllieBot";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsUpdateInProgress
|
||||
=> _updateIdOngoing.Contains(Id);
|
||||
public string FileName { get; } = (OperatingSystem.IsWindows()) ? "NadekoBot.exe" : "NadekoBot";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Guid Id { get; }
|
||||
|
@ -46,16 +32,14 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
public string BotName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a service that checks, downloads, installs, and updates a EllieBot instance.
|
||||
/// Creates a service that checks, downloads, installs, and updates a NadekoBot 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, IMemoryCache memoryCache, IAppConfigManager appConfigManager, Guid botId)
|
||||
public EllieResolver(IHttpClientFactory httpClientFactory, IAppConfigManager appConfigManager, Guid botId)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_memoryCache = memoryCache;
|
||||
_appConfigManager = appConfigManager;
|
||||
Id = botId;
|
||||
BotName = _appConfigManager.AppConfig.BotEntries[Id].Name;
|
||||
|
@ -71,13 +55,13 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
|
||||
var latestVersion = await GetLatestVersionAsync(cToken);
|
||||
|
||||
if (Version.Parse(latestVersion) <= Version.Parse(currentVersion))
|
||||
if (latestVersion.Equals(currentVersion, StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
var http = _httpClientFactory.CreateClient();
|
||||
|
||||
return await http.IsUrlValidAsync(
|
||||
await GetDownloadUrlAsync(latestVersion, cToken),
|
||||
$"https://gitlab.com/api/v4/projects/9321079/packages/generic/NadekoBot-build/{latestVersion}/{GetDownloadFileName(latestVersion)}",
|
||||
cToken
|
||||
);
|
||||
}
|
||||
|
@ -94,8 +78,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}_v{botInstance.Version}_{date}-{now.ToUnixTimeMilliseconds()}.zip";
|
||||
var destinationUri = Path.Join(_appConfigManager.AppConfig.BotsBackupDirectoryUri, backupZipName);
|
||||
var backupZipName = $"{botInstance.Name}_{date}-{now.ToUnixTimeMilliseconds()}.zip";
|
||||
var destinationUri = Path.Combine(_appConfigManager.AppConfig.BotsBackupDirectoryUri, backupZipName);
|
||||
|
||||
// ZipFile does not provide asynchronous implementations, so we have to schedule its
|
||||
// execution to be run in the thread-pool due to how long it takes to finish execution.
|
||||
|
@ -108,36 +92,42 @@ 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 (!File.Exists(executableUri))
|
||||
{
|
||||
await _appConfigManager.UpdateBotEntryAsync(Id, x => x with { Version = null }, cToken);
|
||||
if (!string.IsNullOrWhiteSpace(botEntry.Version))
|
||||
return botEntry.Version;
|
||||
|
||||
var assemblyUri = Path.Combine(botEntry.InstanceDirectoryUri, "NadekoBot.dll");
|
||||
|
||||
if (!File.Exists(assemblyUri))
|
||||
return null;
|
||||
}
|
||||
var nadekoAssembly = Assembly.LoadFile(assemblyUri);
|
||||
var version = nadekoAssembly.GetName().Version
|
||||
?? throw new InvalidOperationException($"Could not find version of the assembly at {assemblyUri}.");
|
||||
|
||||
return (string.IsNullOrWhiteSpace(botEntry.Version))
|
||||
? await GetBotVersionFromAssemblyAsync(executableUri, cToken)
|
||||
: botEntry.Version;
|
||||
var currentVersion = $"{version.Major}.{version.Minor}.{version.Build}";
|
||||
|
||||
await _appConfigManager.UpdateBotEntryAsync(Id, x => x with { Version = currentVersion }, cToken);
|
||||
|
||||
return currentVersion;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<string> GetLatestVersionAsync(CancellationToken cToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
return (await GetLatestVersionFromApiAsync(cToken)).Tag;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return await GetLatestVersionFromUrlAsync(cToken);
|
||||
}
|
||||
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)..];
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string installationUri, CancellationToken cToken = default)
|
||||
{
|
||||
if (IsUpdateInProgress)
|
||||
if (_updateIdOngoing.Contains(Id))
|
||||
return (null, null);
|
||||
|
||||
_updateIdOngoing.Add(Id);
|
||||
|
@ -145,8 +135,8 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
var currentVersion = await GetCurrentVersionAsync(cToken);
|
||||
var latestVersion = await GetLatestVersionAsync(cToken);
|
||||
|
||||
// Already up-to-date, quit
|
||||
if (currentVersion is not null && Version.Parse(latestVersion) <= Version.Parse(currentVersion))
|
||||
// Update
|
||||
if (latestVersion == currentVersion)
|
||||
{
|
||||
_updateIdOngoing.Remove(Id);
|
||||
return (currentVersion, null);
|
||||
|
@ -162,13 +152,13 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
|
||||
var http = _httpClientFactory.CreateClient();
|
||||
var downloadFileName = GetDownloadFileName(latestVersion);
|
||||
var botTempLocation = Path.Join(_tempDirectory, "elliebot-" + _unzippedDirRegex.Match(downloadFileName).Groups[1].Value);
|
||||
var zipTempLocation = Path.Join(_tempDirectory, downloadFileName);
|
||||
var botTempLocation = Path.Combine(_tempDirectory, "nadekobot-" + _unzipedDirRegex.Match(downloadFileName).Groups[1].Value);
|
||||
var zipTempLocation = Path.Combine(_tempDirectory, downloadFileName);
|
||||
|
||||
try
|
||||
{
|
||||
using var downloadStream = await http.GetStreamAsync(
|
||||
await GetDownloadUrlAsync(latestVersion, cToken),
|
||||
$"https://gitlab.com/api/v4/projects/9321079/packages/generic/NadekoBot-build/{latestVersion}/{downloadFileName}",
|
||||
cToken
|
||||
);
|
||||
|
||||
|
@ -186,10 +176,10 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
await _appConfigManager.UpdateBotEntryAsync(Id, x => x with { Version = latestVersion }, cToken);
|
||||
|
||||
// Create creds.yml
|
||||
var credsUri = Path.Join(installationUri, "creds.yml");
|
||||
var credsUri = Path.Combine(installationUri, "creds.yml");
|
||||
|
||||
if (!File.Exists(credsUri))
|
||||
File.Copy(Path.Join(installationUri, "creds_example.yml"), credsUri);
|
||||
File.Copy(Path.Combine(installationUri, "creds_example.yml"), credsUri);
|
||||
|
||||
return (currentVersion, latestVersion);
|
||||
}
|
||||
|
@ -198,13 +188,13 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
_updateIdOngoing.Remove(Id);
|
||||
|
||||
// Cleanup
|
||||
ToastieUtilities.TryDeleteFile(zipTempLocation);
|
||||
ToastieUtilities.TryDeleteDirectory(botTempLocation);
|
||||
Utilities.TryDeleteFile(zipTempLocation);
|
||||
Utilities.TryDeleteDirectory(botTempLocation);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs the Ellie instance on a Unix system.
|
||||
/// Installs the Nadeko 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>
|
||||
|
@ -215,25 +205,27 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
// Extract the tar ball
|
||||
await TarFile.ExtractToDirectoryAsync(downloadStream, _tempDirectory, true, cToken);
|
||||
|
||||
ToastieUtilities.TryMoveDirectory(botTempLocation, installationUri);
|
||||
// Move the bot root directory with "mv" to circumvent this issue on Unix systems: https://github.com/dotnet/runtime/issues/31149
|
||||
using var moveProcess = Utilities.StartProcess("mv", $"\"{botTempLocation}\" \"{installationUri}\"");
|
||||
await moveProcess.WaitForExitAsync(cToken);
|
||||
|
||||
// Set executable permission
|
||||
using var chmod = ToastieUtilities.StartProcess("chmod", ["+x", Path.Join(installationUri, FileName)]);
|
||||
using var chmod = Utilities.StartProcess("chmod", $"+x \"{Path.Combine(installationUri, FileName)}\"");
|
||||
await chmod.WaitForExitAsync(cToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs the Ellie instance on a non-Unix system.
|
||||
/// Installs the Nadeko 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 static async ValueTask InstallToWindowsAsync(Stream downloadStream, string installationUri, string zipTempLocation, string botTempLocation, CancellationToken cToken = default)
|
||||
private async static ValueTask InstallToWindowsAsync(Stream downloadStream, string installationUri, string zipTempLocation, string botTempLocation, CancellationToken cToken = default)
|
||||
{
|
||||
// Save the zip file
|
||||
await using (var fileStream = new FileStream(zipTempLocation, FileMode.Create))
|
||||
using (var fileStream = new FileStream(zipTempLocation, FileMode.Create))
|
||||
await downloadStream.CopyToAsync(fileStream, cToken);
|
||||
|
||||
// Extract the zip file
|
||||
|
@ -249,13 +241,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 static async ValueTask ReaplyBotSettingsAsync(string installationUri, string backupFileUri, CancellationToken cToken = default)
|
||||
private async static 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" // 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/"))
|
||||
x.Name is "creds.yml" or "creds_example.yml"
|
||||
|| (!string.IsNullOrWhiteSpace(x.Name) && x.FullName.Contains("data/"))
|
||||
);
|
||||
|
||||
foreach (var zippedFile in zippedFiles)
|
||||
|
@ -264,7 +256,7 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
.Prepend(Directory.GetParent(installationUri)?.FullName ?? string.Empty)
|
||||
.ToArray();
|
||||
|
||||
await RestoreFileAsync(zippedFile, Path.Join(fileDestinationPath), cToken);
|
||||
await RestoreFileAsync(zippedFile, Path.Combine(fileDestinationPath), cToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -274,10 +266,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 static async ValueTask RestoreFileAsync(ZipArchiveEntry zippedFile, string destinationPath, CancellationToken cToken = default)
|
||||
private async static ValueTask RestoreFileAsync(ZipArchiveEntry zippedFile, string destinationPath, CancellationToken cToken = default)
|
||||
{
|
||||
await using var zipStream = zippedFile.Open();
|
||||
await using var fileStream = new FileStream(destinationPath, FileMode.Create);
|
||||
using var zipStream = zippedFile.Open();
|
||||
using var fileStream = new FileStream(destinationPath, FileMode.Create);
|
||||
|
||||
await zipStream.CopyToAsync(fileStream, cToken);
|
||||
}
|
||||
|
@ -285,7 +277,7 @@ public sealed partial class EllieResolver : IBotResolver
|
|||
/// <summary>
|
||||
/// Gets the name of the file to be downloaded.
|
||||
/// </summary>
|
||||
/// <param name="version">The version of EllieBot.</param>
|
||||
/// <param name="version">The version of NadekoBot.</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)
|
||||
|
@ -303,120 +295,10 @@ 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 EllieBot on this OS.")
|
||||
_ => throw new NotSupportedException($"Architecture of type {RuntimeInformation.OSArchitecture} is not supported by NadekoBot 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,10 +1,9 @@
|
|||
using Toastie.Utilities;
|
||||
using EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace EllieHub.Features.AppConfig.Services;
|
||||
namespace EllieHub.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service that checks, downloads, installs, and updates ffmpeg on Linux.
|
||||
|
@ -15,7 +14,7 @@ public sealed partial class FfmpegLinuxResolver : FfmpegResolver
|
|||
{
|
||||
private readonly Regex _ffmpegLatestVersionRegex = FfmpegLatestVersionRegexGenerator();
|
||||
private readonly string _tempDirectory = Path.GetTempPath();
|
||||
private bool _isUpdating;
|
||||
private bool _isUpdating = false;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
@ -49,7 +48,7 @@ public sealed partial class FfmpegLinuxResolver : FfmpegResolver
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string installationUri, CancellationToken cToken = default)
|
||||
public override async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string dependenciesUri, CancellationToken cToken = default)
|
||||
{
|
||||
if (_isUpdating)
|
||||
return (null, null);
|
||||
|
@ -65,42 +64,42 @@ public sealed partial class FfmpegLinuxResolver : FfmpegResolver
|
|||
if (currentVersion == newVersion)
|
||||
return (currentVersion, null);
|
||||
|
||||
ToastieUtilities.TryDeleteFile(Path.Join(installationUri, FileName));
|
||||
ToastieUtilities.TryDeleteFile(Path.Join(installationUri, "ffprobe"));
|
||||
Utilities.TryDeleteFile(Path.Combine(dependenciesUri, FileName));
|
||||
Utilities.TryDeleteFile(Path.Combine(dependenciesUri, "ffprobe"));
|
||||
}
|
||||
|
||||
// Install
|
||||
Directory.CreateDirectory(installationUri);
|
||||
Directory.CreateDirectory(dependenciesUri);
|
||||
|
||||
var architecture = (RuntimeInformation.OSArchitecture is Architecture.X64) ? "amd" : "arm";
|
||||
var tarFileName = $"ffmpeg-release-{architecture}64-static.tar.xz";
|
||||
var http = _httpClientFactory.CreateClient();
|
||||
await using var downloadStream = await http.GetStreamAsync($"https://johnvansickle.com/ffmpeg/releases/{tarFileName}", cToken);
|
||||
using var downloadStream = await http.GetStreamAsync($"https://johnvansickle.com/ffmpeg/releases/{tarFileName}", cToken);
|
||||
|
||||
// Save tar file to the temporary directory.
|
||||
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))
|
||||
var tarFilePath = Path.Combine(_tempDirectory, tarFileName);
|
||||
var tarExtractDir = Path.Combine(_tempDirectory, $"ffmpeg-{newVersion}-{architecture}64-static");
|
||||
using (var fileStream = new FileStream(tarFilePath, FileMode.Create))
|
||||
await downloadStream.CopyToAsync(fileStream, cToken);
|
||||
|
||||
// Extract the tar file.
|
||||
using var extractProcess = ToastieUtilities.StartProcess("tar", ["xf", tarFilePath, $"--directory=\"{_tempDirectory}\""]);
|
||||
using var extractProcess = Utilities.StartProcess("tar", $"xf \"{tarFilePath}\" --directory=\"{_tempDirectory}\"");
|
||||
await extractProcess.WaitForExitAsync(cToken);
|
||||
|
||||
// Move ffmpeg to the dependencies directory.
|
||||
ToastieUtilities.TryMoveFile(Path.Join(tarExtractDir, FileName), Path.Join(installationUri, FileName), true);
|
||||
ToastieUtilities.TryMoveFile(Path.Join(tarExtractDir, "ffprobe"), Path.Join(installationUri, "ffprobe"), true);
|
||||
File.Move(Path.Combine(tarExtractDir, FileName), Path.Combine(dependenciesUri, FileName), true);
|
||||
File.Move(Path.Combine(tarExtractDir, "ffprobe"), Path.Combine(dependenciesUri, "ffprobe"), true);
|
||||
|
||||
// Mark the files as executable.
|
||||
using var chmod = ToastieUtilities.StartProcess("chmod", ["+x", Path.Join(installationUri, FileName), Path.Join(installationUri, "ffprobe")]);
|
||||
using var chmod = Utilities.StartProcess("chmod", $"+x \"{Path.Combine(dependenciesUri, FileName)}\" \"{Path.Combine(dependenciesUri, "ffprobe")}\"");
|
||||
await chmod.WaitForExitAsync(cToken);
|
||||
|
||||
// Cleanup
|
||||
ToastieUtilities.TryDeleteFile(tarFilePath);
|
||||
ToastieUtilities.TryDeleteDirectory(tarExtractDir, true);
|
||||
File.Delete(tarFilePath);
|
||||
Directory.Delete(tarExtractDir, true);
|
||||
|
||||
// Update environment variable
|
||||
ToastieUtilities.AddPathToPATHEnvar(installationUri);
|
||||
Utilities.AddPathToPATHEnvar(dependenciesUri);
|
||||
|
||||
_isUpdating = false;
|
||||
return (currentVersion, newVersion);
|
|
@ -1,10 +1,9 @@
|
|||
using Toastie.Utilities;
|
||||
using EllieHub.Features.AppConfig.Models.Api.Evermeet;
|
||||
using EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
using EllieHub.Models.Api;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using System.IO.Compression;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace EllieHub.Features.AppConfig.Services;
|
||||
namespace EllieHub.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service that checks, downloads, installs, and updates ffmpeg on MacOS.
|
||||
|
@ -16,7 +15,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;
|
||||
private bool _isUpdating = false;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
@ -39,7 +38,7 @@ public sealed class FfmpegMacResolver : FfmpegResolver
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string installationUri, CancellationToken cToken = default)
|
||||
public override async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string dependenciesUri, CancellationToken cToken = default)
|
||||
{
|
||||
if (_isUpdating)
|
||||
return (null, null);
|
||||
|
@ -58,24 +57,24 @@ public sealed class FfmpegMacResolver : FfmpegResolver
|
|||
return (currentVersion, null);
|
||||
}
|
||||
|
||||
ToastieUtilities.TryDeleteFile(Path.Join(installationUri, FileName));
|
||||
ToastieUtilities.TryDeleteFile(Path.Join(installationUri, "ffprobe"));
|
||||
Utilities.TryDeleteFile(Path.Combine(dependenciesUri, FileName));
|
||||
Utilities.TryDeleteFile(Path.Combine(dependenciesUri, "ffprobe"));
|
||||
}
|
||||
|
||||
// Install
|
||||
Directory.CreateDirectory(installationUri);
|
||||
Directory.CreateDirectory(dependenciesUri);
|
||||
|
||||
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, installationUri, cToken),
|
||||
InstallDependencyAsync(ffprobeResponse, installationUri, cToken)
|
||||
InstallDependencyAsync(ffmpegResponse, dependenciesUri, cToken),
|
||||
InstallDependencyAsync(ffprobeResponse, dependenciesUri, cToken)
|
||||
);
|
||||
|
||||
// Update environment variable
|
||||
ToastieUtilities.AddPathToPATHEnvar(installationUri);
|
||||
Utilities.AddPathToPATHEnvar(dependenciesUri);
|
||||
|
||||
_isUpdating = false;
|
||||
return (currentVersion, newVersion);
|
||||
|
@ -92,26 +91,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.Join(_tempDirectory, zipFileName);
|
||||
var zipFilePath = Path.Combine(_tempDirectory, zipFileName);
|
||||
|
||||
// Download the zip file and save it to the temporary directory.
|
||||
await using var zipStream = await http.GetStreamAsync(downloadUrl, cToken);
|
||||
using var zipStream = await http.GetStreamAsync(downloadUrl, cToken);
|
||||
|
||||
await using (var fileStream = new FileStream(zipFilePath, FileMode.Create))
|
||||
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.Join(dependenciesUri, downloadInfo.Name);
|
||||
ToastieUtilities.TryMoveFile(Path.Join(_tempDirectory, downloadInfo.Name), finalFileUri, true);
|
||||
var finalFileUri = Path.Combine(dependenciesUri, downloadInfo.Name);
|
||||
File.Move(Path.Combine(_tempDirectory, downloadInfo.Name), finalFileUri, true);
|
||||
|
||||
// Mark binary as executable.
|
||||
using var chmod = ToastieUtilities.StartProcess("chmod", $"+x \"{finalFileUri}\"");
|
||||
using var chmod = Utilities.StartProcess("chmod", $"+x \"{finalFileUri}\"");
|
||||
await chmod.WaitForExitAsync(cToken);
|
||||
|
||||
// Cleanup.
|
||||
ToastieUtilities.TryDeleteFile(zipFilePath);
|
||||
File.Delete(zipFilePath);
|
||||
}
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
using Toastie.Utilities;
|
||||
using EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using System.IO.Compression;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace EllieHub.Features.AppConfig.Services;
|
||||
namespace EllieHub.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service that checks, downloads, installs, and updates ffmpeg on Windows.
|
||||
|
@ -14,7 +13,7 @@ namespace EllieHub.Features.AppConfig.Services;
|
|||
public sealed class FfmpegWindowsResolver : FfmpegResolver
|
||||
{
|
||||
private readonly string _tempDirectory = Path.GetTempPath();
|
||||
private bool _isUpdating;
|
||||
private bool _isUpdating = false;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -31,7 +30,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);
|
||||
}
|
||||
|
@ -50,7 +49,7 @@ public sealed class FfmpegWindowsResolver : FfmpegResolver
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string installationUri, CancellationToken cToken = default)
|
||||
public override async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string dependenciesUri, CancellationToken cToken = default)
|
||||
{
|
||||
if (_isUpdating)
|
||||
return (null, null);
|
||||
|
@ -69,22 +68,22 @@ public sealed class FfmpegWindowsResolver : FfmpegResolver
|
|||
return (currentVersion, null);
|
||||
}
|
||||
|
||||
ToastieUtilities.TryDeleteFile(Path.Join(installationUri, FileName));
|
||||
ToastieUtilities.TryDeleteFile(Path.Join(installationUri, "ffprobe.exe"));
|
||||
//ToastieUtilities.TryDeleteFile(Path.Join(dependenciesUri, "ffplay.exe"));
|
||||
Utilities.TryDeleteFile(Path.Combine(dependenciesUri, FileName));
|
||||
Utilities.TryDeleteFile(Path.Combine(dependenciesUri, "ffprobe.exe"));
|
||||
//Utilities.TryDeleteFile(Path.Combine(dependenciesUri, "ffplay.exe"));
|
||||
}
|
||||
|
||||
// Install
|
||||
Directory.CreateDirectory(installationUri);
|
||||
Directory.CreateDirectory(dependenciesUri);
|
||||
|
||||
var zipFileName = $"ffmpeg-{newVersion}-full_build.zip";
|
||||
var http = _httpClientFactory.CreateClient();
|
||||
await using var downloadStream = await http.GetStreamAsync($"https://github.com/GyanD/codexffmpeg/releases/download/{newVersion}/{zipFileName}", cToken);
|
||||
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.Join(_tempDirectory, zipFileName);
|
||||
var zipExtractDir = Path.Join(_tempDirectory, zipFileName[..^4]);
|
||||
await using (var fileStream = new FileStream(zipFilePath, FileMode.Create))
|
||||
var zipFilePath = Path.Combine(_tempDirectory, zipFileName);
|
||||
var zipExtractDir = Path.Combine(_tempDirectory, zipFileName[..^4]);
|
||||
using (var fileStream = new FileStream(zipFilePath, FileMode.Create))
|
||||
await downloadStream.CopyToAsync(fileStream, cToken);
|
||||
|
||||
// Schedule installation to the thread-pool because ffmpeg is pretty
|
||||
|
@ -95,17 +94,17 @@ public sealed class FfmpegWindowsResolver : FfmpegResolver
|
|||
ZipFile.ExtractToDirectory(zipFilePath, _tempDirectory);
|
||||
|
||||
// Move ffmpeg to the dependencies directory.
|
||||
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"));
|
||||
File.Move(Path.Combine(zipExtractDir, "bin", FileName), Path.Combine(dependenciesUri, FileName), true);
|
||||
File.Move(Path.Combine(zipExtractDir, "bin", "ffprobe.exe"), Path.Combine(dependenciesUri, "ffprobe.exe"), true);
|
||||
//File.Move(Path.Combine(zipExtractDir, "bin", "ffplay.exe"), Path.Combine(dependenciesUri, "ffplay.exe"));
|
||||
|
||||
// Cleanup
|
||||
ToastieUtilities.TryDeleteFile(zipFilePath);
|
||||
ToastieUtilities.TryDeleteDirectory(zipExtractDir);
|
||||
File.Delete(zipFilePath);
|
||||
Directory.Delete(zipExtractDir, true);
|
||||
}, cToken);
|
||||
|
||||
// Update environment variable
|
||||
ToastieUtilities.AddPathToPATHEnvar(installationUri);
|
||||
Utilities.AddPathToPATHEnvar(dependenciesUri);
|
||||
|
||||
_isUpdating = false;
|
||||
return (currentVersion, newVersion);
|
|
@ -1,19 +1,18 @@
|
|||
using EllieHub.Features.AppConfig.Models;
|
||||
using EllieHub.Features.BotConfig.Models;
|
||||
using EllieHub.Features.BotConfig.Services.Abstractions;
|
||||
using System.Collections.Concurrent;
|
||||
using EllieHub.Models.Config;
|
||||
using EllieHub.Models.EventArguments;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
|
||||
namespace EllieHub.Features.BotConfig.Services;
|
||||
namespace EllieHub.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a service that writes logs of bot instances to the disk.
|
||||
/// </summary>
|
||||
public sealed class LogWriter : ILogWriter
|
||||
{
|
||||
private readonly ConcurrentDictionary<Guid, StringBuilder> _botLogs = [];
|
||||
private readonly ReadOnlyAppSettings _appConfig;
|
||||
private readonly Dictionary<Guid, StringBuilder> _botLogs = new();
|
||||
private readonly ReadOnlyAppConfig _appConfig;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<ILogWriter, LogFlushEventArgs>? OnLogCreated;
|
||||
|
@ -22,17 +21,13 @@ 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(ReadOnlyAppSettings appConfig)
|
||||
public LogWriter(ReadOnlyAppConfig 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, false, cToken)));
|
||||
|
||||
if (removeFromMemory)
|
||||
_botLogs.Clear();
|
||||
|
||||
var result = await Task.WhenAll(_botLogs.Keys.Select(x => FlushAsync(x, removeFromMemory, cToken)));
|
||||
return result.Any(x => x);
|
||||
}
|
||||
|
||||
|
@ -43,7 +38,7 @@ public sealed class LogWriter : ILogWriter
|
|||
return false;
|
||||
|
||||
if (removeFromMemory)
|
||||
_botLogs.Remove(botId, out _);
|
||||
_botLogs.Remove(botId);
|
||||
|
||||
if (logStringBuilder.Length is 0)
|
||||
return false;
|
||||
|
@ -53,7 +48,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.Join(_appConfig.LogsDirectoryUri, $"{botEntry.Name}_v{botEntry.Version}_{date}-{now.ToUnixTimeSeconds()}.txt");
|
||||
var fileUri = Path.Combine(_appConfig.LogsDirectoryUri, $"{botEntry.Name}_v{botEntry.Version}_{date}-{now.ToUnixTimeSeconds()}.txt");
|
||||
|
||||
await File.WriteAllTextAsync(fileUri, logStringBuilder.ToString(), cToken);
|
||||
|
||||
|
@ -78,7 +73,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.Features.AppConfig.Services.Abstractions;
|
||||
using EllieHub.Services.Abstractions;
|
||||
|
||||
namespace EllieHub.Features.AppConfig.Services.Mocks;
|
||||
namespace EllieHub.Services.Mocks;
|
||||
|
||||
/// <summary>
|
||||
/// Service that pretends to check, download, install, and update ffmpeg.
|
|
@ -1,8 +1,8 @@
|
|||
using EllieHub.Features.AppConfig.Models;
|
||||
using EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
using EllieHub.Features.AppWindow.Models;
|
||||
using EllieHub.Models;
|
||||
using EllieHub.Models.Config;
|
||||
using EllieHub.Services.Abstractions;
|
||||
|
||||
namespace EllieHub.Features.AppConfig.Services.Mocks;
|
||||
namespace EllieHub.Services.Mocks;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a service that pretends to manage the application's settings.
|
||||
|
@ -10,7 +10,7 @@ namespace EllieHub.Features.AppConfig.Services.Mocks;
|
|||
internal sealed class MockAppConfigManager : IAppConfigManager
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public ReadOnlyAppSettings AppConfig { get; } = new(new() { BotEntries = new() { [Guid.Empty] = new("MockBot", Path.Join(AppStatics.AppDefaultBotDirectoryUri, "MockBot"), 0) } });
|
||||
public ReadOnlyAppConfig AppConfig { get; } = new(new() { BotEntries = new() { [Guid.Empty] = new("MockBot", Path.Combine(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<AppSettings> action, CancellationToken cToken = default)
|
||||
public ValueTask UpdateConfigAsync(Action<AppConfig> action, CancellationToken cToken = default)
|
||||
=> ValueTask.CompletedTask;
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
using EllieHub.Features.BotConfig.Services.Abstractions;
|
||||
using EllieHub.Services.Abstractions;
|
||||
|
||||
namespace EllieHub.Features.BotConfig.Services.Mocks;
|
||||
namespace EllieHub.Services.Mocks;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a service that pretends to check, download, install, and update a bot instance.
|
||||
|
@ -14,13 +14,10 @@ internal sealed class MockEllieResolver : IBotResolver
|
|||
public Guid Id { get; } = Guid.Empty;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DependencyName { get; } = "EllieBot";
|
||||
public string DependencyName { get; } = "NadekoBot";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string FileName { get; } = "EllieBot";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsUpdateInProgress { get; } = false;
|
||||
public string FileName { get; } = "NadekoBot";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<bool?> CanUpdateAsync(CancellationToken cToken = default)
|
|
@ -1,9 +1,8 @@
|
|||
using Toastie.Utilities;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace EllieHub.Features.AppConfig.Services;
|
||||
namespace EllieHub.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service that checks, downloads, installs, and updates yt-dlp.
|
||||
|
@ -14,15 +13,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;
|
||||
private bool _isUpdating = false;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DependencyName { get; } = "Yt-dlp";
|
||||
public string DependencyName { get; } = "Youtube-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.
|
||||
|
@ -57,16 +56,16 @@ public sealed class YtdlpResolver : IYtdlpResolver
|
|||
public async ValueTask<string?> GetCurrentVersionAsync(CancellationToken cToken = default)
|
||||
{
|
||||
// If yt-dlp is not accessible from the shell...
|
||||
if (!ToastieUtilities.ProgramExists(_ytdlpProcessName))
|
||||
if (!await Utilities.ProgramExistsAsync(_ytdlpProcessName, cToken))
|
||||
{
|
||||
// And doesn't exist in the dependencies folder,
|
||||
// report that yt-dlp is not installed.
|
||||
if (!File.Exists(Path.Join(AppStatics.AppDepsUri, FileName)))
|
||||
if (!File.Exists(Path.Combine(AppStatics.AppDepsUri, FileName)))
|
||||
return null;
|
||||
|
||||
// Else, add the dependencies directory to the PATH envar,
|
||||
// then try again.
|
||||
ToastieUtilities.AddPathToPATHEnvar(AppStatics.AppDepsUri);
|
||||
Utilities.AddPathToPATHEnvar(AppStatics.AppDepsUri);
|
||||
return await GetCurrentVersionAsync(cToken);
|
||||
}
|
||||
|
||||
|
@ -74,7 +73,7 @@ public sealed class YtdlpResolver : IYtdlpResolver
|
|||
if (_memoryCache.TryGetValue<string>(_cachedCurrentVersionKey, out var currentVersion) && currentVersion is not null)
|
||||
return currentVersion;
|
||||
|
||||
using var ytdlp = ToastieUtilities.StartProcess(_ytdlpProcessName, "--version", true);
|
||||
using var ytdlp = Utilities.StartProcess(_ytdlpProcessName, "--version");
|
||||
|
||||
var currentProcessVersion = (await ytdlp.StandardOutput.ReadToEndAsync(cToken)).Trim();
|
||||
_memoryCache.Set(_cachedCurrentVersionKey, currentProcessVersion, TimeSpan.FromMinutes(1.5));
|
||||
|
@ -96,7 +95,7 @@ public sealed class YtdlpResolver : IYtdlpResolver
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string installationUri, CancellationToken cToken = default)
|
||||
public async ValueTask<(string? OldVersion, string? NewVersion)> InstallOrUpdateAsync(string dependenciesUri, CancellationToken cToken = default)
|
||||
{
|
||||
if (_isUpdating)
|
||||
return (null, null);
|
||||
|
@ -117,7 +116,7 @@ public sealed class YtdlpResolver : IYtdlpResolver
|
|||
return (currentVersion, null);
|
||||
}
|
||||
|
||||
using var ytdlp = ToastieUtilities.StartProcess(_ytdlpProcessName, "-U");
|
||||
using var ytdlp = Utilities.StartProcess(_ytdlpProcessName, "-U");
|
||||
await ytdlp.WaitForExitAsync(cToken);
|
||||
|
||||
_isUpdating = false;
|
||||
|
@ -125,21 +124,21 @@ public sealed class YtdlpResolver : IYtdlpResolver
|
|||
}
|
||||
|
||||
// Install
|
||||
Directory.CreateDirectory(installationUri);
|
||||
Directory.CreateDirectory(dependenciesUri);
|
||||
|
||||
var finalFilePath = Path.Join(installationUri, FileName);
|
||||
var finalFilePath = Path.Combine(dependenciesUri, FileName);
|
||||
var http = _httpClientFactory.CreateClient();
|
||||
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))
|
||||
using var downloadStream = await http.GetStreamAsync($"https://github.com/yt-dlp/yt-dlp/releases/download/{newVersion}/{_downloadedFileName}", cToken);
|
||||
using (var fileStream = new FileStream(finalFilePath, FileMode.Create))
|
||||
await downloadStream.CopyToAsync(fileStream, cToken);
|
||||
|
||||
// Update environment variable
|
||||
ToastieUtilities.AddPathToPATHEnvar(installationUri);
|
||||
Utilities.AddPathToPATHEnvar(dependenciesUri);
|
||||
|
||||
// On Linux and MacOS, we need to mark the file as executable.
|
||||
if (Environment.OSVersion.Platform is PlatformID.Unix)
|
||||
{
|
||||
using var chmod = ToastieUtilities.StartProcess("chmod", ["+x", finalFilePath]);
|
||||
using var chmod = Utilities.StartProcess("chmod", $"+x \"{finalFilePath}\"");
|
||||
await chmod.WaitForExitAsync(cToken);
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
<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" />
|
||||
|
@ -62,7 +63,7 @@
|
|||
<Style Selector="Button.accent">
|
||||
<Setter Property="FontSize" Value="28" />
|
||||
<Setter Property="FontWeight" Value="Bold" />
|
||||
<Setter Property="Padding" Value="13 2 13 2" />
|
||||
<Setter Property="Padding" Value="12 2 12 2" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource NotoSansBoldFont}" />
|
||||
|
@ -85,6 +86,31 @@
|
|||
</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" />
|
|
@ -2,7 +2,7 @@ using Avalonia;
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Media;
|
||||
using EllieHub.Features.Abstractions;
|
||||
using EllieHub.ViewModels.Abstractions;
|
||||
|
||||
namespace EllieHub;
|
||||
|
||||
|
@ -21,7 +21,7 @@ public sealed class ViewLocator : IDataTemplate
|
|||
{
|
||||
return (data is ViewModelBase viewModel && viewModel.GetType().BaseType?.GenericTypeArguments[0] is Type controlType)
|
||||
? (Application.Current as App)?.Services.GetService(controlType) as Control
|
||||
?? new TextBlock { Text = $"View-model of type \"{data.GetType().FullName ?? "null"}\" is not registered in the IoC container.", TextWrapping = TextWrapping.WrapWithOverflow }
|
||||
?? new TextBlock { Text = $"View-model of type \"{data?.GetType().FullName ?? "null"}\" is not registered in the IoC container.", TextWrapping = TextWrapping.WrapWithOverflow }
|
||||
: new TextBlock { Text = $"Component of type \"{data?.GetType().FullName ?? "null"}\" is not a valid view-model.", TextWrapping = TextWrapping.WrapWithOverflow };
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
using ReactiveUI;
|
||||
|
||||
namespace EllieHub.Features.Abstractions;
|
||||
namespace EllieHub.ViewModels.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// The base view-model.
|
|
@ -1,6 +1,6 @@
|
|||
using ReactiveUI;
|
||||
|
||||
namespace EllieHub.Features.Abstractions;
|
||||
namespace EllieHub.ViewModels.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// The base view-model.
|
|
@ -1,21 +1,17 @@
|
|||
using Avalonia.Controls;
|
||||
using Toastie.Utilities;
|
||||
using MsBox.Avalonia.Enums;
|
||||
using EllieHub.Enums;
|
||||
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 EllieHub.Models.EventArguments;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using EllieHub.ViewModels.Abstractions;
|
||||
using EllieHub.Views.Controls;
|
||||
using EllieHub.Views.Windows;
|
||||
using ReactiveUI;
|
||||
using SkiaSharp;
|
||||
using System.Diagnostics;
|
||||
using System.Reactive.Disposables;
|
||||
|
||||
namespace EllieHub.Features.BotConfig.ViewModels;
|
||||
namespace EllieHub.ViewModels.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// View-model for <see cref="BotConfigView"/>, the window with settings and controls for a specific bot instance.
|
||||
|
@ -32,7 +28,6 @@ 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.
|
||||
|
@ -44,6 +39,8 @@ 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>
|
||||
|
@ -103,7 +100,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.Join(_appConfigManager.AppConfig.BotsDirectoryUri, sanitizedValue)}";
|
||||
DirectoryHint = $"Select the absolute path to the bot directory. For example: {Path.Combine(_appConfigManager.AppConfig.BotsDirectoryUri, sanitizedValue)}";
|
||||
this.RaiseAndSetIfChanged(ref _botName, sanitizedValue);
|
||||
}
|
||||
}
|
||||
|
@ -153,7 +150,6 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
_mainWindow = mainWindow;
|
||||
_botOrchestrator = botOrchestrator;
|
||||
_logWriter = logWriter;
|
||||
_lateralBarViewModel = mainWindow.ViewModel!.LateralBarInstance;
|
||||
BotDirectoryUriBar = botDirectoryUriBar;
|
||||
UpdateBar = updateBotBar;
|
||||
FakeConsole = fakeConsole;
|
||||
|
@ -166,7 +162,6 @@ 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...";
|
||||
|
@ -175,15 +170,18 @@ 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(!File.Exists(Path.Join(botEntry.InstanceDirectoryUri, Resolver.FileName)), true);
|
||||
EnableButtons(!Directory.Exists(botEntry.InstanceDirectoryUri), true);
|
||||
|
||||
_ = LoadUpdateBarAsync(botResolver, updateBotBar);
|
||||
|
||||
// Dispose when the view is deactivated
|
||||
this.WhenActivated(disposables => Disposable.Create(Dispose).DisposeWith(disposables));
|
||||
this.WhenActivated(disposables => Disposable.Create(() => Dispose()).DisposeWith(disposables));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -216,12 +214,14 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
Directory.Delete(BotDirectoryUriBar.CurrentUri);
|
||||
}
|
||||
|
||||
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."
|
||||
);
|
||||
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();
|
||||
}
|
||||
|
||||
BotDirectoryUriBar.RecheckCurrentUri();
|
||||
}
|
||||
|
@ -243,11 +243,6 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
}
|
||||
finally
|
||||
{
|
||||
_ = LoadUpdateBarAsync(Resolver, UpdateBar);
|
||||
|
||||
if (!wereButtonsUnlocked && File.Exists(Path.Join(BotDirectoryUriBar.CurrentUri, Resolver.FileName)))
|
||||
EnableButtons(false, true);
|
||||
else
|
||||
EnableButtons(!wereButtonsUnlocked, true);
|
||||
}
|
||||
}
|
||||
|
@ -311,7 +306,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));
|
||||
|
||||
|
@ -327,7 +322,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 reversed.",
|
||||
ContentMessage = $"Are you sure you want to delete {ActualBotName}?{Environment.NewLine}This action cannot be undone.",
|
||||
MaxWidth = int.Parse(WindowConstants.DefaultWindowWidth) / 2.0,
|
||||
SizeToContent = SizeToContent.WidthAndHeight,
|
||||
ShowInCenter = true,
|
||||
|
@ -341,7 +336,7 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
EnableButtons(true, false);
|
||||
|
||||
// Stop the bot instance
|
||||
_botOrchestrator.StopBot(Resolver.Id);
|
||||
_botOrchestrator.Stop(Resolver.Id);
|
||||
|
||||
// Cleanup
|
||||
FakeConsole.Content = string.Empty;
|
||||
|
@ -371,7 +366,6 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
}
|
||||
|
||||
EnableButtons(true, false);
|
||||
_lateralBarViewModel.ToggleEnable(false);
|
||||
|
||||
var originalStatus = UpdateBar.Status;
|
||||
UpdateBar.Status = DependencyStatus.Updating;
|
||||
|
@ -397,10 +391,6 @@ 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>
|
||||
|
@ -408,7 +398,7 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
/// </summary>
|
||||
public void StartBot()
|
||||
{
|
||||
IsBotRunning = _botOrchestrator.StartBot(Id);
|
||||
IsBotRunning = _botOrchestrator.Start(Id);
|
||||
|
||||
if (IsBotRunning)
|
||||
EnableButtons(true, false);
|
||||
|
@ -418,37 +408,32 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
/// Stops the bot instance associated with this view-model.
|
||||
/// </summary>
|
||||
public void StopBot()
|
||||
=> _botOrchestrator.StopBot(Id);
|
||||
=> _botOrchestrator.Stop(Id);
|
||||
|
||||
/// <summary>
|
||||
/// Loads the bot update bar.
|
||||
/// </summary>
|
||||
/// <param name="botResolver">The bot resolver.</param>
|
||||
/// <param name="updateBotBar">The update bar.</param>
|
||||
private static async Task LoadUpdateBarAsync(IBotResolver botResolver, DependencyButtonViewModel updateBotBar)
|
||||
private async static Task LoadUpdateBarAsync(IBotResolver botResolver, DependencyButtonViewModel updateBotBar)
|
||||
{
|
||||
updateBotBar.DependencyName = "Checking...";
|
||||
updateBotBar.Status = DependencyStatus.Checking;
|
||||
|
||||
var currentVersion = await botResolver.GetCurrentVersionAsync();
|
||||
updateBotBar.DependencyName = string.IsNullOrWhiteSpace(currentVersion)
|
||||
updateBotBar.DependencyName = (string.IsNullOrWhiteSpace(currentVersion))
|
||||
? "Not Installed"
|
||||
: "EllieBot v" + currentVersion;
|
||||
|
||||
var canUpdate = await botResolver.CanUpdateAsync();
|
||||
updateBotBar.Status = canUpdate switch
|
||||
{
|
||||
true => DependencyStatus.Update,
|
||||
false => DependencyStatus.Installed,
|
||||
null when botResolver.IsUpdateInProgress => DependencyStatus.Updating,
|
||||
null => DependencyStatus.Install
|
||||
};
|
||||
updateBotBar.Status = (canUpdate is true)
|
||||
? DependencyStatus.Update
|
||||
: (canUpdate is null)
|
||||
? DependencyStatus.Install
|
||||
: DependencyStatus.Installed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Locks or unlocks the settings buttons of this view-model.
|
||||
/// </summary>
|
||||
/// <param name="lockButtons">Whether the setting buttons should be locked.</param>
|
||||
/// <param name="lockButtons">Whether the settings 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)
|
||||
{
|
||||
|
@ -466,8 +451,10 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
if (eventArgs.Id != Resolver.Id)
|
||||
return;
|
||||
|
||||
FakeConsole.Content = FakeConsole.Content.Length > 100_000
|
||||
? FakeConsole.Content[FakeConsole.Content.IndexOf(Environment.NewLine, 60_000, StringComparison.Ordinal)..] + eventArgs.Output + Environment.NewLine
|
||||
_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 + eventArgs.Output + Environment.NewLine;
|
||||
}
|
||||
|
||||
|
@ -481,11 +468,14 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
if (eventArgs.Id != Resolver.Id)
|
||||
return;
|
||||
|
||||
FakeConsole.Content += eventArgs.Message;
|
||||
var message = Environment.NewLine + ActualBotName + " stopped." + Environment.NewLine;
|
||||
|
||||
_logWriter.TryAdd(Resolver.Id, message);
|
||||
FakeConsole.Content += message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-enables the buttons when the bot instance associated with this view-model exits.
|
||||
/// Reenables 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>
|
||||
|
@ -501,13 +491,7 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
|
|||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
UpdateBar.Click -= InstallOrUpdateAsync;
|
||||
_botOrchestrator.OnStdout -= WriteLog;
|
||||
_botOrchestrator.OnStderr -= WriteLog;
|
||||
_botOrchestrator.OnBotExit -= LogBotExit;
|
||||
_botOrchestrator.OnBotExit -= ReenableButtonsOnBotExit;
|
||||
|
||||
BotAvatar.Dispose();
|
||||
BotAvatar?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
|
@ -2,18 +2,15 @@ using Avalonia.Controls;
|
|||
using Avalonia.Styling;
|
||||
using MsBox.Avalonia.Enums;
|
||||
using EllieHub.Enums;
|
||||
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 EllieHub.Services.Abstractions;
|
||||
using EllieHub.ViewModels.Abstractions;
|
||||
using EllieHub.ViewModels.Windows;
|
||||
using EllieHub.Views.Controls;
|
||||
using EllieHub.Views.Windows;
|
||||
using ReactiveUI;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace EllieHub.Features.AppConfig.ViewModels;
|
||||
namespace EllieHub.ViewModels.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// The view-model for the application's settings.
|
||||
|
@ -28,7 +25,6 @@ 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;
|
||||
|
||||
|
@ -99,7 +95,6 @@ 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;
|
||||
|
@ -153,7 +148,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);
|
||||
|
||||
|
@ -201,12 +196,11 @@ public class ConfigViewModel : ViewModelBase<ConfigView>
|
|||
dependencyButton.Click += async (buttonViewModel, _) => await HandleDependencyAsync(buttonViewModel, dependencyResolver);
|
||||
|
||||
var canUpdate = await dependencyResolver.CanUpdateAsync();
|
||||
dependencyButton.Status = canUpdate switch
|
||||
{
|
||||
true => DependencyStatus.Update,
|
||||
false => DependencyStatus.Installed,
|
||||
null => DependencyStatus.Install
|
||||
};
|
||||
dependencyButton.Status = (canUpdate is null)
|
||||
? DependencyStatus.Install
|
||||
: (canUpdate is true)
|
||||
? DependencyStatus.Update
|
||||
: DependencyStatus.Installed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -219,7 +213,6 @@ 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;
|
||||
|
||||
|
@ -241,9 +234,5 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
using Avalonia.Media.Immutable;
|
||||
using EllieHub.Enums;
|
||||
using EllieHub.Features.Abstractions;
|
||||
using EllieHub.Features.AppWindow.Views.Windows;
|
||||
using EllieHub.Features.Common.Views.Controls;
|
||||
using EllieHub.ViewModels.Abstractions;
|
||||
using EllieHub.Views.Controls;
|
||||
using EllieHub.Views.Windows;
|
||||
using ReactiveUI;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace EllieHub.Features.Common.ViewModels;
|
||||
namespace EllieHub.ViewModels.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the view-model for a button that installs a dependency for Ellie.
|
|
@ -1,8 +1,8 @@
|
|||
using EllieHub.Features.Abstractions;
|
||||
using EllieHub.Features.BotConfig.Views.Controls;
|
||||
using EllieHub.ViewModels.Abstractions;
|
||||
using EllieHub.Views.Controls;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace EllieHub.Features.BotConfig.ViewModels;
|
||||
namespace EllieHub.ViewModels.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// View-model for <see cref="FakeConsole"/>, the fake console that displays text.
|
|
@ -1,8 +1,8 @@
|
|||
using EllieHub.Features.Abstractions;
|
||||
using EllieHub.Features.Home.Views.Controls;
|
||||
using EllieHub.ViewModels.Abstractions;
|
||||
using EllieHub.Views.Controls;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace EllieHub.Features.Home.ViewModels;
|
||||
namespace EllieHub.ViewModels.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// View-model for the home window, with links to Ellie resources.
|
|
@ -1,35 +1,25 @@
|
|||
using Avalonia.Controls;
|
||||
using EllieHub.Features.Abstractions;
|
||||
using EllieHub.Features.AppConfig.Services.Abstractions;
|
||||
using EllieHub.Features.AppWindow.Models;
|
||||
using EllieHub.Features.AppWindow.Views.Controls;
|
||||
using EllieHub.Models;
|
||||
using EllieHub.Services.Abstractions;
|
||||
using EllieHub.ViewModels.Abstractions;
|
||||
using EllieHub.Views.Controls;
|
||||
using ReactiveUI;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Reactive.Disposables;
|
||||
|
||||
namespace EllieHub.Features.AppWindow.ViewModels;
|
||||
namespace EllieHub.ViewModels.Controls;
|
||||
|
||||
/// <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; } = [];
|
||||
public ObservableCollection<Button> BotButtonList { get; } = new();
|
||||
|
||||
/// <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);
|
||||
}
|
||||
private readonly IAppConfigManager _appConfigManager;
|
||||
|
||||
/// <summary>
|
||||
/// Creates the view-model for the <see cref="LateralBarView"/>.
|
||||
|
@ -77,23 +67,11 @@ 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>
|
||||
public void ReloadBotButtons(IReadOnlyDictionary<Guid, BotInstanceInfo> botEntires)
|
||||
private void ReloadBotButtons(IReadOnlyDictionary<Guid, BotInstanceInfo> botEntires)
|
||||
{
|
||||
BotButtonList.Clear();
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
using Avalonia.Platform.Storage;
|
||||
using Toastie.Utilities;
|
||||
using EllieHub.Features.Abstractions;
|
||||
using EllieHub.Features.Common.Models;
|
||||
using EllieHub.Features.Common.Views.Controls;
|
||||
using EllieHub.Models.EventArguments;
|
||||
using EllieHub.ViewModels.Abstractions;
|
||||
using EllieHub.Views.Controls;
|
||||
using ReactiveUI;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace EllieHub.Features.Common.ViewModels;
|
||||
namespace EllieHub.ViewModels.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a text box for inputting the absolute path of a directory.
|
||||
|
@ -15,7 +14,7 @@ public class UriInputBarViewModel : ViewModelBase<UriInputBar>
|
|||
{
|
||||
private static readonly FolderPickerOpenOptions _folderPickerOptions = new();
|
||||
private string _lastValidUri = AppStatics.AppDefaultConfigDirectoryUri;
|
||||
private bool _isDirectoryValid;
|
||||
private bool _isDirectoryValid = false;
|
||||
private string _currentUri = string.Empty;
|
||||
private readonly IStorageProvider _storageProvider;
|
||||
|
||||
|
@ -93,5 +92,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) && ToastieUtilities.HasWritePermissionAt(directoryUri);
|
||||
=> Directory.Exists(directoryUri) && Utilities.CanWriteTo(directoryUri);
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
using EllieHub.Features.Abstractions;
|
||||
using EllieHub.Features.AppConfig.Views.Windows;
|
||||
using EllieHub.ViewModels.Abstractions;
|
||||
using EllieHub.Views.Windows;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace EllieHub.Features.AppConfig.ViewModels;
|
||||
namespace EllieHub.ViewModels.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// View-model for the about me dialog window.
|
|
@ -1,10 +1,10 @@
|
|||
using EllieHub.Features.Abstractions;
|
||||
using EllieHub.Features.AppWindow.Views.Controls;
|
||||
using EllieHub.Features.AppWindow.Views.Windows;
|
||||
using EllieHub.Features.Home.ViewModels;
|
||||
using EllieHub.ViewModels.Abstractions;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
using EllieHub.Views.Controls;
|
||||
using EllieHub.Views.Windows;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace EllieHub.Features.AppWindow.ViewModels;
|
||||
namespace EllieHub.ViewModels.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// View-model for the main window.
|
|
@ -1,7 +1,6 @@
|
|||
using EllieHub.Features.Abstractions;
|
||||
using EllieHub.Features.Home.Views.Windows;
|
||||
using EllieHub.ViewModels.Abstractions;
|
||||
|
||||
namespace EllieHub.Features.Home.ViewModels;
|
||||
namespace EllieHub.ViewModels.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// View-model for the update dialog window.
|
|
@ -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.Features.BotConfig.ViewModels"
|
||||
xmlns:dd="using:EllieHub.Avalonia.DesignData.Controls"
|
||||
xmlns:vm="using:EllieHub.ViewModels.Controls"
|
||||
xmlns:dd="using:EllieHub.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.Features.BotConfig.Views.Controls.BotConfigView"
|
||||
x:Class="EllieHub.Views.Controls.BotConfigView"
|
||||
x:DataType="vm:BotConfigViewModel">
|
||||
|
||||
<Design.DataContext>
|
||||
|
@ -22,7 +22,7 @@
|
|||
Fill="{DynamicResource HeavyBackground}"/>
|
||||
|
||||
<Rectangle Grid.Row="1"
|
||||
Grid.RowSpan="2"
|
||||
Grid.RowSpan="3"
|
||||
Fill="{DynamicResource MediumBackground}"/>
|
||||
|
||||
<!--Bot Settings Title-->
|
|
@ -1,9 +1,9 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.ReactiveUI;
|
||||
using EllieHub.Features.BotConfig.ViewModels;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
|
||||
namespace EllieHub.Features.BotConfig.Views.Controls;
|
||||
namespace EllieHub.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 (sender is not Button button)
|
||||
if (!Utilities.TryCastTo<Button>(sender, out var 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 (sender is not Button button)
|
||||
if (!Utilities.TryCastTo<Button>(sender, out var button))
|
||||
return;
|
||||
|
||||
button.Opacity = 0.0;
|
|
@ -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.Features.AppConfig.ViewModels"
|
||||
xmlns:views="using:EllieHub.Features.Common.Views.Controls"
|
||||
xmlns:vm="using:EllieHub.ViewModels.Controls"
|
||||
xmlns:views="using:EllieHub.Views.Controls"
|
||||
xmlns:const="using:EllieHub.Common"
|
||||
xmlns:dd="using:EllieHub.Avalonia.DesignData.Controls"
|
||||
xmlns:dd="using:EllieHub.DesignData.Controls"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="{x:Static const:WindowConstants.MinWindowWidth}"
|
||||
d:DesignHeight="{x:Static const:WindowConstants.DefaultWindowHeight}"
|
||||
x:Class="EllieHub.Features.AppConfig.Views.Controls.ConfigView"
|
||||
x:Class="EllieHub.Views.Controls.ConfigView"
|
||||
x:DataType="vm:ConfigViewModel">
|
||||
|
||||
<Design.DataContext>
|
||||
|
@ -23,6 +23,7 @@
|
|||
Fill="{DynamicResource HeavyBackground}" />
|
||||
|
||||
<Rectangle Grid.Row="1"
|
||||
Grid.RowSpan="2"
|
||||
Fill="{DynamicResource MediumBackground}"/>
|
||||
|
||||
<!--Settings Title-->
|
||||
|
@ -38,7 +39,7 @@
|
|||
<TextBlock Text="Dependencies"
|
||||
FontSize="22"/>
|
||||
|
||||
<TextBlock Text="Make sure the dependencies below are installed if you want to use EllieBot to play music."/>
|
||||
<TextBlock Text="Make sure the dependencies below are installed if you want to use Ellie to play music."/>
|
||||
|
||||
<!--Dependency Buttons-->
|
||||
<ItemsRepeater ItemsSource="{Binding DependencyButtons}">
|
||||
|
@ -86,10 +87,9 @@
|
|||
|
||||
<!--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 backups of the bot instances."/>
|
||||
ToolTip.Tip="Defines the directory the updater is going to store the backup of the bot instances."/>
|
||||
|
||||
<TextBox Grid.Row="3"
|
||||
Grid.Column="0"
|
||||
|
@ -113,7 +113,6 @@
|
|||
|
||||
<!--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.Features.AppConfig.ViewModels;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
|
||||
namespace EllieHub.Features.AppConfig.Views.Controls;
|
||||
namespace EllieHub.Views.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// The view for the application's settings.
|
|
@ -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.Features.Common.ViewModels"
|
||||
xmlns:dd="using:EllieHub.Avalonia.DesignData.Controls"
|
||||
xmlns:vm="using:EllieHub.ViewModels.Controls"
|
||||
xmlns:dd="using:EllieHub.DesignData.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="50"
|
||||
x:Class="EllieHub.Features.Common.Views.Controls.DependencyButton"
|
||||
x:Class="EllieHub.Views.Controls.DependencyButton"
|
||||
x:DataType="vm:DependencyButtonViewModel">
|
||||
|
||||
<Design.DataContext>
|
|
@ -1,7 +1,7 @@
|
|||
using Avalonia.ReactiveUI;
|
||||
using EllieHub.Features.Common.ViewModels;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
|
||||
namespace EllieHub.Features.Common.Views.Controls;
|
||||
namespace EllieHub.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.Features.BotConfig.ViewModels"
|
||||
xmlns:dd="using:EllieHub.Avalonia.DesignData.Controls"
|
||||
xmlns:vm="using:EllieHub.ViewModels.Controls"
|
||||
xmlns:dd="using:EllieHub.DesignData.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="250"
|
||||
x:Class="EllieHub.Features.BotConfig.Views.Controls.FakeConsole"
|
||||
x:Class="EllieHub.Views.Controls.FakeConsole"
|
||||
x:DataType="vm:FakeConsoleViewModel">
|
||||
<Design.DataContext>
|
||||
<dd:DesignFakeConsoleViewModel/>
|
|
@ -1,7 +1,7 @@
|
|||
using Avalonia.ReactiveUI;
|
||||
using EllieHub.Features.BotConfig.ViewModels;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
|
||||
namespace EllieHub.Features.BotConfig.Views.Controls;
|
||||
namespace EllieHub.Views.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a control that mimics the appearance of a terminal emulator.
|
|
@ -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.Features.Home.ViewModels"
|
||||
xmlns:vm="using:EllieHub.ViewModels.Controls"
|
||||
xmlns:const="using:EllieHub.Common"
|
||||
xmlns:dd="using:EllieHub.Avalonia.DesignData.Controls"
|
||||
xmlns:dd="using:EllieHub.DesignData.Controls"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="{x:Static const:WindowConstants.MinWindowWidth}"
|
||||
d:DesignHeight="{x:Static const:WindowConstants.MinWindowHeight}"
|
||||
x:Class="EllieHub.Features.Home.Views.Controls.HomeView"
|
||||
x:Class="EllieHub.Views.Controls.HomeView"
|
||||
x:DataType="vm:HomeViewModel">
|
||||
<Design.DataContext>
|
||||
<dd:DesignHomeViewModel/>
|
||||
|
@ -28,18 +28,18 @@
|
|||
Grid.ColumnSpan="5"
|
||||
Fill="{DynamicResource HeavyBackground}"/>
|
||||
|
||||
<Rectangle Grid.Row="4"
|
||||
<Rectangle Grid.Row="5"
|
||||
Grid.Column="0"
|
||||
Fill="#008DDA"/>
|
||||
|
||||
<Rectangle Grid.Row="4"
|
||||
<Rectangle Grid.Row="5"
|
||||
Grid.Column="1"
|
||||
Fill="#F86752"/>
|
||||
|
||||
<Rectangle Grid.Row="4"
|
||||
<Rectangle Grid.Row="5"
|
||||
Grid.Column="2"
|
||||
Grid.ColumnSpan="3"
|
||||
Fill="#0e3f88"/>
|
||||
Fill="#B90060"/>
|
||||
|
||||
<!--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://app.feedbacky.net/b/ellie-feedback"
|
||||
CommandParameter="https://beta.elliebot.net/"
|
||||
Command="{Binding OpenUrl}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Classes="icon"
|
||||
|
@ -92,7 +92,7 @@
|
|||
Classes="transparent"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
CommandParameter="https://docs.elliebot.net/ellie/"
|
||||
CommandParameter="https://docs.elliebot.net"
|
||||
Command="{Binding OpenUrl}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Classes="icon"
|
||||
|
@ -106,7 +106,7 @@
|
|||
Classes="transparent"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
CommandParameter="https://discord.gg/etQdZxSyEH/"
|
||||
CommandParameter="https://discord.elliebot.net/"
|
||||
Command="{Binding OpenUrl}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Classes="icon"
|
||||
|
@ -147,7 +147,7 @@
|
|||
VerticalAlignment="Top"/>
|
||||
|
||||
<!--Bottom Buttons-->
|
||||
<Button Grid.Row="4"
|
||||
<Button Grid.Row="5"
|
||||
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="4"
|
||||
<Image Grid.Row="5"
|
||||
Grid.Column="0"
|
||||
Classes="icon"
|
||||
MaxHeight="30"
|
||||
|
@ -167,18 +167,18 @@
|
|||
Margin="0 0 3 0"
|
||||
Source="{DynamicResource KofiIcon}"/>
|
||||
|
||||
<Button Grid.Row="4"
|
||||
<Button Grid.Row="5"
|
||||
Grid.Column="1"
|
||||
Classes="transparent"
|
||||
Content="Support EllieBot"
|
||||
Content="Support Ellie"
|
||||
Foreground="White"
|
||||
FontSize="12"
|
||||
Cursor="Hand"
|
||||
HorizontalAlignment="Stretch"
|
||||
CommandParameter="https://patreon.com/toastiet0ast"
|
||||
CommandParameter="https://patreon.com/emotionchild"
|
||||
Command="{Binding OpenUrl}"/>
|
||||
|
||||
<Image Grid.Row="4"
|
||||
<Image Grid.Row="5"
|
||||
Grid.Column="1"
|
||||
Classes="icon"
|
||||
MaxHeight="30"
|
||||
|
@ -187,7 +187,7 @@
|
|||
Margin="0 0 3 0"
|
||||
Source="{DynamicResource PatreonIcon}"/>
|
||||
|
||||
<TextBlock Grid.Row="4"
|
||||
<TextBlock Grid.Row="5"
|
||||
Grid.Column="2"
|
||||
Grid.ColumnSpan="3"
|
||||
Text="Thank You ♡"
|
|
@ -1,7 +1,7 @@
|
|||
using Avalonia.ReactiveUI;
|
||||
using EllieHub.Features.Home.ViewModels;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
|
||||
namespace EllieHub.Features.Home.Views.Controls;
|
||||
namespace EllieHub.Views.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// View for the home window, with buttons linking to official Ellie resources.
|
|
@ -3,10 +3,11 @@
|
|||
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.Features.AppWindow.ViewModels"
|
||||
xmlns:dd="using:EllieHub.Avalonia.DesignData.Controls"
|
||||
xmlns:vm="using:EllieHub.ViewModels.Controls"
|
||||
xmlns:dd="using:EllieHub.DesignData.Controls"
|
||||
xmlns:const="using:EllieHub.Common"
|
||||
mc:Ignorable="d" d:DesignWidth="70" d:DesignHeight="450"
|
||||
x:Class="EllieHub.Features.AppWindow.Views.Controls.LateralBarView"
|
||||
x:Class="EllieHub.Views.Controls.LateralBarView"
|
||||
x:DataType="vm:LateralBarViewModel">
|
||||
|
||||
<Design.DataContext>
|
||||
|
@ -19,8 +20,7 @@
|
|||
<Button Classes="transparent"
|
||||
Name="HomeButton"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
IsEnabled="{Binding IsLateralBarEnabled}">
|
||||
HorizontalContentAlignment="Center">
|
||||
<Image Classes="icon"
|
||||
Source="{DynamicResource HomeIcon}"/>
|
||||
</Button>
|
||||
|
@ -34,8 +34,7 @@
|
|||
<Button Classes="accent"
|
||||
Content="+"
|
||||
Cursor="Hand"
|
||||
Command="{Binding AddBotButtonAsync}"
|
||||
IsEnabled="{Binding IsLateralBarEnabled}"/>
|
||||
Command="{Binding AddBotButtonAsync}"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
|
@ -78,8 +77,7 @@
|
|||
Classes="transparent"
|
||||
Name="ConfigButton"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{Binding IsLateralBarEnabled}">
|
||||
VerticalAlignment="Center">
|
||||
<Image MaxHeight="17"
|
||||
Source="{DynamicResource ConfigIcon}"/>
|
||||
</Button>
|
|
@ -5,22 +5,24 @@ using Avalonia.Interactivity;
|
|||
using Avalonia.Media.Immutable;
|
||||
using Avalonia.ReactiveUI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
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 EllieHub.DesignData.Common;
|
||||
using EllieHub.Models.Config;
|
||||
using EllieHub.Models.EventArguments;
|
||||
using EllieHub.Services.Mocks;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
using SkiaImageView;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace EllieHub.Features.AppWindow.Views.Controls;
|
||||
namespace EllieHub.Views.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// View for the lateral bar with home, bot, and configuration buttons.
|
||||
/// </summary>
|
||||
public partial class LateralBarView : ReactiveUserControl<LateralBarViewModel>
|
||||
{
|
||||
private readonly ReadOnlyAppSettings _appConfig;
|
||||
private static readonly Cursor _pointingHandCursor = new(StandardCursorType.Hand);
|
||||
private static readonly Cursor _arrow = new(StandardCursorType.Arrow);
|
||||
private readonly ReadOnlyAppConfig _appConfig;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the user clicks a bot button.
|
||||
|
@ -39,7 +41,7 @@ public partial class LateralBarView : ReactiveUserControl<LateralBarViewModel>
|
|||
/// Creates the lateral bar of the application.
|
||||
/// </summary>
|
||||
/// <param name="appConfig">The application settings.</param>
|
||||
public LateralBarView(ReadOnlyAppSettings appConfig)
|
||||
public LateralBarView(ReadOnlyAppConfig appConfig)
|
||||
{
|
||||
_appConfig = appConfig;
|
||||
InitializeComponent();
|
||||
|
@ -67,10 +69,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 (button.Parent?.Parent is not Border border)
|
||||
if (!Utilities.TryCastTo<Border>(button.Parent?.Parent, out var border))
|
||||
throw new InvalidOperationException("Visual tree has an unexpected structure.");
|
||||
|
||||
if (this.FindResource(base.ActualThemeVariant, "BotSelectionColor") is not ImmutableSolidColorBrush resourceColor)
|
||||
if (!Utilities.TryCastTo<ImmutableSolidColorBrush>(this.FindResource(base.ActualThemeVariant, "BotSelectionColor"), out var resourceColor))
|
||||
return;
|
||||
|
||||
border.BorderBrush = resourceColor;
|
||||
|
@ -91,11 +93,7 @@ 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)
|
||||
{
|
||||
// "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);
|
||||
}
|
||||
=> BotButtonClick?.Invoke((Button)sender, eventArgs);
|
||||
|
||||
/// <summary>
|
||||
/// Loads the bot avatar when the buttons on the lateral bar are rendered.
|
||||
|
@ -106,11 +104,10 @@ 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 (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)
|
||||
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))
|
||||
throw new InvalidOperationException("Visual tree has an unexpected structure.");
|
||||
|
||||
// Set the avatar
|
||||
|
@ -131,7 +128,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 (sender is not Button button)
|
||||
if (!Utilities.TryCastTo<Button>(sender, out var button))
|
||||
throw new InvalidOperationException($"Sender is not a {nameof(Button)}.");
|
||||
|
||||
var botAvatar = FindAvatarComponent(button);
|
||||
|
@ -148,7 +145,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 (sender is not Button button)
|
||||
if (!Utilities.TryCastTo<Button>(sender, out var button))
|
||||
throw new InvalidOperationException($"Sender is not a {nameof(Button)}.");
|
||||
|
||||
var botAvatar = FindAvatarComponent(button);
|
||||
|
@ -166,7 +163,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 (component.Content is not Guid botId)
|
||||
return (!Utilities.TryCastTo<Guid>(component.Content, out var botId))
|
||||
? throw new InvalidOperationException($"{nameof(T)} does not contain a bot Id.")
|
||||
: FindAvatarComponent(botId);
|
||||
}
|
|
@ -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.Features.Common.ViewModels"
|
||||
xmlns:dd="using:EllieHub.Avalonia.DesignData.Controls"
|
||||
xmlns:vm="using:EllieHub.ViewModels.Controls"
|
||||
xmlns:dd="using:EllieHub.DesignData.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="33"
|
||||
x:Class="EllieHub.Features.Common.Views.Controls.UriInputBar"
|
||||
x:Class="EllieHub.Views.Controls.UriInputBar"
|
||||
x:DataType="vm:UriInputBarViewModel">
|
||||
|
||||
<Design.DataContext>
|
|
@ -1,7 +1,7 @@
|
|||
using Avalonia.ReactiveUI;
|
||||
using EllieHub.Features.Common.ViewModels;
|
||||
using EllieHub.ViewModels.Controls;
|
||||
|
||||
namespace EllieHub.Features.Common.Views.Controls;
|
||||
namespace EllieHub.Views.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a text box that receives the absolute path to a directory.
|
|
@ -2,11 +2,12 @@
|
|||
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.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"
|
||||
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"
|
||||
x:DataType="vm:AboutMeViewModel"
|
||||
Title="About EllieHub"
|
||||
Icon="{DynamicResource EllieHubIcon}"
|
||||
|
@ -17,12 +18,12 @@
|
|||
</Design.DataContext>
|
||||
|
||||
<StackPanel Margin="10 20 10 20">
|
||||
<TextBlock Text="EllieBot is a general purpose open-source Discord bot created by Toastie. Support the project!"
|
||||
<TextBlock Text="Ellie 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://paypal.me/toastiet0ast"
|
||||
<Button CommandParameter="https://www.paypal.com/paypalme/toastiet0ast"
|
||||
Command="{Binding OpenUrl}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="Paypal " />
|
||||
|
@ -41,7 +42,7 @@
|
|||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button CommandParameter="https://www.patreon.com/toastiet0ast"
|
||||
<Button CommandParameter="https://www.patreon.com/emotionchild"
|
||||
Command="{Binding OpenUrl}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="Patreon " />
|
||||
|
@ -54,13 +55,10 @@
|
|||
<Separator Margin="20"
|
||||
HorizontalAlignment="Stretch" />
|
||||
|
||||
<TextBlock Text="This tool was made by Toastie. If it has been useful to you, consider showing your support."
|
||||
TextAlignment="Center"
|
||||
<TextBlock Text="This tool was made by Toastie. If it has been useful to you, consider showing your support by buying me a coffee."
|
||||
Margin="0 0 0 10" />
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<Button Margin="0 0 5 0"
|
||||
HorizontalAlignment="Center"
|
||||
<Button HorizontalAlignment="Center"
|
||||
CommandParameter="https://ko-fi.com/toastie_t0ast"
|
||||
Command="{Binding OpenUrl}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
|
@ -69,26 +67,21 @@
|
|||
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="© 2025 Toastie_t0ast"
|
||||
<TextBlock Text="© 2023 Toastie"
|
||||
FontSize="11"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0 30 0 0"/>
|
||||
<HyperlinkButton FontSize="11"
|
||||
HorizontalAlignment="Center"
|
||||
Content="GNU General Public License Version 3"
|
||||
CommandParameter="https://toastielab.dev/EllieBotDevs/EllieHub/src/branch/main/LICENSE"
|
||||
|
||||
<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>
|
||||
</StackPanel>
|
||||
</Window>
|
|
@ -1,7 +1,7 @@
|
|||
using Avalonia.ReactiveUI;
|
||||
using EllieHub.Features.AppConfig.ViewModels;
|
||||
using EllieHub.ViewModels.Windows;
|
||||
|
||||
namespace EllieHub.Features.AppConfig.Views.Windows;
|
||||
namespace EllieHub.Views.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the about me 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.Features.AppWindow.ViewModels"
|
||||
xmlns:vm="using:EllieHub.ViewModels.Windows"
|
||||
xmlns:const="using:EllieHub.Common"
|
||||
xmlns:dd="using:EllieHub.Avalonia.DesignData.Windows"
|
||||
xmlns:dd="using:EllieHub.DesignData.Windows"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="{x:Static const:WindowConstants.MinWindowWidth}"
|
||||
d:DesignHeight="{x:Static const:WindowConstants.MinWindowHeight}"
|
||||
x:Class="EllieHub.Features.AppWindow.Views.Windows.AppView"
|
||||
x:Class="EllieHub.Views.Windows.AppView"
|
||||
x:DataType="vm:AppViewModel"
|
||||
Icon="{DynamicResource EllieHubIcon}"
|
||||
WindowStartupLocation="CenterScreen"
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue