From e04333a4362d720d30e7435becb21e050b0a21c3 Mon Sep 17 00:00:00 2001 From: DSPAUL Date: Thu, 5 Sep 2024 20:47:50 +0200 Subject: [PATCH 01/20] Create .editorconfig --- .editorconfig | 231 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3222888 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,231 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = false + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_collection_expression = when_types_loosely_match +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = false:silent + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = when_on_single_line:suggestion +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = when_on_single_line:suggestion +csharp_style_expression_bodied_local_functions = when_on_single_line +csharp_style_expression_bodied_methods = true:suggestion +csharp_style_expression_bodied_operators = false +csharp_style_expression_bodied_properties = true + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_local_function = true:silent +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Code-block preferences +csharp_prefer_braces = true +csharp_prefer_simple_using_statement = true +csharp_style_namespace_declarations = block_scoped +csharp_style_prefer_method_group_conversion = true +csharp_style_prefer_primary_constructors = true +csharp_style_prefer_top_level_statements = true + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = no_change +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case From 6989ceacf0f000bbae8c8c187ed91630b392fd96 Mon Sep 17 00:00:00 2001 From: Paul De Smul Date: Thu, 5 Sep 2024 23:17:04 +0200 Subject: [PATCH 02/20] Refactor codexProperties --- .../PropIsEmptyToVisibilityConverter.cs | 3 +- src/Models/Codex.cs | 116 +-------- src/Models/CodexProperties/CodexProperty.cs | 232 ++++++++++++++++++ .../CodexProperties/CoverArtProperty.cs | 15 ++ .../CodexProperties/DateTimeProperty.cs | 17 ++ .../CodexProperties/EnumerableProperty.cs | 39 +++ src/Models/CodexProperties/FileProperty.cs | 13 + src/Models/CodexProperties/NumberProperty.cs | 13 + src/Models/CodexProperties/StringProperty.cs | 11 + src/Models/CodexProperties/TagsProperty.cs | 22 ++ src/Models/CodexProperty.cs | 120 --------- src/Models/Preferences/Preferences.cs | 18 +- src/Models/XmlDtos/CodexPropertyDto.cs | 2 +- src/Services/CoverService.cs | 1 + src/Services/PreferencesService.cs | 2 +- src/Tools/ExtensionMethods.cs | 4 +- src/ViewModels/ChooseMetaDataViewModel.cs | 8 +- src/ViewModels/CodexEditViewModel.cs | 1 + src/ViewModels/CodexViewModel.cs | 26 +- src/ViewModels/SettingsViewModel.cs | 1 + 20 files changed, 406 insertions(+), 258 deletions(-) create mode 100644 src/Models/CodexProperties/CodexProperty.cs create mode 100644 src/Models/CodexProperties/CoverArtProperty.cs create mode 100644 src/Models/CodexProperties/DateTimeProperty.cs create mode 100644 src/Models/CodexProperties/EnumerableProperty.cs create mode 100644 src/Models/CodexProperties/FileProperty.cs create mode 100644 src/Models/CodexProperties/NumberProperty.cs create mode 100644 src/Models/CodexProperties/StringProperty.cs create mode 100644 src/Models/CodexProperties/TagsProperty.cs delete mode 100644 src/Models/CodexProperty.cs diff --git a/src/Converters/PropIsEmptyToVisibilityConverter.cs b/src/Converters/PropIsEmptyToVisibilityConverter.cs index 8add48c..b46d07f 100644 --- a/src/Converters/PropIsEmptyToVisibilityConverter.cs +++ b/src/Converters/PropIsEmptyToVisibilityConverter.cs @@ -1,4 +1,5 @@ using COMPASS.Models; +using COMPASS.Models.CodexProperties; using System; using System.Globalization; using System.Windows; @@ -14,7 +15,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn { throw new ArgumentNullException(nameof(parameter)); } - CodexProperty? prop = Codex.Properties.Find(prop => prop.Name == parameter.ToString()); + CodexProperty? prop = Codex.MedataProperties.Find(prop => prop.Name == parameter.ToString()); if (prop is null) { diff --git a/src/Models/Codex.cs b/src/Models/Codex.cs index 14ff969..d30dfdb 100644 --- a/src/Models/Codex.cs +++ b/src/Models/Codex.cs @@ -1,11 +1,10 @@ using CommunityToolkit.Mvvm.ComponentModel; +using COMPASS.Models.CodexProperties; using COMPASS.Services; using COMPASS.Tools; -using COMPASS.ViewModels.Sources; using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.IO; using System.Linq; using System.Xml.Serialization; @@ -344,110 +343,17 @@ public string? FileType public string FileName => System.IO.Path.GetFileName(Path); #endregion - public static readonly List Properties = new() + public static readonly List MedataProperties = new() { - new(nameof(Title), - isEmpty: codex => String.IsNullOrWhiteSpace(codex.Title), - setProp: (codex,other) => codex.Title = other.Title, - defaultSources: new() - { - MetaDataSource.PDF, - MetaDataSource.File, - MetaDataSource.GmBinder, - MetaDataSource.Homebrewery, - MetaDataSource.GoogleDrive, - MetaDataSource.ISBN, - MetaDataSource.GenericURL - }), - new( nameof(Authors), - isEmpty: codex => codex.Authors is null || !codex.Authors.Any(), - setProp: (codex,other) => codex.Authors = other.Authors, - defaultSources: new() - { - MetaDataSource.PDF, - MetaDataSource.GmBinder, - MetaDataSource.Homebrewery, - MetaDataSource.ISBN, - MetaDataSource.GenericURL - }), - new( nameof(Publisher), - isEmpty: codex => String.IsNullOrEmpty(codex.Publisher), - setProp: (codex,other) => codex.Publisher = other.Publisher, - defaultSources: new() - { - MetaDataSource.ISBN, - MetaDataSource.GmBinder, - MetaDataSource.Homebrewery, - MetaDataSource.GoogleDrive, - }), - new( nameof(Version), - isEmpty: codex => String.IsNullOrEmpty(codex.Version), - setProp: (codex,other) => codex.Version = other.Version, - defaultSources: new() - { - MetaDataSource.Homebrewery - }), - new( nameof(PageCount), - isEmpty: codex => codex.PageCount == 0, - setProp: (codex, other) => codex.PageCount = other.PageCount, - defaultSources: new() - { - MetaDataSource.PDF, - MetaDataSource.Image, - MetaDataSource.GmBinder, - MetaDataSource.Homebrewery, - MetaDataSource.ISBN, - }, - label: "Pagecount"), - new( nameof(Tags), - isEmpty: codex => codex.Tags is null || !codex.Tags.Any(), - setProp: (codex,other) => - { - foreach (var tag in other.Tags) - { - App.SafeDispatcher.Invoke(() => codex.Tags.AddIfMissing(tag)); - } - }, - defaultSources : new() - { - MetaDataSource.File, - MetaDataSource.GenericURL, - }), - new( nameof(Description), - codex => String.IsNullOrEmpty(codex.Description), - (codex,other) => codex.Description = other.Description, - defaultSources : new() - { - MetaDataSource.Homebrewery, - MetaDataSource.ISBN, - MetaDataSource.GenericURL, - }), - new( nameof(ReleaseDate), - isEmpty: codex => codex.ReleaseDate is null || codex.ReleaseDate == DateTime.MinValue, - setProp: (codex, other) => codex.ReleaseDate = other.ReleaseDate, - defaultSources : new() - { - MetaDataSource.Homebrewery, - MetaDataSource.ISBN, - }, - label: "Release Date"), - new( nameof(CoverArt), - isEmpty: codex => !File.Exists(codex.CoverArt), - setProp: (codex,other) => - { - codex.CoverArt = other.CoverArt; - codex.Thumbnail = other.Thumbnail; - }, - defaultSources : new() - { - MetaDataSource.Image, - MetaDataSource.PDF, - MetaDataSource.GmBinder, - MetaDataSource.Homebrewery, - MetaDataSource.GoogleDrive, - MetaDataSource.ISBN, - }, - label: "Cover Art"), + CodexProperty.GetInstance(nameof(Title))!, + CodexProperty.GetInstance(nameof(Authors))!, + CodexProperty.GetInstance(nameof(Publisher))!, + CodexProperty.GetInstance(nameof(Version))!, + CodexProperty.GetInstance(nameof(PageCount))!, + CodexProperty.GetInstance(nameof(Tags))!, + CodexProperty.GetInstance(nameof(Description))!, + CodexProperty.GetInstance(nameof(ReleaseDate))!, + CodexProperty.GetInstance(nameof(CoverArt))!, }; } } diff --git a/src/Models/CodexProperties/CodexProperty.cs b/src/Models/CodexProperties/CodexProperty.cs new file mode 100644 index 0000000..585b5a5 --- /dev/null +++ b/src/Models/CodexProperties/CodexProperty.cs @@ -0,0 +1,232 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using COMPASS.Models.XmlDtos; +using COMPASS.Tools; +using COMPASS.ViewModels.Sources; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace COMPASS.Models.CodexProperties +{ + public abstract class CodexProperty : ObservableObject + { + protected CodexProperty(string propName, string? label = null) + { + Name = propName; + Label = label ?? propName; + _defaultSourcePriority = GetDefaultSources(propName); + } + + #region Properties + + public string Name { get; init; } + + public string Label { get; init; } + + #endregion + + #region Methods + + public abstract bool IsEmpty(Codex codex); + + public abstract void SetProp(Codex target, Codex source); + + /// + /// Checks if the codex to evaluated has a newer value for the property than the reference + /// + /// + /// + /// + public abstract bool HasNewValue(Codex toEvaluate, Codex reference); + + #endregion + + #region Import Sources + + private readonly List _defaultSourcePriority; + + private ObservableCollection? _sourcePriorityNamed; + /// + /// Ordered List of sources that can set this prop, named for data binding + /// + public ObservableCollection SourcePriorityNamed + { + get => _sourcePriorityNamed ??= new(SourcePriority.Select(source => new NamedMetaDataSource(source))); + set => SourcePriority = value.Select(namedSource => namedSource.Source).ToList(); + } + + private List _sourcePriority = new(); + /// + /// Ordered List of sources that can set this prop, used for logic + /// + public List SourcePriority + { + get => _sourcePriority; + set + { + _sourcePriority = value; + UpdateSources(); + } + } + + private MetaDataOverwriteMode _overwriteMode = MetaDataOverwriteMode.IfEmpty; + public MetaDataOverwriteMode OverwriteMode + { + get => _overwriteMode; + set => SetProperty(ref _overwriteMode, value); + } + + #endregion + + #region Mapping + + public void UpdateSources() + { + // If a new possible source was not found in the save, add it + foreach (var source in _defaultSourcePriority) + { + SourcePriority.AddIfMissing(source); + } + + // If a possible source was removed (due to a specific metadata fetch breaking + // or due to an api change or something), remove it from the sources + SourcePriority.RemoveAll(source => !_defaultSourcePriority.Contains(source)); + } + + public CodexPropertyDto ToDto() + { + CodexPropertyDto dto = new() + { + Name = Name, + OverwriteMode = OverwriteMode, + //Use order from NameMetaDataSources (which was reordered by user) + SourcePriority = SourcePriorityNamed.Select(namedSource => namedSource.Source).ToList(), + }; + + return dto; + } + + #endregion + + #region Factory + + public static CodexProperty? GetInstance(string propName) => propName switch + { + nameof(Codex.Title) => new StringProperty(nameof(Codex.Title)), + nameof(Codex.Authors) => new EnumerableProperty(nameof(Codex.Authors)), + nameof(Codex.Publisher) => new StringProperty(nameof(Codex.Publisher)), + nameof(Codex.Version) => new StringProperty(nameof(Codex.Version)), + nameof(Codex.PageCount) => new NumberProperty(nameof(Codex.PageCount), label: "Pagecount"), + nameof(Codex.Tags) => new TagsProperty(nameof(Codex.Tags)), + nameof(Codex.Description) => new StringProperty(nameof(Codex.Description)), + nameof(Codex.ReleaseDate) => new DateTimeProperty(nameof(Codex.ReleaseDate), label: "Release Date"), + nameof(Codex.CoverArt) => new CoverArtProperty(nameof(Codex.CoverArt), label: "Cover Art"), + _ => null //could occur when a new preference file with new props is loaded into an older version of compass + }; + + public static CodexProperty? FromDto(CodexPropertyDto propDto) + { + var prop = GetInstance(propDto.Name); + + if (prop == null) + { + return null; + } + + prop.SourcePriority = propDto.SourcePriority; + prop.OverwriteMode = propDto.OverwriteMode; + + return prop; + } + + private static List GetDefaultSources(string propName) => propName switch + { + nameof(Codex.Title) => new() + { + MetaDataSource.PDF, + MetaDataSource.File, + MetaDataSource.GmBinder, + MetaDataSource.Homebrewery, + MetaDataSource.GoogleDrive, + MetaDataSource.ISBN, + MetaDataSource.GenericURL + }, + nameof(Codex.Authors) => new() + { + MetaDataSource.PDF, + MetaDataSource.GmBinder, + MetaDataSource.Homebrewery, + MetaDataSource.ISBN, + MetaDataSource.GenericURL + }, + nameof(Codex.Publisher) => new() + { + MetaDataSource.ISBN, + MetaDataSource.GmBinder, + MetaDataSource.Homebrewery, + MetaDataSource.GoogleDrive, + }, + nameof(Codex.Version) => new() + { + MetaDataSource.Homebrewery + }, + nameof(Codex.PageCount) => new() + { + MetaDataSource.PDF, + MetaDataSource.Image, + MetaDataSource.GmBinder, + MetaDataSource.Homebrewery, + MetaDataSource.ISBN, + }, + nameof(Codex.Tags) => new() + { + MetaDataSource.File, + MetaDataSource.GenericURL, + }, + nameof(Codex.Description) => new() + { + MetaDataSource.Homebrewery, + MetaDataSource.ISBN, + MetaDataSource.GenericURL, + }, + nameof(Codex.ReleaseDate) => new() + { + MetaDataSource.Homebrewery, + MetaDataSource.ISBN, + }, + nameof(Codex.CoverArt) => new() + { + MetaDataSource.Image, + MetaDataSource.PDF, + MetaDataSource.GmBinder, + MetaDataSource.Homebrewery, + MetaDataSource.GoogleDrive, + MetaDataSource.ISBN, + }, + _ => new(), + }; + + #endregion + } + + public class CodexProperty : CodexProperty + { + public CodexProperty(string propName, string? label = null) : + base(propName, label) + { } + + public override bool IsEmpty(Codex codex) => EqualityComparer.Default.Equals(GetProp(codex), default); + + public T? GetProp(Codex codex) + { + object? value = codex.GetPropertyValue(Name); + return value == null ? default : (T)value; + } + + public override void SetProp(Codex target, Codex source) + => target.SetProperty(Name, GetProp(source)); + + public override bool HasNewValue(Codex toEvaluate, Codex reference) => + !EqualityComparer.Default.Equals(GetProp(toEvaluate), GetProp(reference)); + } +} diff --git a/src/Models/CodexProperties/CoverArtProperty.cs b/src/Models/CodexProperties/CoverArtProperty.cs new file mode 100644 index 0000000..dece795 --- /dev/null +++ b/src/Models/CodexProperties/CoverArtProperty.cs @@ -0,0 +1,15 @@ +namespace COMPASS.Models.CodexProperties +{ + public class CoverArtProperty : FileProperty + { + public CoverArtProperty(string propName, string? label = null) : + base(propName, label) + { } + + public override void SetProp(Codex target, Codex source) + { + target.CoverArt = source.CoverArt; + target.Thumbnail = source.Thumbnail; + } + } +} diff --git a/src/Models/CodexProperties/DateTimeProperty.cs b/src/Models/CodexProperties/DateTimeProperty.cs new file mode 100644 index 0000000..94b94cb --- /dev/null +++ b/src/Models/CodexProperties/DateTimeProperty.cs @@ -0,0 +1,17 @@ +using System; + +namespace COMPASS.Models.CodexProperties +{ + public class DateTimeProperty : CodexProperty + { + public DateTimeProperty(string propName, string? label = null) : + base(propName, label) + { } + + public override bool IsEmpty(Codex codex) + { + DateTime? value = GetProp(codex); + return value is null || value == DateTime.MinValue; + } + } +} diff --git a/src/Models/CodexProperties/EnumerableProperty.cs b/src/Models/CodexProperties/EnumerableProperty.cs new file mode 100644 index 0000000..415391e --- /dev/null +++ b/src/Models/CodexProperties/EnumerableProperty.cs @@ -0,0 +1,39 @@ +using COMPASS.Tools; +using System.Collections.Generic; +using System.Linq; + +namespace COMPASS.Models.CodexProperties +{ + public class EnumerableProperty : CodexProperty> + { + public EnumerableProperty(string propName, string? label = null) : + base(propName, label) + { } + + public override bool IsEmpty(Codex codex) + { + IEnumerable? value = GetProp(codex); + + return !value.SafeAny(); + } + + public override bool HasNewValue(Codex toEvaluate, Codex reference) + { + var newVal = GetProp(toEvaluate); + if (!newVal.SafeAny()) + { + //new value is nothing, don't consider it new data + return false; + } + + var refVal = GetProp(reference); + if (refVal == null) + { + //existing is nothing, so anything is new compared to that + return true; + } + + return !newVal!.SequenceEqual(refVal); + } + } +} diff --git a/src/Models/CodexProperties/FileProperty.cs b/src/Models/CodexProperties/FileProperty.cs new file mode 100644 index 0000000..1e5ad74 --- /dev/null +++ b/src/Models/CodexProperties/FileProperty.cs @@ -0,0 +1,13 @@ +using System.IO; + +namespace COMPASS.Models.CodexProperties +{ + public class FileProperty : StringProperty + { + public FileProperty(string propName, string? label = null) : + base(propName, label) + { } + + public override bool IsEmpty(Codex codex) => !File.Exists(GetProp(codex)); + } +} diff --git a/src/Models/CodexProperties/NumberProperty.cs b/src/Models/CodexProperties/NumberProperty.cs new file mode 100644 index 0000000..e713585 --- /dev/null +++ b/src/Models/CodexProperties/NumberProperty.cs @@ -0,0 +1,13 @@ +using System.Numerics; + +namespace COMPASS.Models.CodexProperties +{ + public class NumberProperty : CodexProperty where T : INumber + { + public NumberProperty(string propName, string? label = null) : + base(propName, label) + { } + + public override bool IsEmpty(Codex codex) => T.IsZero(GetProp(codex)!); + } +} diff --git a/src/Models/CodexProperties/StringProperty.cs b/src/Models/CodexProperties/StringProperty.cs new file mode 100644 index 0000000..16f32f8 --- /dev/null +++ b/src/Models/CodexProperties/StringProperty.cs @@ -0,0 +1,11 @@ +namespace COMPASS.Models.CodexProperties +{ + public class StringProperty : CodexProperty + { + public StringProperty(string propName, string? label = null) : + base(propName, label) + { } + + public override bool IsEmpty(Codex codex) => string.IsNullOrEmpty(GetProp(codex)); + } +} diff --git a/src/Models/CodexProperties/TagsProperty.cs b/src/Models/CodexProperties/TagsProperty.cs new file mode 100644 index 0000000..37a2741 --- /dev/null +++ b/src/Models/CodexProperties/TagsProperty.cs @@ -0,0 +1,22 @@ +using COMPASS.Tools; +using System.Linq; + +namespace COMPASS.Models.CodexProperties +{ + public class TagsProperty : EnumerableProperty + { + public TagsProperty(string propName, string? label = null) : + base(propName, label) + { } + + public override void SetProp(Codex target, Codex source) + { + foreach (var tag in source.Tags) + { + App.SafeDispatcher.Invoke(() => target.Tags.AddIfMissing(tag)); + } + } + + public override bool HasNewValue(Codex toEvaluate, Codex reference) => toEvaluate.Tags.Except(reference.Tags).Any(); + } +} diff --git a/src/Models/CodexProperty.cs b/src/Models/CodexProperty.cs deleted file mode 100644 index cce2bfa..0000000 --- a/src/Models/CodexProperty.cs +++ /dev/null @@ -1,120 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using COMPASS.Models.XmlDtos; -using COMPASS.Tools; -using COMPASS.ViewModels.Sources; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; - -namespace COMPASS.Models -{ - public class CodexProperty : ObservableObject - { - public CodexProperty(string propName, Func isEmpty, Action setProp, List defaultSources, string? label = null) - { - Name = propName; - Label = label ?? propName; - IsEmpty = isEmpty; - SetProp = setProp; - DefaultSourcePriority = defaultSources; - } - - public CodexProperty(CodexPropertyDto dto, CodexProperty defaultProp) - { - Name = dto.Name; - SourcePriority = dto.SourcePriority; - OverwriteMode = dto.OverwriteMode; - - Label = defaultProp.Label; - IsEmpty = defaultProp.IsEmpty; - SetProp = defaultProp.SetProp; - DefaultSourcePriority = defaultProp.DefaultSourcePriority; - - UpdateSources(); - } - - public string Name { get; init; } = ""; - - public string Label { get; init; } = ""; - - private Func? _isEmpty; - public Func IsEmpty - { - get => _isEmpty ??= Codex.Properties.First(prop => prop.Name == Name).IsEmpty; - private init => _isEmpty = value; - } - - private Func? _getProp; - public Func GetProp => _getProp ??= codex => codex.GetPropertyValue(Name); - - private Action? _setProp; - public Action SetProp - { - get => _setProp ??= Codex.Properties.First(prop => prop.Name == Name).SetProp; - private init => _setProp = value; - } - - #region Import Sources - private List? _defaultSources; - private List DefaultSourcePriority - { - get => _defaultSources ??= Codex.Properties.First(prop => prop.Name == Name).DefaultSourcePriority; - init => _defaultSources = value; - } - - private ObservableCollection? _sourcePriorityNamed; - /// - /// Ordered List of sources that can set this prop, named for data binding - /// - public ObservableCollection SourcePriorityNamed - { - get => _sourcePriorityNamed ??= new(SourcePriority.Select(source => new NamedMetaDataSource(source))); - set => SourcePriority = value.Select(namedSource => namedSource.Source).ToList(); - } - - /// - /// Ordered List of sources that can set this prop, used for logic - /// - public List SourcePriority { get; set; } = new(); - - private MetaDataOverwriteMode _overwriteMode = MetaDataOverwriteMode.IfEmpty; - public MetaDataOverwriteMode OverwriteMode - { - get => _overwriteMode; - set => SetProperty(ref _overwriteMode, value); - } - - #endregion - - #region Mapping - - public void UpdateSources() - { - // If a new possible source was not found in the save, add it - foreach (var source in DefaultSourcePriority) - { - SourcePriority.AddIfMissing(source); - } - - // If a possible source was removed (due to a specific metadata fetch breaking - // or due to an api change or something), remove it from the sources - SourcePriority.RemoveAll(source => !DefaultSourcePriority.Contains(source)); - } - - public CodexPropertyDto ToDto() - { - CodexPropertyDto dto = new() - { - Name = Name, - OverwriteMode = OverwriteMode, - //Use order from NameMetaDataSources (which was reordered by user) - SourcePriority = SourcePriorityNamed.Select(namedSource => namedSource.Source).ToList(), - }; - - return dto; - } - - #endregion - } -} diff --git a/src/Models/Preferences/Preferences.cs b/src/Models/Preferences/Preferences.cs index c849e0c..0bada2a 100644 --- a/src/Models/Preferences/Preferences.cs +++ b/src/Models/Preferences/Preferences.cs @@ -1,4 +1,5 @@ using CommunityToolkit.Mvvm.ComponentModel; +using COMPASS.Models.CodexProperties; using COMPASS.Models.XmlDtos; using COMPASS.ViewModels; using System.Collections.Generic; @@ -12,7 +13,7 @@ public class Preferences : ObservableObject public Preferences() { _openCodexPriority = new(_openCodexFunctions); - CodexProperties = Codex.Properties.ToList(); + CodexProperties = Codex.MedataProperties.ToList(); ListLayoutPreferences = new(); CardLayoutPreferences = new(); TileLayoutPreferences = new(); @@ -120,7 +121,7 @@ private static ObservableCollection> MapCodexPriority( /// private static List MapCodexProperties(List propertyDtos) { -#pragma warning disable CS0612 // Type or member "Label" is obsolete +#pragma warning disable CS0618 // Type or member "Label" is obsolete //In versions 1.6.0 and lower, label was stored instead of name var useLabel = propertyDtos.All(prop => string.IsNullOrEmpty(prop.Name) && !string.IsNullOrEmpty(prop.Label)); @@ -129,23 +130,26 @@ private static List MapCodexProperties(List pro for (int i = 0; i < propertyDtos.Count; i++) { CodexPropertyDto propDto = propertyDtos[i]; - var foundProp = Codex.Properties.Find(p => p.Label == propDto.Label); + var foundProp = Codex.MedataProperties.Find(p => p.Label == propDto.Label); if (foundProp != null) { propDto.Name = foundProp.Name; } } } -#pragma warning restore CS0612 // Type or member "Label" is obsolete +#pragma warning restore CS0618 // Type or member "Label" is obsolete var props = new List(); - foreach (var defaultProp in Codex.Properties) + foreach (var defaultProp in Codex.MedataProperties) { CodexPropertyDto? propDto = propertyDtos.Find(p => p.Name == defaultProp.Name); // Add Preferences from defaults if they weren't found on the loaded Preferences - CodexProperty prop = propDto is null ? defaultProp : new(propDto, defaultProp); - props.Add(prop); + CodexProperty? prop = propDto is null ? defaultProp : CodexProperty.FromDto(propDto); + if (prop is not null) + { + props.Add(prop); + } } return props; diff --git a/src/Models/XmlDtos/CodexPropertyDto.cs b/src/Models/XmlDtos/CodexPropertyDto.cs index 24045f6..101e1c1 100644 --- a/src/Models/XmlDtos/CodexPropertyDto.cs +++ b/src/Models/XmlDtos/CodexPropertyDto.cs @@ -10,7 +10,7 @@ public class CodexPropertyDto { public string Name { get; set; } = string.Empty; - [Obsolete] + [Obsolete("Label is now determined based on the Name")] public string Label { get; set; } = string.Empty; diff --git a/src/Services/CoverService.cs b/src/Services/CoverService.cs index 2a052f0..4740bda 100644 --- a/src/Services/CoverService.cs +++ b/src/Services/CoverService.cs @@ -1,4 +1,5 @@ using COMPASS.Models; +using COMPASS.Models.CodexProperties; using COMPASS.Tools; using COMPASS.ViewModels; using COMPASS.ViewModels.Sources; diff --git a/src/Services/PreferencesService.cs b/src/Services/PreferencesService.cs index cb27177..199dc9c 100644 --- a/src/Services/PreferencesService.cs +++ b/src/Services/PreferencesService.cs @@ -1,4 +1,4 @@ -using COMPASS.Models; +using COMPASS.Models.CodexProperties; using COMPASS.Models.Preferences; using COMPASS.Models.XmlDtos; using COMPASS.Tools; diff --git a/src/Tools/ExtensionMethods.cs b/src/Tools/ExtensionMethods.cs index 545e458..47c8189 100644 --- a/src/Tools/ExtensionMethods.cs +++ b/src/Tools/ExtensionMethods.cs @@ -128,7 +128,7 @@ public static string RemoveDiacritics(this string text) => return result; } - public static void SetProperty(this object obj, string propName, object value) + public static void SetProperty(this object obj, string propName, object? value) { var propInfo = obj.GetPropertyInfo(propName); @@ -171,6 +171,8 @@ public static IEnumerable Flatten(this IEnumerable l, string method = " } } } + + public static bool SafeAny(this IEnumerable? l) => l is not null && l.Any(); #endregion } } diff --git a/src/ViewModels/ChooseMetaDataViewModel.cs b/src/ViewModels/ChooseMetaDataViewModel.cs index 0acbc7a..eb60d5a 100644 --- a/src/ViewModels/ChooseMetaDataViewModel.cs +++ b/src/ViewModels/ChooseMetaDataViewModel.cs @@ -1,4 +1,5 @@ using COMPASS.Models; +using COMPASS.Models.CodexProperties; using COMPASS.Services; using COMPASS.Tools; using System; @@ -107,12 +108,7 @@ private Dictionary DefaultShouldUseNewValue Dictionary dict = new(); foreach (var prop in PropsToAsk) { - bool useNew = prop.Name == nameof(Codex.Tags) ? - //for tags, new value was chosen when there are more tags in the list - ((IList)prop.GetProp(_codicesWithMadeChoices[StepCounter])!).Count > ((IList)prop.GetProp(CurrentPair.Item1)!).Count - // for all the other, do a string compare to see if the new options was chosen - : prop.GetProp(CurrentPair.Item1)?.ToString() != prop.GetProp(_codicesWithMadeChoices[StepCounter])?.ToString(); - + bool useNew = prop.HasNewValue(_codicesWithMadeChoices[StepCounter], CurrentPair.Item1); dict.Add(prop.Name, useNew); } return dict; diff --git a/src/ViewModels/CodexEditViewModel.cs b/src/ViewModels/CodexEditViewModel.cs index 4550787..22ac62c 100644 --- a/src/ViewModels/CodexEditViewModel.cs +++ b/src/ViewModels/CodexEditViewModel.cs @@ -1,5 +1,6 @@ using CommunityToolkit.Mvvm.Input; using COMPASS.Models; +using COMPASS.Models.CodexProperties; using COMPASS.Resources.Controls.MultiSelectCombobox; using COMPASS.Services; using COMPASS.Tools; diff --git a/src/ViewModels/CodexViewModel.cs b/src/ViewModels/CodexViewModel.cs index 9511aa7..737b235 100644 --- a/src/ViewModels/CodexViewModel.cs +++ b/src/ViewModels/CodexViewModel.cs @@ -2,6 +2,7 @@ using CommunityToolkit.Mvvm.Input; using COMPASS.Interfaces; using COMPASS.Models; +using COMPASS.Models.CodexProperties; using COMPASS.Models.Enums; using COMPASS.Services; using COMPASS.Tools; @@ -419,7 +420,7 @@ private static async Task GetMetaData(Codex codex, ChooseMetaDataViewModel choos if (prop.OverwriteMode == MetaDataOverwriteMode.Never) continue; if (prop.OverwriteMode == MetaDataOverwriteMode.IfEmpty && !prop.IsEmpty(codex)) continue; - if (prop.Name == nameof(Codex.CoverArt)) continue; //Covers are done separately + if (prop is CoverArtProperty) continue; //Covers are done separately //propHolder will hold the property from the top preferred source Codex propHolder = new(); @@ -430,19 +431,20 @@ private static async Task GetMetaData(Codex codex, ChooseMetaDataViewModel choos ProgressViewModel.GlobalCancellationTokenSource.Token.ThrowIfCancellationRequested(); // Check if there is metadata from this source to use - if (!metaDataFromSource.ContainsKey(source)) + if (!metaDataFromSource.TryGetValue(source, out Codex? value)) { SourceViewModel? sourceVM = SourceViewModel.GetSourceVM(source); if (sourceVM is null) continue; if (!sourceVM.IsValidSource(codex)) continue; var metaDataHolder = await sourceVM.GetMetaData(metaDatalessCodex); - metaDataFromSource.Add(source, metaDataHolder); + value = metaDataHolder; + metaDataFromSource.Add(source, value); } // Set the prop Data from this source in propHolder // if the new value is not null/default/empty - if (!prop.IsEmpty(metaDataFromSource[source])) + if (!prop.IsEmpty(value)) { - prop.SetProp(propHolder, metaDataFromSource[source]); + prop.SetProp(propHolder, value); } } @@ -453,18 +455,10 @@ private static async Task GetMetaData(Codex codex, ChooseMetaDataViewModel choos { prop.SetProp(codex, propHolder); } - else if (prop.OverwriteMode == MetaDataOverwriteMode.Ask) + else if (prop.OverwriteMode == MetaDataOverwriteMode.Ask && prop.HasNewValue(propHolder, codex)) { - bool isDifferent = prop.Name == nameof(Codex.Tags) ? - // in case of tags, check if source adds tags that aren't there yet - ((IList)prop.GetProp(propHolder)!).Except((IList)prop.GetProp(codex)!).Any() - //check if ToString() representations are different, doesn't work for tags - : prop.GetProp(codex)?.ToString() != prop.GetProp(propHolder)?.ToString(); - if (isDifferent) - { - prop.SetProp(toAsk, propHolder); - shouldAsk = true; //set shouldAsk to true when we found at lease one none empty prop that should be asked - } + prop.SetProp(toAsk, propHolder); + shouldAsk = true; //set shouldAsk to true when we found at lease one none empty prop that should be asked } } diff --git a/src/ViewModels/SettingsViewModel.cs b/src/ViewModels/SettingsViewModel.cs index f1e5b79..9b8c988 100644 --- a/src/ViewModels/SettingsViewModel.cs +++ b/src/ViewModels/SettingsViewModel.cs @@ -2,6 +2,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using COMPASS.Models; +using COMPASS.Models.CodexProperties; using COMPASS.Models.Enums; using COMPASS.Models.Preferences; using COMPASS.Services; From 617c99dd39b910e51ed77e01b1cf20785db82934 Mon Sep 17 00:00:00 2001 From: Paul De Smul Date: Thu, 5 Sep 2024 23:44:58 +0200 Subject: [PATCH 03/20] Small fixes --- src/Models/Codex.cs | 8 ++++++-- src/Models/CodexProperties/TagsProperty.cs | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Models/Codex.cs b/src/Models/Codex.cs index d30dfdb..0d80dfe 100644 --- a/src/Models/Codex.cs +++ b/src/Models/Codex.cs @@ -185,6 +185,10 @@ public string SourceURL set { value = IOService.SanitizeXmlString(value); + if (value.StartsWith("www.")) + { + value = @"https://" + value; + } SetProperty(ref _sourceURL, value); } } @@ -227,10 +231,10 @@ public ObservableCollection Tags { get { + //order them in same order as alltags by starting with alltags and keeping the ones we need using intersect + List orderedTags = _tags.FirstOrDefault()?.AllTags.Intersect(_tags).ToList() ?? new List(); App.SafeDispatcher.Invoke(() => { - //order them in same order as alltags by starting with alltags and keeping the ones we need using intersect - List orderedTags = _tags.FirstOrDefault()?.AllTags.Intersect(_tags).ToList() ?? new List(); _tags.Clear(); //will fail when called from non UI thread which happens during import _tags.AddRange(orderedTags); }); diff --git a/src/Models/CodexProperties/TagsProperty.cs b/src/Models/CodexProperties/TagsProperty.cs index 37a2741..4710a91 100644 --- a/src/Models/CodexProperties/TagsProperty.cs +++ b/src/Models/CodexProperties/TagsProperty.cs @@ -11,7 +11,7 @@ public TagsProperty(string propName, string? label = null) : public override void SetProp(Codex target, Codex source) { - foreach (var tag in source.Tags) + foreach (var tag in source.Tags.ToList()) { App.SafeDispatcher.Invoke(() => target.Tags.AddIfMissing(tag)); } From 71b8acb655ebf4751072e088e2c4bfb4ebc85417 Mon Sep 17 00:00:00 2001 From: Paul De Smul Date: Fri, 6 Sep 2024 19:03:21 +0200 Subject: [PATCH 04/20] Refactor Filters --- src/COMPASS.csproj | 6 +- src/Models/Filter.cs | 153 ------------------- src/Models/Filters/AuthorFilter.cs | 16 ++ src/Models/Filters/DomainFilter.cs | 18 +++ src/Models/Filters/FavoriteFilter.cs | 17 +++ src/Models/Filters/FileExtensionFilter.cs | 18 +++ src/Models/Filters/FilterBase.cs | 63 ++++++++ src/Models/Filters/FilterType.cs | 21 +++ src/Models/Filters/HasBrokenPathFilter.cs | 17 +++ src/Models/Filters/HasISBNFilter.cs | 15 ++ src/Models/Filters/MinimumRatingFilter.cs | 14 ++ src/Models/Filters/OfflineSourceFilter.cs | 14 ++ src/Models/Filters/OnlineSourceFilter.cs | 15 ++ src/Models/Filters/PhysicalSourceFilter.cs | 14 ++ src/Models/Filters/PublisherFilter.cs | 17 +++ src/Models/Filters/SearchFilter.cs | 29 ++++ src/Models/Filters/StartReleaseDateFilter.cs | 15 ++ src/Models/Filters/StopReleaseDateFilter.cs | 15 ++ src/Models/Filters/TagFilter.cs | 19 +++ src/ViewModels/CodexInfoViewModel.cs | 7 +- src/ViewModels/FilterViewModel.cs | 111 ++++++-------- src/ViewModels/SettingsViewModel.cs | 3 +- src/ViewModels/TagsViewModel.cs | 5 +- 23 files changed, 394 insertions(+), 228 deletions(-) delete mode 100644 src/Models/Filter.cs create mode 100644 src/Models/Filters/AuthorFilter.cs create mode 100644 src/Models/Filters/DomainFilter.cs create mode 100644 src/Models/Filters/FavoriteFilter.cs create mode 100644 src/Models/Filters/FileExtensionFilter.cs create mode 100644 src/Models/Filters/FilterBase.cs create mode 100644 src/Models/Filters/FilterType.cs create mode 100644 src/Models/Filters/HasBrokenPathFilter.cs create mode 100644 src/Models/Filters/HasISBNFilter.cs create mode 100644 src/Models/Filters/MinimumRatingFilter.cs create mode 100644 src/Models/Filters/OfflineSourceFilter.cs create mode 100644 src/Models/Filters/OnlineSourceFilter.cs create mode 100644 src/Models/Filters/PhysicalSourceFilter.cs create mode 100644 src/Models/Filters/PublisherFilter.cs create mode 100644 src/Models/Filters/SearchFilter.cs create mode 100644 src/Models/Filters/StartReleaseDateFilter.cs create mode 100644 src/Models/Filters/StopReleaseDateFilter.cs create mode 100644 src/Models/Filters/TagFilter.cs diff --git a/src/COMPASS.csproj b/src/COMPASS.csproj index 8d0d20f..746c305 100644 --- a/src/COMPASS.csproj +++ b/src/COMPASS.csproj @@ -59,12 +59,12 @@ - + - + @@ -77,7 +77,7 @@ - + diff --git a/src/Models/Filter.cs b/src/Models/Filter.cs deleted file mode 100644 index 19c339b..0000000 --- a/src/Models/Filter.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using System.IO; -using System.Windows.Media; - -namespace COMPASS.Models -{ - public sealed class Filter : ITag, IEquatable - { - public Filter(FilterType filterType, object? filterValue = null) - { - Type = filterType; - FilterValue = filterValue; - } - - public enum FilterType - { - Tag, - Search, - Author, - Publisher, - StartReleaseDate, - StopReleaseDate, - MinimumRating, - OnlineSource, - OfflineSource, - PhysicalSource, - Favorite, - FileExtension, - HasBrokenPath, - HasISBN, - Domain - } - - public Func Method => Type switch - { - FilterType.Author => codex => FilterValue is string author && codex.Authors.Contains(author), - FilterType.Publisher => codex => FilterValue is string publisher && codex.Publisher == publisher, - FilterType.StartReleaseDate => codex => FilterValue is DateTime date && codex.ReleaseDate >= date, - FilterType.StopReleaseDate => codex => FilterValue is DateTime date && codex.ReleaseDate < date, - FilterType.MinimumRating => codex => FilterValue is int rating && codex.Rating >= rating, - FilterType.OfflineSource => codex => codex.HasOfflineSource(), - FilterType.OnlineSource => codex => codex.HasOnlineSource(), - FilterType.HasISBN => codex => !String.IsNullOrEmpty(codex.ISBN), - FilterType.PhysicalSource => codex => codex.PhysicallyOwned, - FilterType.Favorite => codex => codex.Favorite, - FilterType.FileExtension => codex => FilterValue is string extension && codex.FileType == extension, - FilterType.HasBrokenPath => codex => codex.HasOfflineSource() && !Path.Exists(codex.Path), - FilterType.Domain => codex => FilterValue is string domain && codex.HasOnlineSource() && codex.SourceURL.Contains(domain), - _ => _ => true - }; - - //Implement ITag interface - public string Content - { - get - { - if (FilterValue is null) return Label; - string formattedFilterValue = FilterValue switch - { - DateTime date => date.ToShortDateString(), - ITag tag => tag.Content, - _ => FilterValue.ToString() ?? string.Empty - }; - return $"{Label} {formattedFilterValue} {Suffix}".Trim(); - } - } - - public Color BackgroundColor => Type switch - { - FilterType.Author => Colors.Orange, - FilterType.Publisher => Colors.MediumPurple, - FilterType.StartReleaseDate => Colors.DeepSkyBlue, - FilterType.StopReleaseDate => Colors.DeepSkyBlue, - FilterType.MinimumRating => Colors.Goldenrod, - FilterType.OfflineSource => Colors.DarkSeaGreen, - FilterType.OnlineSource => Colors.DarkSeaGreen, - FilterType.PhysicalSource => Colors.DarkSeaGreen, - FilterType.HasISBN => Colors.DarkSeaGreen, - FilterType.Favorite => Colors.HotPink, - FilterType.FileExtension => Colors.OrangeRed, - FilterType.Search => Colors.Salmon, - FilterType.Tag => ((ITag?)FilterValue)!.BackgroundColor, - FilterType.HasBrokenPath => Colors.Gold, - FilterType.Domain => Colors.MediumTurquoise, - _ => throw new NotImplementedException(), - }; - - //Properties - public FilterType Type { get; init; } - public object? FilterValue { get; init; } - public string Label => Type switch - { - FilterType.Author => "Author:", - FilterType.Publisher => "Publisher:", - FilterType.StartReleaseDate => "After:", - FilterType.StopReleaseDate => "Before:", - FilterType.MinimumRating => "At least", - FilterType.OfflineSource => "Available Offline", - FilterType.OnlineSource => "Available Online", - FilterType.PhysicalSource => "Physically Owned", - FilterType.Favorite => "Favorite", - FilterType.FileExtension => "File Type:", - FilterType.Search => "Search:", - FilterType.HasBrokenPath => "Has Broken Path", - FilterType.HasISBN => "Has ISBN", - FilterType.Domain => "From:", - _ => "", - }; - public string Suffix => Type switch - { - FilterType.MinimumRating => "stars", - _ => "" - }; - public bool Unique => Type switch - { - FilterType.StartReleaseDate => true, - FilterType.StopReleaseDate => true, - FilterType.MinimumRating => true, - FilterType.Search => true, - FilterType.HasBrokenPath => true, - _ => false - }; - - //Overwrite Equal operator - public override bool Equals(object? obj) => Equals(obj as Filter); - - public bool Equals(Filter? other) - { - if (other is null) - return false; - if (ReferenceEquals(this, other)) - return true; - if (GetType() != other.GetType()) - return false; - return Type == other.Type && FilterValue == other.FilterValue; - } - public static bool operator ==(Filter lhs, Filter rhs) - { - if (lhs is null) - { - return rhs is null; //if lhs is null, only equal if rhs is also null - } - // Equals handles case of null on right side. - return lhs.Equals(rhs); - } - public static bool operator !=(Filter lhs, Filter rhs) - { - return !(lhs == rhs); - } - - public override int GetHashCode() => Content.GetHashCode(); - } -} diff --git a/src/Models/Filters/AuthorFilter.cs b/src/Models/Filters/AuthorFilter.cs new file mode 100644 index 0000000..6ec6a19 --- /dev/null +++ b/src/Models/Filters/AuthorFilter.cs @@ -0,0 +1,16 @@ +using System.Windows.Media; + +namespace COMPASS.Models.Filters +{ + internal class AuthorFilter : FilterBase + { + public AuthorFilter(string author) : base(FilterType.Author, author) + { + AllowMultiple = true; + } + + public override string Content => $"Author: {FilterValue}"; + public override Color BackgroundColor => Colors.Orange; + public override bool Apply(Codex codex) => FilterValue is string author && codex.Authors.Contains(author); + } +} diff --git a/src/Models/Filters/DomainFilter.cs b/src/Models/Filters/DomainFilter.cs new file mode 100644 index 0000000..2d1dbda --- /dev/null +++ b/src/Models/Filters/DomainFilter.cs @@ -0,0 +1,18 @@ +using System.Windows.Media; + +namespace COMPASS.Models.Filters +{ + internal class DomainFilter : FilterBase + { + public DomainFilter(string domain) : base(FilterType.Domain, domain) + { + AllowMultiple = true; + } + + public override Color BackgroundColor => Colors.MediumTurquoise; + + public override string Content => $"From: {FilterValue}"; + + public override bool Apply(Codex codex) => FilterValue is string domain && codex.HasOnlineSource() && codex.SourceURL.Contains(domain); + } +} diff --git a/src/Models/Filters/FavoriteFilter.cs b/src/Models/Filters/FavoriteFilter.cs new file mode 100644 index 0000000..c22aafc --- /dev/null +++ b/src/Models/Filters/FavoriteFilter.cs @@ -0,0 +1,17 @@ +using System.Windows.Media; + +namespace COMPASS.Models.Filters +{ + internal class FavoriteFilter : FilterBase + { + + public FavoriteFilter() : base(FilterType.Favorite) + { } + + public override Color BackgroundColor => Colors.HotPink; + + public override string Content => "Favorite"; + + public override bool Apply(Codex codex) => codex.Favorite; + } +} diff --git a/src/Models/Filters/FileExtensionFilter.cs b/src/Models/Filters/FileExtensionFilter.cs new file mode 100644 index 0000000..c119164 --- /dev/null +++ b/src/Models/Filters/FileExtensionFilter.cs @@ -0,0 +1,18 @@ +using System.Windows.Media; + +namespace COMPASS.Models.Filters +{ + public class FileExtensionFilter : FilterBase + { + public FileExtensionFilter(string extension) : base(FilterType.FileExtension, extension) + { + AllowMultiple = true; + } + + public override Color BackgroundColor => Colors.OrangeRed; + + public override string Content => $"File Type: {FilterValue}"; + + public override bool Apply(Codex codex) => FilterValue is string extension && codex.FileType == extension; + } +} diff --git a/src/Models/Filters/FilterBase.cs b/src/Models/Filters/FilterBase.cs new file mode 100644 index 0000000..d4113e5 --- /dev/null +++ b/src/Models/Filters/FilterBase.cs @@ -0,0 +1,63 @@ +using System; +using System.Windows.Media; + +namespace COMPASS.Models.Filters +{ + public abstract class FilterBase : ITag, IEquatable + { + public FilterBase(FilterType filterType, object? filterValue = null) + { + Type = filterType; + FilterValue = filterValue; + } + + public FilterType Type { get; init; } + public object? FilterValue { get; init; } + + /// + /// Allow multiple filters of this type to be active at once + /// + public bool AllowMultiple { get; init; } + + #region ITag + public abstract Color BackgroundColor { get; } + + public abstract string Content { get; } + #endregion + + public abstract bool Apply(Codex codex); + + + #region IEquatable + public override bool Equals(object? obj) => Equals(obj as FilterBase); + + public bool Equals(FilterBase? other) + { + if (other is null) + return false; + if (ReferenceEquals(this, other)) + return true; + if (GetType() != other.GetType()) + return false; + return Type == other.Type && FilterValue == other.FilterValue; + } + public static bool operator ==(FilterBase lhs, FilterBase rhs) + { + if (lhs is null) + { + return rhs is null; //if lhs is null, only equal if rhs is also null + } + // Equals handles case of null on right side. + return lhs.Equals(rhs); + } + public static bool operator !=(FilterBase lhs, FilterBase rhs) + { + return !(lhs == rhs); + } + + public override int GetHashCode() => Content.GetHashCode(); + + #endregion + + } +} diff --git a/src/Models/Filters/FilterType.cs b/src/Models/Filters/FilterType.cs new file mode 100644 index 0000000..1a52d8a --- /dev/null +++ b/src/Models/Filters/FilterType.cs @@ -0,0 +1,21 @@ +namespace COMPASS.Models.Filters +{ + public enum FilterType + { + Tag, + Search, + Author, + Publisher, + StartReleaseDate, + StopReleaseDate, + MinimumRating, + OnlineSource, + OfflineSource, + PhysicalSource, + Favorite, + FileExtension, + HasBrokenPath, + HasISBN, + Domain + } +} diff --git a/src/Models/Filters/HasBrokenPathFilter.cs b/src/Models/Filters/HasBrokenPathFilter.cs new file mode 100644 index 0000000..56a5e0c --- /dev/null +++ b/src/Models/Filters/HasBrokenPathFilter.cs @@ -0,0 +1,17 @@ +using System.IO; +using System.Windows.Media; + +namespace COMPASS.Models.Filters +{ + internal class HasBrokenPathFilter : FilterBase + { + public HasBrokenPathFilter() : base(FilterType.HasBrokenPath) + { } + + public override Color BackgroundColor => Colors.Gold; + + public override string Content => "Has broken path"; + + public override bool Apply(Codex codex) => codex.HasOfflineSource() && !Path.Exists(codex.Path); + } +} diff --git a/src/Models/Filters/HasISBNFilter.cs b/src/Models/Filters/HasISBNFilter.cs new file mode 100644 index 0000000..87caa4f --- /dev/null +++ b/src/Models/Filters/HasISBNFilter.cs @@ -0,0 +1,15 @@ +using System; +using System.Windows.Media; + +namespace COMPASS.Models.Filters +{ + internal class HasISBNFilter : FilterBase + { + public HasISBNFilter() : base(FilterType.HasISBN) + { } + + public override Color BackgroundColor => Colors.DarkSeaGreen; + public override string Content => "Has ISBN"; + public override bool Apply(Codex codex) => !String.IsNullOrEmpty(codex.ISBN); + } +} diff --git a/src/Models/Filters/MinimumRatingFilter.cs b/src/Models/Filters/MinimumRatingFilter.cs new file mode 100644 index 0000000..4b2d424 --- /dev/null +++ b/src/Models/Filters/MinimumRatingFilter.cs @@ -0,0 +1,14 @@ +using System.Windows.Media; + +namespace COMPASS.Models.Filters +{ + internal class MinimumRatingFilter : FilterBase + { + public MinimumRatingFilter(int minRating) : base(FilterType.MinimumRating, minRating) + { } + + public override string Content => $"At least {FilterValue} stars"; + public override Color BackgroundColor => Colors.Goldenrod; + public override bool Apply(Codex codex) => FilterValue is int rating && codex.Rating >= rating; + } +} diff --git a/src/Models/Filters/OfflineSourceFilter.cs b/src/Models/Filters/OfflineSourceFilter.cs new file mode 100644 index 0000000..2c50f40 --- /dev/null +++ b/src/Models/Filters/OfflineSourceFilter.cs @@ -0,0 +1,14 @@ +using System.Windows.Media; + +namespace COMPASS.Models.Filters +{ + public class OfflineSourceFilter : FilterBase + { + public OfflineSourceFilter() : base(FilterType.OfflineSource) + { } + + public override string Content => "Available Offline"; + public override Color BackgroundColor => Colors.DarkSeaGreen; + public override bool Apply(Codex codex) => codex.HasOfflineSource(); + } +} diff --git a/src/Models/Filters/OnlineSourceFilter.cs b/src/Models/Filters/OnlineSourceFilter.cs new file mode 100644 index 0000000..f91bfe5 --- /dev/null +++ b/src/Models/Filters/OnlineSourceFilter.cs @@ -0,0 +1,15 @@ +using System.Windows.Media; + +namespace COMPASS.Models.Filters +{ + internal class OnlineSourceFilter : FilterBase + { + public OnlineSourceFilter() : base(FilterType.OnlineSource) + { } + + public override bool Apply(Codex codex) => codex.HasOnlineSource(); + + public override string Content => "Available Online"; + public override Color BackgroundColor => Colors.DarkSeaGreen; + } +} diff --git a/src/Models/Filters/PhysicalSourceFilter.cs b/src/Models/Filters/PhysicalSourceFilter.cs new file mode 100644 index 0000000..7077588 --- /dev/null +++ b/src/Models/Filters/PhysicalSourceFilter.cs @@ -0,0 +1,14 @@ +using System.Windows.Media; + +namespace COMPASS.Models.Filters +{ + internal class PhysicalSourceFilter : FilterBase + { + public PhysicalSourceFilter() : base(FilterType.PhysicalSource) + { } + + public override string Content => "Physically Owned"; + public override Color BackgroundColor => Colors.DarkSeaGreen; + public override bool Apply(Codex codex) => codex.PhysicallyOwned; + } +} \ No newline at end of file diff --git a/src/Models/Filters/PublisherFilter.cs b/src/Models/Filters/PublisherFilter.cs new file mode 100644 index 0000000..e0c9437 --- /dev/null +++ b/src/Models/Filters/PublisherFilter.cs @@ -0,0 +1,17 @@ +using System.Windows.Media; + +namespace COMPASS.Models.Filters +{ + public class PublisherFilter : FilterBase + { + public PublisherFilter(string publisher) : base(FilterType.Publisher, publisher) + { + AllowMultiple = true; + } + + + public override string Content => $"Publisher: {FilterValue}"; + public override Color BackgroundColor => Colors.MediumPurple; + public override bool Apply(Codex codex) => FilterValue is string publisher && codex.Publisher == publisher; + } +} diff --git a/src/Models/Filters/SearchFilter.cs b/src/Models/Filters/SearchFilter.cs new file mode 100644 index 0000000..bd98c31 --- /dev/null +++ b/src/Models/Filters/SearchFilter.cs @@ -0,0 +1,29 @@ +using FuzzySharp; +using System; +using System.Windows.Media; + +namespace COMPASS.Models.Filters +{ + internal class SearchFilter : FilterBase + { + public SearchFilter(string searchTerm) : base(FilterType.Search, searchTerm) + { + } + + public override Color BackgroundColor => Colors.Salmon; + + public override string Content => $"Search: {FilterValue}"; + + public override bool Apply(Codex codex) + { + string? searchTerm = FilterValue as string; + + if (string.IsNullOrWhiteSpace(searchTerm)) return false; + + return + Fuzz.TokenInitialismRatio(codex.Title.ToLowerInvariant(), searchTerm) > 80 || //include acronyms + codex.Title.Contains(searchTerm, StringComparison.InvariantCultureIgnoreCase) || //include string fragments + Fuzz.PartialRatio(codex.Title.ToLowerInvariant(), searchTerm) > 80; //include spelling errors + } + } +} diff --git a/src/Models/Filters/StartReleaseDateFilter.cs b/src/Models/Filters/StartReleaseDateFilter.cs new file mode 100644 index 0000000..b67feaa --- /dev/null +++ b/src/Models/Filters/StartReleaseDateFilter.cs @@ -0,0 +1,15 @@ +using System; +using System.Windows.Media; + +namespace COMPASS.Models.Filters +{ + public class StartReleaseDateFilter : FilterBase + { + public StartReleaseDateFilter(DateTime date) : base(FilterType.StartReleaseDate, date) + { } + + public override string Content => $"After: {(FilterValue as DateTime?)?.ToShortDateString()}"; + public override Color BackgroundColor => Colors.DeepSkyBlue; + public override bool Apply(Codex codex) => FilterValue is DateTime date && codex.ReleaseDate >= date; + } +} diff --git a/src/Models/Filters/StopReleaseDateFilter.cs b/src/Models/Filters/StopReleaseDateFilter.cs new file mode 100644 index 0000000..7d98d50 --- /dev/null +++ b/src/Models/Filters/StopReleaseDateFilter.cs @@ -0,0 +1,15 @@ +using System; +using System.Windows.Media; + +namespace COMPASS.Models.Filters +{ + internal class StopReleaseDateFilter : FilterBase + { + public StopReleaseDateFilter(DateTime date) : base(FilterType.StopReleaseDate, date) + { } + + public override string Content => $"Before: {(FilterValue as DateTime?)?.ToShortDateString()}"; + public override Color BackgroundColor => Colors.DeepSkyBlue; + public override bool Apply(Codex codex) => FilterValue is DateTime date && codex.ReleaseDate < date; + } +} diff --git a/src/Models/Filters/TagFilter.cs b/src/Models/Filters/TagFilter.cs new file mode 100644 index 0000000..70aaf03 --- /dev/null +++ b/src/Models/Filters/TagFilter.cs @@ -0,0 +1,19 @@ +using System.Windows.Media; + +namespace COMPASS.Models.Filters +{ + internal class TagFilter : FilterBase + { + public TagFilter(Tag tag) : base(FilterType.Tag, tag) + { + AllowMultiple = true; + } + + public override Color BackgroundColor => ((ITag)FilterValue!).BackgroundColor; + + public override string Content => ((ITag)FilterValue!).Content; + + //Tag logic is contained in the FilterViewmodel, so here just make it match everything + public override bool Apply(Codex codex) => true; + } +} diff --git a/src/ViewModels/CodexInfoViewModel.cs b/src/ViewModels/CodexInfoViewModel.cs index 9f90ecd..a098738 100644 --- a/src/ViewModels/CodexInfoViewModel.cs +++ b/src/ViewModels/CodexInfoViewModel.cs @@ -1,5 +1,6 @@ using CommunityToolkit.Mvvm.Input; using COMPASS.Models; +using COMPASS.Models.Filters; using COMPASS.Services; namespace COMPASS.ViewModels @@ -47,14 +48,14 @@ public bool AutoHide private RelayCommand? _addAuthorFilterCommand; public RelayCommand AddAuthorFilterCommand => _addAuthorFilterCommand ??= new(AddAuthorFilter); - private void AddAuthorFilter(string? author) => MainViewModel.CollectionVM.FilterVM.AddFilter(new(Filter.FilterType.Author, author)); + private void AddAuthorFilter(string? author) => MainViewModel.CollectionVM.FilterVM.AddFilter(new AuthorFilter(author ?? "")); private RelayCommand? _addPublisherFilterCommand; public RelayCommand AddPublisherFilterCommand => _addPublisherFilterCommand ??= new(AddPublisherFilter); - private void AddPublisherFilter(string? publisher) => MainViewModel.CollectionVM.FilterVM.AddFilter(new(Filter.FilterType.Publisher, publisher)); + private void AddPublisherFilter(string? publisher) => MainViewModel.CollectionVM.FilterVM.AddFilter(new PublisherFilter(publisher ?? "")); private RelayCommand? _addTagFilterCommand; public RelayCommand AddTagFilterCommand => _addTagFilterCommand ??= new(AddTagFilter); - private void AddTagFilter(Tag? tag) => MainViewModel.CollectionVM.FilterVM.AddFilter(new(Filter.FilterType.Tag, tag)); + private void AddTagFilter(Tag? tag) => MainViewModel.CollectionVM.FilterVM.AddFilter(new TagFilter(tag!)); } } diff --git a/src/ViewModels/FilterViewModel.cs b/src/ViewModels/FilterViewModel.cs index 359d6a5..bb0e7db 100644 --- a/src/ViewModels/FilterViewModel.cs +++ b/src/ViewModels/FilterViewModel.cs @@ -1,8 +1,8 @@ using CommunityToolkit.Mvvm.Input; using COMPASS.Models; +using COMPASS.Models.Filters; using COMPASS.Services; using COMPASS.Tools; -using FuzzySharp; using GongSolutions.Wpf.DragDrop; using System; using System.Collections.Generic; @@ -61,8 +61,8 @@ public bool Include set => SetProperty(ref _include, value); } - public ObservableCollection IncludedFilters { get; set; } = new(); - public ObservableCollection ExcludedFilters { get; set; } = new(); + public ObservableCollection IncludedFilters { get; set; } = new(); + public ObservableCollection ExcludedFilters { get; set; } = new(); public bool HasActiveFilters => IncludedFilters.Any() || ExcludedFilters.Any(); @@ -89,13 +89,13 @@ public string SearchTerm set => SetProperty(ref _searchTerm, value); } - public List BooleanFilters { get; } = new() + public List BooleanFilters { get; } = new() { - new(Filter.FilterType.OfflineSource), - new(Filter.FilterType.OnlineSource), - new(Filter.FilterType.PhysicalSource), - new(Filter.FilterType.HasISBN), - new(Filter.FilterType.Favorite), + new OfflineSourceFilter(), + new OnlineSourceFilter(), + new PhysicalSourceFilter(), + new HasISBNFilter(), + new FavoriteFilter(), }; public string SelectedAuthor @@ -103,7 +103,7 @@ public string SelectedAuthor set { if (String.IsNullOrEmpty(value)) return; - Filter authorFilter = new(Filter.FilterType.Author, value); + FilterBase authorFilter = new AuthorFilter(value); AddFilter(authorFilter, Include); } } @@ -120,7 +120,7 @@ public string SelectedPublisher set { if (String.IsNullOrEmpty(value)) return; - Filter publisherFilter = new(Filter.FilterType.Publisher, value); + FilterBase publisherFilter = new PublisherFilter(value); AddFilter(publisherFilter, Include); } } @@ -137,7 +137,7 @@ public string SelectedFileType set { if (String.IsNullOrEmpty(value)) return; - Filter fileExtensionFilter = new(Filter.FilterType.FileExtension, value); + FilterBase fileExtensionFilter = new FileExtensionFilter(value); AddFilter(fileExtensionFilter, Include); } } @@ -153,7 +153,7 @@ public string SelectedDomain set { if (String.IsNullOrEmpty(value)) return; - Filter domainFilter = new(Filter.FilterType.Domain, value); + FilterBase domainFilter = new DomainFilter(value); AddFilter(domainFilter, Include); } } @@ -175,7 +175,7 @@ public DateTime? StartReleaseDate { SetProperty(ref _startReleaseDate, value); if (value is null) return; - Filter startDateFilter = new(Filter.FilterType.StartReleaseDate, value); + FilterBase startDateFilter = new StartReleaseDateFilter(value.Value); AddFilter(startDateFilter, Include); } } @@ -188,7 +188,7 @@ public DateTime? StopReleaseDate SetProperty(ref _stopReleaseDate, value); if (value != null) { - Filter stopDateFilter = new(Filter.FilterType.StopReleaseDate, value); + FilterBase stopDateFilter = new StopReleaseDateFilter(value.Value); AddFilter(stopDateFilter, Include); } } @@ -204,7 +204,7 @@ public int MinRating SetProperty(ref _minRating, value); if (value is > 0 and < 6) { - Filter minRatFilter = new(Filter.FilterType.MinimumRating, value); + FilterBase minRatFilter = new MinimumRatingFilter(value); AddFilter(minRatFilter, Include); } } @@ -286,8 +286,8 @@ public void PopulateMetaDataCollections() => App.SafeDispatcher.Invoke(() => //Populate Domain Collection if (c.HasOnlineSource()) { - string domain = Uri.IsWellFormedUriString(c.SourceURL, UriKind.Absolute) ? - new Uri(c.SourceURL).Host : + string domain = Uri.IsWellFormedUriString(c.SourceURL, UriKind.Absolute) ? + new Uri(c.SourceURL).Host : c.SourceURL; if (!string.IsNullOrEmpty(domain)) DomainList.AddIfMissing(domain); } @@ -304,25 +304,25 @@ public void PopulateMetaDataCollections() => App.SafeDispatcher.Invoke(() => //------------- Adding, Removing, ect ------------// // Remove Filter - private RelayCommand? _removeFromItemsControlCommand; - public RelayCommand RemoveFromItemsControlCommand => _removeFromItemsControlCommand ??= new(RemoveFilter); - public void RemoveFilter(Filter? filter) + private RelayCommand? _removeFromItemsControlCommand; + public RelayCommand RemoveFromItemsControlCommand => _removeFromItemsControlCommand ??= new(RemoveFilter); + public void RemoveFilter(FilterBase? filter) { if (filter is null) return; IncludedFilters.Remove(filter); ExcludedFilters.Remove(filter); } - public void RemoveFilterType(Filter.FilterType filterType) + public void RemoveFilterType(FilterType filterType) { IncludedFilters.RemoveAll(filter => filter.Type == filterType); ExcludedFilters.RemoveAll(filter => filter.Type == filterType); } // Add Filter - private RelayCommand? _addSourceFilterCommand; - public RelayCommand AddSourceFilterCommand => _addSourceFilterCommand ??= new(AddSourceFilter); - public void AddSourceFilter(Filter? filter) => AddFilter(filter, Include); - public void AddFilter(Filter? filter, bool include = true) + private RelayCommand? _addSourceFilterCommand; + public RelayCommand AddSourceFilterCommand => _addSourceFilterCommand ??= new(AddSourceFilter); + public void AddSourceFilter(FilterBase? filter) => AddFilter(filter, Include); + public void AddFilter(FilterBase? filter, bool include = true) { if (filter is null) return; if (Keyboard.Modifiers == ModifierKeys.Alt) @@ -330,11 +330,11 @@ public void AddFilter(Filter? filter, bool include = true) include = false; } - ObservableCollection target = include ? IncludedFilters : ExcludedFilters; - ObservableCollection other = !include ? IncludedFilters : ExcludedFilters; + ObservableCollection target = include ? IncludedFilters : ExcludedFilters; + ObservableCollection other = !include ? IncludedFilters : ExcludedFilters; - //if Filter is unique, remove previous instance(s) of that Filter before adding - if (filter.Unique && target.Any(f => f.Type == filter.Type)) + //if Filter does not allow multiple instances, remove previous instance(s) of that Filter before adding + if (!filter.AllowMultiple && target.Any(f => f.Type == filter.Type)) { target.RemoveAll(f => f.Type == filter.Type); } @@ -347,14 +347,14 @@ public void AddFilter(Filter? filter, bool include = true) public RelayCommand SearchCommand => _searchCommand ??= new(SearchCommandHelper); private void SearchCommandHelper(string? searchTerm) { - Filter searchFilter = new(Filter.FilterType.Search, searchTerm); if (!String.IsNullOrEmpty(searchTerm)) { + FilterBase searchFilter = new SearchFilter(searchTerm); AddFilter(searchFilter); } else { - RemoveFilterType(Filter.FilterType.Search); + RemoveFilterType(FilterType.Search); } } @@ -377,7 +377,7 @@ public void ClearFilters() private void UpdateIncludedCodices(bool apply = true) { _includedCodices = new(_allCodices); - foreach (Filter.FilterType filterType in Enum.GetValues(typeof(Filter.FilterType))) + foreach (FilterType filterType in Enum.GetValues(typeof(FilterType))) { // Included codices must match filters of all types so IntersectWith() _includedCodices.IntersectWith(GetFilteredCodicesByType(IncludedFilters, filterType, true)); @@ -387,7 +387,7 @@ private void UpdateIncludedCodices(bool apply = true) private void UpdateExcludedCodices(bool apply = true) { _excludedCodices = new(); - foreach (Filter.FilterType filterType in Enum.GetValues(typeof(Filter.FilterType))) + foreach (FilterType filterType in Enum.GetValues(typeof(FilterType))) { // Codex is excluded as soon as it matches any excluded filter so UnionWith() _excludedCodices.UnionWith(GetFilteredCodicesByType(ExcludedFilters, filterType, false)); @@ -402,43 +402,22 @@ private void UpdateExcludedCodices(bool apply = true) /// /// Determines whether returned codices should be included or excluded /// - private IEnumerable GetFilteredCodicesByType(IEnumerable filters, Filter.FilterType filterType, bool include) + private IEnumerable GetFilteredCodicesByType(IEnumerable filters, FilterType filterType, bool include) { - List relevantFilters = new(filters.Where(filter => filter.Type == filterType)); + List relevantFilters = new(filters.Where(filter => filter.Type == filterType)); if (relevantFilters.Count == 0) return include ? _allCodices : Enumerable.Empty(); return filterType switch { - Filter.FilterType.Search => GetFilteredCodicesBySearch(relevantFilters.First()), - Filter.FilterType.Tag => GetFilteredCodicesByTags(relevantFilters, include), - _ => _allCodices.Where(codex => relevantFilters.Any(filter => filter.Method(codex))) + FilterType.Tag => GetFilteredCodicesByTags(relevantFilters, include), + _ => _allCodices.Where(codex => relevantFilters.Any(filter => filter.Apply(codex))) }; } - private HashSet GetFilteredCodicesBySearch(Filter searchFilter) - { - string? searchTerm = searchFilter.FilterValue as string; - - if (String.IsNullOrEmpty(searchTerm)) return new(_allCodices); - - HashSet includedCodicesBySearch = new(); - //include acronyms - includedCodicesBySearch.UnionWith(_allCodices - .Where(f => Fuzz.TokenInitialismRatio(f.Title.ToLowerInvariant(), SearchTerm) > 80)); - //include string fragments - includedCodicesBySearch.UnionWith(_allCodices - .Where(f => f.Title.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase))); - //include spelling errors - includedCodicesBySearch.UnionWith(_allCodices - .Where(f => Fuzz.PartialRatio(f.Title.ToLowerInvariant(), SearchTerm) > 80)); - - return includedCodicesBySearch; - } - - private HashSet GetFilteredCodicesByTags(IEnumerable filters, bool include) + private HashSet GetFilteredCodicesByTags(IEnumerable filters, bool include) => include ? GetIncludedCodicesByTags(filters) : GetExcludedCodicesByTags(filters); - private HashSet GetIncludedCodicesByTags(IEnumerable filters) + private HashSet GetIncludedCodicesByTags(IEnumerable filters) { HashSet includedCodices = new(_allCodices); @@ -470,7 +449,7 @@ private HashSet GetIncludedCodicesByTags(IEnumerable filters) } return includedCodices; } - private HashSet GetExcludedCodicesByTags(IEnumerable filters) + private HashSet GetExcludedCodicesByTags(IEnumerable filters) { HashSet excludedCodices = new(); @@ -545,7 +524,7 @@ void IDropTarget.DragOver(IDropInfo dropInfo) } break; //Move Filter included/excluded - case Filter: + case FilterBase: //Do filter specific stuff here if needed //Move Tag between included/excluded case Tag: @@ -564,14 +543,14 @@ void IDropTarget.Drop(IDropInfo dropInfo) { //Tree to Filter Box case TreeViewNode draggedTreeViewNode: - AddFilter(new(Filter.FilterType.Tag, draggedTreeViewNode.Tag), toIncluded); + AddFilter(new TagFilter(draggedTreeViewNode.Tag), toIncluded); break; //Between include and exclude - case Filter draggedFilter: + case FilterBase draggedFilter: AddFilter(draggedFilter, toIncluded); break; case Tag draggedTag: - AddFilter(new(Filter.FilterType.Tag, draggedTag), toIncluded); + AddFilter(new TagFilter(draggedTag), toIncluded); break; } } diff --git a/src/ViewModels/SettingsViewModel.cs b/src/ViewModels/SettingsViewModel.cs index 9b8c988..55b7e07 100644 --- a/src/ViewModels/SettingsViewModel.cs +++ b/src/ViewModels/SettingsViewModel.cs @@ -4,6 +4,7 @@ using COMPASS.Models; using COMPASS.Models.CodexProperties; using COMPASS.Models.Enums; +using COMPASS.Models.Filters; using COMPASS.Models.Preferences; using COMPASS.Services; using COMPASS.Tools; @@ -334,7 +335,7 @@ private void BrokenCodicesChanged() public RelayCommand ShowBrokenCodicesCommand => _showBrokenCodicesCommand ??= new(ShowBrokenCodices); private void ShowBrokenCodices() { - MainViewModel.CollectionVM.FilterVM.AddFilter(new(Filter.FilterType.HasBrokenPath)); + MainViewModel.CollectionVM.FilterVM.AddFilter(new HasBrokenPathFilter()); Application.Current.MainWindow!.Activate(); } diff --git a/src/ViewModels/TagsViewModel.cs b/src/ViewModels/TagsViewModel.cs index ec1299b..435b6c4 100644 --- a/src/ViewModels/TagsViewModel.cs +++ b/src/ViewModels/TagsViewModel.cs @@ -3,6 +3,7 @@ using COMPASS.Interfaces; using COMPASS.Models; using COMPASS.Models.Enums; +using COMPASS.Models.Filters; using COMPASS.Services; using COMPASS.Tools; using COMPASS.ViewModels.Import; @@ -137,7 +138,7 @@ public void AddTagFilterHelper(object[]? par) //needed because relay command only takes functions with one arg Tag tag = (Tag)par[0]; bool include = (bool)par[1]; - _filterVM.AddFilter(new(Filter.FilterType.Tag, tag), include); + _filterVM.AddFilter(new TagFilter(tag), include); } private RelayCommand? _importTagsFromOtherCollectionsCommand; @@ -285,7 +286,7 @@ public void DeleteTag() //tag to delete is context, because DeleteTag is called from context menu if (ContextTag is null) return; MainViewModel.CollectionVM.CurrentCollection.DeleteTag(ContextTag); - _filterVM.RemoveFilter(new(Filter.FilterType.Tag, ContextTag)); + _filterVM.RemoveFilter(new TagFilter(ContextTag)); //Go over all files and remove the tag from tag list foreach (var f in _codexCollection.AllCodices) From cfae240369d6564b6014190e85e387bc7b053e22 Mon Sep 17 00:00:00 2001 From: Paul De Smul Date: Sat, 7 Sep 2024 15:26:51 +0200 Subject: [PATCH 05/20] Rename FilterBase to Filter --- src/Models/Filters/AuthorFilter.cs | 2 +- src/Models/Filters/DomainFilter.cs | 2 +- src/Models/Filters/FavoriteFilter.cs | 2 +- src/Models/Filters/FileExtensionFilter.cs | 2 +- .../Filters/{FilterBase.cs => Filter.cs} | 12 ++-- src/Models/Filters/HasBrokenPathFilter.cs | 2 +- src/Models/Filters/HasISBNFilter.cs | 2 +- src/Models/Filters/MinimumRatingFilter.cs | 2 +- src/Models/Filters/OfflineSourceFilter.cs | 2 +- src/Models/Filters/OnlineSourceFilter.cs | 2 +- src/Models/Filters/PhysicalSourceFilter.cs | 2 +- src/Models/Filters/PublisherFilter.cs | 2 +- src/Models/Filters/SearchFilter.cs | 2 +- src/Models/Filters/StartReleaseDateFilter.cs | 2 +- src/Models/Filters/StopReleaseDateFilter.cs | 2 +- src/Models/Filters/TagFilter.cs | 2 +- src/ViewModels/FilterViewModel.cs | 56 +++++++++---------- 17 files changed, 49 insertions(+), 49 deletions(-) rename src/Models/Filters/{FilterBase.cs => Filter.cs} (80%) diff --git a/src/Models/Filters/AuthorFilter.cs b/src/Models/Filters/AuthorFilter.cs index 6ec6a19..738ce46 100644 --- a/src/Models/Filters/AuthorFilter.cs +++ b/src/Models/Filters/AuthorFilter.cs @@ -2,7 +2,7 @@ namespace COMPASS.Models.Filters { - internal class AuthorFilter : FilterBase + internal class AuthorFilter : Filter { public AuthorFilter(string author) : base(FilterType.Author, author) { diff --git a/src/Models/Filters/DomainFilter.cs b/src/Models/Filters/DomainFilter.cs index 2d1dbda..263115f 100644 --- a/src/Models/Filters/DomainFilter.cs +++ b/src/Models/Filters/DomainFilter.cs @@ -2,7 +2,7 @@ namespace COMPASS.Models.Filters { - internal class DomainFilter : FilterBase + internal class DomainFilter : Filter { public DomainFilter(string domain) : base(FilterType.Domain, domain) { diff --git a/src/Models/Filters/FavoriteFilter.cs b/src/Models/Filters/FavoriteFilter.cs index c22aafc..fd152bb 100644 --- a/src/Models/Filters/FavoriteFilter.cs +++ b/src/Models/Filters/FavoriteFilter.cs @@ -2,7 +2,7 @@ namespace COMPASS.Models.Filters { - internal class FavoriteFilter : FilterBase + internal class FavoriteFilter : Filter { public FavoriteFilter() : base(FilterType.Favorite) diff --git a/src/Models/Filters/FileExtensionFilter.cs b/src/Models/Filters/FileExtensionFilter.cs index c119164..467e9d1 100644 --- a/src/Models/Filters/FileExtensionFilter.cs +++ b/src/Models/Filters/FileExtensionFilter.cs @@ -2,7 +2,7 @@ namespace COMPASS.Models.Filters { - public class FileExtensionFilter : FilterBase + public class FileExtensionFilter : Filter { public FileExtensionFilter(string extension) : base(FilterType.FileExtension, extension) { diff --git a/src/Models/Filters/FilterBase.cs b/src/Models/Filters/Filter.cs similarity index 80% rename from src/Models/Filters/FilterBase.cs rename to src/Models/Filters/Filter.cs index d4113e5..34cb0f1 100644 --- a/src/Models/Filters/FilterBase.cs +++ b/src/Models/Filters/Filter.cs @@ -3,9 +3,9 @@ namespace COMPASS.Models.Filters { - public abstract class FilterBase : ITag, IEquatable + public abstract class Filter : ITag, IEquatable { - public FilterBase(FilterType filterType, object? filterValue = null) + public Filter(FilterType filterType, object? filterValue = null) { Type = filterType; FilterValue = filterValue; @@ -29,9 +29,9 @@ public FilterBase(FilterType filterType, object? filterValue = null) #region IEquatable - public override bool Equals(object? obj) => Equals(obj as FilterBase); + public override bool Equals(object? obj) => Equals(obj as Filter); - public bool Equals(FilterBase? other) + public bool Equals(Filter? other) { if (other is null) return false; @@ -41,7 +41,7 @@ public bool Equals(FilterBase? other) return false; return Type == other.Type && FilterValue == other.FilterValue; } - public static bool operator ==(FilterBase lhs, FilterBase rhs) + public static bool operator ==(Filter lhs, Filter rhs) { if (lhs is null) { @@ -50,7 +50,7 @@ public bool Equals(FilterBase? other) // Equals handles case of null on right side. return lhs.Equals(rhs); } - public static bool operator !=(FilterBase lhs, FilterBase rhs) + public static bool operator !=(Filter lhs, Filter rhs) { return !(lhs == rhs); } diff --git a/src/Models/Filters/HasBrokenPathFilter.cs b/src/Models/Filters/HasBrokenPathFilter.cs index 56a5e0c..1a578d7 100644 --- a/src/Models/Filters/HasBrokenPathFilter.cs +++ b/src/Models/Filters/HasBrokenPathFilter.cs @@ -3,7 +3,7 @@ namespace COMPASS.Models.Filters { - internal class HasBrokenPathFilter : FilterBase + internal class HasBrokenPathFilter : Filter { public HasBrokenPathFilter() : base(FilterType.HasBrokenPath) { } diff --git a/src/Models/Filters/HasISBNFilter.cs b/src/Models/Filters/HasISBNFilter.cs index 87caa4f..bc74ca1 100644 --- a/src/Models/Filters/HasISBNFilter.cs +++ b/src/Models/Filters/HasISBNFilter.cs @@ -3,7 +3,7 @@ namespace COMPASS.Models.Filters { - internal class HasISBNFilter : FilterBase + internal class HasISBNFilter : Filter { public HasISBNFilter() : base(FilterType.HasISBN) { } diff --git a/src/Models/Filters/MinimumRatingFilter.cs b/src/Models/Filters/MinimumRatingFilter.cs index 4b2d424..5aae910 100644 --- a/src/Models/Filters/MinimumRatingFilter.cs +++ b/src/Models/Filters/MinimumRatingFilter.cs @@ -2,7 +2,7 @@ namespace COMPASS.Models.Filters { - internal class MinimumRatingFilter : FilterBase + internal class MinimumRatingFilter : Filter { public MinimumRatingFilter(int minRating) : base(FilterType.MinimumRating, minRating) { } diff --git a/src/Models/Filters/OfflineSourceFilter.cs b/src/Models/Filters/OfflineSourceFilter.cs index 2c50f40..7c46148 100644 --- a/src/Models/Filters/OfflineSourceFilter.cs +++ b/src/Models/Filters/OfflineSourceFilter.cs @@ -2,7 +2,7 @@ namespace COMPASS.Models.Filters { - public class OfflineSourceFilter : FilterBase + public class OfflineSourceFilter : Filter { public OfflineSourceFilter() : base(FilterType.OfflineSource) { } diff --git a/src/Models/Filters/OnlineSourceFilter.cs b/src/Models/Filters/OnlineSourceFilter.cs index f91bfe5..0bd71bc 100644 --- a/src/Models/Filters/OnlineSourceFilter.cs +++ b/src/Models/Filters/OnlineSourceFilter.cs @@ -2,7 +2,7 @@ namespace COMPASS.Models.Filters { - internal class OnlineSourceFilter : FilterBase + internal class OnlineSourceFilter : Filter { public OnlineSourceFilter() : base(FilterType.OnlineSource) { } diff --git a/src/Models/Filters/PhysicalSourceFilter.cs b/src/Models/Filters/PhysicalSourceFilter.cs index 7077588..1001fa0 100644 --- a/src/Models/Filters/PhysicalSourceFilter.cs +++ b/src/Models/Filters/PhysicalSourceFilter.cs @@ -2,7 +2,7 @@ namespace COMPASS.Models.Filters { - internal class PhysicalSourceFilter : FilterBase + internal class PhysicalSourceFilter : Filter { public PhysicalSourceFilter() : base(FilterType.PhysicalSource) { } diff --git a/src/Models/Filters/PublisherFilter.cs b/src/Models/Filters/PublisherFilter.cs index e0c9437..f6320d4 100644 --- a/src/Models/Filters/PublisherFilter.cs +++ b/src/Models/Filters/PublisherFilter.cs @@ -2,7 +2,7 @@ namespace COMPASS.Models.Filters { - public class PublisherFilter : FilterBase + public class PublisherFilter : Filter { public PublisherFilter(string publisher) : base(FilterType.Publisher, publisher) { diff --git a/src/Models/Filters/SearchFilter.cs b/src/Models/Filters/SearchFilter.cs index bd98c31..c1f37de 100644 --- a/src/Models/Filters/SearchFilter.cs +++ b/src/Models/Filters/SearchFilter.cs @@ -4,7 +4,7 @@ namespace COMPASS.Models.Filters { - internal class SearchFilter : FilterBase + internal class SearchFilter : Filter { public SearchFilter(string searchTerm) : base(FilterType.Search, searchTerm) { diff --git a/src/Models/Filters/StartReleaseDateFilter.cs b/src/Models/Filters/StartReleaseDateFilter.cs index b67feaa..f066684 100644 --- a/src/Models/Filters/StartReleaseDateFilter.cs +++ b/src/Models/Filters/StartReleaseDateFilter.cs @@ -3,7 +3,7 @@ namespace COMPASS.Models.Filters { - public class StartReleaseDateFilter : FilterBase + public class StartReleaseDateFilter : Filter { public StartReleaseDateFilter(DateTime date) : base(FilterType.StartReleaseDate, date) { } diff --git a/src/Models/Filters/StopReleaseDateFilter.cs b/src/Models/Filters/StopReleaseDateFilter.cs index 7d98d50..e3ddd06 100644 --- a/src/Models/Filters/StopReleaseDateFilter.cs +++ b/src/Models/Filters/StopReleaseDateFilter.cs @@ -3,7 +3,7 @@ namespace COMPASS.Models.Filters { - internal class StopReleaseDateFilter : FilterBase + internal class StopReleaseDateFilter : Filter { public StopReleaseDateFilter(DateTime date) : base(FilterType.StopReleaseDate, date) { } diff --git a/src/Models/Filters/TagFilter.cs b/src/Models/Filters/TagFilter.cs index 70aaf03..a4792a2 100644 --- a/src/Models/Filters/TagFilter.cs +++ b/src/Models/Filters/TagFilter.cs @@ -2,7 +2,7 @@ namespace COMPASS.Models.Filters { - internal class TagFilter : FilterBase + internal class TagFilter : Filter { public TagFilter(Tag tag) : base(FilterType.Tag, tag) { diff --git a/src/ViewModels/FilterViewModel.cs b/src/ViewModels/FilterViewModel.cs index bb0e7db..fdf3133 100644 --- a/src/ViewModels/FilterViewModel.cs +++ b/src/ViewModels/FilterViewModel.cs @@ -61,8 +61,8 @@ public bool Include set => SetProperty(ref _include, value); } - public ObservableCollection IncludedFilters { get; set; } = new(); - public ObservableCollection ExcludedFilters { get; set; } = new(); + public ObservableCollection IncludedFilters { get; set; } = new(); + public ObservableCollection ExcludedFilters { get; set; } = new(); public bool HasActiveFilters => IncludedFilters.Any() || ExcludedFilters.Any(); @@ -89,7 +89,7 @@ public string SearchTerm set => SetProperty(ref _searchTerm, value); } - public List BooleanFilters { get; } = new() + public List BooleanFilters { get; } = new() { new OfflineSourceFilter(), new OnlineSourceFilter(), @@ -103,7 +103,7 @@ public string SelectedAuthor set { if (String.IsNullOrEmpty(value)) return; - FilterBase authorFilter = new AuthorFilter(value); + Filter authorFilter = new AuthorFilter(value); AddFilter(authorFilter, Include); } } @@ -120,7 +120,7 @@ public string SelectedPublisher set { if (String.IsNullOrEmpty(value)) return; - FilterBase publisherFilter = new PublisherFilter(value); + Filter publisherFilter = new PublisherFilter(value); AddFilter(publisherFilter, Include); } } @@ -137,7 +137,7 @@ public string SelectedFileType set { if (String.IsNullOrEmpty(value)) return; - FilterBase fileExtensionFilter = new FileExtensionFilter(value); + Filter fileExtensionFilter = new FileExtensionFilter(value); AddFilter(fileExtensionFilter, Include); } } @@ -153,7 +153,7 @@ public string SelectedDomain set { if (String.IsNullOrEmpty(value)) return; - FilterBase domainFilter = new DomainFilter(value); + Filter domainFilter = new DomainFilter(value); AddFilter(domainFilter, Include); } } @@ -175,7 +175,7 @@ public DateTime? StartReleaseDate { SetProperty(ref _startReleaseDate, value); if (value is null) return; - FilterBase startDateFilter = new StartReleaseDateFilter(value.Value); + Filter startDateFilter = new StartReleaseDateFilter(value.Value); AddFilter(startDateFilter, Include); } } @@ -188,7 +188,7 @@ public DateTime? StopReleaseDate SetProperty(ref _stopReleaseDate, value); if (value != null) { - FilterBase stopDateFilter = new StopReleaseDateFilter(value.Value); + Filter stopDateFilter = new StopReleaseDateFilter(value.Value); AddFilter(stopDateFilter, Include); } } @@ -204,7 +204,7 @@ public int MinRating SetProperty(ref _minRating, value); if (value is > 0 and < 6) { - FilterBase minRatFilter = new MinimumRatingFilter(value); + Filter minRatFilter = new MinimumRatingFilter(value); AddFilter(minRatFilter, Include); } } @@ -304,9 +304,9 @@ public void PopulateMetaDataCollections() => App.SafeDispatcher.Invoke(() => //------------- Adding, Removing, ect ------------// // Remove Filter - private RelayCommand? _removeFromItemsControlCommand; - public RelayCommand RemoveFromItemsControlCommand => _removeFromItemsControlCommand ??= new(RemoveFilter); - public void RemoveFilter(FilterBase? filter) + private RelayCommand? _removeFromItemsControlCommand; + public RelayCommand RemoveFromItemsControlCommand => _removeFromItemsControlCommand ??= new(RemoveFilter); + public void RemoveFilter(Filter? filter) { if (filter is null) return; IncludedFilters.Remove(filter); @@ -319,10 +319,10 @@ public void RemoveFilterType(FilterType filterType) } // Add Filter - private RelayCommand? _addSourceFilterCommand; - public RelayCommand AddSourceFilterCommand => _addSourceFilterCommand ??= new(AddSourceFilter); - public void AddSourceFilter(FilterBase? filter) => AddFilter(filter, Include); - public void AddFilter(FilterBase? filter, bool include = true) + private RelayCommand? _addSourceFilterCommand; + public RelayCommand AddSourceFilterCommand => _addSourceFilterCommand ??= new(AddSourceFilter); + public void AddSourceFilter(Filter? filter) => AddFilter(filter, Include); + public void AddFilter(Filter? filter, bool include = true) { if (filter is null) return; if (Keyboard.Modifiers == ModifierKeys.Alt) @@ -330,8 +330,8 @@ public void AddFilter(FilterBase? filter, bool include = true) include = false; } - ObservableCollection target = include ? IncludedFilters : ExcludedFilters; - ObservableCollection other = !include ? IncludedFilters : ExcludedFilters; + ObservableCollection target = include ? IncludedFilters : ExcludedFilters; + ObservableCollection other = !include ? IncludedFilters : ExcludedFilters; //if Filter does not allow multiple instances, remove previous instance(s) of that Filter before adding if (!filter.AllowMultiple && target.Any(f => f.Type == filter.Type)) @@ -349,7 +349,7 @@ private void SearchCommandHelper(string? searchTerm) { if (!String.IsNullOrEmpty(searchTerm)) { - FilterBase searchFilter = new SearchFilter(searchTerm); + Filter searchFilter = new SearchFilter(searchTerm); AddFilter(searchFilter); } else @@ -402,9 +402,9 @@ private void UpdateExcludedCodices(bool apply = true) /// /// Determines whether returned codices should be included or excluded /// - private IEnumerable GetFilteredCodicesByType(IEnumerable filters, FilterType filterType, bool include) + private IEnumerable GetFilteredCodicesByType(IEnumerable filters, FilterType filterType, bool include) { - List relevantFilters = new(filters.Where(filter => filter.Type == filterType)); + List relevantFilters = new(filters.Where(filter => filter.Type == filterType)); if (relevantFilters.Count == 0) return include ? _allCodices : Enumerable.Empty(); @@ -415,9 +415,9 @@ private IEnumerable GetFilteredCodicesByType(IEnumerable filt }; } - private HashSet GetFilteredCodicesByTags(IEnumerable filters, bool include) + private HashSet GetFilteredCodicesByTags(IEnumerable filters, bool include) => include ? GetIncludedCodicesByTags(filters) : GetExcludedCodicesByTags(filters); - private HashSet GetIncludedCodicesByTags(IEnumerable filters) + private HashSet GetIncludedCodicesByTags(IEnumerable filters) { HashSet includedCodices = new(_allCodices); @@ -449,7 +449,7 @@ private HashSet GetIncludedCodicesByTags(IEnumerable filters) } return includedCodices; } - private HashSet GetExcludedCodicesByTags(IEnumerable filters) + private HashSet GetExcludedCodicesByTags(IEnumerable filters) { HashSet excludedCodices = new(); @@ -459,7 +459,7 @@ private HashSet GetExcludedCodicesByTags(IEnumerable filters) { // If parent is excluded, so should all the children excludedTags = excludedTags.Flatten().ToList(); - excludedCodices = new(_allCodices.Where(f => excludedTags.Intersect(f.Tags).Any())); + excludedCodices = new(_allCodices.Where(c => excludedTags.Intersect(c.Tags).Any())); } return excludedCodices; @@ -524,7 +524,7 @@ void IDropTarget.DragOver(IDropInfo dropInfo) } break; //Move Filter included/excluded - case FilterBase: + case Filter: //Do filter specific stuff here if needed //Move Tag between included/excluded case Tag: @@ -546,7 +546,7 @@ void IDropTarget.Drop(IDropInfo dropInfo) AddFilter(new TagFilter(draggedTreeViewNode.Tag), toIncluded); break; //Between include and exclude - case FilterBase draggedFilter: + case Filter draggedFilter: AddFilter(draggedFilter, toIncluded); break; case Tag draggedTag: From 0d855c9c38b0e579ac3a04498c428b183414846a Mon Sep 17 00:00:00 2001 From: Paul De Smul Date: Sun, 8 Sep 2024 00:04:35 +0200 Subject: [PATCH 06/20] Fix homebrewery thumbnail generation --- Tests/Sources/HomeBrewery.cs | 1 + src/COMPASS.csproj | 6 ++- src/Services/CoverService.cs | 26 +++++------ .../Sources/HomebrewerySourceViewModel.cs | 43 ++++++++++++++++--- 4 files changed, 53 insertions(+), 23 deletions(-) diff --git a/Tests/Sources/HomeBrewery.cs b/Tests/Sources/HomeBrewery.cs index ce0db43..bf6a2ff 100644 --- a/Tests/Sources/HomeBrewery.cs +++ b/Tests/Sources/HomeBrewery.cs @@ -47,6 +47,7 @@ public async Task GetCoverFromHomeBrewery() //Setup var vm = SourceViewModel.GetSourceVM(MetaDataSource.Homebrewery); CodexCollection cc = new(TEST_COLLECTION); + cc.InitAsNew(); cc.Load(); Codex codex = new(cc) diff --git a/src/COMPASS.csproj b/src/COMPASS.csproj index 746c305..d8f4052 100644 --- a/src/COMPASS.csproj +++ b/src/COMPASS.csproj @@ -62,20 +62,22 @@ + - - + + + diff --git a/src/Services/CoverService.cs b/src/Services/CoverService.cs index 4740bda..c994462 100644 --- a/src/Services/CoverService.cs +++ b/src/Services/CoverService.cs @@ -5,6 +5,7 @@ using COMPASS.ViewModels.Sources; using COMPASS.Windows; using ImageMagick; +using ImageMagick.Factories; using OpenQA.Selenium; using System; using System.Collections.Generic; @@ -120,7 +121,9 @@ public static void SaveCover(IMagickImage image, Codex destCodex) } if (image.Width > 850) image.Resize(850, 0); + image.Write(destCodex.CoverArt); + image.Dispose(); CreateThumbnail(destCodex); } @@ -177,13 +180,13 @@ public static void CreateThumbnail(Codex c) return; } - int newWidth = 200; //sets resolution of thumbnail in pixels + uint newWidth = 200; //sets resolution of thumbnail in pixels if (!Path.Exists(c.CoverArt)) return; using MagickImage image = new(c.CoverArt); //preserve aspect ratio - int width = image.Width; - int height = image.Height; - int newHeight = newWidth / width * height; + uint width = image.Width; + uint height = image.Height; + uint newHeight = newWidth / width * height; //create thumbnail image.Thumbnail(newWidth, newHeight); image.Write(c.Thumbnail); @@ -194,21 +197,14 @@ public static void CreateThumbnail(Codex c) public static IMagickImage GetCroppedScreenShot(IWebDriver driver, IWebElement webElement) => GetCroppedScreenShot(driver, webElement.Location, webElement.Size); - private static IMagickImage GetCroppedScreenShot(IWebDriver driver, Point location, Size size) + public static IMagickImage GetCroppedScreenShot(IWebDriver driver, Point location, Size size) { //take the screenshot Screenshot ss = ((ITakesScreenshot)driver).GetScreenshot(); - using Bitmap? img = Image.FromStream(new MemoryStream(ss.AsByteArray)) as Bitmap; - - if (img == null) - { - Logger.Debug("Screenshot from webdriver was null"); - return new MagickImage(); - } - - using Bitmap imgCropped = img.Clone(new Rectangle(location, size), img.PixelFormat); var mf = new MagickImageFactory(); - return mf.Create(imgCropped); + using var img = mf.Create(ss.AsByteArray); + img.Resize(3000, 3000); //same size as headless window + return img.Clone(location.X, location.Y, (uint)size.Width, (uint)size.Height); } } } diff --git a/src/ViewModels/Sources/HomebrewerySourceViewModel.cs b/src/ViewModels/Sources/HomebrewerySourceViewModel.cs index 1c67860..16ddb60 100644 --- a/src/ViewModels/Sources/HomebrewerySourceViewModel.cs +++ b/src/ViewModels/Sources/HomebrewerySourceViewModel.cs @@ -6,10 +6,15 @@ using HtmlAgilityPack; using ImageMagick; using Newtonsoft.Json.Linq; +using OpenQA.Selenium; +using OpenQA.Selenium.Support.UI; +using SeleniumExtras.WaitHelpers; using System; using System.Collections.Generic; using System.Diagnostics; +using System.Drawing; using System.Globalization; +using System.Threading; using System.Threading.Tasks; namespace COMPASS.ViewModels.Sources @@ -65,15 +70,41 @@ public override async Task FetchCover(Codex codex) { if (String.IsNullOrEmpty(codex.SourceURL)) { return false; } ProgressVM.AddLogEntry(new(Severity.Info, $"Downloading cover from Homebrewery")); - OpenQA.Selenium.WebDriver driver = await WebDriverService.GetWebDriver(); + WebDriver driver = await WebDriverService.GetWebDriver(); + WebDriverWait wait = new(driver, TimeSpan.FromSeconds(5)); try { - string url = codex.SourceURL.Replace("/share/", "/print/"); //use print API to only show doc itself - await Task.Run(() => driver.Navigate().GoToUrl(url)); - var coverPage = driver.FindElement(OpenQA.Selenium.By.Id("p1")) - ?? throw new Exception($"Couldn't find p1 on {url}"); + string url = codex.SourceURL; + var frameSelector = By.Id("BrewRenderer"); + var pageSelector = By.Id("p1"); + + await Task.Run(() => + { + driver.Navigate().GoToUrl(url); + wait.Until(ExpectedConditions.ElementExists(frameSelector)); + }); + + wait.Until(ExpectedConditions.ElementExists(frameSelector)); + + Thread.Sleep(500); + + var frame = driver.FindElement(frameSelector); + Point location = frame.Location; + + await Task.Run(() => + { + wait.Until(ExpectedConditions.FrameToBeAvailableAndSwitchToIt(frameSelector)); + wait.Until(ExpectedConditions.ElementExists(pageSelector)); + }); + + Thread.Sleep(500); + + var coverPage = driver.FindElement(pageSelector); + location.X += coverPage.Location.X; + location.Y += coverPage.Location.Y; + //screenshot and download the image - IMagickImage image = CoverService.GetCroppedScreenShot(driver, coverPage); + IMagickImage image = CoverService.GetCroppedScreenShot(driver, location, coverPage.Size); CoverService.SaveCover(image, codex); return true; } From 6cb10aeef5155e18640c946abd287ce53646ce96 Mon Sep 17 00:00:00 2001 From: Paul De Smul Date: Sun, 8 Sep 2024 11:12:58 +0200 Subject: [PATCH 07/20] Fix delete collection not working --- src/ViewModels/CollectionViewModel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ViewModels/CollectionViewModel.cs b/src/ViewModels/CollectionViewModel.cs index 1551c83..c80bf72 100644 --- a/src/ViewModels/CollectionViewModel.cs +++ b/src/ViewModels/CollectionViewModel.cs @@ -324,6 +324,9 @@ public void RaiseDeleteCollectionWarning() Notification areYouSure = Notification.AreYouSureNotification; areYouSure.Body = CurrentCollection.AllCodices.Count == 1 ? messageSingle : messageMultiple; + var windowedNotificationService = App.Container.ResolveKeyed(NotificationDisplayType.Windowed); + windowedNotificationService.Show(areYouSure); + if (areYouSure.Result == NotificationAction.Confirm) { DeleteCollection(CurrentCollection); From 23c47c4a476e44db1cf3bbc83114fd23b67dc1cd Mon Sep 17 00:00:00 2001 From: Paul De Smul Date: Sun, 8 Sep 2024 11:14:15 +0200 Subject: [PATCH 08/20] Don't crash on exceptions during filtering --- src/ViewModels/FilterViewModel.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/ViewModels/FilterViewModel.cs b/src/ViewModels/FilterViewModel.cs index fdf3133..f50836e 100644 --- a/src/ViewModels/FilterViewModel.cs +++ b/src/ViewModels/FilterViewModel.cs @@ -496,9 +496,16 @@ private void ApplyFilters(bool force = false) public void ReFilter(bool force = false) { - UpdateIncludedCodices(false); - UpdateExcludedCodices(false); - ApplyFilters(force); + try + { + UpdateIncludedCodices(false); + UpdateExcludedCodices(false); + ApplyFilters(force); + } + catch (Exception ex) + { + Logger.Warn("Something when wrong during filtering", ex); + } } public void RemoveCodex(Codex c) { From d3d323fdaeede6da0cf16b8b7338bcf0c651892a Mon Sep 17 00:00:00 2001 From: Paul De Smul Date: Sun, 8 Sep 2024 11:25:23 +0200 Subject: [PATCH 09/20] Make seperate OrderedTags for databinding --- src/Models/Codex.cs | 35 ++++++++++++++-------- src/Models/CodexProperties/TagsProperty.cs | 2 +- src/Views/CardLayout.xaml | 2 +- src/Views/CodexInfoView.xaml | 3 +- src/Views/ListLayout.xaml | 6 ++-- src/Windows/ChooseMetaDataWindow.xaml | 7 +++-- src/Windows/CodexEditWindow.xaml | 7 +++-- 7 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/Models/Codex.cs b/src/Models/Codex.cs index 0d80dfe..66f7686 100644 --- a/src/Models/Codex.cs +++ b/src/Models/Codex.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Collections.Specialized; using System.Linq; using System.Xml.Serialization; @@ -14,7 +15,8 @@ public class Codex : ObservableObject, IHasID { public Codex() { - Authors.CollectionChanged += (_, _) => OnPropertyChanged(nameof(AuthorsAsString)); + Authors.CollectionChanged += OnCollectionChanged; + Tags.CollectionChanged += OnCollectionChanged; } public Codex(CodexCollection cc) : this() @@ -137,7 +139,9 @@ public ObservableCollection Authors get => _authors; set { + _authors.CollectionChanged -= OnCollectionChanged; SetProperty(ref _authors, value); + _authors.CollectionChanged += OnCollectionChanged; OnPropertyChanged(nameof(AuthorsAsString)); } } @@ -229,20 +233,20 @@ public bool PhysicallyOwned [XmlIgnore] public ObservableCollection Tags { - get + get => _tags; + set { - //order them in same order as alltags by starting with alltags and keeping the ones we need using intersect - List orderedTags = _tags.FirstOrDefault()?.AllTags.Intersect(_tags).ToList() ?? new List(); - App.SafeDispatcher.Invoke(() => - { - _tags.Clear(); //will fail when called from non UI thread which happens during import - _tags.AddRange(orderedTags); - }); - return _tags; + _tags.CollectionChanged -= OnCollectionChanged; + _tags = value; + _tags.CollectionChanged += OnCollectionChanged; + OnPropertyChanged(nameof(OrderedTags)); } - - set => SetProperty(ref _tags, value); } + + [XmlIgnore] + //order them in same order as alltags by starting with alltags and keeping the ones we need using intersect + public IEnumerable OrderedTags => _tags.FirstOrDefault()?.AllTags.Intersect(_tags) ?? Enumerable.Empty(); + public List TagIDs { get; set; } = new(); private string _description = ""; @@ -359,6 +363,13 @@ public string? FileType CodexProperty.GetInstance(nameof(ReleaseDate))!, CodexProperty.GetInstance(nameof(CoverArt))!, }; + + private void OnCollectionChanged(object? o, NotifyCollectionChangedEventArgs args) + { + if (o == Tags) OnPropertyChanged(nameof(OrderedTags)); + if (o == Authors) OnPropertyChanged(nameof(AuthorsAsString)); + } + } } diff --git a/src/Models/CodexProperties/TagsProperty.cs b/src/Models/CodexProperties/TagsProperty.cs index 4710a91..31252b7 100644 --- a/src/Models/CodexProperties/TagsProperty.cs +++ b/src/Models/CodexProperties/TagsProperty.cs @@ -13,7 +13,7 @@ public override void SetProp(Codex target, Codex source) { foreach (var tag in source.Tags.ToList()) { - App.SafeDispatcher.Invoke(() => target.Tags.AddIfMissing(tag)); + target.Tags.AddIfMissing(tag); } } diff --git a/src/Views/CardLayout.xaml b/src/Views/CardLayout.xaml index faaa300..99c3593 100644 --- a/src/Views/CardLayout.xaml +++ b/src/Views/CardLayout.xaml @@ -179,7 +179,7 @@ diff --git a/src/Views/CodexInfoView.xaml b/src/Views/CodexInfoView.xaml index c6e30bb..b904238 100644 --- a/src/Views/CodexInfoView.xaml +++ b/src/Views/CodexInfoView.xaml @@ -136,7 +136,8 @@ - diff --git a/src/Views/ListLayout.xaml b/src/Views/ListLayout.xaml index 4c42ce2..0936c10 100644 --- a/src/Views/ListLayout.xaml +++ b/src/Views/ListLayout.xaml @@ -178,8 +178,10 @@ Visibility="{Binding Source={StaticResource LayoutProxy}, Path=Data.Preferences.ShowTags, Converter={StaticResource ToVisibilityConverter}}"> - + diff --git a/src/Windows/ChooseMetaDataWindow.xaml b/src/Windows/ChooseMetaDataWindow.xaml index 4657193..48e8a2d 100644 --- a/src/Windows/ChooseMetaDataWindow.xaml +++ b/src/Windows/ChooseMetaDataWindow.xaml @@ -150,7 +150,8 @@ Visibility="{Binding CurrentPair.Item2, Converter={StaticResource PropIsEmptyToVisibilityConverter}, ConverterParameter=Tags}"> + ItemsSource="{Binding CurrentPair.Item1.OrderedTags, Mode=OneWay}" + Background="{x:Null}" BorderBrush="{x:Null}" ItemTemplate="{StaticResource TagTemplate}"> @@ -166,8 +167,8 @@ Background="{x:Null}" BorderBrush="{x:Null}" ItemTemplate="{StaticResource TagTemplate}"> - - + + diff --git a/src/Windows/CodexEditWindow.xaml b/src/Windows/CodexEditWindow.xaml index 12416d0..a7b9d70 100644 --- a/src/Windows/CodexEditWindow.xaml +++ b/src/Windows/CodexEditWindow.xaml @@ -226,9 +226,10 @@ - + From 9372d53bb77e368ad69e6e2847a3d4bf80751d58 Mon Sep 17 00:00:00 2001 From: Paul De Smul Date: Sun, 8 Sep 2024 11:40:27 +0200 Subject: [PATCH 10/20] Code quality improvements --- src/ViewModels/CollectionContentSelectorViewModel.cs | 3 +-- src/ViewModels/ExportCollectionViewModel.cs | 6 +++--- src/ViewModels/Import/ImportCollectionViewModel.cs | 2 +- src/ViewModels/WizardViewModel.cs | 5 ++++- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/ViewModels/CollectionContentSelectorViewModel.cs b/src/ViewModels/CollectionContentSelectorViewModel.cs index 4a41228..bf32957 100644 --- a/src/ViewModels/CollectionContentSelectorViewModel.cs +++ b/src/ViewModels/CollectionContentSelectorViewModel.cs @@ -280,13 +280,12 @@ public void ApplySelectedPreferences() /// /// Builds the curated collection based on the selection /// - public override Task Finish() + public void ApplyAllSelections() { //order is important! ApplySelectedCodices(); //first codices, makes copies, so further operations don't modify the existing ones ApplySelectedTags(); ApplySelectedPreferences(); - return Task.CompletedTask; } public void UpdateSteps() diff --git a/src/ViewModels/ExportCollectionViewModel.cs b/src/ViewModels/ExportCollectionViewModel.cs index 142ae49..dc38627 100644 --- a/src/ViewModels/ExportCollectionViewModel.cs +++ b/src/ViewModels/ExportCollectionViewModel.cs @@ -66,7 +66,7 @@ private void ApplyActiveFilters() public override async Task Finish() { - await ApplyChoices(); + ApplyChoices(); CloseAction?.Invoke(); @@ -76,7 +76,7 @@ public override async Task Finish() await ExportToFile(targetPath); } - public async Task ApplyChoices() + public void ApplyChoices() { //if we do a quick import, set all the things in the contentSelector have the right value if (!AdvancedExport) @@ -101,7 +101,7 @@ public async Task ApplyChoices() } //Apply the selection - await ContentSelectorVM.Finish(); + ContentSelectorVM.ApplyAllSelections(); } private string? ChooseDestination() diff --git a/src/ViewModels/Import/ImportCollectionViewModel.cs b/src/ViewModels/Import/ImportCollectionViewModel.cs index d961620..a0e30bc 100644 --- a/src/ViewModels/Import/ImportCollectionViewModel.cs +++ b/src/ViewModels/Import/ImportCollectionViewModel.cs @@ -154,7 +154,7 @@ public override async Task Finish() } //Apply the selection - await ContentSelectorVM.Finish(); + ContentSelectorVM.ApplyAllSelections(); //Save the changes to a permanent collection var targetCollection = MergeIntoCollection ? diff --git a/src/ViewModels/WizardViewModel.cs b/src/ViewModels/WizardViewModel.cs index 2c1b7b4..6fda898 100644 --- a/src/ViewModels/WizardViewModel.cs +++ b/src/ViewModels/WizardViewModel.cs @@ -74,7 +74,10 @@ protected void RefreshNavigationBtns() private AsyncRelayCommand? _finishCommand; public AsyncRelayCommand FinishCommand => _finishCommand ??= new(Finish, ShowFinishButton); - public abstract Task Finish(); + public virtual Task Finish() + { + return Task.CompletedTask; + } public virtual bool ShowFinishButton() => StepCounter == Steps.Count - 1; public Action? CloseAction; From b24db165f1da0d9ae86dc5e13ef8647753519707 Mon Sep 17 00:00:00 2001 From: Paul De Smul Date: Sun, 8 Sep 2024 11:58:31 +0200 Subject: [PATCH 11/20] No longer show import folder dialog if file selection was cancelled --- src/Services/IOService.cs | 7 ++-- .../Import/ImportFolderViewModel.cs | 35 ++++++++++++------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/Services/IOService.cs b/src/Services/IOService.cs index 0a9a037..8d29845 100644 --- a/src/Services/IOService.cs +++ b/src/Services/IOService.cs @@ -255,16 +255,17 @@ public static void ShowInExplorer(string path) /// /// Allow the user to select multiple folders using a dialog /// + /// the selected Paths /// Ilist with selected paths, empty list if canceled/failed / whatever - public static string[] PickFolders() + public static bool TryPickFolders(out string[] selectedPaths) { VistaFolderBrowserDialog openFolderDialog = new() { Multiselect = true, }; var dialogResult = openFolderDialog.ShowDialog(); - if (dialogResult == false) return Array.Empty(); - return openFolderDialog.SelectedPaths; + selectedPaths = openFolderDialog.SelectedPaths; + return dialogResult == true; } public static async Task OpenSatchel(string? path = null) diff --git a/src/ViewModels/Import/ImportFolderViewModel.cs b/src/ViewModels/Import/ImportFolderViewModel.cs index d84ee97..67c8652 100644 --- a/src/ViewModels/Import/ImportFolderViewModel.cs +++ b/src/ViewModels/Import/ImportFolderViewModel.cs @@ -1,4 +1,7 @@ -using COMPASS.Models; +using Autofac; +using COMPASS.Interfaces; +using COMPASS.Models; +using COMPASS.Models.Enums; using COMPASS.Services; using COMPASS.Tools; using COMPASS.Windows; @@ -44,11 +47,21 @@ public async Task Import() //if no files are given to import, don't if (_manuallyTriggered && RecursiveDirectories.Count + NonRecursiveDirectories.Count + ExistingFolders.Count + Files.Count == 0) { - LetUserSelectFolders(); + bool success = LetUserSelectFolders(); + if (!success) return; } var toImport = GetPathsToImport(); - toImport = LetUserFilterToImport(toImport); + if (toImport.Any()) + { + toImport = LetUserFilterToImport(toImport); + } + else + { + Notification noFilesFound = new("No files found", "The selected folder did not contain any files."); + var windowedNotificationService = App.Container.ResolveKeyed(NotificationDisplayType.Windowed); + windowedNotificationService.Show(noFilesFound); + } await ImportViewModel.ImportFilesAsync(toImport, _targetCollection); } @@ -56,14 +69,12 @@ public async Task Import() /// Lets a user select folders using a dialog /// and stores them in RecursiveDirectories /// - /// A list of paths - private void LetUserSelectFolders() + /// A bool indicating whether the user successfully chose a set of folders + private bool LetUserSelectFolders() { - string[] selectedPath = IOService.PickFolders(); - - if (!selectedPath.Any()) return; - - RecursiveDirectories = selectedPath.ToList(); + bool success = IOService.TryPickFolders(out string[] selectedPaths); + RecursiveDirectories = selectedPaths.ToList(); + return success; } /// @@ -228,8 +239,8 @@ public override Task Finish() //go over every folder and set the HasAllSubFolder Flag foreach (var checkableFolder in CheckableFolders.Flatten()) { - checkableFolder.Item.HasAllSubFolders = - checkableFolder.IsChecked == true && + checkableFolder.Item.HasAllSubFolders = + checkableFolder.IsChecked == true && checkableFolder.Children.All(child => child.IsChecked == true); //need this check as well because folder might also be checked without any children } From edecdf8755cc9950c8ecb71b57c323d81fdd91c7 Mon Sep 17 00:00:00 2001 From: Paul De Smul Date: Sun, 8 Sep 2024 13:08:22 +0200 Subject: [PATCH 12/20] Fix test build --- Tests/IntegrationTests/Satchels_Test.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/IntegrationTests/Satchels_Test.cs b/Tests/IntegrationTests/Satchels_Test.cs index ddd8c71..1eceea8 100644 --- a/Tests/IntegrationTests/Satchels_Test.cs +++ b/Tests/IntegrationTests/Satchels_Test.cs @@ -23,7 +23,7 @@ public async Task TestSatchelExportImport() //Export var filePath = Path.GetTempPath() + Guid.NewGuid().ToString() + Constants.SatchelExtension; ExportCollectionViewModel exportViewModel = new(testCollection); - await exportViewModel.ApplyChoices(); + exportViewModel.ApplyChoices(); await exportViewModel.ExportToFile(filePath); //Assert export succesfull From 6c1d150c13c070b65e039d67540df9946b084ac9 Mon Sep 17 00:00:00 2001 From: Paul De Smul Date: Sun, 8 Sep 2024 18:21:26 +0200 Subject: [PATCH 13/20] Don't show warning on auto refresh --- src/ViewModels/Import/ImportFolderViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ViewModels/Import/ImportFolderViewModel.cs b/src/ViewModels/Import/ImportFolderViewModel.cs index 67c8652..dd442cd 100644 --- a/src/ViewModels/Import/ImportFolderViewModel.cs +++ b/src/ViewModels/Import/ImportFolderViewModel.cs @@ -56,7 +56,7 @@ public async Task Import() { toImport = LetUserFilterToImport(toImport); } - else + else if (_manuallyTriggered) { Notification noFilesFound = new("No files found", "The selected folder did not contain any files."); var windowedNotificationService = App.Container.ResolveKeyed(NotificationDisplayType.Windowed); From af18f03a24ddba0bbdcf0d9e3f3877d198716498 Mon Sep 17 00:00:00 2001 From: Paul De Smul Date: Sun, 8 Sep 2024 19:24:35 +0200 Subject: [PATCH 14/20] Add dto for codex --- Tests/DataGenerators/RandomGenerator.cs | 11 +- Tests/UnitTests/Models/XmlMapper.cs | 41 ++++ src/Models/Codex.cs | 289 +++++++++++------------- src/Models/CodexCollection.cs | 47 +--- src/Models/Tag.cs | 7 +- src/Models/XmlDtos/CodexDto.cs | 53 +++++ src/Models/XmlDtos/XmlMapper.cs | 125 ++++++++++ src/Services/IOService.cs | 31 --- 8 files changed, 369 insertions(+), 235 deletions(-) create mode 100644 Tests/UnitTests/Models/XmlMapper.cs create mode 100644 src/Models/XmlDtos/CodexDto.cs create mode 100644 src/Models/XmlDtos/XmlMapper.cs diff --git a/Tests/DataGenerators/RandomGenerator.cs b/Tests/DataGenerators/RandomGenerator.cs index 74adef9..f8006ce 100644 --- a/Tests/DataGenerators/RandomGenerator.cs +++ b/Tests/DataGenerators/RandomGenerator.cs @@ -72,19 +72,20 @@ public static Codex GetRandomCodex() { ID = random.Next(), Title = GetRandomString(), + SortingTitle = GetRandomString(), Authors = new(GetRandomList(maxLength: 3)), Publisher = GetRandomString(), Description = GetRandomString(10, 500), - Favorite = GetRandomBool(falseFreq: 10), - OpenedCount = random.Next(100), - ISBN = GetRandomISBN(), ReleaseDate = GetRandomDate(), - Path = GetRandomBool() ? GetRandomPath() : String.Empty, //randomly decide if it has a path - SourceURL = GetRandomBool() ? GetRandomUrl() : String.Empty, //randomly decide if it has a url PageCount = random.Next(1, 400), Version = GetRandomString(minLength: 1, maxLength: 3), PhysicallyOwned = GetRandomBool(), Rating = random.Next(0, 6), + Favorite = GetRandomBool(falseFreq: 10), + OpenedCount = random.Next(100), + ISBN = GetRandomISBN(), + Path = GetRandomBool() ? GetRandomPath() : String.Empty, //randomly decide if it has a path + SourceURL = GetRandomBool() ? GetRandomUrl() : String.Empty, //randomly decide if it has a url }; codex.DateAdded = GetRandomDate((DateTime)codex.ReleaseDate); diff --git a/Tests/UnitTests/Models/XmlMapper.cs b/Tests/UnitTests/Models/XmlMapper.cs new file mode 100644 index 0000000..ebdb12b --- /dev/null +++ b/Tests/UnitTests/Models/XmlMapper.cs @@ -0,0 +1,41 @@ +using COMPASS.Models; +using COMPASS.Models.XmlDtos; +using System.Text.Json; +using Tests.DataGenerators; + +namespace Tests.UnitTests.Models +{ + [TestClass] + public class XmlMapper + { + [TestMethod] + public void MapCodex() + { + AssertAllPropMapped(typeof(Codex), typeof(CodexDto)); + + Codex codex = RandomGenerator.GetRandomCodex(); + + CodexDto dto = codex.ToDto(); + Codex reconstrution = dto.ToModel(codex.Tags); + + Assert.AreEqual( + JsonSerializer.Serialize(reconstrution), + JsonSerializer.Serialize(codex)); + } + + private void AssertAllPropMapped(Type modelType, Type dtoType) + { + var modelPropsCount = modelType + .GetProperties() + .Where(prop => prop.CanWrite) + .Count(); + + var dtoPropCount = dtoType + .GetProperties() + .Where(prop => prop.CanWrite) + .Count(); + + Assert.AreEqual(modelPropsCount, dtoPropCount); + } + } +} diff --git a/src/Models/Codex.cs b/src/Models/Codex.cs index 66f7686..65d7cf6 100644 --- a/src/Models/Codex.cs +++ b/src/Models/Codex.cs @@ -1,18 +1,18 @@ using CommunityToolkit.Mvvm.ComponentModel; using COMPASS.Models.CodexProperties; -using COMPASS.Services; using COMPASS.Tools; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; -using System.Xml.Serialization; namespace COMPASS.Models { public class Codex : ObservableObject, IHasID { + #region Constructors + public Codex() { Authors.CollectionChanged += OnCollectionChanged; @@ -30,62 +30,38 @@ public Codex(Codex codex) : this() Copy(codex); } + #endregion + public void SetImagePaths(CodexCollection collection) { CoverArt = System.IO.Path.Combine(collection.CoverArtPath, $"{ID}.png"); Thumbnail = System.IO.Path.Combine(collection.ThumbnailsPath, $"{ID}.png"); } - public void Copy(Codex c) - { - Title = c.Title; - _sortingTitle = c._sortingTitle; //copy field instead of property, or it will copy _title - Path = c.Path; - Authors = new(c.Authors); - Publisher = c.Publisher; - Version = c.Version; - SourceURL = c.SourceURL; - ID = c.ID; - CoverArt = c.CoverArt; - Thumbnail = c.Thumbnail; - PhysicallyOwned = c.PhysicallyOwned; - Description = c.Description; - ReleaseDate = c.ReleaseDate; - Rating = c.Rating; - PageCount = c.PageCount; - Tags = new(c.Tags); - LastOpened = c.LastOpened; - DateAdded = c.DateAdded; - Favorite = c.Favorite; - OpenedCount = c.OpenedCount; - ISBN = c.ISBN; - } + #region Properties - public void RefreshThumbnail() => OnPropertyChanged(nameof(Thumbnail)); + #region COMPASS related Metadata - public void ClearPersonalData() + public int ID { get; set; } + + private string _coverArt = ""; + public string CoverArt { - Favorite = false; - PhysicallyOwned = false; - DateAdded = DateTime.Now; - OpenedCount = 0; - LastOpened = default; - Rating = 0; + get => _coverArt; + set => SetProperty(ref _coverArt, value); } - #region Properties - - private string _path = ""; - public string Path + private string _thumbnail = ""; + public string Thumbnail { - get => _path; - set - { - value = IOService.SanitizeXmlString(value); - SetProperty(ref _path, value); - } + get => _thumbnail; + set => SetProperty(ref _thumbnail, value); } + #endregion + + #region Codex related Metadata + private string _title = ""; public string Title { @@ -93,39 +69,27 @@ public string Title set { if (value is null) return; - value = IOService.SanitizeXmlString(value); SetProperty(ref _title, value); OnPropertyChanged(nameof(SortingTitle)); OnPropertyChanged(nameof(SortingTitleContainsNumbers)); } } - private string _sortingTitle = ""; - [XmlIgnore] + private string _userDefinedSortingTitle = ""; + /// + /// Sorting title defined by the user, will only have a value if it is different from the title + /// + public string UserDefinedSortingTitle => _userDefinedSortingTitle; public string SortingTitle { - get => (String.IsNullOrEmpty(_sortingTitle) ? _title : _sortingTitle).PadNumbers(); + get => (String.IsNullOrEmpty(_userDefinedSortingTitle) ? _title : _userDefinedSortingTitle).PadNumbers(); set { - SetProperty(ref _sortingTitle, value); + SetProperty(ref _userDefinedSortingTitle, value); OnPropertyChanged(nameof(SortingTitleContainsNumbers)); } } - //separate property needed for serialization or it will get _title and save that - //instead of saving an empty and mirroring _title during runtime - public string SerializableSortingTitle - { - get => _sortingTitle; - set - { - value = IOService.SanitizeXmlString(value); - SetProperty(ref _sortingTitle, value); - } - } - - [XmlIgnore] public bool SortingTitleContainsNumbers => Constants.RegexNumbersOnly().IsMatch(SortingTitle); - [XmlIgnore] public string ZeroPaddingExplainer => "What's with all the 0's? \n \n" + "Zero-padding numbers ensures numerical sorting instead of alphabetical sorting. \n" + @@ -145,7 +109,6 @@ public ObservableCollection Authors OnPropertyChanged(nameof(AuthorsAsString)); } } - public string AuthorsAsString { get @@ -164,73 +127,42 @@ public string AuthorsAsString public string Publisher { get => _publisher; - set - { - value = IOService.SanitizeXmlString(value); - SetProperty(ref _publisher, value); - } + set => SetProperty(ref _publisher, value); } - private string _version = ""; - public string Version + private string _description = ""; + public string Description { - get => _version; - set - { - value = IOService.SanitizeXmlString(value); - SetProperty(ref _version, value); - } + get => _description; + set => SetProperty(ref _description, value); } - private string _sourceURL = ""; - public string SourceURL + private DateTime? _releaseDate; + public DateTime? ReleaseDate { - get => _sourceURL; - set - { - value = IOService.SanitizeXmlString(value); - if (value.StartsWith("www.")) - { - value = @"https://" + value; - } - SetProperty(ref _sourceURL, value); - } + get => _releaseDate; + set => SetProperty(ref _releaseDate, value); } - public int ID { get; set; } - - private string _coverArt = ""; - public string CoverArt + private int _pageCount; + public int PageCount { - get => _coverArt; - set - { - value = IOService.SanitizeXmlString(value); - SetProperty(ref _coverArt, value); - } + get => _pageCount; + set => SetProperty(ref _pageCount, value); } - private string _thumbnail = ""; - public string Thumbnail + private string _version = ""; + public string Version { - get => _thumbnail; - set - { - value = IOService.SanitizeXmlString(value); - SetProperty(ref _thumbnail, value); - } + get => _version; + set => SetProperty(ref _version, value); } - private bool _physicallyOwned; - public bool PhysicallyOwned - { - get => _physicallyOwned; - set => SetProperty(ref _physicallyOwned, value); - } + #endregion + + #region User related Metadata private ObservableCollection _tags = new(); - //Don't save all the tags, only save ID's instead - [XmlIgnore] public ObservableCollection Tags { get => _tags; @@ -243,28 +175,14 @@ public ObservableCollection Tags } } - [XmlIgnore] //order them in same order as alltags by starting with alltags and keeping the ones we need using intersect public IEnumerable OrderedTags => _tags.FirstOrDefault()?.AllTags.Intersect(_tags) ?? Enumerable.Empty(); - public List TagIDs { get; set; } = new(); - - private string _description = ""; - public string Description - { - get => _description; - set - { - value = IOService.SanitizeXmlString(value); - SetProperty(ref _description, value); - } - } - - private DateTime? _releaseDate; - public DateTime? ReleaseDate + private bool _physicallyOwned; + public bool PhysicallyOwned { - get => _releaseDate; - set => SetProperty(ref _releaseDate, value); + get => _physicallyOwned; + set => SetProperty(ref _physicallyOwned, value); } private int _rating; @@ -274,13 +192,17 @@ public int Rating set => SetProperty(ref _rating, value); } - private int _pageCount; - public int PageCount + private bool _favorite; + public bool Favorite { - get => _pageCount; - set => SetProperty(ref _pageCount, value); + get => _favorite; + set => SetProperty(ref _favorite, value); } + #endregion + + #region User behaviour metadata + private DateTime _dateAdded = DateTime.Now; public DateTime DateAdded { @@ -302,27 +224,37 @@ public int OpenedCount set => SetProperty(ref _openedCount, value); } - private bool _favorite; - public bool Favorite - { - get => _favorite; - set => SetProperty(ref _favorite, value); - } + #endregion - private string _isbn = ""; - public string ISBN + #region Sources + private string _sourceURL = ""; + public string SourceURL { - get => _isbn; + get => _sourceURL; set { - value = IOService.SanitizeXmlString(value); - SetProperty(ref _isbn, value); + if (value.StartsWith("www.")) + { + value = @"https://" + value; + } + SetProperty(ref _sourceURL, value); } } - public bool HasOfflineSource() => !String.IsNullOrWhiteSpace(Path); + private string _path = ""; + public string Path + { + get => _path; + set => SetProperty(ref _path, value); + } - public bool HasOnlineSource() => !String.IsNullOrWhiteSpace(SourceURL); + private string _isbn = ""; + public string ISBN + { + get => _isbn; + set => SetProperty(ref _isbn, value); + } + #endregion public string? FileType { @@ -349,7 +281,55 @@ public string? FileType } } public string FileName => System.IO.Path.GetFileName(Path); - #endregion + #endregion + + #region Methods + public void Copy(Codex c) + { + Title = c.Title; + SortingTitle = c.UserDefinedSortingTitle; //copy field instead of property, or it will copy _title + Path = c.Path; + Authors = new(c.Authors); + Publisher = c.Publisher; + Version = c.Version; + SourceURL = c.SourceURL; + ID = c.ID; + CoverArt = c.CoverArt; + Thumbnail = c.Thumbnail; + PhysicallyOwned = c.PhysicallyOwned; + Description = c.Description; + ReleaseDate = c.ReleaseDate; + Rating = c.Rating; + PageCount = c.PageCount; + Tags = new(c.Tags); + LastOpened = c.LastOpened; + DateAdded = c.DateAdded; + Favorite = c.Favorite; + OpenedCount = c.OpenedCount; + ISBN = c.ISBN; + } + + public void RefreshThumbnail() => OnPropertyChanged(nameof(Thumbnail)); + + public void ClearPersonalData() + { + Favorite = false; + PhysicallyOwned = false; + DateAdded = DateTime.Now; + OpenedCount = 0; + LastOpened = default; + Rating = 0; + } + + private void OnCollectionChanged(object? o, NotifyCollectionChangedEventArgs args) + { + if (o == Tags) OnPropertyChanged(nameof(OrderedTags)); + if (o == Authors) OnPropertyChanged(nameof(AuthorsAsString)); + } + #endregion + public bool HasOfflineSource() => !String.IsNullOrWhiteSpace(Path); + + public bool HasOnlineSource() => !String.IsNullOrWhiteSpace(SourceURL); public static readonly List MedataProperties = new() { @@ -363,13 +343,6 @@ public string? FileType CodexProperty.GetInstance(nameof(ReleaseDate))!, CodexProperty.GetInstance(nameof(CoverArt))!, }; - - private void OnCollectionChanged(object? o, NotifyCollectionChangedEventArgs args) - { - if (o == Tags) OnPropertyChanged(nameof(OrderedTags)); - if (o == Authors) OnPropertyChanged(nameof(AuthorsAsString)); - } - } } diff --git a/src/Models/CodexCollection.cs b/src/Models/CodexCollection.cs index 460faee..1c696b8 100644 --- a/src/Models/CodexCollection.cs +++ b/src/Models/CodexCollection.cs @@ -2,6 +2,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using COMPASS.Interfaces; using COMPASS.Models.Enums; +using COMPASS.Models.XmlDtos; using COMPASS.Services; using COMPASS.Tools; using COMPASS.ViewModels; @@ -25,7 +26,7 @@ public CodexCollection(string collectionDirectory) } private PreferencesService _preferencesService; - private static readonly object writeLocker = new object(); + private static readonly object writeLocker = new(); public static string CollectionsPath => Path.Combine(SettingsViewModel.CompassDataPath, "Collections"); public string FullDataPath => Path.Combine(CollectionsPath, DirectoryName); @@ -136,24 +137,19 @@ private void CompleteLoadingTags() { t.AllTags = AllTags; } - - foreach (Codex c in AllCodices) - { - //set the new object on the open codices - c.Tags = new(AllTags.Where(t => c.TagIDs.Contains(t.ID))); - } } //Loads AllCodices list from Files public bool LoadCodices() { + CodexDto[] dtos = Array.Empty(); if (File.Exists(CodicesDataFilePath)) { using var reader = new StreamReader(CodicesDataFilePath); - XmlSerializer serializer = new(typeof(ObservableCollection)); + XmlSerializer serializer = new(typeof(CodexDto[])); try { - AllCodices = serializer.Deserialize(reader) as ObservableCollection ?? new(); + dtos = serializer.Deserialize(reader) as CodexDto[] ?? Array.Empty(); } catch (Exception ex) { @@ -161,28 +157,12 @@ public bool LoadCodices() return false; } } - else - { - AllCodices = new(); - } - CompleteLoadingCodices(); + AllCodices = new(dtos.Select(dto => dto.ToModel(AllTags))); _loadedCodices = true; return true; } - private void CompleteLoadingCodices() - { - foreach (Codex c in AllCodices) - { - //reconstruct tags from ID's - c.Tags = new(AllTags.Where(t => c.TagIDs.Contains(t.ID))); - - //double check image location, redundant but got fucked in an update - c.SetImagePaths(this); - } - } - public bool LoadInfo() { if (File.Exists(CollectionInfoFilePath)) @@ -314,22 +294,19 @@ public bool SaveCodices() //Should always load a collection before it can be saved return false; } - //Copy id's of tags into list for serialisation - foreach (Codex codex in AllCodices) - { - codex.TagIDs = codex.Tags.Select(t => t.ID).ToList(); - } + + var toSave = AllCodices.Select(c => c.ToDto()).ToList(); try { string tempFileName = CodicesDataFilePath + ".tmp"; - + lock (writeLocker) { using (var writer = XmlWriter.Create(tempFileName, XmlService.XmlWriteSettings)) { - XmlSerializer serializer = new(typeof(ObservableCollection)); - serializer.Serialize(writer, AllCodices); + XmlSerializer serializer = new(typeof(List)); + serializer.Serialize(writer, toSave); } //if successfully written to the tmp file, move to actual path @@ -337,7 +314,7 @@ public bool SaveCodices() File.Delete(tempFileName); } } - catch(UnauthorizedAccessException ex) + catch (UnauthorizedAccessException ex) { Logger.Error($"Access denied when trying to save Codex Info to {CodicesDataFilePath}", ex); return false; diff --git a/src/Models/Tag.cs b/src/Models/Tag.cs index 8cd3dcd..4b806be 100644 --- a/src/Models/Tag.cs +++ b/src/Models/Tag.cs @@ -1,5 +1,4 @@ using CommunityToolkit.Mvvm.ComponentModel; -using COMPASS.Services; using COMPASS.Tools; using System; using System.Collections.Generic; @@ -46,11 +45,7 @@ public ObservableCollection Children public string Content { get => _content; - set - { - value = IOService.SanitizeXmlString(value); - SetProperty(ref _content, value, true); //needs to broadcast so TagEdit can validate the input - } + set => SetProperty(ref _content, value, true); //needs to broadcast so TagEdit can validate the input } // Add [XmlIgnoreAttribute] in a future update and delete the setter diff --git a/src/Models/XmlDtos/CodexDto.cs b/src/Models/XmlDtos/CodexDto.cs new file mode 100644 index 0000000..c9856bd --- /dev/null +++ b/src/Models/XmlDtos/CodexDto.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace COMPASS.Models.XmlDtos +{ + [XmlRoot("Codex"), XmlType("Codex")] + public class CodexDto + { + + #region COMPASS related Metadata + public int ID { get; set; } + public string CoverArt { get; set; } = ""; + public string Thumbnail { get; set; } = ""; + #endregion + + #region Codex related Metadata + + public string Title { get; set; } = ""; + [XmlElement("SerializableSortingTitle")] //Backwards compatibility + public string SortingTitle { get; set; } = ""; + public List Authors { get; set; } = new List(); + public string Publisher { get; set; } = ""; + public string Description { get; set; } = ""; + public DateTime? ReleaseDate { get; set; } + public int PageCount { get; set; } + public string Version { get; set; } = ""; + + #endregion + + #region User related Metadata + public List TagIDs { get; set; } = new(); + public bool PhysicallyOwned { get; set; } + public int Rating { get; set; } + public bool Favorite { get; set; } + + #endregion + + #region User behaviour metadata + public DateTime DateAdded { get; set; } = DateTime.Now; + public DateTime LastOpened { get; set; } + public int OpenedCount { get; set; } + #endregion + + #region Sources + public string SourceURL { get; set; } = ""; + public string Path { get; set; } = ""; + public string ISBN { get; set; } = ""; + + #endregion + + } +} diff --git a/src/Models/XmlDtos/XmlMapper.cs b/src/Models/XmlDtos/XmlMapper.cs new file mode 100644 index 0000000..b8fbfa5 --- /dev/null +++ b/src/Models/XmlDtos/XmlMapper.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace COMPASS.Models.XmlDtos +{ + public static class XmlMapper + { + #region Sanitize xml data + //based on https://seattlesoftware.wordpress.com/2008/09/11/hexadecimal-value-0-is-an-invalid-character/ + /// + /// Remove illegal XML characters from a string. + /// + private static string Sanitize(this string str) + { + if (str is null) return ""; + + StringBuilder buffer = new(str.Length); + foreach (char c in str.Where(c => IsLegalXmlChar(c))) + { + buffer.Append(c); + } + return buffer.ToString(); + } + + /// + /// Whether a given character is allowed by XML 1.0. + /// + private static bool IsLegalXmlChar(int character) => character switch + { + 0x9 => true, // '\t' == 9 + 0xA => true, // '\n' == 10 + 0xD => true, // '\r' == 13 + (>= 0x20) and (<= 0xD7FF) => true, + (>= 0xE000) and (<= 0xFFFD) => true, + (>= 0x10000) and (<= 0x10FFFF) => true, + _ => false + }; + + #endregion + + #region Codex + + public static Codex ToModel(this CodexDto dto, IList tags) + { + Codex codex = new() + { + // COMPASS related Metadata + ID = dto.ID, + CoverArt = dto.CoverArt, + Thumbnail = dto.Thumbnail, + + //Codex related Metadata + Title = dto.Title, + SortingTitle = dto.SortingTitle, + Authors = new(dto.Authors), + Publisher = dto.Publisher, + Description = dto.Description, + ReleaseDate = dto.ReleaseDate, + PageCount = dto.PageCount, + Version = dto.Version, + + //User related Metadata + PhysicallyOwned = dto.PhysicallyOwned, + Rating = dto.Rating, + Favorite = dto.Favorite, + + //User behaviour metadata + DateAdded = dto.DateAdded, + LastOpened = dto.LastOpened, + OpenedCount = dto.OpenedCount, + + //Sources + SourceURL = dto.SourceURL, + Path = dto.Path, + ISBN = dto.ISBN, + }; + + codex.Tags = new(tags.Where(tag => dto.TagIDs.Contains(tag.ID))); + + return codex; + } + + public static CodexDto ToDto(this Codex model) + { + CodexDto dto = new() + { + // COMPASS related Metadata + ID = model.ID, + CoverArt = model.CoverArt.Sanitize(), + Thumbnail = model.Thumbnail.Sanitize(), + + //Codex related Metadata + Title = model.Title.Sanitize(), + SortingTitle = model.UserDefinedSortingTitle.Sanitize(), + Authors = model.Authors.Select(author => author.Sanitize()).ToList(), + Publisher = model.Publisher.Sanitize(), + Description = model.Description.Sanitize(), + ReleaseDate = model.ReleaseDate, + PageCount = model.PageCount, + Version = model.Version, + + //User related Metadata + PhysicallyOwned = model.PhysicallyOwned, + Rating = model.Rating, + Favorite = model.Favorite, + TagIDs = model.Tags.Select(t => t.ID).ToList(), + + //User behaviour metadata + DateAdded = model.DateAdded, + LastOpened = model.LastOpened, + OpenedCount = model.OpenedCount, + + //Sources + SourceURL = model.SourceURL.Sanitize(), + Path = model.Path.Sanitize(), + ISBN = model.ISBN.Sanitize(), + }; + + return dto; + } + + #endregion + } +} diff --git a/src/Services/IOService.cs b/src/Services/IOService.cs index 8d29845..9c398f1 100644 --- a/src/Services/IOService.cs +++ b/src/Services/IOService.cs @@ -18,7 +18,6 @@ using System.Net.Http; using System.Net.NetworkInformation; using System.Reflection; -using System.Text; using System.Text.Json; using System.Threading.Tasks; @@ -163,36 +162,6 @@ public static void ClearTmpData(string? tempPath = null) } } - //based on https://seattlesoftware.wordpress.com/2008/09/11/hexadecimal-value-0-is-an-invalid-character/ - /// - /// Remove illegal XML characters from a string. - /// - public static string SanitizeXmlString(string xml) - { - if (xml is null) { throw new ArgumentNullException(nameof(xml)); } - - StringBuilder buffer = new(xml.Length); - foreach (char c in xml.Where(c => IsLegalXmlChar(c))) - { - buffer.Append(c); - } - return buffer.ToString(); - } - - /// - /// Whether a given character is allowed by XML 1.0. - /// - public static bool IsLegalXmlChar(int character) => character switch - { - 0x9 => true, // '\t' == 9 - 0xA => true, // '\n' == 10 - 0xD => true, // '\r' == 13 - (>= 0x20) and (<= 0xD7FF) => true, - (>= 0xE000) and (<= 0xFFFD) => true, - (>= 0x10000) and (<= 0x10FFFF) => true, - _ => false - }; - #endregion #region File formats From ac0f68f418e422a1de8e4abb3a5a3ff9816d93a3 Mon Sep 17 00:00:00 2001 From: Paul De Smul Date: Sun, 8 Sep 2024 19:53:14 +0200 Subject: [PATCH 15/20] Add some sleep for IO operations --- Tests/IntegrationTests/Satchels_Test.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/IntegrationTests/Satchels_Test.cs b/Tests/IntegrationTests/Satchels_Test.cs index 1eceea8..73deebb 100644 --- a/Tests/IntegrationTests/Satchels_Test.cs +++ b/Tests/IntegrationTests/Satchels_Test.cs @@ -28,6 +28,7 @@ public async Task TestSatchelExportImport() //Assert export succesfull Assert.IsTrue(File.Exists(filePath)); + Thread.Sleep(100); CodexCollection? deserializedCollection = null; CodexCollection? importedCollection = null; @@ -39,6 +40,7 @@ public async Task TestSatchelExportImport() //Deserialize Satchel deserializedCollection = await IOService.OpenSatchel(filePath); Assert.IsNotNull(deserializedCollection); + Thread.Sleep(100); ImportCollectionViewModel importViewModel = new(deserializedCollection); Assert.IsTrue(importViewModel.ContentSelectorVM.HasCodices, "deserialized satchel has no Codices"); From 6a10e6fd5a1fc0689d75999a65e87e928e979415 Mon Sep 17 00:00:00 2001 From: Paul De Smul Date: Mon, 9 Sep 2024 20:46:42 +0200 Subject: [PATCH 16/20] Seperate Sources into its own object --- Tests/DataGenerators/RandomGenerator.cs | 9 ++- Tests/Sources/HomeBrewery.cs | 13 +++- src/Models/Codex.cs | 66 ++--------------- src/Models/CodexCollection.cs | 12 +-- src/Models/CodexProperties/CodexProperty.cs | 14 ++-- .../CodexProperties/CoverArtProperty.cs | 2 +- .../CodexProperties/DateTimeProperty.cs | 2 +- .../CodexProperties/EnumerableProperty.cs | 4 +- src/Models/CodexProperties/FileProperty.cs | 2 +- src/Models/CodexProperties/NumberProperty.cs | 2 +- src/Models/CodexProperties/StringProperty.cs | 2 +- src/Models/CodexProperties/TagsProperty.cs | 42 +++++++++-- src/Models/Filters/DomainFilter.cs | 5 +- src/Models/Filters/FileExtensionFilter.cs | 2 +- src/Models/Filters/HasBrokenPathFilter.cs | 3 +- src/Models/Filters/HasISBNFilter.cs | 2 +- src/Models/Filters/OfflineSourceFilter.cs | 2 +- src/Models/Filters/OnlineSourceFilter.cs | 2 +- src/Models/Interfaces.cs | 35 ++++++++- src/Models/SourceSet.cs | 74 +++++++++++++++++++ src/Models/XmlDtos/CodexDto.cs | 2 +- src/Models/XmlDtos/XmlMapper.cs | 15 ++-- src/Services/CoverService.cs | 6 +- src/ViewModels/CodexEditViewModel.cs | 6 +- src/ViewModels/CodexViewModel.cs | 69 ++++++++--------- .../CollectionContentSelectorViewModel.cs | 3 +- src/ViewModels/ExportCollectionViewModel.cs | 14 ++-- src/ViewModels/FileNotFoundViewModel.cs | 12 +-- src/ViewModels/FilterViewModel.cs | 10 +-- .../Import/ImportCollectionViewModel.cs | 6 +- src/ViewModels/Import/ImportURLViewModel.cs | 4 +- src/ViewModels/Import/ImportViewModel.cs | 5 +- src/ViewModels/SettingsViewModel.cs | 22 +++--- .../Sources/DndBeyondSourceViewModel.cs | 26 +++---- src/ViewModels/Sources/FileSourceViewModel.cs | 21 +++--- .../Sources/GenericOnlineSourceViewModel.cs | 21 +++--- .../Sources/GmBinderSourceViewModel.cs | 33 +++++---- .../Sources/GoogleDriveSourceViewModel.cs | 24 +++--- .../Sources/HomebrewerySourceViewModel.cs | 32 ++++---- src/ViewModels/Sources/ISBNSourceViewModel.cs | 33 +++++---- .../Sources/ImageSourceViewModel.cs | 10 +-- src/ViewModels/Sources/PdfSourceViewModel.cs | 29 ++++---- src/ViewModels/Sources/SourceViewModel.cs | 5 +- src/Views/ListLayout.xaml | 2 +- src/Windows/CodexEditWindow.xaml | 6 +- src/Windows/ExportCollectionWizard.xaml | 4 +- src/Windows/ImportCollectionWizard.xaml | 4 +- 47 files changed, 408 insertions(+), 311 deletions(-) create mode 100644 src/Models/SourceSet.cs diff --git a/Tests/DataGenerators/RandomGenerator.cs b/Tests/DataGenerators/RandomGenerator.cs index f8006ce..fd793bf 100644 --- a/Tests/DataGenerators/RandomGenerator.cs +++ b/Tests/DataGenerators/RandomGenerator.cs @@ -83,9 +83,12 @@ public static Codex GetRandomCodex() Rating = random.Next(0, 6), Favorite = GetRandomBool(falseFreq: 10), OpenedCount = random.Next(100), - ISBN = GetRandomISBN(), - Path = GetRandomBool() ? GetRandomPath() : String.Empty, //randomly decide if it has a path - SourceURL = GetRandomBool() ? GetRandomUrl() : String.Empty, //randomly decide if it has a url + Sources = new() + { + ISBN = GetRandomISBN(), + Path = GetRandomBool() ? GetRandomPath() : String.Empty, //randomly decide if it has a path + SourceURL = GetRandomBool() ? GetRandomUrl() : String.Empty, //randomly decide if it has a url + } }; codex.DateAdded = GetRandomDate((DateTime)codex.ReleaseDate); diff --git a/Tests/Sources/HomeBrewery.cs b/Tests/Sources/HomeBrewery.cs index bf6a2ff..7dc23ff 100644 --- a/Tests/Sources/HomeBrewery.cs +++ b/Tests/Sources/HomeBrewery.cs @@ -1,4 +1,5 @@ using COMPASS.Models; +using COMPASS.Models.XmlDtos; using COMPASS.ViewModels; using COMPASS.ViewModels.Sources; @@ -29,12 +30,15 @@ public async Task GetMetaDataFromHomeBrewery() { Codex codex = new() { - SourceURL = @"https://homebrewery.naturalcrit.com/share/FegJIEB2KUUo" + Sources = new() + { + SourceURL = @"https://homebrewery.naturalcrit.com/share/FegJIEB2KUUo" + } }; var vm = SourceViewModel.GetSourceVM(MetaDataSource.Homebrewery); - Codex response = await vm!.GetMetaData(codex); + CodexDto response = await vm!.GetMetaData(codex.Sources); Assert.IsNotNull(response); Assert.IsFalse(String.IsNullOrEmpty(response.Title)); @@ -52,7 +56,10 @@ public async Task GetCoverFromHomeBrewery() Codex codex = new(cc) { - SourceURL = @"https://homebrewery.naturalcrit.com/share/FegJIEB2KUUo" + Sources = new() + { + SourceURL = @"https://homebrewery.naturalcrit.com/share/FegJIEB2KUUo" + } }; //Clear existing data diff --git a/src/Models/Codex.cs b/src/Models/Codex.cs index 65d7cf6..6e6a5d8 100644 --- a/src/Models/Codex.cs +++ b/src/Models/Codex.cs @@ -9,7 +9,7 @@ namespace COMPASS.Models { - public class Codex : ObservableObject, IHasID + public class Codex : ObservableObject, IHasID, IHasCodexMetadata { #region Constructors @@ -226,61 +226,12 @@ public int OpenedCount #endregion - #region Sources - private string _sourceURL = ""; - public string SourceURL + private SourceSet _sources = new(); + public SourceSet Sources { - get => _sourceURL; - set - { - if (value.StartsWith("www.")) - { - value = @"https://" + value; - } - SetProperty(ref _sourceURL, value); - } - } - - private string _path = ""; - public string Path - { - get => _path; - set => SetProperty(ref _path, value); - } - - private string _isbn = ""; - public string ISBN - { - get => _isbn; - set => SetProperty(ref _isbn, value); - } - #endregion - - public string? FileType - { - get - { - if (HasOfflineSource()) - { - return System.IO.Path.GetExtension(Path); - } - - else if (HasOnlineSource()) - { - // online sources can also also point to file - // either hosted on cloud service like Google drive - // or services like homebrewery are always .pdf - // skip this for now though - return "webpage"; - } - - else - { - return null; - } - } + get => _sources; + set => SetProperty(ref _sources, value); } - public string FileName => System.IO.Path.GetFileName(Path); #endregion #region Methods @@ -288,11 +239,10 @@ public void Copy(Codex c) { Title = c.Title; SortingTitle = c.UserDefinedSortingTitle; //copy field instead of property, or it will copy _title - Path = c.Path; + Sources = c.Sources.Copy(); Authors = new(c.Authors); Publisher = c.Publisher; Version = c.Version; - SourceURL = c.SourceURL; ID = c.ID; CoverArt = c.CoverArt; Thumbnail = c.Thumbnail; @@ -306,7 +256,6 @@ public void Copy(Codex c) DateAdded = c.DateAdded; Favorite = c.Favorite; OpenedCount = c.OpenedCount; - ISBN = c.ISBN; } public void RefreshThumbnail() => OnPropertyChanged(nameof(Thumbnail)); @@ -327,9 +276,6 @@ private void OnCollectionChanged(object? o, NotifyCollectionChangedEventArgs arg if (o == Authors) OnPropertyChanged(nameof(AuthorsAsString)); } #endregion - public bool HasOfflineSource() => !String.IsNullOrWhiteSpace(Path); - - public bool HasOnlineSource() => !String.IsNullOrWhiteSpace(SourceURL); public static readonly List MedataProperties = new() { diff --git a/src/Models/CodexCollection.cs b/src/Models/CodexCollection.cs index 1c696b8..b1f70ca 100644 --- a/src/Models/CodexCollection.cs +++ b/src/Models/CodexCollection.cs @@ -500,18 +500,18 @@ public void ImportCodicesFrom(CodexCollection source) } //move user files included in import - if (codex.Path.StartsWith(source.UserFilesPath) && File.Exists(codex.Path)) + if (codex.Sources.Path.StartsWith(source.UserFilesPath) && File.Exists(codex.Sources.Path)) { try { - string newPath = codex.Path.Replace(source.UserFilesPath, UserFilesPath); + string newPath = codex.Sources.Path.Replace(source.UserFilesPath, UserFilesPath); string? newDir = Path.GetDirectoryName(newPath); if (newDir != null) { Directory.CreateDirectory(newDir); } - File.Copy(codex.Path, newPath, true); - codex.Path = newPath; + File.Copy(codex.Sources.Path, newPath, true); + codex.Sources.Path = newPath; } catch (Exception ex) { @@ -555,8 +555,8 @@ public void DeleteCodices(IList toDelete) public void BanishCodices(IList toBanish) { if (toBanish is null) return; - IEnumerable toBanishPaths = toBanish.Select(codex => codex.Path); - IEnumerable toBanishURLs = toBanish.Select(codex => codex.SourceURL); + IEnumerable toBanishPaths = toBanish.Select(codex => codex.Sources.Path); + IEnumerable toBanishURLs = toBanish.Select(codex => codex.Sources.SourceURL); IEnumerable toBanishStrings = toBanishPaths .Concat(toBanishURLs) .Where(s => !String.IsNullOrWhiteSpace(s)) diff --git a/src/Models/CodexProperties/CodexProperty.cs b/src/Models/CodexProperties/CodexProperty.cs index 585b5a5..f32ad7e 100644 --- a/src/Models/CodexProperties/CodexProperty.cs +++ b/src/Models/CodexProperties/CodexProperty.cs @@ -27,9 +27,9 @@ protected CodexProperty(string propName, string? label = null) #region Methods - public abstract bool IsEmpty(Codex codex); + public abstract bool IsEmpty(IHasCodexMetadata codex); - public abstract void SetProp(Codex target, Codex source); + public abstract void SetProp(IHasCodexMetadata target, IHasCodexMetadata source); /// /// Checks if the codex to evaluated has a newer value for the property than the reference @@ -37,7 +37,7 @@ protected CodexProperty(string propName, string? label = null) /// /// /// - public abstract bool HasNewValue(Codex toEvaluate, Codex reference); + public abstract bool HasNewValue(IHasCodexMetadata toEvaluate, IHasCodexMetadata reference); #endregion @@ -215,18 +215,18 @@ public CodexProperty(string propName, string? label = null) : base(propName, label) { } - public override bool IsEmpty(Codex codex) => EqualityComparer.Default.Equals(GetProp(codex), default); + public override bool IsEmpty(IHasCodexMetadata codex) => EqualityComparer.Default.Equals(GetProp(codex), default); - public T? GetProp(Codex codex) + public T? GetProp(IHasCodexMetadata codex) { object? value = codex.GetPropertyValue(Name); return value == null ? default : (T)value; } - public override void SetProp(Codex target, Codex source) + public override void SetProp(IHasCodexMetadata target, IHasCodexMetadata source) => target.SetProperty(Name, GetProp(source)); - public override bool HasNewValue(Codex toEvaluate, Codex reference) => + public override bool HasNewValue(IHasCodexMetadata toEvaluate, IHasCodexMetadata reference) => !EqualityComparer.Default.Equals(GetProp(toEvaluate), GetProp(reference)); } } diff --git a/src/Models/CodexProperties/CoverArtProperty.cs b/src/Models/CodexProperties/CoverArtProperty.cs index dece795..58fddb5 100644 --- a/src/Models/CodexProperties/CoverArtProperty.cs +++ b/src/Models/CodexProperties/CoverArtProperty.cs @@ -6,7 +6,7 @@ public CoverArtProperty(string propName, string? label = null) : base(propName, label) { } - public override void SetProp(Codex target, Codex source) + public override void SetProp(IHasCodexMetadata target, IHasCodexMetadata source) { target.CoverArt = source.CoverArt; target.Thumbnail = source.Thumbnail; diff --git a/src/Models/CodexProperties/DateTimeProperty.cs b/src/Models/CodexProperties/DateTimeProperty.cs index 94b94cb..e48ad67 100644 --- a/src/Models/CodexProperties/DateTimeProperty.cs +++ b/src/Models/CodexProperties/DateTimeProperty.cs @@ -8,7 +8,7 @@ public DateTimeProperty(string propName, string? label = null) : base(propName, label) { } - public override bool IsEmpty(Codex codex) + public override bool IsEmpty(IHasCodexMetadata codex) { DateTime? value = GetProp(codex); return value is null || value == DateTime.MinValue; diff --git a/src/Models/CodexProperties/EnumerableProperty.cs b/src/Models/CodexProperties/EnumerableProperty.cs index 415391e..423224a 100644 --- a/src/Models/CodexProperties/EnumerableProperty.cs +++ b/src/Models/CodexProperties/EnumerableProperty.cs @@ -10,14 +10,14 @@ public EnumerableProperty(string propName, string? label = null) : base(propName, label) { } - public override bool IsEmpty(Codex codex) + public override bool IsEmpty(IHasCodexMetadata codex) { IEnumerable? value = GetProp(codex); return !value.SafeAny(); } - public override bool HasNewValue(Codex toEvaluate, Codex reference) + public override bool HasNewValue(IHasCodexMetadata toEvaluate, IHasCodexMetadata reference) { var newVal = GetProp(toEvaluate); if (!newVal.SafeAny()) diff --git a/src/Models/CodexProperties/FileProperty.cs b/src/Models/CodexProperties/FileProperty.cs index 1e5ad74..0c69b79 100644 --- a/src/Models/CodexProperties/FileProperty.cs +++ b/src/Models/CodexProperties/FileProperty.cs @@ -8,6 +8,6 @@ public FileProperty(string propName, string? label = null) : base(propName, label) { } - public override bool IsEmpty(Codex codex) => !File.Exists(GetProp(codex)); + public override bool IsEmpty(IHasCodexMetadata codex) => !File.Exists(GetProp(codex)); } } diff --git a/src/Models/CodexProperties/NumberProperty.cs b/src/Models/CodexProperties/NumberProperty.cs index e713585..a182ef2 100644 --- a/src/Models/CodexProperties/NumberProperty.cs +++ b/src/Models/CodexProperties/NumberProperty.cs @@ -8,6 +8,6 @@ public NumberProperty(string propName, string? label = null) : base(propName, label) { } - public override bool IsEmpty(Codex codex) => T.IsZero(GetProp(codex)!); + public override bool IsEmpty(IHasCodexMetadata codex) => T.IsZero(GetProp(codex)!); } } diff --git a/src/Models/CodexProperties/StringProperty.cs b/src/Models/CodexProperties/StringProperty.cs index 16f32f8..ec5cb3d 100644 --- a/src/Models/CodexProperties/StringProperty.cs +++ b/src/Models/CodexProperties/StringProperty.cs @@ -6,6 +6,6 @@ public StringProperty(string propName, string? label = null) : base(propName, label) { } - public override bool IsEmpty(Codex codex) => string.IsNullOrEmpty(GetProp(codex)); + public override bool IsEmpty(IHasCodexMetadata codex) => string.IsNullOrEmpty(GetProp(codex)); } } diff --git a/src/Models/CodexProperties/TagsProperty.cs b/src/Models/CodexProperties/TagsProperty.cs index 31252b7..1f36de5 100644 --- a/src/Models/CodexProperties/TagsProperty.cs +++ b/src/Models/CodexProperties/TagsProperty.cs @@ -1,4 +1,6 @@ -using COMPASS.Tools; +using COMPASS.Models.XmlDtos; +using COMPASS.Tools; +using System; using System.Linq; namespace COMPASS.Models.CodexProperties @@ -9,14 +11,44 @@ public TagsProperty(string propName, string? label = null) : base(propName, label) { } - public override void SetProp(Codex target, Codex source) + public override void SetProp(IHasCodexMetadata target, IHasCodexMetadata source) { - foreach (var tag in source.Tags.ToList()) + if (source is Codex sourceCodex && target is Codex targetCodex) { - target.Tags.AddIfMissing(tag); + foreach (var tag in sourceCodex.Tags.ToList()) + { + targetCodex.Tags.AddIfMissing(tag); + } } + + else if (source is CodexDto sourceDto && target is CodexDto targetDto) + { + foreach (var tag in sourceDto.TagIDs.ToList()) + { + targetDto.TagIDs.AddIfMissing(tag); + } + } + + else + { + throw new InvalidOperationException("Target and source must be of same type"); + } + } - public override bool HasNewValue(Codex toEvaluate, Codex reference) => toEvaluate.Tags.Except(reference.Tags).Any(); + public override bool HasNewValue(IHasCodexMetadata toEvaluate, IHasCodexMetadata reference) + { + if (toEvaluate is Codex toEvalCodex && reference is Codex referenceCodex) + { + return toEvalCodex.Tags.Except(referenceCodex.Tags).Any(); + } + + if (toEvaluate is CodexDto toEvaluateDto && reference is CodexDto referenceDto) + { + return toEvaluateDto.TagIDs.Except(referenceDto.TagIDs).Any(); + } + //TODO check which combination of Codex and CodexDto occur and need to be handled + throw new InvalidOperationException("Target and source must be of same type"); + } } } diff --git a/src/Models/Filters/DomainFilter.cs b/src/Models/Filters/DomainFilter.cs index 263115f..4101642 100644 --- a/src/Models/Filters/DomainFilter.cs +++ b/src/Models/Filters/DomainFilter.cs @@ -13,6 +13,9 @@ public DomainFilter(string domain) : base(FilterType.Domain, domain) public override string Content => $"From: {FilterValue}"; - public override bool Apply(Codex codex) => FilterValue is string domain && codex.HasOnlineSource() && codex.SourceURL.Contains(domain); + public override bool Apply(Codex codex) => + FilterValue is string domain && + codex.Sources.HasOnlineSource() && + codex.Sources.SourceURL.Contains(domain); } } diff --git a/src/Models/Filters/FileExtensionFilter.cs b/src/Models/Filters/FileExtensionFilter.cs index 467e9d1..f0bbc00 100644 --- a/src/Models/Filters/FileExtensionFilter.cs +++ b/src/Models/Filters/FileExtensionFilter.cs @@ -13,6 +13,6 @@ public FileExtensionFilter(string extension) : base(FilterType.FileExtension, ex public override string Content => $"File Type: {FilterValue}"; - public override bool Apply(Codex codex) => FilterValue is string extension && codex.FileType == extension; + public override bool Apply(Codex codex) => FilterValue is string extension && codex.Sources.FileType == extension; } } diff --git a/src/Models/Filters/HasBrokenPathFilter.cs b/src/Models/Filters/HasBrokenPathFilter.cs index 1a578d7..2557017 100644 --- a/src/Models/Filters/HasBrokenPathFilter.cs +++ b/src/Models/Filters/HasBrokenPathFilter.cs @@ -12,6 +12,7 @@ public HasBrokenPathFilter() : base(FilterType.HasBrokenPath) public override string Content => "Has broken path"; - public override bool Apply(Codex codex) => codex.HasOfflineSource() && !Path.Exists(codex.Path); + public override bool Apply(Codex codex) => + codex.Sources.HasOfflineSource() && !Path.Exists(codex.Sources.Path); } } diff --git a/src/Models/Filters/HasISBNFilter.cs b/src/Models/Filters/HasISBNFilter.cs index bc74ca1..63adcc0 100644 --- a/src/Models/Filters/HasISBNFilter.cs +++ b/src/Models/Filters/HasISBNFilter.cs @@ -10,6 +10,6 @@ public HasISBNFilter() : base(FilterType.HasISBN) public override Color BackgroundColor => Colors.DarkSeaGreen; public override string Content => "Has ISBN"; - public override bool Apply(Codex codex) => !String.IsNullOrEmpty(codex.ISBN); + public override bool Apply(Codex codex) => !String.IsNullOrEmpty(codex.Sources.ISBN); } } diff --git a/src/Models/Filters/OfflineSourceFilter.cs b/src/Models/Filters/OfflineSourceFilter.cs index 7c46148..abde647 100644 --- a/src/Models/Filters/OfflineSourceFilter.cs +++ b/src/Models/Filters/OfflineSourceFilter.cs @@ -9,6 +9,6 @@ public OfflineSourceFilter() : base(FilterType.OfflineSource) public override string Content => "Available Offline"; public override Color BackgroundColor => Colors.DarkSeaGreen; - public override bool Apply(Codex codex) => codex.HasOfflineSource(); + public override bool Apply(Codex codex) => codex.Sources.HasOfflineSource(); } } diff --git a/src/Models/Filters/OnlineSourceFilter.cs b/src/Models/Filters/OnlineSourceFilter.cs index 0bd71bc..34de659 100644 --- a/src/Models/Filters/OnlineSourceFilter.cs +++ b/src/Models/Filters/OnlineSourceFilter.cs @@ -7,7 +7,7 @@ internal class OnlineSourceFilter : Filter public OnlineSourceFilter() : base(FilterType.OnlineSource) { } - public override bool Apply(Codex codex) => codex.HasOnlineSource(); + public override bool Apply(Codex codex) => codex.Sources.HasOnlineSource(); public override string Content => "Available Online"; public override Color BackgroundColor => Colors.DarkSeaGreen; diff --git a/src/Models/Interfaces.cs b/src/Models/Interfaces.cs index da4fe89..9e84734 100644 --- a/src/Models/Interfaces.cs +++ b/src/Models/Interfaces.cs @@ -1,4 +1,5 @@ -using System.Collections.ObjectModel; +using System; +using System.Collections.ObjectModel; using System.Windows.Media; namespace COMPASS.Models @@ -25,4 +26,36 @@ public interface IDealsWithTabControl public int SelectedTab { get; set; } public bool Collapsed { get; set; } } + + public interface IHasCodexMetadata + { + #region COMPASS related Metadata + public int ID { get; set; } + public string CoverArt { get; set; } + public string Thumbnail { get; set; } + #endregion + + #region Codex related Metadata + + public string Title { get; set; } + public string SortingTitle { get; set; } + + //public IList Authors { get; set; } + + public string Publisher { get; set; } + public string Description { get; set; } + public DateTime? ReleaseDate { get; set; } + public int PageCount { get; set; } + public string Version { get; set; } + + #endregion + + #region User related Metadata + public bool PhysicallyOwned { get; set; } + public int Rating { get; set; } + public bool Favorite { get; set; } + + #endregion + + } } diff --git a/src/Models/SourceSet.cs b/src/Models/SourceSet.cs new file mode 100644 index 0000000..d326537 --- /dev/null +++ b/src/Models/SourceSet.cs @@ -0,0 +1,74 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System; + +namespace COMPASS.Models +{ + public class SourceSet : ObservableObject + { + public SourceSet Copy() => new() + { + SourceURL = SourceURL, + Path = Path, + ISBN = ISBN + }; + + + private string _sourceURL = ""; + public string SourceURL + { + get => _sourceURL; + set + { + if (value.StartsWith("www.")) + { + value = @"https://" + value; + } + SetProperty(ref _sourceURL, value); + } + } + + private string _path = ""; + public string Path + { + get => _path; + set => SetProperty(ref _path, value); + } + + private string _isbn = ""; + public string ISBN + { + get => _isbn; + set => SetProperty(ref _isbn, value); + } + + public string? FileType + { + get + { + if (HasOfflineSource()) + { + return System.IO.Path.GetExtension(Path); + } + + else if (HasOnlineSource()) + { + // online sources can also also point to file + // either hosted on cloud service like Google drive + // or services like homebrewery are always .pdf + // skip this for now though + return "webpage"; + } + + else + { + return null; + } + } + } + public string FileName => System.IO.Path.GetFileName(Path); + + public bool HasOfflineSource() => !String.IsNullOrWhiteSpace(Path); + + public bool HasOnlineSource() => !String.IsNullOrWhiteSpace(SourceURL); + } +} diff --git a/src/Models/XmlDtos/CodexDto.cs b/src/Models/XmlDtos/CodexDto.cs index c9856bd..64f6059 100644 --- a/src/Models/XmlDtos/CodexDto.cs +++ b/src/Models/XmlDtos/CodexDto.cs @@ -5,7 +5,7 @@ namespace COMPASS.Models.XmlDtos { [XmlRoot("Codex"), XmlType("Codex")] - public class CodexDto + public class CodexDto : IHasCodexMetadata { #region COMPASS related Metadata diff --git a/src/Models/XmlDtos/XmlMapper.cs b/src/Models/XmlDtos/XmlMapper.cs index b8fbfa5..bbbc036 100644 --- a/src/Models/XmlDtos/XmlMapper.cs +++ b/src/Models/XmlDtos/XmlMapper.cs @@ -71,9 +71,12 @@ public static Codex ToModel(this CodexDto dto, IList tags) OpenedCount = dto.OpenedCount, //Sources - SourceURL = dto.SourceURL, - Path = dto.Path, - ISBN = dto.ISBN, + Sources = new SourceSet() + { + SourceURL = dto.SourceURL, + Path = dto.Path, + ISBN = dto.ISBN, + } }; codex.Tags = new(tags.Where(tag => dto.TagIDs.Contains(tag.ID))); @@ -112,9 +115,9 @@ public static CodexDto ToDto(this Codex model) OpenedCount = model.OpenedCount, //Sources - SourceURL = model.SourceURL.Sanitize(), - Path = model.Path.Sanitize(), - ISBN = model.ISBN.Sanitize(), + SourceURL = model.Sources.SourceURL.Sanitize(), + Path = model.Sources.Path.Sanitize(), + ISBN = model.Sources.ISBN.Sanitize(), }; return dto; diff --git a/src/Services/CoverService.cs b/src/Services/CoverService.cs index c994462..f692184 100644 --- a/src/Services/CoverService.cs +++ b/src/Services/CoverService.cs @@ -23,9 +23,7 @@ public static async Task GetCover(Codex codex, ChooseMetaDataViewModel? chooseMe { Codex MetaDatalessCodex = new() { - Path = codex.Path, - SourceURL = codex.SourceURL, - ISBN = codex.ISBN, + Sources = codex.Sources, ID = codex.ID, }; @@ -60,7 +58,7 @@ public static async Task GetCover(Codex codex, ChooseMetaDataViewModel? chooseMe ProgressViewModel.GlobalCancellationTokenSource.Token.ThrowIfCancellationRequested(); SourceViewModel? sourceVM = SourceViewModel.GetSourceVM(source); - if (sourceVM == null || !sourceVM.IsValidSource(codex)) continue; + if (sourceVM == null || !sourceVM.IsValidSource(codex.Sources)) continue; getCoverSuccessful = await sourceVM.FetchCover(MetaDatalessCodex); if (getCoverSuccessful) break; } diff --git a/src/ViewModels/CodexEditViewModel.cs b/src/ViewModels/CodexEditViewModel.cs index 22ac62c..0498bc1 100644 --- a/src/ViewModels/CodexEditViewModel.cs +++ b/src/ViewModels/CodexEditViewModel.cs @@ -74,11 +74,11 @@ private void BrowsePath() OpenFileDialog openFileDialog = new() { AddExtension = false, - InitialDirectory = Path.GetDirectoryName(TempCodex.Path) ?? String.Empty + InitialDirectory = Path.GetDirectoryName(TempCodex.Sources.Path) ?? String.Empty }; if (openFileDialog.ShowDialog() == true) { - TempCodex.Path = openFileDialog.FileName; + TempCodex.Sources.Path = openFileDialog.FileName; } } @@ -96,7 +96,7 @@ private void BrowseURL() public RelayCommand BrowseISBNCommand => _browseISBNCommand ??= new(BrowseISBN); private void BrowseISBN() { - string url = $"https://openlibrary.org/search?q={TempCodex.ISBN}&mode=everything"; + string url = $"https://openlibrary.org/search?q={TempCodex.Sources.ISBN}&mode=everything"; Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); } diff --git a/src/ViewModels/CodexViewModel.cs b/src/ViewModels/CodexViewModel.cs index 737b235..57038f3 100644 --- a/src/ViewModels/CodexViewModel.cs +++ b/src/ViewModels/CodexViewModel.cs @@ -4,6 +4,7 @@ using COMPASS.Models; using COMPASS.Models.CodexProperties; using COMPASS.Models.Enums; +using COMPASS.Models.XmlDtos; using COMPASS.Services; using COMPASS.Tools; using COMPASS.ViewModels.Sources; @@ -45,18 +46,18 @@ public static bool OpenCodex(Codex codex) public static bool OpenCodexLocally(Codex? toOpen) { if (toOpen is null) return false; - if (!toOpen.HasOfflineSource()) return false; + if (!toOpen.Sources.HasOfflineSource()) return false; try { - Process.Start(new ProcessStartInfo(toOpen.Path) { UseShellExecute = true }); + Process.Start(new ProcessStartInfo(toOpen.Sources.Path) { UseShellExecute = true }); toOpen.LastOpened = DateTime.Now; toOpen.OpenedCount++; - Logger.Info($"Opened {toOpen.Path}"); + Logger.Info($"Opened {toOpen.Sources.Path}"); return true; } catch (Exception ex) { - Logger.Warn($"Failed to open {toOpen.Path}", ex); + Logger.Warn($"Failed to open {toOpen.Sources.Path}", ex); FileNotFoundWindow fileNotFoundWindow = new(new(toOpen)) { @@ -69,7 +70,7 @@ public static bool CanOpenCodexLocally(Codex? toOpen) { if (toOpen == null) return false; - return toOpen.HasOfflineSource(); + return toOpen.Sources.HasOfflineSource(); } //Open codex Online @@ -80,15 +81,15 @@ public static bool OpenCodexOnline(Codex? toOpen) if (!CanOpenCodexOnline(toOpen)) return false; try { - Process.Start(new ProcessStartInfo(toOpen!.SourceURL) { UseShellExecute = true }); + Process.Start(new ProcessStartInfo(toOpen!.Sources.SourceURL) { UseShellExecute = true }); toOpen.LastOpened = DateTime.Now; toOpen.OpenedCount++; - Logger.Info($"Opened {toOpen.SourceURL}"); + Logger.Info($"Opened {toOpen.Sources.SourceURL}"); return true; } catch (Exception ex) { - Logger.Error($"Failed to open {toOpen!.SourceURL}", ex); + Logger.Error($"Failed to open {toOpen!.Sources.SourceURL}", ex); //fails if no internet, pinging 8.8.8.8 DNS instead of server because some sites like gm binder block ping if (!IOService.PingURL()) Logger.Warn($"Cannot open this item online when not connected to the internet", ex); return false; @@ -99,7 +100,7 @@ public static bool CanOpenCodexOnline(Codex? toOpen) { if (toOpen is null) return false; - return toOpen.HasOnlineSource(); + return toOpen.Sources.HasOnlineSource(); } //Open Multiple Files @@ -209,8 +210,8 @@ private static void FavoriteCodices(IList? toFavorite) public RelayCommand ShowInExplorerCommand => _showInExplorerCommand ??= new(ShowInExplorer, CanOpenCodexLocally); public static void ShowInExplorer(Codex? toShow) { - if (String.IsNullOrEmpty(toShow?.Path) || !File.Exists(toShow.Path)) return; - string? folderPath = Path.GetDirectoryName(toShow.Path); + if (String.IsNullOrEmpty(toShow?.Sources.Path) || !File.Exists(toShow.Sources.Path)) return; + string? folderPath = Path.GetDirectoryName(toShow.Sources.Path); if (String.IsNullOrEmpty(folderPath) || !Directory.Exists(folderPath)) return; IOService.ShowInExplorer(folderPath); } @@ -385,24 +386,14 @@ public static async Task StartGetMetaDataProcess(IList codices) private static async Task GetMetaData(Codex codex, ChooseMetaDataViewModel chooseMetaDataVM) { // Lazy load metadata from all the sources, use dict to store - Dictionary metaDataFromSource = new(); - - //Make Codex with only sources which can be filled with new data - Codex metaDatalessCodex = new() - { - Path = codex.Path, - SourceURL = codex.SourceURL, - ISBN = codex.ISBN - }; + Dictionary metaDataFromSource = new(); //First try to get sources from other sources //Pdf can contain ISBN number PdfSourceViewModel pdfSourceVM = new(); - if (pdfSourceVM.IsValidSource(codex) && String.IsNullOrEmpty(codex.ISBN)) + if (pdfSourceVM.IsValidSource(codex.Sources) && String.IsNullOrEmpty(codex.Sources.ISBN)) { - var pdfData = await pdfSourceVM.GetMetaData(metaDatalessCodex); - codex.ISBN = pdfData.ISBN; - metaDatalessCodex.ISBN = pdfData.ISBN; + CodexDto pdfData = await pdfSourceVM.GetMetaData(codex.Sources); //already store this so pdf doesn't need to be opened twice metaDataFromSource.Add(MetaDataSource.PDF, pdfData); @@ -411,7 +402,7 @@ private static async Task GetMetaData(Codex codex, ChooseMetaDataViewModel choos // Now use bits and pieces of the Codices in MetaDataFromSource to set the actual metadata based on preferences //Codex with metadata that will be shown to the user, and asked if they want to use it - Codex toAsk = new(); + CodexDto toAsk = new(); bool shouldAsk = false; //Iterate over all the properties and set them @@ -422,8 +413,8 @@ private static async Task GetMetaData(Codex codex, ChooseMetaDataViewModel choos if (prop.OverwriteMode == MetaDataOverwriteMode.IfEmpty && !prop.IsEmpty(codex)) continue; if (prop is CoverArtProperty) continue; //Covers are done separately - //propHolder will hold the property from the top preferred source - Codex propHolder = new(); + //preferedMetadata will hold the metadata from the top preferred source + CodexDto preferedMetadata = new(); //iterate over the sources in reverse because overwriting causes the last ones to remain foreach (var source in prop.SourcePriority.AsEnumerable().Reverse()) @@ -431,40 +422,40 @@ private static async Task GetMetaData(Codex codex, ChooseMetaDataViewModel choos ProgressViewModel.GlobalCancellationTokenSource.Token.ThrowIfCancellationRequested(); // Check if there is metadata from this source to use - if (!metaDataFromSource.TryGetValue(source, out Codex? value)) + if (!metaDataFromSource.TryGetValue(source, out CodexDto? metadata)) { SourceViewModel? sourceVM = SourceViewModel.GetSourceVM(source); if (sourceVM is null) continue; - if (!sourceVM.IsValidSource(codex)) continue; - var metaDataHolder = await sourceVM.GetMetaData(metaDatalessCodex); - value = metaDataHolder; - metaDataFromSource.Add(source, value); + if (!sourceVM.IsValidSource(codex.Sources)) continue; + metadata = await sourceVM.GetMetaData(codex.Sources); + metaDataFromSource.Add(source, metadata); } // Set the prop Data from this source in propHolder // if the new value is not null/default/empty - if (!prop.IsEmpty(value)) + if (!prop.IsEmpty(metadata)) { - prop.SetProp(propHolder, value); + prop.SetProp(preferedMetadata, metadata); } } //if no value was found for this prop, do nothing - if (prop.IsEmpty(propHolder)) continue; + if (prop.IsEmpty(preferedMetadata)) continue; if (prop.OverwriteMode == MetaDataOverwriteMode.Always || prop.IsEmpty(codex)) { - prop.SetProp(codex, propHolder); + prop.SetProp(codex, preferedMetadata); } - else if (prop.OverwriteMode == MetaDataOverwriteMode.Ask && prop.HasNewValue(propHolder, codex)) + else if (prop.OverwriteMode == MetaDataOverwriteMode.Ask && prop.HasNewValue(preferedMetadata, codex)) { - prop.SetProp(toAsk, propHolder); + prop.SetProp(toAsk, preferedMetadata); shouldAsk = true; //set shouldAsk to true when we found at lease one none empty prop that should be asked } } if (shouldAsk) { - chooseMetaDataVM.AddCodexPair(codex, toAsk); + var allTags = MainViewModel.CollectionVM.CurrentCollection.AllTags; + chooseMetaDataVM.AddCodexPair(codex, toAsk.ToModel(allTags)); } ProgressViewModel.GetInstance().IncrementCounter(); diff --git a/src/ViewModels/CollectionContentSelectorViewModel.cs b/src/ViewModels/CollectionContentSelectorViewModel.cs index bf32957..774e3cd 100644 --- a/src/ViewModels/CollectionContentSelectorViewModel.cs +++ b/src/ViewModels/CollectionContentSelectorViewModel.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using System.Threading.Tasks; using System.Windows.Data; namespace COMPASS.ViewModels @@ -179,7 +178,7 @@ public SelectableFolderTagLink(string path, Tag t, IEnumerable existingTags public class SelectableCodex : SelectableWithPathHelper { private CollectionContentSelectorViewModel _vm; - public SelectableCodex(Codex codex, CollectionContentSelectorViewModel vm) : base(codex.Path) + public SelectableCodex(Codex codex, CollectionContentSelectorViewModel vm) : base(codex.Sources.Path) { Codex = codex; _vm = vm; diff --git a/src/ViewModels/ExportCollectionViewModel.cs b/src/ViewModels/ExportCollectionViewModel.cs index dc38627..a658bc1 100644 --- a/src/ViewModels/ExportCollectionViewModel.cs +++ b/src/ViewModels/ExportCollectionViewModel.cs @@ -134,25 +134,25 @@ public async Task ExportToFile(string targetPath) //Change Codex Path to relative and add those files if the options is set var itemsWithOfflineSource = ContentSelectorVM.CuratedCollection.AllCodices - .Where(codex => codex.HasOfflineSource()) + .Where(codex => codex.Sources.HasOfflineSource()) .ToList(); - string commonFolder = IOService.GetCommonFolder(itemsWithOfflineSource.Select(codex => codex.Path).ToList()); + string commonFolder = IOService.GetCommonFolder(itemsWithOfflineSource.Select(codex => codex.Sources.Path).ToList()); foreach (Codex codex in itemsWithOfflineSource) { - string relativePath = codex.Path[commonFolder.Length..].TrimStart(Path.DirectorySeparatorChar); + string relativePath = codex.Sources.Path[commonFolder.Length..].TrimStart(Path.DirectorySeparatorChar); //Add the file - if (IncludeFiles && File.Exists(codex.Path)) + if (IncludeFiles && File.Exists(codex.Sources.Path)) { - archive.AddEntry(Path.Combine("Files", relativePath), codex.Path); + archive.AddEntry(Path.Combine("Files", relativePath), codex.Sources.Path); //keep the relative path, will be used during import to link the included files - codex.Path = relativePath; + codex.Sources.Path = relativePath; } //absolute path is user specific, so counts as personal data if (ContentSelectorVM.RemovePersonalData) { - codex.Path = relativePath; + codex.Sources.Path = relativePath; } //Add cover art diff --git a/src/ViewModels/FileNotFoundViewModel.cs b/src/ViewModels/FileNotFoundViewModel.cs index edf663a..86f1867 100644 --- a/src/ViewModels/FileNotFoundViewModel.cs +++ b/src/ViewModels/FileNotFoundViewModel.cs @@ -32,24 +32,24 @@ public void FindFile() if (openFileDialog.ShowDialog() == true) { //find the replaced parh of the path - string oldPath = _codex.Path; + string oldPath = _codex.Sources.Path; string newPath = openFileDialog.FileName; var (toReplace, replaceWith) = IOService.GetDifferingRoot(oldPath, newPath); //fix the path of this codex - _codex.Path = openFileDialog.FileName; + _codex.Sources.Path = openFileDialog.FileName; int fixedRefs = 1; //try to fix the path of all codices var codicesWithBrokenPaths = MainViewModel.CollectionVM.CurrentCollection.AllCodices - .Where(c => c.HasOfflineSource() && !File.Exists(c.Path)) + .Where(c => c.Sources.HasOfflineSource() && !File.Exists(c.Sources.Path)) .ToList(); foreach (var c in codicesWithBrokenPaths) { - string possiblePath = Path.Combine(replaceWith, c.Path[toReplace.Length..]); + string possiblePath = Path.Combine(replaceWith, c.Sources.Path[toReplace.Length..]); if (File.Exists(possiblePath)) { - c.Path = possiblePath; + c.Sources.Path = possiblePath; fixedRefs++; } } @@ -69,7 +69,7 @@ public void FindFile() public RelayCommand RemovePathCommand => _removePathCommand ??= new(RemovePath); private void RemovePath() { - _codex.Path = ""; + _codex.Sources.Path = ""; CloseAction?.Invoke(); } diff --git a/src/ViewModels/FilterViewModel.cs b/src/ViewModels/FilterViewModel.cs index f50836e..5c5ba85 100644 --- a/src/ViewModels/FilterViewModel.cs +++ b/src/ViewModels/FilterViewModel.cs @@ -281,14 +281,14 @@ public void PopulateMetaDataCollections() => App.SafeDispatcher.Invoke(() => if (!String.IsNullOrEmpty(c.Publisher)) PublisherList.AddIfMissing(c.Publisher); //Populate FileType Collection - if (!String.IsNullOrEmpty(c.FileType)) FileTypeList.AddIfMissing(c.FileType); + if (!String.IsNullOrEmpty(c.Sources.FileType)) FileTypeList.AddIfMissing(c.Sources.FileType); //Populate Domain Collection - if (c.HasOnlineSource()) + if (c.Sources.HasOnlineSource()) { - string domain = Uri.IsWellFormedUriString(c.SourceURL, UriKind.Absolute) ? - new Uri(c.SourceURL).Host : - c.SourceURL; + string domain = Uri.IsWellFormedUriString(c.Sources.SourceURL, UriKind.Absolute) ? + new Uri(c.Sources.SourceURL).Host : + c.Sources.SourceURL; if (!string.IsNullOrEmpty(domain)) DomainList.AddIfMissing(domain); } } diff --git a/src/ViewModels/Import/ImportCollectionViewModel.cs b/src/ViewModels/Import/ImportCollectionViewModel.cs index a0e30bc..b402f2f 100644 --- a/src/ViewModels/Import/ImportCollectionViewModel.cs +++ b/src/ViewModels/Import/ImportCollectionViewModel.cs @@ -26,12 +26,12 @@ public ImportCollectionViewModel(CodexCollection collectionToImport) //if files were included in compass file, set paths of codices to those files if (Directory.Exists(CollectionToImport.UserFilesPath)) { - foreach (Codex codex in CollectionToImport.AllCodices.Where(c => c.HasOfflineSource())) + foreach (Codex codex in CollectionToImport.AllCodices.Where(c => c.Sources.HasOfflineSource())) { - string includedFilePath = Path.Combine(CollectionToImport.UserFilesPath, codex.Path); //TODO, what it this path becomes too long? + string includedFilePath = Path.Combine(CollectionToImport.UserFilesPath, codex.Sources.Path); //TODO, what it this path becomes too long? if (File.Exists(includedFilePath)) { - codex.Path = includedFilePath; + codex.Sources.Path = includedFilePath; } } } diff --git a/src/ViewModels/Import/ImportURLViewModel.cs b/src/ViewModels/Import/ImportURLViewModel.cs index 370449a..1e136e0 100644 --- a/src/ViewModels/Import/ImportURLViewModel.cs +++ b/src/ViewModels/Import/ImportURLViewModel.cs @@ -132,11 +132,11 @@ public async Task ImportURLAsync() Codex newCodex = new(MainViewModel.CollectionVM.CurrentCollection); if (_importSource == ImportSource.ISBN) { - newCodex.ISBN = InputURL; + newCodex.Sources.ISBN = InputURL; } else { - newCodex.SourceURL = InputURL; + newCodex.Sources.SourceURL = InputURL; } MainViewModel.CollectionVM.CurrentCollection.AllCodices.Add(newCodex); progressVM.IncrementCounter(); diff --git a/src/ViewModels/Import/ImportViewModel.cs b/src/ViewModels/Import/ImportViewModel.cs index f0cfc06..e587733 100644 --- a/src/ViewModels/Import/ImportViewModel.cs +++ b/src/ViewModels/Import/ImportViewModel.cs @@ -72,7 +72,7 @@ public static async Task ImportFilesAsync(List paths, CodexCollection? t targetCollection ??= MainViewModel.CollectionVM.CurrentCollection; //filter out codices already in collection & banned paths - IEnumerable existingPaths = targetCollection.AllCodices.Select(codex => codex.Path); + IEnumerable existingPaths = targetCollection.AllCodices.Select(codex => codex.Sources.Path); paths = paths .Except(existingPaths) .Except(targetCollection.Info.BanishedPaths) @@ -93,7 +93,8 @@ public static async Task ImportFilesAsync(List paths, CodexCollection? t { ProgressViewModel.GlobalCancellationTokenSource.Token.ThrowIfCancellationRequested(); - Codex newCodex = new(targetCollection) { Path = path }; + Codex newCodex = new(targetCollection); + newCodex.Sources.Path = path; newCodices.Add(newCodex); targetCollection.AllCodices.Add(newCodex); diff --git a/src/ViewModels/SettingsViewModel.cs b/src/ViewModels/SettingsViewModel.cs index 55b7e07..58a0bb8 100644 --- a/src/ViewModels/SettingsViewModel.cs +++ b/src/ViewModels/SettingsViewModel.cs @@ -260,8 +260,8 @@ private void AddFolderTagPair(FolderTagPair? pair) private void DetectFolderTagPairs() { var splitFolders = MainViewModel.CollectionVM.CurrentCollection.AllCodices - .Where(codex => codex.HasOfflineSource()) - .Select(codex => codex.Path) + .Where(codex => codex.Sources.HasOfflineSource()) + .Select(codex => codex.Sources.Path) .SelectMany(path => path.Split("\\")) .ToHashSet() .Select(folder => @"\" + folder + @"\"); @@ -269,8 +269,8 @@ private void DetectFolderTagPairs() foreach (string folder in splitFolders) { var codicesInFolder = MainViewModel.CollectionVM.CurrentCollection.AllCodices - .Where(codex => codex.HasOfflineSource()) - .Where(codex => codex.Path.Contains(folder)) + .Where(codex => codex.Sources.HasOfflineSource()) + .Where(codex => codex.Sources.Path.Contains(folder)) .ToList(); if (codicesInFolder.Count < 3) continue; //Require at least 3 codices in same folder before we can speak of a pattern @@ -319,8 +319,8 @@ public bool AutoLinkFolderTagSameName #region Fix Broken refs public IEnumerable BrokenCodices => MainViewModel.CollectionVM.CurrentCollection.AllCodices - .Where(codex => codex.HasOfflineSource()) //do this check so message doesn't count codices that never had a path to begin with - .Where(codex => !Path.Exists(codex.Path)); + .Where(codex => codex.Sources.HasOfflineSource()) //do this check so message doesn't count codices that never had a path to begin with + .Where(codex => !Path.Exists(codex.Sources.Path)); public int BrokenCodicesAmount => BrokenCodices.Count(); public string BrokenCodicesMessage => $"Broken references detected: {BrokenCodicesAmount}."; @@ -370,13 +370,13 @@ private void RenameFolderReferences(string? oldpath, string? newpath) AmountRenamed = 0; foreach (Codex codex in MainViewModel.CollectionVM.CurrentCollection.AllCodices) { - if (codex.HasOfflineSource() && codex.Path.Contains(oldpath)) + if (codex.Sources.HasOfflineSource() && codex.Sources.Path.Contains(oldpath)) { - string updatedPath = codex.Path.Replace(oldpath, newpath); + string updatedPath = codex.Sources.Path.Replace(oldpath, newpath); //only replace path if old one was broken and new one exists - if (!File.Exists(codex.Path) && File.Exists(updatedPath)) + if (!File.Exists(codex.Sources.Path) && File.Exists(updatedPath)) { - codex.Path = updatedPath; + codex.Sources.Path = updatedPath; AmountRenamed++; } } @@ -391,7 +391,7 @@ public void RemoveBrokenReferences() { foreach (Codex codex in BrokenCodices) { - codex.Path = ""; + codex.Sources.Path = ""; } BrokenCodicesChanged(); MainViewModel.CollectionVM.CurrentCollection.SaveCodices(); diff --git a/src/ViewModels/Sources/DndBeyondSourceViewModel.cs b/src/ViewModels/Sources/DndBeyondSourceViewModel.cs index 1e6cf12..22cb9f7 100644 --- a/src/ViewModels/Sources/DndBeyondSourceViewModel.cs +++ b/src/ViewModels/Sources/DndBeyondSourceViewModel.cs @@ -1,5 +1,6 @@ using COMPASS.Models; using COMPASS.Models.Enums; +using COMPASS.Models.XmlDtos; using COMPASS.Services; using COMPASS.Tools; using HtmlAgilityPack; @@ -12,35 +13,30 @@ public class DndBeyondSourceViewModel : SourceViewModel { public override MetaDataSource Source => MetaDataSource.DnDBeyond; - public override async Task GetMetaData(Codex codex) + public override async Task GetMetaData(SourceSet sources) { - // Work on a copy - codex = new Codex(codex); - //Scrape metadata by going to store page, get to store page by using that /credits redirects there ProgressVM.AddLogEntry(new(Severity.Info, $"Connecting to DnD Beyond")); - HtmlDocument? doc = await IOService.ScrapeSite(String.Concat(codex.SourceURL, "/credits")); + HtmlDocument? doc = await IOService.ScrapeSite(String.Concat(sources.SourceURL, "/credits")); HtmlNode? src = doc?.DocumentNode; - if (src is null) - { - return codex; - } - //Set known metadata - codex.Publisher = "D&D Beyond"; - codex.Authors = new() { "Wizards of the Coast" }; + CodexDto codex = new() + { + Publisher = "D&D Beyond", + Authors = new() { "Wizards of the Coast" } + }; return codex; } public override async Task FetchCover(Codex codex) { - if (String.IsNullOrEmpty(codex.SourceURL)) { return false; } + if (String.IsNullOrEmpty(codex.Sources.SourceURL)) { return false; } try { //cover art is on store page, redirect there by going to /credits which every book has - HtmlDocument? doc = await IOService.ScrapeSite(String.Concat(codex.SourceURL, "/credits")); + HtmlDocument? doc = await IOService.ScrapeSite(String.Concat(codex.Sources.SourceURL, "/credits")); HtmlNode? src = doc?.DocumentNode; if (src is null) return false; @@ -57,6 +53,6 @@ public override async Task FetchCover(Codex codex) } } - public override bool IsValidSource(Codex codex) => false; + public override bool IsValidSource(SourceSet sources) => false; } } diff --git a/src/ViewModels/Sources/FileSourceViewModel.cs b/src/ViewModels/Sources/FileSourceViewModel.cs index 07f24d6..9344a20 100644 --- a/src/ViewModels/Sources/FileSourceViewModel.cs +++ b/src/ViewModels/Sources/FileSourceViewModel.cs @@ -1,4 +1,5 @@ using COMPASS.Models; +using COMPASS.Models.XmlDtos; using COMPASS.Services; using COMPASS.Tools; using FuzzySharp; @@ -20,23 +21,23 @@ public FileSourceViewModel() public override MetaDataSource Source => MetaDataSource.File; public override Task FetchCover(Codex codex) => throw new System.NotImplementedException(); - public override bool IsValidSource(Codex codex) => codex.HasOfflineSource(); + public override bool IsValidSource(SourceSet sources) => sources.HasOfflineSource(); - public override Task GetMetaData(Codex codex) + public override Task GetMetaData(SourceSet sources) { - // Work on a copy - codex = new Codex(codex); + // Use a codex dto to tranfer the data + CodexDto codex = new(); // Title - codex.Title = Path.GetFileNameWithoutExtension(codex.Path); + codex.Title = Path.GetFileNameWithoutExtension(sources.Path); // Tags based on file path foreach (var folderTagPair in TargetCollection.Info.FolderTagPairs) { - Debug.Assert(IsValidSource(codex), "Codex without path was referenced in file source"); - if (codex.Path!.Contains(folderTagPair.Folder)) + Debug.Assert(IsValidSource(sources), "Codex without path was referenced in file source"); + if (sources.Path!.Contains(folderTagPair.Folder)) { - App.SafeDispatcher.Invoke(() => codex.Tags.AddIfMissing(folderTagPair.Tag!)); + codex.TagIDs.AddIfMissing(folderTagPair.Tag!.ID); } } @@ -44,10 +45,10 @@ public override Task GetMetaData(Codex codex) { foreach (Tag tag in MainViewModel.CollectionVM.CurrentCollection.AllTags) { - var splitFolders = codex.Path!.Split("\\"); + var splitFolders = sources.Path!.Split("\\"); if (splitFolders.Any(folder => Fuzz.Ratio(folder.ToLowerInvariant(), tag.Content.ToLowerInvariant()) > 90)) { - App.SafeDispatcher.Invoke(() => codex.Tags.AddIfMissing(tag)); + codex.TagIDs.AddIfMissing(tag.ID); } } } diff --git a/src/ViewModels/Sources/GenericOnlineSourceViewModel.cs b/src/ViewModels/Sources/GenericOnlineSourceViewModel.cs index 2c3413b..b9e84a1 100644 --- a/src/ViewModels/Sources/GenericOnlineSourceViewModel.cs +++ b/src/ViewModels/Sources/GenericOnlineSourceViewModel.cs @@ -1,5 +1,6 @@ using COMPASS.Models; using COMPASS.Models.Enums; +using COMPASS.Models.XmlDtos; using COMPASS.Services; using COMPASS.Tools; using HtmlAgilityPack; @@ -14,26 +15,26 @@ public class GenericOnlineSourceViewModel : SourceViewModel public override MetaDataSource Source => MetaDataSource.GenericURL; public override Task FetchCover(Codex codex) => throw new NotImplementedException(); - public override bool IsValidSource(Codex codex) => codex.HasOnlineSource(); + public override bool IsValidSource(SourceSet sources) => sources.HasOnlineSource(); - public override async Task GetMetaData(Codex codex) + public override async Task GetMetaData(SourceSet sources) { - // Work on a copy - codex = new Codex(codex); - ProgressVM.AddLogEntry(new(Severity.Info, $"Extracting metadata from website header")); // Scrape metadata - Debug.Assert(IsValidSource(codex), "Codex without URL was used in Generic URL source"); - HtmlDocument? doc = await IOService.ScrapeSite(codex.SourceURL); + Debug.Assert(IsValidSource(sources), "Codex without URL was used in Generic URL source"); + HtmlDocument? doc = await IOService.ScrapeSite(sources.SourceURL); HtmlNode? src = doc?.DocumentNode; if (src is null) { - ProgressVM.AddLogEntry(new(Severity.Error, $"Could not reach {codex.SourceURL}")); - return codex; + ProgressVM.AddLogEntry(new(Severity.Error, $"Could not reach {sources.SourceURL}")); + return new(); } + // Use a codex dto to tranfer the data + CodexDto codex = new(); + // Title codex.Title = src.SelectSingleNode("//meta[@property='og:title']")?.GetAttributeValue("content", null) ?? codex.Title; @@ -52,7 +53,7 @@ public override async Task GetMetaData(Codex codex) { if (codex.SourceURL.Contains(folderTagPair.Folder)) { - codex.Tags.AddIfMissing(folderTagPair.Tag!); + codex.TagIDs.AddIfMissing(folderTagPair.Tag!.ID); } } return codex; diff --git a/src/ViewModels/Sources/GmBinderSourceViewModel.cs b/src/ViewModels/Sources/GmBinderSourceViewModel.cs index 5b502f0..4dcf86c 100644 --- a/src/ViewModels/Sources/GmBinderSourceViewModel.cs +++ b/src/ViewModels/Sources/GmBinderSourceViewModel.cs @@ -1,5 +1,6 @@ using COMPASS.Models; using COMPASS.Models.Enums; +using COMPASS.Models.XmlDtos; using COMPASS.Services; using COMPASS.Tools; using COMPASS.ViewModels.Import; @@ -17,27 +18,27 @@ public class GmBinderSourceViewModel : SourceViewModel { public override MetaDataSource Source => MetaDataSource.GmBinder; - public override bool IsValidSource(Codex codex) => - codex.HasOnlineSource() && codex.SourceURL.Contains(new ImportURLViewModel(ImportSource.GmBinder).ExampleURL); + public override bool IsValidSource(SourceSet sources) => + sources.HasOnlineSource() && sources.SourceURL.Contains(new ImportURLViewModel(ImportSource.GmBinder).ExampleURL); - public override async Task GetMetaData(Codex codex) + public override async Task GetMetaData(SourceSet sources) { - // Work on a copy - codex = new Codex(codex); - ProgressVM.AddLogEntry(new(Severity.Info, $"Downloading metadata from GM Binder")); - Debug.Assert(IsValidSource(codex), "Invalid Codex was used in GM Binder source"); - HtmlDocument? doc = await IOService.ScrapeSite(codex.SourceURL); + Debug.Assert(IsValidSource(sources), "Invalid Codex was used in GM Binder source"); + HtmlDocument? doc = await IOService.ScrapeSite(sources.SourceURL); HtmlNode? src = doc?.DocumentNode; if (doc is null || src is null) { - ProgressVM.AddLogEntry(new(Severity.Error, $"Could not reach {codex.SourceURL}")); - return codex; + ProgressVM.AddLogEntry(new(Severity.Error, $"Could not reach {sources.SourceURL}")); + return new(); } - //Set known metadata - codex.Publisher = "GM Binder"; + // Use a codex dto to tranfer the data + CodexDto codex = new() + { + Publisher = "GM Binder" + }; //get pagecount HtmlNode? previewDiv = doc.GetElementbyId("preview"); @@ -49,12 +50,12 @@ public override async Task GetMetaData(Codex codex) public override async Task FetchCover(Codex codex) { - if (String.IsNullOrEmpty(codex.SourceURL)) { return false; } - ProgressVM.AddLogEntry(new(Severity.Info, $"Downloading cover from {codex.SourceURL}")); + if (String.IsNullOrEmpty(codex.Sources.SourceURL)) { return false; } + ProgressVM.AddLogEntry(new(Severity.Info, $"Downloading cover from {codex.Sources.SourceURL}")); OpenQA.Selenium.WebDriver driver = await WebDriverService.GetWebDriver(); try { - await Task.Run(() => driver.Navigate().GoToUrl(codex.SourceURL)); + await Task.Run(() => driver.Navigate().GoToUrl(codex.Sources.SourceURL)); var coverPage = driver.FindElement(OpenQA.Selenium.By.Id("p1")); //screenshot and download the image IMagickImage image = CoverService.GetCroppedScreenShot(driver, coverPage); @@ -63,7 +64,7 @@ public override async Task FetchCover(Codex codex) } catch (Exception ex) { - string msg = $"Failed to get cover from {codex.SourceURL}"; + string msg = $"Failed to get cover from {codex.Sources.SourceURL}"; Logger.Error(msg, ex); ProgressVM.AddLogEntry(new(Severity.Error, msg)); return false; diff --git a/src/ViewModels/Sources/GoogleDriveSourceViewModel.cs b/src/ViewModels/Sources/GoogleDriveSourceViewModel.cs index 4eb9ab3..fc14b09 100644 --- a/src/ViewModels/Sources/GoogleDriveSourceViewModel.cs +++ b/src/ViewModels/Sources/GoogleDriveSourceViewModel.cs @@ -1,5 +1,6 @@ using COMPASS.Models; using COMPASS.Models.Enums; +using COMPASS.Models.XmlDtos; using COMPASS.Services; using COMPASS.Tools; using COMPASS.ViewModels.Import; @@ -13,27 +14,30 @@ namespace COMPASS.ViewModels.Sources public class GoogleDriveSourceViewModel : SourceViewModel { public override MetaDataSource Source => MetaDataSource.GoogleDrive; - public override bool IsValidSource(Codex codex) => - codex.HasOnlineSource() && codex.SourceURL.Contains(new ImportURLViewModel(ImportSource.GoogleDrive).ExampleURL); + public override bool IsValidSource(SourceSet soures) => + soures.HasOnlineSource() && soures.SourceURL.Contains(new ImportURLViewModel(ImportSource.GoogleDrive).ExampleURL); - public override Task GetMetaData(Codex codex) + public override Task GetMetaData(SourceSet sources) { - // Work on a copy - codex = new Codex(codex); - Debug.Assert(IsValidSource(codex), "Invalid Codex was used in Google drive source"); - codex.Publisher = "Google Drive"; + Debug.Assert(IsValidSource(sources), "Invalid Codex was used in Google drive source"); + + // Use a codex dto to tranfer the data + CodexDto codex = new() + { + Publisher = "Google Drive" + }; return Task.FromResult(codex); } public override async Task FetchCover(Codex codex) { - if (String.IsNullOrEmpty(codex.SourceURL)) { return false; } + if (String.IsNullOrEmpty(codex.Sources.SourceURL)) { return false; } ProgressVM.AddLogEntry(new(Severity.Info, $"Downloading cover from Google Drive")); try { //cover art is on store page, redirect there by going to /credits which every book has - HtmlDocument? doc = await IOService.ScrapeSite(codex.SourceURL); + HtmlDocument? doc = await IOService.ScrapeSite(codex.Sources.SourceURL); HtmlNode? src = doc?.DocumentNode; if (src is null) return false; @@ -45,7 +49,7 @@ public override async Task FetchCover(Codex codex) } catch (Exception ex) { - string msg = $"Failed to get cover from {codex.SourceURL}"; + string msg = $"Failed to get cover from {codex.Sources.SourceURL}"; Logger.Error(msg, ex); ProgressVM.AddLogEntry(new(Severity.Error, msg)); return false; diff --git a/src/ViewModels/Sources/HomebrewerySourceViewModel.cs b/src/ViewModels/Sources/HomebrewerySourceViewModel.cs index 16ddb60..8830c3d 100644 --- a/src/ViewModels/Sources/HomebrewerySourceViewModel.cs +++ b/src/ViewModels/Sources/HomebrewerySourceViewModel.cs @@ -1,5 +1,6 @@ using COMPASS.Models; using COMPASS.Models.Enums; +using COMPASS.Models.XmlDtos; using COMPASS.Services; using COMPASS.Tools; using COMPASS.ViewModels.Import; @@ -22,27 +23,27 @@ namespace COMPASS.ViewModels.Sources public class HomebrewerySourceViewModel : SourceViewModel { public override MetaDataSource Source => MetaDataSource.Homebrewery; - public override bool IsValidSource(Codex codex) - => codex.HasOnlineSource() && codex.SourceURL.Contains(new ImportURLViewModel(ImportSource.Homebrewery).ExampleURL); + public override bool IsValidSource(SourceSet sources) + => sources.HasOnlineSource() && sources.SourceURL.Contains(new ImportURLViewModel(ImportSource.Homebrewery).ExampleURL); - public override async Task GetMetaData(Codex codex) + public override async Task GetMetaData(SourceSet sources) { - // Work on a copy - codex = new Codex(codex); - ProgressVM.AddLogEntry(new(Severity.Info, $"Downloading metadata from Homebrewery")); - Debug.Assert(IsValidSource(codex), "Invalid Codex was used in Homebrewery source"); - HtmlDocument? doc = await IOService.ScrapeSite(codex.SourceURL); + Debug.Assert(IsValidSource(sources), "Invalid Codex was used in Homebrewery source"); + HtmlDocument? doc = await IOService.ScrapeSite(sources.SourceURL); HtmlNode? src = doc?.DocumentNode; if (src is null) { - ProgressVM.AddLogEntry(new(Severity.Error, $"Could not reach {codex.SourceURL}")); - return codex; + ProgressVM.AddLogEntry(new(Severity.Error, $"Could not reach {sources.SourceURL}")); + return new(); } - //Set known metadata - codex.Publisher = "Homebrewery"; + // Use a codex dto to tranfer the data + CodexDto codex = new() + { + Publisher = "Homebrewery" + }; //Scrape metadata //Select script tag with all metadata in JSON format @@ -68,13 +69,13 @@ public override async Task GetMetaData(Codex codex) public override async Task FetchCover(Codex codex) { - if (String.IsNullOrEmpty(codex.SourceURL)) { return false; } + if (String.IsNullOrEmpty(codex.Sources.SourceURL)) { return false; } ProgressVM.AddLogEntry(new(Severity.Info, $"Downloading cover from Homebrewery")); WebDriver driver = await WebDriverService.GetWebDriver(); WebDriverWait wait = new(driver, TimeSpan.FromSeconds(5)); try { - string url = codex.SourceURL; + string url = codex.Sources.SourceURL; var frameSelector = By.Id("BrewRenderer"); var pageSelector = By.Id("p1"); @@ -110,7 +111,7 @@ await Task.Run(() => } catch (Exception ex) { - string msg = $"Failed to get cover from {codex.SourceURL}"; + string msg = $"Failed to get cover from {codex.Sources.SourceURL}"; Logger.Error(msg, ex); ProgressVM.AddLogEntry(new(Severity.Error, msg)); return false; @@ -120,6 +121,5 @@ await Task.Run(() => driver.Quit(); } } - } } diff --git a/src/ViewModels/Sources/ISBNSourceViewModel.cs b/src/ViewModels/Sources/ISBNSourceViewModel.cs index ee7931c..455658c 100644 --- a/src/ViewModels/Sources/ISBNSourceViewModel.cs +++ b/src/ViewModels/Sources/ISBNSourceViewModel.cs @@ -1,5 +1,6 @@ using COMPASS.Models; using COMPASS.Models.Enums; +using COMPASS.Models.XmlDtos; using COMPASS.Services; using COMPASS.Tools; using Newtonsoft.Json.Linq; @@ -14,27 +15,24 @@ namespace COMPASS.ViewModels.Sources public class ISBNSourceViewModel : SourceViewModel { public override MetaDataSource Source => MetaDataSource.ISBN; - public override bool IsValidSource(Codex codex) => !String.IsNullOrWhiteSpace(codex.ISBN); + public override bool IsValidSource(SourceSet sources) => !String.IsNullOrWhiteSpace(sources.ISBN); - public override async Task GetMetaData(Codex codex) + public override async Task GetMetaData(SourceSet sources) { - // Work on a copy - codex = new Codex(codex); - ProgressVM.AddLogEntry(new(Severity.Info, $"Downloading Metadata from openlibrary.org")); - Debug.Assert(IsValidSource(codex), "Codex without ISBN was used in ISBN Source"); - string uri = $"https://openlibrary.org/api/books?bibkeys=ISBN:{codex.ISBN.Trim('-', ' ')}&format=json&jscmd=details"; + Debug.Assert(IsValidSource(sources), "Codex without ISBN was used in ISBN Source"); + string uri = $"https://openlibrary.org/api/books?bibkeys=ISBN:{sources.ISBN.Trim('-', ' ')}&format=json&jscmd=details"; JObject? metadata = await IOService.GetJsonAsync(uri); if (metadata is null || !metadata.HasValues) { - string message = $"ISBN {codex.ISBN} was not found on openlibrary.org \n" + + string message = $"ISBN {sources.ISBN} was not found on openlibrary.org \n" + $"You can contribute by submitting this book at \n" + $"https://openlibrary.org/books/add"; ProgressVM.AddLogEntry(new(Severity.Warning, message)); - Logger.Warn($"Could not find ISBN {codex.ISBN} on openlibrary.org"); - return codex; + Logger.Warn($"Could not find ISBN {sources.ISBN} on openlibrary.org"); + return new(); } // Start parsing json @@ -42,7 +40,7 @@ public override async Task GetMetaData(Codex codex) if (details is null) { Logger.Warn("Unable to parse metadata from openlibrary"); - return codex; + return new(); } // Title @@ -50,6 +48,9 @@ public override async Task GetMetaData(Codex codex) string title = details.SelectToken("title")?.ToString() ?? ""; string subTitle = details.SelectToken("subtitle")?.ToString() ?? ""; + // Use a codex dto to tranfer the data + CodexDto codex = new(); + if (!String.IsNullOrWhiteSpace(fullTitle)) { codex.Title = fullTitle; @@ -90,20 +91,20 @@ public override async Task GetMetaData(Codex codex) public override async Task FetchCover(Codex codex) { - if (String.IsNullOrEmpty(codex.ISBN)) return false; + if (String.IsNullOrEmpty(codex.Sources.ISBN)) return false; ProgressVM.AddLogEntry(new(Severity.Info, $"Downloading cover from openlibrary.org")); try { - string uri = $"https://openlibrary.org/isbn/{codex.ISBN}.json"; + string uri = $"https://openlibrary.org/isbn/{codex.Sources.ISBN}.json"; JObject? metadata = await IOService.GetJsonAsync(uri); if (metadata == null || !metadata.HasValues) { - string message = $"ISBN {codex.ISBN} was not found on openlibrary.org \n" + + string message = $"ISBN {codex.Sources.ISBN} was not found on openlibrary.org \n" + $"You can contribute by submitting this book at \n" + $"https://openlibrary.org/books/add"; ProgressVM.AddLogEntry(new(Severity.Warning, message)); - Logger.Warn($"Could not find ISBN {codex.ISBN} on openlibrary.org"); + Logger.Warn($"Could not find ISBN {codex.Sources.ISBN} on openlibrary.org"); return false; } @@ -115,7 +116,7 @@ public override async Task FetchCover(Codex codex) } catch (Exception ex) { - string msg = $"Failed to get cover from OpenLibrary for ISBN {codex.ISBN}"; + string msg = $"Failed to get cover from OpenLibrary for ISBN {codex.Sources.ISBN}"; Logger.Error(msg, ex); ProgressVM.AddLogEntry(new(Severity.Warning, msg)); return false; diff --git a/src/ViewModels/Sources/ImageSourceViewModel.cs b/src/ViewModels/Sources/ImageSourceViewModel.cs index 0f0ba2e..0cf4aa4 100644 --- a/src/ViewModels/Sources/ImageSourceViewModel.cs +++ b/src/ViewModels/Sources/ImageSourceViewModel.cs @@ -1,6 +1,6 @@ using COMPASS.Models; +using COMPASS.Models.XmlDtos; using COMPASS.Services; -using COMPASS.Tools; using System.IO; using System.Threading.Tasks; @@ -10,12 +10,12 @@ class ImageSourceViewModel : SourceViewModel { public override MetaDataSource Source => MetaDataSource.Image; - public override bool IsValidSource(Codex codex) => File.Exists(codex.Path) && IOService.IsImageFile(codex.Path); + public override bool IsValidSource(SourceSet sources) => File.Exists(sources.Path) && IOService.IsImageFile(sources.Path); - public override Task GetMetaData(Codex codex) + public override Task GetMetaData(SourceSet sources) { // Work on a copy - codex = new Codex(codex) + CodexDto codex = new() { PageCount = 1 }; @@ -23,6 +23,6 @@ public override Task GetMetaData(Codex codex) return Task.FromResult(codex); } public override async Task FetchCover(Codex codex) => - await Task.Run(() => CoverService.GetCoverFromImage(codex.Path, codex)); + await Task.Run(() => CoverService.GetCoverFromImage(codex.Sources.Path, codex)); } } diff --git a/src/ViewModels/Sources/PdfSourceViewModel.cs b/src/ViewModels/Sources/PdfSourceViewModel.cs index 6f28d4c..1fe91e4 100644 --- a/src/ViewModels/Sources/PdfSourceViewModel.cs +++ b/src/ViewModels/Sources/PdfSourceViewModel.cs @@ -1,5 +1,6 @@ using COMPASS.Models; using COMPASS.Models.Enums; +using COMPASS.Models.XmlDtos; using COMPASS.Services; using COMPASS.Tools; using ImageMagick; @@ -17,20 +18,20 @@ namespace COMPASS.ViewModels.Sources public class PdfSourceViewModel : SourceViewModel { public override MetaDataSource Source => MetaDataSource.PDF; - public override bool IsValidSource(Codex codex) => IOService.IsPDFFile(codex.Path); + public override bool IsValidSource(SourceSet sources) => IOService.IsPDFFile(sources.Path); - public override async Task GetMetaData(Codex codex) + public override async Task GetMetaData(SourceSet sources) { - // Work on a copy - codex = new Codex(codex); - - Debug.Assert(IsValidSource(codex), "Codex without pdf found in pdf source"); + Debug.Assert(IsValidSource(sources), "Codex without pdf found in pdf source"); PdfDocument? pdfDoc = null; + + // Use a codex dto to tranfer the data + CodexDto codex = new(); try { var info = await Task.Run(() => { - PdfReader pdfReader = new(codex.Path); + PdfReader pdfReader = new(sources.Path); pdfDoc = new PdfDocument(pdfReader); return pdfDoc.GetDocumentInfo(); }); @@ -43,7 +44,7 @@ public override async Task GetMetaData(Codex codex) codex.PageCount = pdfDoc!.GetNumberOfPages(); // If it already has an ISBN, no need to check again - if (!String.IsNullOrEmpty(codex.ISBN)) return codex; + if (!String.IsNullOrEmpty(sources.ISBN)) return codex; //Search for ISBN number in first 5 pages for (int page = 1; page <= Math.Min(5, pdfDoc.GetNumberOfPages()); page++) @@ -56,7 +57,7 @@ public override async Task GetMetaData(Codex codex) string isbn = Constants.RegexISBN().Match(pageContent).Value; if (!String.IsNullOrEmpty(isbn)) { - codex.ISBN = isbn; + sources.ISBN = isbn; break; } } @@ -66,7 +67,7 @@ public override async Task GetMetaData(Codex codex) { //in case pdf is corrupt: PdfReader will throw error //in those cases: import the pdf without opening it - Logger.Error($"Failed to read metadata from {Path.GetFileName(codex.Path)}", ex); + Logger.Error($"Failed to read metadata from {Path.GetFileName(sources.Path)}", ex); LogEntry logEntry = new(Severity.Warning, $"Failed to read metadata from {codex.Title}"); ProgressVM.AddLogEntry(logEntry); } @@ -80,8 +81,8 @@ public override async Task GetMetaData(Codex codex) public override async Task FetchCover(Codex codex) { //return false if file doesn't exist - if (!IOService.IsPDFFile(codex.Path) || - !File.Exists(codex.Path)) + if (!IOService.IsPDFFile(codex.Sources.Path) || + !File.Exists(codex.Sources.Path)) { return false; } @@ -89,7 +90,7 @@ public override async Task FetchCover(Codex codex) try //image.Read can throw exception if file can not be opened/read { using MagickImage image = new(); - await image.ReadAsync(codex.Path, ReadSettings); + await image.ReadAsync(codex.Sources.Path, ReadSettings); image.Format = MagickFormat.Png; image.BackgroundColor = new MagickColor("#000000"); //set background color as transparent image.Trim(); //cut off all transparency @@ -100,7 +101,7 @@ public override async Task FetchCover(Codex codex) } catch (Exception ex) { - Logger.Error($"Failed to generate cover from {Path.GetFileName(codex.Path)}", ex); + Logger.Error($"Failed to generate cover from {Path.GetFileName(codex.Sources.Path)}", ex); LogEntry logEntry = new(Severity.Warning, $"Failed to generate cover from {codex.Title}"); ProgressVM.AddLogEntry(logEntry); return false; diff --git a/src/ViewModels/Sources/SourceViewModel.cs b/src/ViewModels/Sources/SourceViewModel.cs index 6f957ed..846ae45 100644 --- a/src/ViewModels/Sources/SourceViewModel.cs +++ b/src/ViewModels/Sources/SourceViewModel.cs @@ -1,5 +1,6 @@ using CommunityToolkit.Mvvm.ComponentModel; using COMPASS.Models; +using COMPASS.Models.XmlDtos; using System.Threading.Tasks; namespace COMPASS.ViewModels.Sources @@ -34,9 +35,9 @@ protected SourceViewModel(CodexCollection targetCollection) public abstract MetaDataSource Source { get; } - public abstract bool IsValidSource(Codex codex); + public abstract bool IsValidSource(SourceSet sources); - public abstract Task GetMetaData(Codex codex); + public abstract Task GetMetaData(SourceSet sources); public abstract Task FetchCover(Codex codex); #endregion diff --git a/src/Views/ListLayout.xaml b/src/Views/ListLayout.xaml index 0936c10..187e581 100644 --- a/src/Views/ListLayout.xaml +++ b/src/Views/ListLayout.xaml @@ -170,7 +170,7 @@ Visibility="{Binding Source={StaticResource LayoutProxy}, Path=Data.Preferences.ShowISBN, Converter={StaticResource ToVisibilityConverter}}"> - + diff --git a/src/Windows/CodexEditWindow.xaml b/src/Windows/CodexEditWindow.xaml index a7b9d70..5b3071b 100644 --- a/src/Windows/CodexEditWindow.xaml +++ b/src/Windows/CodexEditWindow.xaml @@ -299,19 +299,19 @@ -