diff --git a/.editorconfig b/.editorconfig index 9a561d9..c259c56 100644 --- a/.editorconfig +++ b/.editorconfig @@ -121,7 +121,7 @@ csharp_style_conditional_delegate_call = true # Предпочтения модификатора csharp_prefer_static_anonymous_function = true csharp_prefer_static_local_function = true -csharp_preferred_modifier_order = public, private, protected, file, static, extern, internal, virtual, abstract, override, sealed, readonly, new, unsafe, required, volatile, async:suggestion +csharp_preferred_modifier_order = public, private, protected, internal, file, static, extern, virtual, abstract, override, sealed, readonly, new, unsafe, required, volatile, async:suggestion csharp_style_prefer_readonly_struct = true csharp_style_prefer_readonly_struct_member = true diff --git a/Pf2eDataScrapper/App/ProcessorCollection.cs b/Pf2eDataScrapper/App/ProcessorCollection.cs index 65c2f41..e2b7edf 100644 --- a/Pf2eDataScrapper/App/ProcessorCollection.cs +++ b/Pf2eDataScrapper/App/ProcessorCollection.cs @@ -1,5 +1,4 @@ -using System.ComponentModel; -using System.Reflection; +using System.Reflection; using Pf2eDataScrapper.Processors; namespace Pf2eDataScrapper.App; @@ -77,6 +76,7 @@ internal class ProcessorCollection Dependencies = dependenciesAttribute?.Dependencies ?? [], IsCollectionProcessor = isCollection, InnerType = processorAttribute.InnerType, + Active = processorAttribute.Active, }; } @@ -202,6 +202,11 @@ internal class ProcessorCollectionItem /// public ICollection Dependencies { get; init; } = []; + /// + /// Процессор включён по умолчанию. + /// + public bool Active { get; init; } = true; + /// /// Приоритет выполнения обработчика. /// diff --git a/Pf2eDataScrapper/ApplicationSettings.cs b/Pf2eDataScrapper/ApplicationSettings.cs index 4dc914d..6e8da60 100644 --- a/Pf2eDataScrapper/ApplicationSettings.cs +++ b/Pf2eDataScrapper/ApplicationSettings.cs @@ -66,19 +66,7 @@ internal class ApplicationSettings #region Основные настройки - /// - /// Флаг, указывающий, что все старые записи необходимо удалить. - /// - [Browsable(true)] - [DisplayName("Удаление всех записей перед запуском.")] - [Description("Редактирование через .env-файл.")] - public bool TruncateAllTables { get; set; } = true; - - /// - /// Список обработчиков, которые должны быть выполнены. - /// Пустой список означает, что должны быть выполнены все обработчики. - /// - public ICollection ProcessorNames { get; } = []; + // #endregion } diff --git a/Pf2eDataScrapper/Forms/Controls/CoreBindingNavigator.cs b/Pf2eDataScrapper/Forms/Controls/CoreBindingNavigator.cs new file mode 100644 index 0000000..dc1f2e2 --- /dev/null +++ b/Pf2eDataScrapper/Forms/Controls/CoreBindingNavigator.cs @@ -0,0 +1,22 @@ +namespace Pf2eViewer; + +/// +/// Элемент управления прокрутки страниц +/// +internal sealed class CoreBindingNavigator : BindingNavigator +{ + public CoreBindingNavigator() + { + AddStandardItems(); + + if (DeleteItem != null) + { + DeleteItem.Visible = false; + } + + if (AddNewItem != null) + { + AddNewItem.Visible = false; + } + } +} diff --git a/Pf2eDataScrapper/Forms/Controls/DatabaseViewer/DatabaseQueryPerformer.cs b/Pf2eDataScrapper/Forms/Controls/DatabaseViewer/DatabaseQueryPerformer.cs new file mode 100644 index 0000000..3623ce1 --- /dev/null +++ b/Pf2eDataScrapper/Forms/Controls/DatabaseViewer/DatabaseQueryPerformer.cs @@ -0,0 +1,130 @@ +using System.Collections.Concurrent; +using Pf2eModel; +using Pf2eModel.Model.Base; +using Pf2eModel.Model.Enum; + +namespace Pf2eViewer.DatabaseViewer; + +/// +/// Класс, выполняющий страничные запросы к базе данных. +/// +internal class DatabaseQueryPerformer +{ + /// + /// Тип сущностей, которую необходимо получить. + /// + public required EntityType Type; + + /// + /// Номер страницы, которую необходимо получить. + /// + public required int Page; + + /// + /// Количество элементов на странице. + /// + public required int PerPage; + + /// + /// Поисковая строка, по которой будет производиться фильтрация. + /// + public required string Search = ""; + + /// + /// Поток, в котором будет выполняться запрос к базе данных. + /// + private readonly Thread _queryThread; + + /// + /// Конструктор класса, который инициализирует поток для выполнения запроса к базе данных. + /// + public DatabaseQueryPerformer() + { + _queryThread = new(PerformQuery); + } + + /// + /// Запускает поток для выполнения запроса к базе данных. + /// + public void Query() + { + _queryThread.Start(); + } + + /// + /// Прерывает выполнение потока, если он ещё не завершён. + /// + public void Abort() + { + _queryThread.Interrupt(); + } + + /// + /// Выполняет запрос к базе данных и обрабатывает результаты. + /// + private void PerformQuery() + { + try + { + Pf2eDbContext dbContext = new(); + + IQueryable query = + Search == "" + ? dbContext.Entities.Where(e => e.Type == Type) + : + // ReSharper disable once EntityFramework.UnsupportedServerSideFunctionCall + dbContext.Entities.Where(e => + e.Type == Type && Pf2eDbContext.LocaleSearch(e.Name, Search) + ); + + int count = query.Count(); + + List entities = query + .OrderBy(e => e.Id) + .Skip((Page - 1) * PerPage) + .Take(PerPage) + .ToList(); + + DatabaseQueryResult result = new(count, entities); + + OnQueryCompleted?.Invoke(this, result); + } + catch (ThreadInterruptedException) + { + // Ignore + } + } + + /// + /// Делегат, который будет вызван по завершении выполнения запроса к базе данных. + /// + /// Объект класса, выполневшего запрос. + /// Результат запроса. + public delegate void DatabaseQueryPerformerDelegate( + DatabaseQueryPerformer sender, + DatabaseQueryResult result + ); + + /// + /// Событие, которое будет вызвано по завершении выполнения запроса к базе данных. + /// + public event DatabaseQueryPerformerDelegate? OnQueryCompleted; +} + +/// +/// Класс, который содержит результат выполнения запроса к базе данных. +/// +/// Общее количество найденных сущностей. +/// Набор найденных сущностей. +internal class DatabaseQueryResult(int count, ICollection entities) +{ + /// + /// Общее количество найденных сущностей. + /// + public int Count { get; } = count; + + /// + /// Набор найденных сущностей. + /// + public ConcurrentBag Entities { get; } = new(entities); +} diff --git a/Pf2eDataScrapper/Forms/Controls/DatabaseViewer/DatabaseViewerControl.Designer.cs b/Pf2eDataScrapper/Forms/Controls/DatabaseViewer/DatabaseViewerControl.Designer.cs new file mode 100644 index 0000000..51bca07 --- /dev/null +++ b/Pf2eDataScrapper/Forms/Controls/DatabaseViewer/DatabaseViewerControl.Designer.cs @@ -0,0 +1,148 @@ +namespace Pf2eViewer.DatabaseViewer; + +partial class DatabaseViewerControl +{ + /// + /// Обязательная переменная конструктора. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Освободить все используемые ресурсы. + /// + /// истинно, если управляемый ресурс должен быть удален; иначе ложно. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Код, автоматически созданный конструктором компонентов + + /// + /// Требуемый метод для поддержки конструктора — не изменяйте + /// содержимое этого метода с помощью редактора кода. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + BindingNavigator = new CoreBindingNavigator(); + SearchTextBox = new TextBox(); + DataGridView = new DataGridView(); + DbBindingSource = new BindingSource(components); + PagesBindingSource = new BindingSource(components); + Id = new DataGridViewTextBoxColumn(); + EntityName = new DataGridViewTextBoxColumn(); + Rarity = new DataGridViewTextBoxColumn(); + (BindingNavigator).BeginInit(); + ((System.ComponentModel.ISupportInitialize)DataGridView).BeginInit(); + ((System.ComponentModel.ISupportInitialize)DbBindingSource).BeginInit(); + ((System.ComponentModel.ISupportInitialize)PagesBindingSource).BeginInit(); + SuspendLayout(); + // + // BindingNavigator + // + BindingNavigator.AddNewItem = null; + BindingNavigator.DeleteItem = null; + BindingNavigator.ImeMode = ImeMode.NoControl; + BindingNavigator.LayoutStyle = ToolStripLayoutStyle.HorizontalStackWithOverflow; + BindingNavigator.Location = new Point(0, 0); + BindingNavigator.Name = "BindingNavigator"; + BindingNavigator.Size = new Size(345, 25); + BindingNavigator.Stretch = true; + BindingNavigator.TabIndex = 1; + BindingNavigator.Text = "coreBindingNavigator1"; + BindingNavigator.RefreshItems += BindingNavigator_RefreshItems; + // + // SearchTextBox + // + SearchTextBox.Dock = DockStyle.Top; + SearchTextBox.Location = new Point(0, 25); + SearchTextBox.Name = "SearchTextBox"; + SearchTextBox.PlaceholderText = "Поиск"; + SearchTextBox.Size = new Size(345, 23); + SearchTextBox.TabIndex = 3; + SearchTextBox.TextChanged += SearchTextBox_TextChanged; + SearchTextBox.Leave += SearchTextBox_Leave; + // + // DataGridView + // + DataGridView.AllowUserToAddRows = false; + DataGridView.AllowUserToDeleteRows = false; + DataGridView.AutoGenerateColumns = false; + DataGridView.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCellsExceptHeader; + DataGridView.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.AllCellsExceptHeaders; + DataGridView.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize; + DataGridView.Columns.AddRange(new DataGridViewColumn[] { Id, EntityName, Rarity }); + DataGridView.DataSource = DbBindingSource; + DataGridView.Dock = DockStyle.Fill; + DataGridView.Location = new Point(0, 48); + DataGridView.MultiSelect = false; + DataGridView.Name = "DataGridView"; + DataGridView.ReadOnly = true; + DataGridView.RowHeadersVisible = false; + DataGridView.SelectionMode = DataGridViewSelectionMode.FullRowSelect; + DataGridView.Size = new Size(345, 345); + DataGridView.TabIndex = 4; + DataGridView.RowStateChanged += DataGridView_RowStateChanged; + // + // Id + // + Id.DataPropertyName = "Id"; + Id.HeaderText = "Id"; + Id.Name = "Id"; + Id.ReadOnly = true; + Id.Resizable = DataGridViewTriState.False; + Id.SortMode = DataGridViewColumnSortMode.NotSortable; + Id.Width = 5; + // + // EntityName + // + EntityName.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill; + EntityName.DataPropertyName = "Name"; + EntityName.HeaderText = "Name"; + EntityName.Name = "EntityName"; + EntityName.ReadOnly = true; + EntityName.Resizable = DataGridViewTriState.False; + EntityName.SortMode = DataGridViewColumnSortMode.NotSortable; + // + // Rarity + // + Rarity.DataPropertyName = "Rarity"; + Rarity.HeaderText = "Rarity"; + Rarity.Name = "Rarity"; + Rarity.ReadOnly = true; + Rarity.Resizable = DataGridViewTriState.False; + Rarity.SortMode = DataGridViewColumnSortMode.NotSortable; + Rarity.Width = 5; + // + // DatabaseViewerControl + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + Controls.Add(DataGridView); + Controls.Add(SearchTextBox); + Controls.Add(BindingNavigator); + Name = "DatabaseViewerControl"; + Size = new Size(345, 393); + (BindingNavigator).EndInit(); + ((System.ComponentModel.ISupportInitialize)DataGridView).EndInit(); + ((System.ComponentModel.ISupportInitialize)DbBindingSource).EndInit(); + ((System.ComponentModel.ISupportInitialize)PagesBindingSource).EndInit(); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + private CoreBindingNavigator BindingNavigator; + private TextBox SearchTextBox; + private DataGridView DataGridView; + private BindingSource DbBindingSource; + private BindingSource PagesBindingSource; + private DataGridViewTextBoxColumn Id; + private DataGridViewTextBoxColumn EntityName; + private DataGridViewTextBoxColumn Rarity; +} diff --git a/Pf2eDataScrapper/Forms/Controls/DatabaseViewer/DatabaseViewerControl.cs b/Pf2eDataScrapper/Forms/Controls/DatabaseViewer/DatabaseViewerControl.cs new file mode 100644 index 0000000..d056a03 --- /dev/null +++ b/Pf2eDataScrapper/Forms/Controls/DatabaseViewer/DatabaseViewerControl.cs @@ -0,0 +1,390 @@ +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using Pf2eModel; +using Pf2eModel.Model.Base; +using Pf2eModel.Model.Common.I18n; +using Pf2eModel.Model.Enum; +using Timer = System.Windows.Forms.Timer; + +namespace Pf2eViewer.DatabaseViewer; + +/// +/// Класс элемента управления, который отображает сущности в виде таблицы. +/// +public partial class DatabaseViewerControl : UserControl +{ + #region "Поля и свойства" + + /// + /// Контекст базы данных, который используется для получения сущностей. + /// + public readonly Pf2eDbContext Context = new(); + + /// + /// Количество элементов на странице. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public int PerPage + { + get => _perPage; + set + { + _perPage = value; + UpdateView(); + } + } + private int _perPage = 30; + + /// + /// Номер страницы, которую необходимо отобразить. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public int Page + { + get => _page; + set + { + _page = value; + UpdateView(); + } + } + private int _page = 1; + + /// + /// Тип сущности, которую необходимо отобразить. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public EntityType Type + { + get => _type; + set + { + _page = 1; + _type = value; + UpdateView(); + } + } + private EntityType _type; + + /// + /// Количество элементов, которые были получены из базы данных. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public int Count { get; private set; } + + /// + /// Номер последней страницы, которая может быть отображена. + /// + public int MaxPage => Convert.ToInt32(Math.Ceiling(1.0 * Count / PerPage)); + + /// + /// Выбранная в элементе управления сущность. + /// + [AllowNull] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public EntityView CurrentEntity { get; private set; } + + /// + /// Флаг, указывающий, что элемент управления занят выполнением запроса к базе данных. + /// + private bool _isBusy = true; + + /// + /// Объект, который выполняет запрос к базе данных. + /// + private DatabaseQueryPerformer? _performer; + + /// + /// Таймер, который используется для задержки обработки события изменения текста в поле поиска. + /// + private Timer? _typingTimer; + + /// + /// Строка, которая хранит последнее значение, введенное в поле поиска. + /// + private string _lastSearch = ""; + + #endregion + + /// + /// Конструктор класса, который инициализирует элемент управления и связывает его с источником данных. + /// + public DatabaseViewerControl() + { + InitializeComponent(); + + BindingNavigator.BindingSource = DbBindingSource; + //DataGridView.AutoGenerateColumns = true; + DataGridView.SelectionMode = DataGridViewSelectionMode.FullRowSelect; + + _isBusy = false; + } + + #region "События элемента управления" + + /// + /// Обработчик события, который вызывается при изменении текущей страницы в элементе управления. + /// + /// Источник события. + /// Объект, содержащий данные события. + private void BindingNavigator_RefreshItems(object sender, EventArgs e) + { + if (_isBusy) + { + return; + } + + Page = (int)(BindingNavigator?.BindingSource?.Current ?? 1); + UpdateView(); + } + + /// + /// Обработчик события, который вызывается при изменении выделенной строки в элементе управления DataGridView. + /// + /// Источник события. + /// Объект, содержащий данные события. + private void DataGridView_RowStateChanged(object sender, DataGridViewRowStateChangedEventArgs e) + { + if (_isBusy) + { + return; + } + + UpdateEntity(); + } + + /// + /// Обработчик события, который вызывается когда поле поиска теряет фокус. + /// + /// Источник события. + /// Объект, содержащий данные события. + private void SearchTextBox_Leave(object sender, EventArgs e) + { + if (_isBusy || SearchTextBox.Text == _lastSearch) + { + return; + } + + UpdateView(); + } + + /// + /// Обработчик события, который вызывается при изменении текста в поле поиска. + /// + /// Источник события. + /// Объект, содержащий данные события. + private void SearchTextBox_TextChanged(object sender, EventArgs e) + { + if (_isBusy) + { + return; + } + + if (_typingTimer == null) + { + _typingTimer = new(); + _typingTimer.Interval = 3000; + _typingTimer.Tick += HandleTypingTimerTimeout; + } + _typingTimer.Stop(); // Resets the timer + _typingTimer.Tag = (sender as TextBox)!.Text; // This should be done with EventArgs + _typingTimer.Start(); + } + + /// + /// Обработчик события, который вызывается при истечении времени таймера ввода текста. + /// + /// Источник события. + /// Объект, содержащий данные события. + private void HandleTypingTimerTimeout(object? sender, EventArgs e) + { + if (_isBusy || sender is not Timer timer) + { + return; + } + + UpdateView(); + timer.Stop(); + } + + #endregion + + #region "Обновление данных" + + /// + /// Устанавливает статус занятости элемента управления. + /// + /// Новый статус занятости. + private void SetBusyStatus(bool busy) + { + if (InvokeRequired) + { + Invoke(SetBusyStatus, busy); + return; + } + + _isBusy = busy; + + BindingNavigator.Enabled = !busy; + DataGridView.Enabled = !busy; + SearchTextBox.Enabled = !busy; + + DataGridView.ColumnHeadersHeightSizeMode = !busy + ? DataGridViewColumnHeadersHeightSizeMode.AutoSize + : DataGridViewColumnHeadersHeightSizeMode.DisableResizing; + } + + /// + /// Обновляет данные в элементе управления. + /// + /// Проигнорировать флаг занятости. + public void UpdateView(bool force = false) + { + if (_isBusy && !force) + { + return; + } + + SetBusyStatus(true); + + _performer?.Abort(); + + _performer = new() + { + Type = Type, + Page = Page, + PerPage = PerPage, + Search = SearchTextBox.Text, + }; + + _performer.OnQueryCompleted += HandleQueryCompleted; + + _performer.Query(); + } + + /// + /// Обработчик события, который вызывается по завершении выполнения запроса к базе данных. + /// + /// Источник события. + /// Результат выполнения события + private void HandleQueryCompleted(DatabaseQueryPerformer sender, DatabaseQueryResult result) + { + if (sender != _performer) + { + return; + } + + Count = result.Count; + _lastSearch = SearchTextBox.Text; + + InvokeControl( + BindingNavigator, + b => + { + PagesBindingSource.DataSource = Enumerable.Range(1, MaxPage).ToList(); + b.BindingSource = PagesBindingSource; + } + ); + + InvokeControl( + DataGridView, + view => + { + view.DataSource = result + .Entities.Select(e => new EntityGridRow(e)) + .OrderBy(r => r.Id) + .ToList(); + + SetBusyStatus(false); + + if (view.Rows.Count > 0) + { + view.Rows[0].Selected = true; + UpdateEntity(); + } + } + ); + } + + /// + /// Вызывает действие на указанном элементе управления в нужном потоке , если это необходимо. + /// + /// + /// + /// + private void InvokeControl(T control, Action action) + where T : Control + { + if (control.InvokeRequired) + { + control.Invoke(action, control); + } + else + { + action(control); + } + } + + /// + /// Обновить сущность, которая была выбрана в элементе управления DataGridView. + /// + private void UpdateEntity() + { + if (DataGridView.SelectedRows.Count == 0) + { + return; + } + + EntityGridRow? data = (EntityGridRow?)DataGridView.SelectedRows[0].DataBoundItem; + + try + { + CurrentEntity = data is not null ? new(Context, Type, data.Id) : null; + } + catch (Exception) + { + // ignored + } + + EntityChange?.Invoke(CurrentEntity); + } + + #endregion + + #region Output events + + /// + /// Делегат, который будет вызван при изменении выбранной сущности. + /// + /// Новая выбранная сущность. + public delegate void EntityChangeHandler(EntityView newEntity); + + /// + /// Событие, которое вызывается при изменении выбранной сущности. + /// + public event EntityChangeHandler? EntityChange; + + #endregion + + /// + /// Класс, который представляет строку в таблице сущностей. + /// + /// Сущность, связанная со строкой + internal class EntityGridRow(Entity entity) + { + /// + /// Идентификатор сущности. + /// + public int Id { get; set; } = entity.Id; + + /// + /// Название сущности. + /// + public string Name { get; set; } = entity.Name[LocaleType.English]; + + /// + /// Редкость сущности. + /// + public EntityRarity Rarity { get; set; } = entity.Rarity; + } +} diff --git a/Pf2eDataScrapper/Forms/Controls/DatabaseViewer/DatabaseViewerControl.resx b/Pf2eDataScrapper/Forms/Controls/DatabaseViewer/DatabaseViewerControl.resx new file mode 100644 index 0000000..aea47c3 --- /dev/null +++ b/Pf2eDataScrapper/Forms/Controls/DatabaseViewer/DatabaseViewerControl.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + True + + + True + + + 17, 17 + + + 163, 17 + + \ No newline at end of file diff --git a/Pf2eDataScrapper/Forms/Controls/EntityTreeViewer/EntityTreeViewerControl.Designer.cs b/Pf2eDataScrapper/Forms/Controls/EntityTreeViewer/EntityTreeViewerControl.Designer.cs new file mode 100644 index 0000000..a0d535f --- /dev/null +++ b/Pf2eDataScrapper/Forms/Controls/EntityTreeViewer/EntityTreeViewerControl.Designer.cs @@ -0,0 +1,60 @@ +namespace Pf2eViewer.EntityTreeViewer; + +sealed partial class EntityTreeViewerControl +{ + /// + /// Обязательная переменная конструктора. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Освободить все используемые ресурсы. + /// + /// истинно, если управляемый ресурс должен быть удален; иначе ложно. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Код, автоматически созданный конструктором компонентов + + /// + /// Требуемый метод для поддержки конструктора — не изменяйте + /// содержимое этого метода с помощью редактора кода. + /// + private void InitializeComponent() + { + ContentPanel = new PictureBox(); + ((System.ComponentModel.ISupportInitialize)ContentPanel).BeginInit(); + SuspendLayout(); + // + // ContentPanel + // + ContentPanel.Location = new Point(0, 0); + ContentPanel.Name = "ContentPanel"; + ContentPanel.Size = new Size(400, 500); + ContentPanel.TabIndex = 0; + ContentPanel.TabStop = false; + ContentPanel.MouseMove += ContentPanel_MouseMove; + // + // EntityTreeViewerControl + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + AutoScroll = true; + Controls.Add(ContentPanel); + Name = "EntityTreeViewerControl"; + Size = new Size(400, 500); + Resize += EntityTreeViewerControl_Resize; + ((System.ComponentModel.ISupportInitialize)ContentPanel).EndInit(); + ResumeLayout(false); + } + + #endregion + + public PictureBox ContentPanel; +} diff --git a/Pf2eDataScrapper/Forms/Controls/EntityTreeViewer/EntityTreeViewerControl.cs b/Pf2eDataScrapper/Forms/Controls/EntityTreeViewer/EntityTreeViewerControl.cs new file mode 100644 index 0000000..daef077 --- /dev/null +++ b/Pf2eDataScrapper/Forms/Controls/EntityTreeViewer/EntityTreeViewerControl.cs @@ -0,0 +1,394 @@ +using System.ComponentModel; + +namespace Pf2eViewer.EntityTreeViewer; + +/// +/// Элемент управления для отображения структуры сущности. +/// +public sealed partial class EntityTreeViewerControl : UserControl +{ + #region "Поля элемента управления" + + /// + /// Высота строки поля структуры. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + [Category("Entity Render")] + [Description("The height of each item in the tree view.")] + public int ItemHeight + { + get => _itemHeight; + set + { + _itemHeight = value; + UpdateView(); + } + } + private int _itemHeight = 20; + + /// + /// Сущность, которая будет отображаться в элементе управления. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public EntityView? EntityView + { + get => _entityView; + set + { + _entityView = value; + UpdateData(); + } + } + private EntityView? _entityView; + + /// + /// Ширина отступа глубины. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + [Category("Entity Render")] + [Description("The size of tab.")] + public int DepthSize + { + get => _depthSize; + set + { + _depthSize = value; + UpdateView(); + } + } + private int _depthSize = 20; + + /// + /// Размер шрифта типа поля сущности. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + [Category("Entity Render")] + [Description("The size of tab.")] + public float TypeFontSize + { + get => _typeFontSize; + set + { + _typeFontSize = value; + UpdateView(); + } + } + private float _typeFontSize = 9.5f; + + /// + /// Размер кнопки плюс/минус. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + [Category("Entity Render")] + [Description("The size of plus/minus button.")] + public int ButtonSize + { + get => _buttonSize; + set + { + _buttonSize = value; + UpdateView(); + } + } + private int _buttonSize = 20; + + /// + /// Ширина имени поля сущности. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public int NameWidth { get; private set; } = 200; + + /// + /// Цвет фона строки поля при наведении на элемент управления. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] + [Category("Entity Render")] + [Description("Color on item hover.")] + public Color HoverColor + { + get => _hoverColor; + set + { + _hoverColor = value; + UpdateView(); + } + } + private Color _hoverColor = SystemColors.ControlDark; + + /// + /// Изображение, используемое для отображения содержимого элемента управления. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public Bitmap ContentImage { get; set; } + + #endregion + + #region "Вычисляемые поля" + + /// + /// Ширина области содержимого элемента управления. + /// + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public int ContentWidth => Width - SystemInformation.VerticalScrollBarWidth - 2; + + #endregion + + #region "Приватные поля" + + + /// + /// Коллекция полей структуры. + /// + private readonly List _rows = []; + + /// + /// Коллекция строк полей структуры, которые будут отображаться в элементе управления. + /// + private List _viewerRows = []; + + /// + /// Флаг, указывающий, что элемент управления в данный момент перерисовывается. + /// + private bool _rendering; + + #endregion + + /// + /// Конструктор класса . + /// + public EntityTreeViewerControl() + { + InitializeComponent(); + ContentImage = new(ContentWidth, Height); + DoubleBuffered = true; + VerticalScroll.Visible = true; + HorizontalScroll.Visible = false; + + ContentPanel.MouseWheel += ContentPanel_MouseWheel; + } + + #region "Методы обновления" + + /// + /// Обновление данных в элементе управления. + /// + public void UpdateData() + { + if (EntityView?.Entity is null) + { + return; + } + + _rows.Clear(); + ContentPanel.Controls.Clear(); + + StructureBuilder builder = new(); + + _rows.Add(builder.Create("Entity", EntityView.Entity)); + + if (EntityView.SubEntity is not null) + { + string typeName = EntityView.SubEntity.GetType().Name; + + _rows.Add(builder.Create(typeName, EntityView.SubEntity)); + } + + _viewerRows = _rows + .SelectMany(r => r.GetRows(row => new EntityTreeViewerControlRow(this, row))) + .ToList(); + + UpdateRenders(); + } + + /// + /// Обновление отрисовок в элементе управления. + /// + public void UpdateRenders() + { + int maxNameWidth = -1; + + foreach (EntityTreeViewerControlRow row in _viewerRows) + { + int nameWidth = row.NameWidth + row.Item.Depth * DepthSize; + maxNameWidth = Math.Max(maxNameWidth, nameWidth); + } + + NameWidth = maxNameWidth + 3; + + // Debug.WriteLine($@"Height Inner {ContentWidth} / Outer {Width}"); + // Debug.WriteLine($@"Name {NameWidth} / Full {ContentWidth}"); + + foreach (EntityTreeViewerControlRow row in _viewerRows) + { + row.UpdateCache(); + } + + UpdateView(); + } + + /// + /// Обновление отображения в элементе управления. + /// + public void UpdateView() + { + _viewerRows.ForEach(row => row.UpdateButton()); + + List rows = _viewerRows.Where(r => r.Item.Visible).ToList(); + if (rows.Count == 0) + { + return; + } + + BuildLadder(rows); + + _rendering = true; + ContentImage = new(ContentWidth, rows.Count * ItemHeight); + Graphics g = Graphics.FromImage(ContentImage); + + int viewRow = 0; + foreach (EntityTreeViewerControlRow row in rows) + { + row.ViewIndex = viewRow++; + row.Invalidate(g); + } + + g.Save(); + + ContentPanel.Image = ContentImage; + ContentPanel.Width = ContentImage.Width; + ContentPanel.Height = ContentImage.Height; + + _rendering = false; + } + + #endregion + + #region "Вспомогательные методы" + + /// + /// Построение лестницы для строк полей структуры. + /// + /// Набор строк полей структуры. + private void BuildLadder(List rows) + { + List lastLadder = []; + + for (int i = rows.Count - 1; i >= 0; i--) + { + EntityTreeViewerControlRow row = rows[i]; + List ladder; + + if (row.Depth == 0) + { + ladder = []; + } + else if (row.Depth < lastLadder.Count) + { + ladder = + [ + .. lastLadder[..(row.Depth - 1)], + lastLadder[row.Depth - 1] == LadderType.Empty + ? LadderType.Last + : LadderType.Through, + ]; + } + else if (row.Depth == lastLadder.Count) + { + ladder = lastLadder; + } + else + { + ladder = + [ + .. lastLadder, + .. Enumerable.Repeat(LadderType.Empty, row.Depth - lastLadder.Count - 1), + LadderType.Last, + ]; + } + + row.Ladder = ladder; + + lastLadder = ladder + .Select(l => (l == LadderType.Last) ? LadderType.Through : l) + .ToList(); + } + } + + #endregion + + #region "События элемента управления" + + /// + /// Обработчик события изменения размера элемента управления. + /// + /// Источник события. + /// Объект события. + private void EntityTreeViewerControl_Resize(object sender, EventArgs e) + { + if (_rendering) + { + return; + } + + ContentPanel.Width = Width; + + UpdateRenders(); + } + + /// + /// Обработчик события прокрутки элемента управления. + /// + /// Источник события. + /// Объект события. + private void ContentPanel_MouseWheel(object? sender, MouseEventArgs e) + { + /* + int initValue = VerticalScroll.Value; + int delta = -e.Delta / 120; + + VerticalScroll.Value = Math.Clamp( + initValue + delta, + VerticalScrollBar.Minimum, + VerticalScrollBar.Maximum - VerticalScrollBar.LargeChange + 1 + ); + + if (VerticalScrollBar.Value != initValue) + { + ContentPanel.Invalidate(); + } + */ + } + + /// + /// Обработчик события движения мыши над элементом управления. + /// + /// Источник события. + /// Объект события. + private void ContentPanel_MouseMove(object sender, MouseEventArgs e) + { + PointF p = new(e.X, e.Y); + + foreach (EntityTreeViewerControlRow row in _viewerRows) + { + bool contained = row.Target.Contains(p); + + if (contained != row.Hovered) + { + Rectangle r = new(); + row.RealTarget.Intersect(r); + ContentPanel.Invalidate(r); + } + + row.Hovered = contained; + } + } + + #endregion +} diff --git a/Pf2eDataScrapper/Forms/Controls/EntityTreeViewer/EntityTreeViewerControl.resx b/Pf2eDataScrapper/Forms/Controls/EntityTreeViewer/EntityTreeViewerControl.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/Pf2eDataScrapper/Forms/Controls/EntityTreeViewer/EntityTreeViewerControl.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Pf2eDataScrapper/Forms/Controls/EntityTreeViewer/EntityTreeViewerControlRow.cs b/Pf2eDataScrapper/Forms/Controls/EntityTreeViewer/EntityTreeViewerControlRow.cs new file mode 100644 index 0000000..1baecfb --- /dev/null +++ b/Pf2eDataScrapper/Forms/Controls/EntityTreeViewer/EntityTreeViewerControlRow.cs @@ -0,0 +1,606 @@ +using FontAwesome.Sharp; +using Font = System.Drawing.Font; +using FontStyle = System.Drawing.FontStyle; +using Image = System.Drawing.Image; + +namespace Pf2eViewer.EntityTreeViewer; + +/// +/// Элемент управления для отображения строки поля сущности. +/// +internal class EntityTreeViewerControlRow +{ + /// + /// Элемент управления, которому принадлежит эта строка. + /// + public readonly EntityTreeViewerControl ParentControl; + + /// + /// Отображаемая строка поля сущности. + /// + public readonly StructureField Item; + + #region "Поля строки" + + /// + /// Кэш изображения строки. + /// + public Image Cache + { + get + { + if (_cache == null) + { + UpdateCache(); + } + return _cache!; + } + set => _cache = value; + } + private Image? _cache; + + /// + /// Кэш изображения строки при наведении. + /// + public Image HoveredCache + { + get + { + if (_hoveredCache == null) + { + UpdateCache(); + } + return _hoveredCache!; + } + set => _hoveredCache = value; + } + private Image? _hoveredCache; + + /// + /// Список типов лестницы. + /// + public List Ladder + { + get => _ladder; + set + { + _ladder = value; + _cache = null; + } + } + private List _ladder = []; + + /// + /// Проверяет, находится ли курсор над строкой. + /// + public bool Hovered + { + get => _hovered; + set + { + if (value != _hovered) + { + _hovered = value; + Invalidate(); + } + } + } + private bool _hovered = false; + + /// + /// Индекс строки в представлении. + /// + public int ViewIndex { get; set; } // = -1; + + /// + /// Кнопка раскрытия/сворачивания. + /// + public IconButton? Button { get; set; } = null; + + #endregion + + #region "Поля для рендеринга" + + /// + public Font Font + { + get => ParentControl.Font; + set => ParentControl.Font = value; + } + + /// + public Color ForeColor + { + get => ParentControl.ForeColor; + set => ParentControl.ForeColor = value; + } + + /// + public Color BackColor + { + get => ParentControl.BackColor; + set => ParentControl.BackColor = value; + } + + /// + public Color HoverColor + { + get => ParentControl.HoverColor; + set => ParentControl.HoverColor = value; + } + + /// + public int ItemHeight + { + get => ParentControl.ItemHeight; + set => ParentControl.ItemHeight = value; + } + + /// + public int DepthSize + { + get => ParentControl.DepthSize; + set => ParentControl.DepthSize = value; + } + + /// + public int ButtonSize + { + get => ParentControl.ButtonSize; + set => ParentControl.ButtonSize = value; + } + + /// + public float TypeFontSize + { + get => ParentControl.TypeFontSize; + set => ParentControl.TypeFontSize = value; + } + + #endregion + + #region "Вычисляемые поля" + + /// + /// Ширина строки. + /// + public int Width => ParentControl.ContentWidth; + + /// + /// Высота строки. + /// + public int Height => ItemHeight; + + /// + /// Флаг наличия кнопки. + /// + public bool HasButton => Item.FieldType == StructureFieldType.Complex; + + /// + /// Глубина отображения строки. + /// + public int Depth => Item.Depth; + + /// + /// Ширина имени в строке. + /// + public int NameWidth + { + get + { + Graphics g = Graphics.FromImage(Cache); + return Convert.ToInt32(g.MeasureString(Item.Name, Font).Width + DepthSize); + } + } + + /// + /// Строковое представление типа поля. + /// + public string TypeText + { + get + { + string result = Item.GetTypeString(); + if (result != "") + { + result = $@"({result})"; + } + return result; + } + } + + /// + /// Шрифт для отображения типа поля. + /// + public Font TypeFont => new(Font.FontFamily, TypeFontSize); + + /// + /// Строковое представление значения поля. + /// + public string ValueText => Item.GetValueString(); + + /// + /// Шрифт для отображения значения поля. + /// + public Font ValueFont => + new( + Font.FontFamily, + Font.Size, + Item.FieldType == StructureFieldType.Complex ? FontStyle.Italic : FontStyle.Regular + ); + + /// + /// Прямоугольник, занимаемый строкой в представлении. + /// + public RectangleF Target => new(0, ViewIndex * Height, Width, Height); + + /// + /// Прямоугольник, занимаемый строкой в родительском представлении. + /// + public RectangleF RealTarget => + new(0, ViewIndex * Height - ParentControl.VerticalScroll.Value, Width, Height); + + #endregion + + /// + /// Список данных для рендеринга строк. + /// + private readonly List _renderData = []; + + #region "Основные методы" + + /// + /// Создание элемента управления строки поля сущности. + /// + /// Родительский элемент управления. + /// Строка поля структуры объекта. + public EntityTreeViewerControlRow(EntityTreeViewerControl parent, StructureField item) + { + ParentControl = parent; + Item = item; + ApplyItem(); + UpdateCache(); + + UpdateButton(); + } + + /// + /// Применение свойств к элементу управления. + /// + private void ApplyItem() + { + _renderData.Clear(); + + _renderData.Add(new(Item.Name, Font, ForeColor)); + _renderData.Add(new(TypeText, TypeFont, GetValueColor())); + _renderData.Add( + new(ValueText.Replace("\r", "").Replace('\n', ' '), ValueFont, GetValueColor()) + ); + + if (HasButton) + { + Button = new() + { + Width = ButtonSize, + Height = ButtonSize, + IconSize = ButtonSize, + IconChar = IconChar.MinusSquare, + ImageAlign = ContentAlignment.MiddleCenter, + TextAlign = ContentAlignment.MiddleCenter, + Visible = false, + }; + + Button.Click += Button_Click; + + ParentControl.ContentPanel.Controls.Add(Button); + } + } + + /// + /// Обновление кэша изображения строки. + /// + public void UpdateCache() + { + Cache = RenderCache(BackColor); + HoveredCache = RenderCache(HoverColor); + } + + /// + /// Создание кэша изображения строки. + /// + /// Цвет заднего фона. + /// Кеш изображения строки. + private Image RenderCache(Color backColor) + { + Image cache = new Bitmap(Width, Height); + Graphics g = Graphics.FromImage(cache); + + g.FillRectangle(new SolidBrush(backColor), 0, 0, Width, Height); + + RenderLadder(g); + RenderContent(g); + + g.Save(); + return cache; + } + + /// + /// Запрос на перерисовку строки. + /// + /// Графический объект. + public void Invalidate(Graphics? g = null) + { + g ??= Graphics.FromImage(ParentControl.ContentImage); + + g.DrawImage(Hovered ? HoveredCache : Cache, Target); + + UpdateButton(); + } + + #endregion + + #region "Кнопка" + + /// + /// Обновление кнопки раскрытия/сворачивания. + /// + public void UpdateButton() + { + if (Button is null) + { + return; + } + + int cx = Convert.ToInt32((0.5 + Depth) * DepthSize - 0.5 * ButtonSize); + int cy = Convert.ToInt32((0.5 + ViewIndex) * ItemHeight - 0.5 * ButtonSize); + + Button.Left = cx; + Button.Top = cy; + + Button!.IconChar = ((StructureFieldComplex)Item).Collapsed + ? IconChar.PlusSquare + : IconChar.MinusSquare; + Button.Visible = Item.Visible; + } + + /// + /// Событие нажатия кнопки раскрытия/сворачивания. + /// + /// Источник события. + /// Событие нажатие кнопки + private void Button_Click(object? sender, EventArgs e) + { + if (Item is StructureFieldComplex complex) + { + complex.Collapsed = !complex.Collapsed; + ParentControl.UpdateView(); + } + } + + #endregion + + #region "Отрисовка" + + /// + /// Отрисовка лестницы. + /// + /// Графический объект. + private void RenderLadder(Graphics g) + { + int n = Depth; + int h = ItemHeight; + int w = DepthSize; + + if (_ladder.Count < Depth) + { + return; + } + + Pen pen = new(Color.FromArgb(127, ForeColor.R, ForeColor.G, ForeColor.B), 1); + pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dot; + + float cy = 0.5f * h; + + for (int i = 0; i < n; i++) + { + float cx = (i + 1.5f) * w; + + switch (_ladder[i]) + { + case LadderType.Last: + g.DrawLine(pen, cx, 0, cx, cy); + break; + case LadderType.Through: + g.DrawLine(pen, cx, 0, cx, h); + break; + case LadderType.Empty: + default: + break; + } + + if (i == n - 1) + { + g.DrawLine(pen, cx, cy, (i + 2) * w, cy); + } + } + } + + /// + /// Отрисовка содержимого строки. + /// + /// Графический объект. + private void RenderContent(Graphics g) + { + float[] xs = + [ + DepthSize * (Item.Depth + 1), + ParentControl.NameWidth, + ParentControl.NameWidth + g.MeasureString(TypeText, TypeFont).Width + 3, + Width, + ]; + + Pen p = new(Color.Aqua, 1); + + for (int i = 0; i < 3; i++) + { + StringRenderData item = _renderData[i]; + + item.Left = xs[i]; + item.Width = xs[i + 1] - xs[i]; + item.Height = ItemHeight; + + // g.DrawRectangle(p, item.Rect); + item.Draw(g); + } + } + + #endregion + + #region Функции-помощники в отрисовке + + /// + /// Смешивание цвета с цветом текста. + /// + /// Цвет оттенка. + /// Степень смешения. + /// Смешенный цвет. + private Color BlendColor(Color color, double rate = 0.5) + { + return BlendColor(ForeColor, color, rate); + } + + /// + /// Смешивание цветов. + /// + /// Первый смешиваемый цвет. + /// Второй смешиваемый цвет. + /// Степень смешения. + /// Смешенный цвет. + private Color BlendColor(Color color1, Color color2, double rate) + { + return Color.FromArgb( + Mix(color1.A, color2.A), + Mix(color1.R, color2.R), + Mix(color1.G, color2.G), + Mix(color1.B, color2.B) + ); + + int Mix(int a, int b) => Convert.ToInt32(Math.Round(a * rate + b * (1 - rate))); + } + + /// + /// Получение цвета текста по типу значения поля. + /// + /// Цвет текста. + public Color GetValueColor() + { + return Item switch + { + StructureFieldPrimitive primitive => Type.GetTypeCode(primitive.Type) switch + { + TypeCode.Boolean => BlendColor(Color.DarkGoldenrod), + TypeCode.Int32 => BlendColor(Color.Blue), + TypeCode.Double => BlendColor(Color.Aqua), + TypeCode.String => BlendColor(Color.Green), + _ => ForeColor, + }, + StructureFieldEnum => BlendColor(Color.Red), + StructureFieldNull => BlendColor(BackColor), + _ => ForeColor, + }; + } + + #endregion + + /// + /// Класс для хранения данных рендеринга строки. + /// + /// Текст отображаемой строки. + /// Шрифт отображаемой строки. + /// Цвет отображаемой строки. + private class StringRenderData(string text, Font font, Color color) + { + /// + /// Текст отображаемой строки. + /// + public string Text { get; } = text; + + /// + /// Шрифт отображаемой строки. + /// + public Font Font { get; } = font; + + /// + /// Цвет отображаемой строки. + /// + public Color Color { get; } = color; + + /// + /// Левый край отображаемой строки. + /// + public float Left { get; set; } + + /// + /// Ширина отображаемой строки. + /// + public float Width { get; set; } + + /// + /// Высота отображаемой строки. + /// + public float Height { get; set; } + + /// + /// Прямоугольник, занимаемый строкой. + /// + public RectangleF Rect => new(Left, 0, Width, Height); + + /// + /// Уместить строку в занимаемый прямоугольник. + /// + /// Элемент графики. + /// Занимаемый прямоугольник + /// Строка, на которую будет заканчиваться значнеие при переполнении. + /// + private string Fit(Graphics g, RectangleF rect, string filler = "…") + { + if (Text.Length == 0 || g.MeasureString(Text, Font).Width <= rect.Width) + { + return Text; + } + + string txt = Text.Length > 100 ? Text[..100] : Text; + + int i = txt.Length; + + while (g.MeasureString(txt + filler, Font).Width > rect.Width) + { + txt = txt[..--i]; + if (i == 0) + break; + } + + return txt + filler; + } + + /// + /// Отрисовка строки. + /// + /// Элемент графики. + public void Draw(Graphics g) + { + g.DrawString( + Fit(g, Rect), + Font, + new SolidBrush(Color), + Rect, + new StringFormat + { + Alignment = StringAlignment.Near, + LineAlignment = StringAlignment.Center, + FormatFlags = StringFormatFlags.NoWrap, + } + ); + } + } +} diff --git a/Pf2eDataScrapper/Forms/Controls/EntityTreeViewer/StructureBuilder.cs b/Pf2eDataScrapper/Forms/Controls/EntityTreeViewer/StructureBuilder.cs new file mode 100644 index 0000000..0dfea73 --- /dev/null +++ b/Pf2eDataScrapper/Forms/Controls/EntityTreeViewer/StructureBuilder.cs @@ -0,0 +1,627 @@ +using System.Collections; +using System.Reflection; +using LinqToDB.Extensions; +using Pf2eModel.Model.Common.I18n; + +namespace Pf2eViewer.EntityTreeViewer; + +/// +/// Класс для построения структуры данных класса. +/// +internal class StructureBuilder +{ + /// + /// Список обработанных объектов. + /// + private readonly List _processedObjects = []; + + /// + /// Создание поля структуры. + /// + /// Название поля. + /// Значение поля. + /// Глубина, на котором поле отображается. + /// Поле структуры. + public StructureFieldComplex Create(string name, object value, int depth = 0) + { + _ = RegisterProcessed(value); + + StructureFieldComplex field = new(this, value, name, depth); + + CompleteProcessed(value, field); + + return field; + } + + /// + /// Регистрация обработанного объекта. + /// + /// Регистрируемый объект. + /// `true`, если объект был добавлен; `false`, если объект был добавлен ранее. + public bool RegisterProcessed(object value) + { + if (_processedObjects.Any(p => p.Value == value)) + { + return false; + } + + StructureProcessedObject processed = new(value); + + _processedObjects.Add(processed); + + return true; + } + + /// + /// Дополнить обработанный объект. + /// + /// Значение. + /// Поле объекта. + public void CompleteProcessed(object value, StructureFieldComplex field) + { + StructureProcessedObject? q = _processedObjects.FirstOrDefault(p => p.Value == value); + + if (q == null) + { + return; + } + + q.Field = field; + } + + /// + /// Выбор поля структуры по значению. + /// + /// Название поля. + /// Значение поля. + /// Глубина, на котором поле отображается. + /// + public StructureField Resolve(string name, object? value, int depth) + { + switch (GetFieldType(value)) + { + case StructureFieldType.Null: + return new StructureFieldNull(this, name, depth); + + case StructureFieldType.Primitive: + return new StructureFieldPrimitive(this, value!, name, depth); + + case StructureFieldType.Enum: + return new StructureFieldEnum(this, value!, name, depth); + + case StructureFieldType.Reference: + break; + + case StructureFieldType.Complex: + default: + + StructureProcessedObject? processed = _processedObjects.FirstOrDefault(p => + p.Value == value + ); + + if (processed is null) + { + return Create(name, value!, depth); + } + + StructureFieldReference reference = new StructureFieldReference( + this, + name, + null, + depth + ); + processed.AddReference(reference); + return reference; + } + + throw new("Impossible"); + } + + /// + /// Получить тип поля структуры по значению. + /// + /// Значение поля. + /// Тип поля структуры. + public static StructureFieldType GetFieldType(object? value) + { + if (value is null) + { + return StructureFieldType.Null; + } + + Type type = value.GetType(); + + if (type.IsEnum) + { + return StructureFieldType.Enum; + } + + if (type.IsPrimitive || type == typeof(string)) + { + return StructureFieldType.Primitive; + } + + return StructureFieldType.Complex; + } +} + +/// +/// Класс для хранения обработанного объекта. +/// +/// Объект. +internal class StructureProcessedObject(object value) +{ + public readonly object Value = value; + + private StructureFieldComplex? _field = null; + + public StructureFieldComplex? Field + { + get => _field; + set + { + _field = value; + foreach (StructureFieldReference reference in _references) + { + reference.Reference = Field; + } + } + } + + private readonly List _references = []; + + public void AddReference(StructureFieldReference reference) + { + _references.Add(reference); + + if (_field is not null) + { + reference.Reference = Field; + } + } +} + +/// +/// Тип поля структуры. +/// +internal enum StructureFieldType +{ + /// + /// Пустое поле. + /// + Null, + + /// + /// Простое поле. + /// + Primitive, + + /// + /// Поле с перечислением. + /// + Enum, + + /// + /// Поле с объектом. + /// + Complex, + + /// + /// Поле с ссылкой на объект. + /// + Reference, +} + +/// +/// Тип лестницы глубины. +/// +internal enum LadderType +{ + /// + /// Пустой элемент: [ ]. + /// + Empty, + + /// + /// Последний элемент: [└]. + /// + Last, + + /// + /// Проходной элемент: [├] или [│]. + /// + Through, +} + +/// +/// Базовый класс для полей структуры. +/// +/// Класс построения структуры данных. +/// Название поля. +/// Глубина поля. +internal abstract class StructureField(StructureBuilder builder, string name, int depth) +{ + /// + /// Название поля. + /// + public readonly string Name = name; + + /// + /// Тип поля структуры. + /// + public abstract StructureFieldType FieldType { get; } + + /// + /// Класс построения структуры данных. + /// + public readonly StructureBuilder Builder = builder; + + /// + /// Флаг, указывающий, отображается ли поле. + /// + public virtual bool Visible { get; set; } = true; + + /// + /// Глубина поля структуры. + /// + public readonly int Depth = depth; + + /// + /// Получить строку, представляющую тип поля. + /// + /// Строка, представляющая тип поля. + public abstract string GetTypeString(); + + /// + /// Получить строку, представляющую значение поля. + /// + /// Строка, представляющая значение поля. + public abstract string GetValueString(); + + /// + /// Получить набор строк, используя результат функция. + /// + /// Тип результирующей строки. + /// Функция, оборажения поля в результирующую строку. + /// Коллекция, содержащая набор строк. + public virtual ICollection GetRows(Func func) + { + StructureField[] q = [this]; + return q.Select(func).ToList(); + } +} + +/// +/// Пустое поле структуры. +/// +/// Класс построения структуры данных. +/// Название поля. +/// Глубина поля. +internal class StructureFieldNull(StructureBuilder builder, string name, int depth) + : StructureField(builder, name, depth) +{ + /// + public override StructureFieldType FieldType => StructureFieldType.Null; + + /// + public override string GetTypeString() => "null"; + + /// + public override string GetValueString() => ""; +} + +/// +/// Простое поле структуры. +/// +/// Класс построения структуры данных. +/// Значение поля. +/// Название поля. +/// Глубина поля. +internal class StructureFieldPrimitive( + StructureBuilder builder, + object value, + string name, + int depth +) : StructureField(builder, name, depth) +{ + /// + public override StructureFieldType FieldType => StructureFieldType.Primitive; + + /// + /// Значение поля. + /// + public readonly object Value = value; + + /// + /// Тип значения поля. + /// + public readonly Type Type = value.GetType(); + + /// + public override string GetTypeString() + { + return Type.GetTypeCode(Type) switch + { + TypeCode.Boolean => "bool", + TypeCode.Int32 => "int", + TypeCode.String => "string", + TypeCode.Double => "double", + _ => Type.Name, + }; + } + + /// + public override string GetValueString() + { + if (Type == typeof(string)) + { + return "\"" + ((string)Value).Replace("\"", "\\\"") + "\""; + } + + return Value.ToString() ?? string.Empty; + } +} + +/// +/// Поле структуры с перечислением. +/// +/// Класс построения структуры данных. +/// Значение поля. +/// Название поля. +/// Глубина поля. +internal class StructureFieldEnum(StructureBuilder builder, object value, string name, int depth) + : StructureField(builder, name, depth) +{ + /// + public override StructureFieldType FieldType => StructureFieldType.Enum; + + /// + /// Значение поля. + /// + public readonly object Value = value; + + /// + /// Тип значения поля. + /// + public readonly Type Type = value.GetType(); + + /// + public override string GetTypeString() => "enum"; + + /// + public override string GetValueString() => $"{Type.Name}.{Value}"; +} + +/// +/// Поле стркутуры с объектом. +/// +internal class StructureFieldComplex : StructureField +{ + /// + public override StructureFieldType FieldType => StructureFieldType.Complex; + + /// + /// Набор полей объекта. + /// + public readonly List Fields = []; + + /// + /// Тип значения поля. + /// + public readonly Type Type; + + /// + /// Флаг, указывающий, отображается ли поля объекта. + /// + public bool Collapsed + { + get => _collapsed; + set + { + _collapsed = value; + UpdateFieldVisibility(); + } + } + private bool _collapsed = false; + + /// + public override bool Visible + { + get => _visible; + set + { + _visible = value; + UpdateFieldVisibility(); + } + } + private bool _visible = true; + + /// + /// Конструктор класса поля структуры с объектом. + /// + /// Класс построения структуры данных. + /// Значение поля. + /// Название поля. + /// Глубина поля. + public StructureFieldComplex(StructureBuilder builder, object value, string name, int depth) + : base(builder, name, depth) + { + Type = value.GetType(); + + SetFields((dynamic)value); + } + + /// + /// Обновить видимость полей структуры. + /// + private void UpdateFieldVisibility() + { + foreach (StructureField field in Fields) + { + field.Visible = Visible && !Collapsed; + } + } + + /// + /// Установить поля структуры `LocaleString`. + /// + /// Объект строки локализации. + private void SetFields(LocaleString localeString) + { + SetFieldsFromRecords( + localeString + .Select(field => new FieldRecord(field.Locale.ToString(), field.Value)) + .ToList() + ); + } + + /// + /// Установить поля структуры `IEnumerable`. + /// + /// Объект-перечисление. + private void SetFields(IEnumerable enumerable) + { + int i = 0; + + List records = []; + + foreach (object? item in enumerable) + { + records.Add(new($"[{i++}]", item)); + } + + SetFieldsFromRecords(records); + } + + /// + /// Установить поля структуры общего объекта. + /// + /// Объект. + private void SetFields(object value) + { + Type type = value.GetType(); + PropertyInfo[] properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + + SetFieldsFromRecords( + properties + .Where(property => + property.GetAttribute() is null + ) + .Select(property => new FieldRecord(property.Name, property.GetValue(value, null))) + .ToList() + ); + } + + /// + /// Установить поля структуры из списка записей. + /// + /// + private void SetFieldsFromRecords(List records) + { + foreach ( + FieldRecord record in records.Where(record => + record.FieldType == StructureFieldType.Complex + ) + ) + { + record.Allowed = Builder.RegisterProcessed(record.Value!); + } + + foreach (FieldRecord record in records) + { + Fields.Add( + record is { FieldType: StructureFieldType.Complex, Allowed: true } + ? Builder.Create(record.Name, record.Value!, Depth + 1) + : Builder.Resolve(record.Name, record.Value!, Depth + 1) + ); + } + } + + /// + /// Запись поля структуры. + /// + /// Название поля. + /// Значение поля. + private class FieldRecord(string name, object? value) + { + /// + /// Название поля. + /// + public readonly string Name = name; + + /// + /// Значение поля. + /// + public readonly object? Value = value; + + /// + /// Тип поля структуры. + /// + public StructureFieldType FieldType => StructureBuilder.GetFieldType(Value); + + /// + /// Флаг, указывающий, использовать ли поле. + /// + public bool Allowed = true; + } + + /// + public override string GetTypeString() + { + if (Type.IsGenericEnumerableType()) + { + Type elementType = Type.GetGenericArguments().Single(); + + return $"{elementType.Name}[]"; + } + else + { + return Type.Name; + } + } + + /// + public override string GetValueString() + { + if (Type.IsGenericEnumerableType()) + { + return Fields.Count.ToString() + (Fields.Count == 1 ? " item" : " items"); + } + return ""; + } + + /// + public override ICollection GetRows(Func func) + { + return [.. base.GetRows(func), .. Fields.SelectMany(f => f.GetRows(func)).ToList()]; + } +} + +/// +/// Поле структуры с ссылкой на объект. +/// +/// Класс построения структуры данных. +/// Название поля. +/// Ссылка на объект. +/// Глубина поля. +internal class StructureFieldReference( + StructureBuilder builder, + string name, + StructureFieldComplex? reference, + int depth +) : StructureField(builder, name, depth) +{ + /// + /// Ссылка на объект. + /// + public StructureFieldComplex? Reference = reference; + + /// + public override StructureFieldType FieldType => StructureFieldType.Reference; + + /// + public override string GetTypeString() => Reference?.GetTypeString() ?? "???"; + + /// + public override string GetValueString() => ""; +} diff --git a/Pf2eDataScrapper/Forms/Controls/ProcessorCollectionControl.cs b/Pf2eDataScrapper/Forms/Controls/ProcessorCollectionControl.cs index 02d879b..37f32d7 100644 --- a/Pf2eDataScrapper/Forms/Controls/ProcessorCollectionControl.cs +++ b/Pf2eDataScrapper/Forms/Controls/ProcessorCollectionControl.cs @@ -6,16 +6,16 @@ namespace Pf2eDataScrapper.Forms.Controls; internal partial class ProcessorCollectionControl : UserControl { [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public ProcessorCollection? Collection + public ProcessorCollection Collection { get => _collection; set { _collection = value; - UpdateContent(); + SetContent(); } } - private ProcessorCollection? _collection = null; + private ProcessorCollection _collection = null!; public ProcessorCollectionControl() { @@ -28,27 +28,24 @@ internal partial class ProcessorCollectionControl : UserControl private Dictionary ItemCheckBoxes { get; } = []; - private void UpdateContent() + private void SetContent() { RootFlowLayoutPanel.Controls.Clear(); - if (Collection == null) - { - return; - } - foreach (ProcessorGroupEnum group in Enum.GetValues()) { GroupCheckBoxes[group] = new() { Text = group.GetDescription(), - //AutoSize = false, + Tag = group, Dock = DockStyle.Fill, + ThreeState = true, Checked = true, + AutoCheck = false, CheckAlign = ContentAlignment.TopLeft, }; - GroupCheckBoxes[group].CheckedChanged += GroupCheckBox_CheckedChanged; + GroupCheckBoxes[group].Click += GroupCheckBox_Click; GroupFlowLayoutPanels[group] = new() { @@ -69,20 +66,108 @@ internal partial class ProcessorCollectionControl : UserControl ItemCheckBoxes[item.Name] = new() { Text = item.Description, + Tag = item, AutoSize = true, // Dock = DockStyle.Fill, // MaximumSize = new Size(Width, 20), // Width = 200, - Checked = true, + Checked = item.Active, CheckAlign = ContentAlignment.TopLeft, }; + ItemCheckBoxes[item.Name].CheckedChanged += ItemCheckBox_CheckedChanged; + GroupFlowLayoutPanels[item.Group].Controls.Add(ItemCheckBoxes[item.Name]); } + + UpdateContent(); } - private void GroupCheckBox_CheckedChanged(object? sender, EventArgs e) + private void UpdateContent() { - // + Dictionary checkStates = []; + List checkedItems = []; + + foreach ((string key, CheckBox checkBox) in ItemCheckBoxes) + { + ProcessorCollectionItem item = Collection![key]; + + if (checkStates.TryGetValue(item.Group, out CheckState state)) + { + /* + * | Checked | Unchecked | Indeterminate + * -------+---------------+---------------+--------------- + * true | Checked | Indeterminate | Indeterminate + * false | Indeterminate | Unchecked | Indeterminate + */ + + checkStates[item.Group] = + checkBox.Checked && state == CheckState.Checked ? CheckState.Checked + : !checkBox.Checked && state == CheckState.Unchecked ? CheckState.Unchecked + : CheckState.Indeterminate; + } + else + { + checkStates[item.Group] = checkBox.Checked + ? CheckState.Checked + : CheckState.Unchecked; + } + + if (checkBox.Checked) + { + checkedItems.Add(key); + } + } + + foreach ((ProcessorGroupEnum groupEnum, CheckBox checkBox) in GroupCheckBoxes) + { + checkBox.CheckState = checkStates.GetValueOrDefault( + groupEnum, + CheckState.Indeterminate + ); + } + + Collection.FilterProcessors(checkedItems); + } + + private void GroupCheckBox_Click(object? sender, EventArgs e) + { + if (sender is null) + { + return; + } + + CheckBox groupCheckBox = (CheckBox)sender; + ProcessorGroupEnum groupEnum = GroupCheckBoxes + .First(pair => pair.Value == groupCheckBox) + .Key; + + CheckState nextState = groupCheckBox.CheckState switch + { + CheckState.Unchecked => CheckState.Checked, + CheckState.Checked => CheckState.Unchecked, + CheckState.Indeterminate => CheckState.Checked, + _ => throw new ArgumentOutOfRangeException( + $"Неизвестное состояние {groupCheckBox.CheckState}" + ), + }; + + groupCheckBox.CheckState = nextState; + + foreach ( + (string _, CheckBox checkBox) in ItemCheckBoxes.Where(pair => + ((ProcessorCollectionItem?)pair.Value.Tag)?.Group == groupEnum + ) + ) + { + checkBox.Checked = nextState == CheckState.Checked; + } + + UpdateContent(); + } + + private void ItemCheckBox_CheckedChanged(object? sender, EventArgs e) + { + UpdateContent(); } } diff --git a/Pf2eDataScrapper/Forms/EntityViewForm.Designer.cs b/Pf2eDataScrapper/Forms/EntityViewForm.Designer.cs new file mode 100644 index 0000000..a063fba --- /dev/null +++ b/Pf2eDataScrapper/Forms/EntityViewForm.Designer.cs @@ -0,0 +1,244 @@ +using Microsoft.EntityFrameworkCore.Scaffolding.Metadata; +using Pf2eViewer.DatabaseViewer; + +namespace Pf2eViewer; + +partial class EntityViewForm +{ + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + RootSplitContainer = new SplitContainer(); + InnerSplitContainer = new SplitContainer(); + TypesListBox = new ListBox(); + ResultSplitContainer = new SplitContainer(); + DescriptionTextBox = new TextBox(); + DatabaseViewer = new DatabaseViewerControl(); + EntityTreeViewerControl = new Pf2eViewer.EntityTreeViewer.EntityTreeViewerControl(); + MainMenuStrip = new MenuStrip(); + FileToolStripMenuItem = new ToolStripMenuItem(); + ExitToolStripMenuItem = new ToolStripMenuItem(); + WindowsToolStripMenuItem = new ToolStripMenuItem(); + ImportSettingsWindowsToolStripMenuItem = new ToolStripMenuItem(); + EntityViewWindowToolStripMenuItem = new ToolStripMenuItem(); + ((System.ComponentModel.ISupportInitialize)RootSplitContainer).BeginInit(); + RootSplitContainer.Panel1.SuspendLayout(); + RootSplitContainer.Panel2.SuspendLayout(); + RootSplitContainer.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)InnerSplitContainer).BeginInit(); + InnerSplitContainer.Panel1.SuspendLayout(); + InnerSplitContainer.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)ResultSplitContainer).BeginInit(); + ResultSplitContainer.Panel2.SuspendLayout(); + ResultSplitContainer.SuspendLayout(); + MainMenuStrip.SuspendLayout(); + SuspendLayout(); + // + // RootSplitContainer + // + RootSplitContainer.Dock = DockStyle.Fill; + RootSplitContainer.Location = new Point(0, 24); + RootSplitContainer.Name = "RootSplitContainer"; + // + // RootSplitContainer.Panel1 + // + RootSplitContainer.Panel1.Controls.Add(InnerSplitContainer); + RootSplitContainer.Panel1MinSize = 200; + // + // RootSplitContainer.Panel2 + // + RootSplitContainer.Panel2.Controls.Add(ResultSplitContainer); + RootSplitContainer.Panel2MinSize = 400; + RootSplitContainer.Size = new Size(1384, 837); + RootSplitContainer.SplitterDistance = 322; + RootSplitContainer.TabIndex = 0; + // + // InnerSplitContainer + // + InnerSplitContainer.Dock = DockStyle.Fill; + InnerSplitContainer.Location = new Point(0, 0); + InnerSplitContainer.Name = "InnerSplitContainer"; + InnerSplitContainer.Orientation = Orientation.Horizontal; + // + // InnerSplitContainer.Panel1 + // + InnerSplitContainer.Panel1.Controls.Add(TypesListBox); + // + // InnerSplitContainer.Panel2 + // + InnerSplitContainer.Panel2.Controls.Add(DatabaseViewer); + InnerSplitContainer.Size = new Size(322, 861); + InnerSplitContainer.SplitterDistance = 126; + InnerSplitContainer.TabIndex = 1; + // + // TypesListBox + // + TypesListBox.Dock = DockStyle.Fill; + TypesListBox.FormattingEnabled = true; + TypesListBox.Location = new Point(0, 0); + TypesListBox.Name = "TypesListBox"; + TypesListBox.Size = new Size(322, 122); + TypesListBox.TabIndex = 0; + TypesListBox.SelectedIndexChanged += TypesListBox_SelectedIndexChanged; + // + // ResultSplitContainer + // + ResultSplitContainer.Dock = DockStyle.Fill; + ResultSplitContainer.Location = new Point(0, 0); + ResultSplitContainer.Name = "ResultSplitContainer"; + // + // ResultSplitContainer.Panel1 + // + ResultSplitContainer.Panel1.Controls.Add(EntityTreeViewerControl); + // + // ResultSplitContainer.Panel2 + // + ResultSplitContainer.Panel2.Controls.Add(DescriptionTextBox); + ResultSplitContainer.Size = new Size(1058, 837); + ResultSplitContainer.SplitterDistance = 582; + ResultSplitContainer.TabIndex = 1; + // + // DescriptionTextBox + // + DescriptionTextBox.Dock = DockStyle.Fill; + DescriptionTextBox.Font = new Font("Consolas", 9F); + DescriptionTextBox.Location = new Point(0, 0); + DescriptionTextBox.Multiline = true; + DescriptionTextBox.Name = "DescriptionTextBox"; + DescriptionTextBox.ReadOnly = true; + DescriptionTextBox.ScrollBars = ScrollBars.Vertical; + DescriptionTextBox.Size = new Size(472, 837); + DescriptionTextBox.TabIndex = 0; + // + // DatabaseViewer + // + DatabaseViewer.Dock = DockStyle.Fill; + DatabaseViewer.Location = new Point(0, 0); + DatabaseViewer.Name = "DatabaseViewer"; + DatabaseViewer.Size = new Size(322, 731); + DatabaseViewer.TabIndex = 1; + // + // EntityTreeViewerControl + // + EntityTreeViewerControl.AutoScroll = true; + EntityTreeViewerControl.ButtonSize = 15; + EntityTreeViewerControl.DepthSize = 15; + EntityTreeViewerControl.Dock = DockStyle.Fill; + EntityTreeViewerControl.Font = new Font("Consolas", 9F); + EntityTreeViewerControl.HoverColor = SystemColors.ControlDark; + EntityTreeViewerControl.ItemHeight = 20; + EntityTreeViewerControl.Location = new Point(0, 0); + EntityTreeViewerControl.Name = "EntityTreeViewerControl"; + EntityTreeViewerControl.Size = new Size(582, 861); + EntityTreeViewerControl.TabIndex = 0; + EntityTreeViewerControl.TypeFontSize = 7F; + // + // MainMenuStrip + // + MainMenuStrip.Items.AddRange(new ToolStripItem[] { FileToolStripMenuItem, WindowsToolStripMenuItem }); + MainMenuStrip.Location = new Point(0, 0); + MainMenuStrip.Name = "MainMenuStrip"; + MainMenuStrip.Size = new Size(1384, 24); + MainMenuStrip.TabIndex = 1; + MainMenuStrip.Text = "menuStrip1"; + // + // FileToolStripMenuItem + // + FileToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { ExitToolStripMenuItem }); + FileToolStripMenuItem.Name = "FileToolStripMenuItem"; + FileToolStripMenuItem.Size = new Size(48, 20); + FileToolStripMenuItem.Text = "Файл"; + // + // ExitToolStripMenuItem + // + ExitToolStripMenuItem.Name = "ExitToolStripMenuItem"; + ExitToolStripMenuItem.Size = new Size(180, 22); + ExitToolStripMenuItem.Text = "Выход"; + ExitToolStripMenuItem.Click += ExitToolStripMenuItem_Click; + // + // WindowsToolStripMenuItem + // + WindowsToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { ImportSettingsWindowsToolStripMenuItem, EntityViewWindowToolStripMenuItem }); + WindowsToolStripMenuItem.Name = "WindowsToolStripMenuItem"; + WindowsToolStripMenuItem.Size = new Size(47, 20); + WindowsToolStripMenuItem.Text = "Окна"; + // + // ImportSettingsWindowsToolStripMenuItem + // + ImportSettingsWindowsToolStripMenuItem.Name = "ImportSettingsWindowsToolStripMenuItem"; + ImportSettingsWindowsToolStripMenuItem.Size = new Size(228, 22); + ImportSettingsWindowsToolStripMenuItem.Text = "Настройка импорта данных"; + ImportSettingsWindowsToolStripMenuItem.Click += ImportSettingsWindowsToolStripMenuItem_Click; + // + // EntityViewWindowToolStripMenuItem + // + EntityViewWindowToolStripMenuItem.Enabled = false; + EntityViewWindowToolStripMenuItem.Name = "EntityViewWindowToolStripMenuItem"; + EntityViewWindowToolStripMenuItem.Size = new Size(228, 22); + EntityViewWindowToolStripMenuItem.Text = "Просмотр сущностей"; + // + // EntityViewForm + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(1384, 861); + Controls.Add(RootSplitContainer); + Controls.Add(MainMenuStrip); + Name = "EntityViewForm"; + Text = "DataBaseViewer"; + RootSplitContainer.Panel1.ResumeLayout(false); + RootSplitContainer.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)RootSplitContainer).EndInit(); + RootSplitContainer.ResumeLayout(false); + InnerSplitContainer.Panel1.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)InnerSplitContainer).EndInit(); + InnerSplitContainer.ResumeLayout(false); + ResultSplitContainer.Panel2.ResumeLayout(false); + ResultSplitContainer.Panel2.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)ResultSplitContainer).EndInit(); + ResultSplitContainer.ResumeLayout(false); + MainMenuStrip.ResumeLayout(false); + MainMenuStrip.PerformLayout(); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private SplitContainer RootSplitContainer; + private SplitContainer InnerSplitContainer; + private ListBox TypesListBox; + private SplitContainer ResultSplitContainer; + private TextBox DescriptionTextBox; + private EntityTreeViewer.EntityTreeViewerControl EntityTreeViewerControl; + private DatabaseViewerControl DatabaseViewer; + private MenuStrip MainMenuStrip; + private ToolStripMenuItem FileToolStripMenuItem; + private ToolStripMenuItem ExitToolStripMenuItem; + private ToolStripMenuItem WindowsToolStripMenuItem; + private ToolStripMenuItem ImportSettingsWindowsToolStripMenuItem; + private ToolStripMenuItem EntityViewWindowToolStripMenuItem; +} diff --git a/Pf2eDataScrapper/Forms/EntityViewForm.cs b/Pf2eDataScrapper/Forms/EntityViewForm.cs new file mode 100644 index 0000000..8aa18e1 --- /dev/null +++ b/Pf2eDataScrapper/Forms/EntityViewForm.cs @@ -0,0 +1,99 @@ +using System.ComponentModel; +using Pf2eDataScrapper; +using Pf2eModel; +using Pf2eModel.Model.Enum; + +namespace Pf2eViewer; + +/// +/// Основная форма приложения. +/// +internal partial class EntityViewForm : Form +{ + public Pf2eDbContext Context { get; } + + /// + /// Элемент управления для отображения дерева сущностей. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public EntityView? CurrentView + { + get => EntityTreeViewerControl.EntityView; + set + { + EntityTreeViewerControl.EntityView = value; + Application.DoEvents(); + } + } + + /// + /// Конструктор класса . + /// + public EntityViewForm(Pf2eDbContext context) + { + Context = context; + + InitializeComponent(); + + DatabaseViewer.EntityChange += DatabaseViewer_EntityChange; + + Reset(); + } + + /// + /// Сброс состояния формы. + /// + public void Reset() + { + TypesListBox.DataSource = Context + .Entities.Select(e => e.Type) + .Distinct() + .OrderBy(t => t.ToString()) + .ToList(); + + if (TypesListBox.Items.Count > 0) + { + TypesListBox.SelectedIndex = 0; + } + } + + /// + /// Обработчик события изменения выбранного элемента в списке типов сущностей. + /// + /// Источник события. + /// Данные события. + private void TypesListBox_SelectedIndexChanged(object sender, EventArgs _) + { + if (TypesListBox.SelectedItem is null) + { + return; + } + + CurrentView = null; + + DatabaseViewer.Type = (EntityType)TypesListBox.SelectedItem; + } + + /// + /// Обработчик события изменения выбранной сущности в дереве сущностей. + /// + /// Новая выбранная сущность. + private void DatabaseViewer_EntityChange(EntityView newEntity) + { + CurrentView = newEntity; + + DescriptionTextBox.Text = newEntity.Entity!.Description; + } + + private void ExitToolStripMenuItem_Click(object sender, EventArgs e) + { + Close(); + } + + private void ImportSettingsWindowsToolStripMenuItem_Click(object sender, EventArgs e) + { + ApplicationStateChange?.Invoke(this, ApplicationStage.InitialSettings); + } + + public event ImportApplication.ApplicationStateChangeDelegate? ApplicationStateChange; +} diff --git a/Pf2eDataScrapper/Forms/EntityViewForm.resx b/Pf2eDataScrapper/Forms/EntityViewForm.resx new file mode 100644 index 0000000..51c499e --- /dev/null +++ b/Pf2eDataScrapper/Forms/EntityViewForm.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + + 51 + + \ No newline at end of file diff --git a/Pf2eDataScrapper/Forms/ProgressForm.Designer.cs b/Pf2eDataScrapper/Forms/ProgressForm.Designer.cs index ec7a110..1d2a967 100644 --- a/Pf2eDataScrapper/Forms/ProgressForm.Designer.cs +++ b/Pf2eDataScrapper/Forms/ProgressForm.Designer.cs @@ -33,6 +33,9 @@ partial class ProgressForm LocalLabel = new Label(); LocalProgressBar = new ProgressBar(); MessageListView = new ListView(); + TimeColumnHeader = new ColumnHeader(); + TypeColumnHeader = new ColumnHeader(); + MessageColumnHeader = new ColumnHeader(); SuspendLayout(); // // ProcessorsProgressBar @@ -40,7 +43,7 @@ partial class ProgressForm ProcessorsProgressBar.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; ProcessorsProgressBar.Location = new Point(12, 27); ProcessorsProgressBar.Name = "ProcessorsProgressBar"; - ProcessorsProgressBar.Size = new Size(439, 23); + ProcessorsProgressBar.Size = new Size(537, 23); ProcessorsProgressBar.TabIndex = 0; // // ProcessorsLabel @@ -48,7 +51,7 @@ partial class ProgressForm ProcessorsLabel.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; ProcessorsLabel.Location = new Point(12, 9); ProcessorsLabel.Name = "ProcessorsLabel"; - ProcessorsLabel.Size = new Size(439, 15); + ProcessorsLabel.Size = new Size(537, 15); ProcessorsLabel.TabIndex = 1; ProcessorsLabel.Text = "ProcessorsLabel"; // @@ -57,7 +60,7 @@ partial class ProgressForm LocalLabel.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; LocalLabel.Location = new Point(12, 59); LocalLabel.Name = "LocalLabel"; - LocalLabel.Size = new Size(439, 15); + LocalLabel.Size = new Size(537, 15); LocalLabel.TabIndex = 2; LocalLabel.Text = "LocalLabel"; // @@ -66,24 +69,42 @@ partial class ProgressForm LocalProgressBar.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; LocalProgressBar.Location = new Point(12, 77); LocalProgressBar.Name = "LocalProgressBar"; - LocalProgressBar.Size = new Size(439, 23); + LocalProgressBar.Size = new Size(537, 23); LocalProgressBar.TabIndex = 3; // // MessageListView // MessageListView.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + MessageListView.Columns.AddRange(new ColumnHeader[] { TimeColumnHeader, TypeColumnHeader, MessageColumnHeader }); + MessageListView.GridLines = true; + MessageListView.HeaderStyle = ColumnHeaderStyle.Nonclickable; MessageListView.Location = new Point(12, 114); MessageListView.Name = "MessageListView"; - MessageListView.Size = new Size(439, 316); + MessageListView.Size = new Size(537, 450); MessageListView.TabIndex = 4; MessageListView.UseCompatibleStateImageBehavior = false; - MessageListView.View = View.List; + MessageListView.View = View.Details; + // + // TimeColumnHeader + // + TimeColumnHeader.Text = "Время"; + TimeColumnHeader.Width = 80; + // + // TypeColumnHeader + // + TypeColumnHeader.Text = "Тип"; + TypeColumnHeader.Width = 80; + // + // MessageColumnHeader + // + MessageColumnHeader.Text = "Сообщение"; + MessageColumnHeader.Width = 2000; // // ProgressForm // AutoScaleDimensions = new SizeF(7F, 15F); AutoScaleMode = AutoScaleMode.Font; - ClientSize = new Size(463, 442); + ClientSize = new Size(561, 576); Controls.Add(MessageListView); Controls.Add(LocalProgressBar); Controls.Add(LocalLabel); @@ -101,4 +122,7 @@ partial class ProgressForm private Label LocalLabel; private ProgressBar LocalProgressBar; private ListView MessageListView; + private ColumnHeader TimeColumnHeader; + private ColumnHeader TypeColumnHeader; + private ColumnHeader MessageColumnHeader; } \ No newline at end of file diff --git a/Pf2eDataScrapper/Forms/ProgressForm.cs b/Pf2eDataScrapper/Forms/ProgressForm.cs index f262e04..18f6a89 100644 --- a/Pf2eDataScrapper/Forms/ProgressForm.cs +++ b/Pf2eDataScrapper/Forms/ProgressForm.cs @@ -15,6 +15,14 @@ internal partial class ProgressForm : Form Progress.ProgressUpdate += Progress_ProgressUpdate; Progress.ProgressMessage += Progress_ProgressMessage; + + MessageListView + .GetType() + .GetProperty( + "DoubleBuffered", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic + )! + .SetValue(MessageListView, true, null); } private void Progress_ProgressUpdate(ProcessProgress sender) @@ -36,15 +44,29 @@ internal partial class ProgressForm : Form Application.DoEvents(); } - private void Progress_ProgressMessage(ProcessProgress sender, string message) + private void Progress_ProgressMessage(ProcessProgress sender, LogLevel level, string message) { if (InvokeRequired) { - Invoke(Progress_ProgressMessage, sender, message); + Invoke(Progress_ProgressMessage, sender, level, message); return; } - MessageListView.Items.Add(message); + ListViewItem item = new([DateTime.Now.ToString("HH:mm:ss.fff"), level.ToString(), message]) + { + BackColor = level switch + { + LogLevel.Important => Color.Salmon, + LogLevel.Warning => Color.Khaki, + LogLevel.Information => Color.White, + LogLevel.Progress => Color.LightGray, + _ => Color.White, + }, + }; + + MessageListView.Items.Add(item); + //MessageListView.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent); + //MessageListView.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize); Application.DoEvents(); } diff --git a/Pf2eDataScrapper/Forms/StartForm.Designer.cs b/Pf2eDataScrapper/Forms/StartForm.Designer.cs index 06e6f1b..297306d 100644 --- a/Pf2eDataScrapper/Forms/StartForm.Designer.cs +++ b/Pf2eDataScrapper/Forms/StartForm.Designer.cs @@ -36,18 +36,26 @@ partial class StartForm SelectAllButton = new Button(); DeselectAllButton = new Button(); StartButton = new Button(); + MainMenuStrip = new MenuStrip(); + FileToolStripMenuItem = new ToolStripMenuItem(); + ExitToolStripMenuItem = new ToolStripMenuItem(); + WindowsToolStripMenuItem = new ToolStripMenuItem(); + ImportSettingsWindowsToolStripMenuItem = new ToolStripMenuItem(); + EntityViewWindowToolStripMenuItem = new ToolStripMenuItem(); + StartToolStripMenuItem = new ToolStripMenuItem(); ((System.ComponentModel.ISupportInitialize)MainSplitContainer).BeginInit(); MainSplitContainer.Panel1.SuspendLayout(); MainSplitContainer.Panel2.SuspendLayout(); MainSplitContainer.SuspendLayout(); PropertiesGroupBox.SuspendLayout(); ProcessorsGroupBox.SuspendLayout(); + MainMenuStrip.SuspendLayout(); SuspendLayout(); // // MainSplitContainer // MainSplitContainer.Dock = DockStyle.Fill; - MainSplitContainer.Location = new Point(0, 0); + MainSplitContainer.Location = new Point(0, 24); MainSplitContainer.Name = "MainSplitContainer"; // // MainSplitContainer.Panel1 @@ -60,7 +68,7 @@ partial class StartForm MainSplitContainer.Panel2.Controls.Add(ProcessorsGroupBox); MainSplitContainer.Panel2.Controls.Add(StartButton); MainSplitContainer.Panel2MinSize = 300; - MainSplitContainer.Size = new Size(931, 677); + MainSplitContainer.Size = new Size(931, 653); MainSplitContainer.SplitterDistance = 300; MainSplitContainer.TabIndex = 0; // @@ -70,7 +78,7 @@ partial class StartForm PropertiesGroupBox.Controls.Add(SettingsPropertyGrid); PropertiesGroupBox.Location = new Point(3, 3); PropertiesGroupBox.Name = "PropertiesGroupBox"; - PropertiesGroupBox.Size = new Size(294, 671); + PropertiesGroupBox.Size = new Size(294, 647); PropertiesGroupBox.TabIndex = 0; PropertiesGroupBox.TabStop = false; PropertiesGroupBox.Text = "Свойства"; @@ -81,7 +89,7 @@ partial class StartForm SettingsPropertyGrid.Dock = DockStyle.Fill; SettingsPropertyGrid.Location = new Point(3, 19); SettingsPropertyGrid.Name = "SettingsPropertyGrid"; - SettingsPropertyGrid.Size = new Size(288, 649); + SettingsPropertyGrid.Size = new Size(288, 625); SettingsPropertyGrid.TabIndex = 0; // // ProcessorsGroupBox @@ -92,7 +100,7 @@ partial class StartForm ProcessorsGroupBox.Controls.Add(DeselectAllButton); ProcessorsGroupBox.Location = new Point(3, 3); ProcessorsGroupBox.Name = "ProcessorsGroupBox"; - ProcessorsGroupBox.Size = new Size(621, 602); + ProcessorsGroupBox.Size = new Size(621, 578); ProcessorsGroupBox.TabIndex = 1; ProcessorsGroupBox.TabStop = false; ProcessorsGroupBox.Text = "Обработчики"; @@ -102,7 +110,7 @@ partial class StartForm ProcessorCollectionControl.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; ProcessorCollectionControl.Location = new Point(6, 48); ProcessorCollectionControl.Name = "ProcessorCollectionControl"; - ProcessorCollectionControl.Size = new Size(609, 548); + ProcessorCollectionControl.Size = new Size(609, 524); ProcessorCollectionControl.TabIndex = 2; // // SelectAllButton @@ -127,7 +135,7 @@ partial class StartForm // StartButton.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; StartButton.Font = new Font("Segoe UI", 18F, FontStyle.Bold); - StartButton.Location = new Point(3, 611); + StartButton.Location = new Point(3, 587); StartButton.Name = "StartButton"; StartButton.Size = new Size(621, 63); StartButton.TabIndex = 0; @@ -135,12 +143,64 @@ partial class StartForm StartButton.UseVisualStyleBackColor = true; StartButton.Click += StartButton_Click; // + // MainMenuStrip + // + MainMenuStrip.Items.AddRange(new ToolStripItem[] { FileToolStripMenuItem, WindowsToolStripMenuItem, StartToolStripMenuItem }); + MainMenuStrip.Location = new Point(0, 0); + MainMenuStrip.Name = "MainMenuStrip"; + MainMenuStrip.Size = new Size(931, 24); + MainMenuStrip.TabIndex = 1; + MainMenuStrip.Text = "menuStrip1"; + // + // FileToolStripMenuItem + // + FileToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { ExitToolStripMenuItem }); + FileToolStripMenuItem.Name = "FileToolStripMenuItem"; + FileToolStripMenuItem.Size = new Size(48, 20); + FileToolStripMenuItem.Text = "Файл"; + // + // ExitToolStripMenuItem + // + ExitToolStripMenuItem.Name = "ExitToolStripMenuItem"; + ExitToolStripMenuItem.Size = new Size(109, 22); + ExitToolStripMenuItem.Text = "Выход"; + ExitToolStripMenuItem.Click += ExitToolStripMenuItem_Click; + // + // WindowsToolStripMenuItem + // + WindowsToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { ImportSettingsWindowsToolStripMenuItem, EntityViewWindowToolStripMenuItem }); + WindowsToolStripMenuItem.Name = "WindowsToolStripMenuItem"; + WindowsToolStripMenuItem.Size = new Size(47, 20); + WindowsToolStripMenuItem.Text = "Окна"; + // + // ImportSettingsWindowsToolStripMenuItem + // + ImportSettingsWindowsToolStripMenuItem.Enabled = false; + ImportSettingsWindowsToolStripMenuItem.Name = "ImportSettingsWindowsToolStripMenuItem"; + ImportSettingsWindowsToolStripMenuItem.Size = new Size(228, 22); + ImportSettingsWindowsToolStripMenuItem.Text = "Настройка импорта данных"; + // + // EntityViewWindowToolStripMenuItem + // + EntityViewWindowToolStripMenuItem.Name = "EntityViewWindowToolStripMenuItem"; + EntityViewWindowToolStripMenuItem.Size = new Size(228, 22); + EntityViewWindowToolStripMenuItem.Text = "Просмотр сущностей"; + EntityViewWindowToolStripMenuItem.Click += EntityViewWindowToolStripMenuItem_Click; + // + // StartToolStripMenuItem + // + StartToolStripMenuItem.Name = "StartToolStripMenuItem"; + StartToolStripMenuItem.Size = new Size(108, 20); + StartToolStripMenuItem.Text = "Запуск импорта"; + StartToolStripMenuItem.Click += StartToolStripMenuItem_Click; + // // StartForm // AutoScaleDimensions = new SizeF(7F, 15F); AutoScaleMode = AutoScaleMode.Font; ClientSize = new Size(931, 677); Controls.Add(MainSplitContainer); + Controls.Add(MainMenuStrip); Name = "StartForm"; Text = "Настройки импорта данных"; Load += StartForm_Load; @@ -150,7 +210,10 @@ partial class StartForm MainSplitContainer.ResumeLayout(false); PropertiesGroupBox.ResumeLayout(false); ProcessorsGroupBox.ResumeLayout(false); + MainMenuStrip.ResumeLayout(false); + MainMenuStrip.PerformLayout(); ResumeLayout(false); + PerformLayout(); } #endregion @@ -163,4 +226,11 @@ partial class StartForm private Button SelectAllButton; private Button DeselectAllButton; private Controls.ProcessorCollectionControl ProcessorCollectionControl; + private MenuStrip MainMenuStrip; + private ToolStripMenuItem FileToolStripMenuItem; + private ToolStripMenuItem ExitToolStripMenuItem; + private ToolStripMenuItem WindowsToolStripMenuItem; + private ToolStripMenuItem ImportSettingsWindowsToolStripMenuItem; + private ToolStripMenuItem EntityViewWindowToolStripMenuItem; + private ToolStripMenuItem StartToolStripMenuItem; } \ No newline at end of file diff --git a/Pf2eDataScrapper/Forms/StartForm.cs b/Pf2eDataScrapper/Forms/StartForm.cs index a782517..899bbda 100644 --- a/Pf2eDataScrapper/Forms/StartForm.cs +++ b/Pf2eDataScrapper/Forms/StartForm.cs @@ -1,16 +1,16 @@ -using System.ComponentModel; -using System.Reflection; -using Pf2eDataScrapper.App; +using Pf2eDataScrapper.App; namespace Pf2eDataScrapper.Forms; internal partial class StartForm : Form { private ProcessorCollection ProcessorCollection { get; } + public ApplicationSettings Settings { get; } - public StartForm(ProcessorCollection processorCollection) + public StartForm(ProcessorCollection processorCollection, ApplicationSettings settings) { ProcessorCollection = processorCollection; + Settings = settings; InitializeComponent(); } @@ -18,6 +18,8 @@ internal partial class StartForm : Form private void StartForm_Load(object sender, EventArgs e) { ProcessorCollectionControl.Collection = ProcessorCollection; + + SettingsPropertyGrid.SelectedObject = Settings; } /* @@ -76,12 +78,30 @@ internal partial class StartForm : Form private void StartButton_Click(object sender, EventArgs e) { - StartButtonPressed?.Invoke(); + ApplicationStateChange?.Invoke(this, ApplicationStage.ImportProcess); } - public delegate void StartButtonPressedDelegate(); + #endregion - public event StartButtonPressedDelegate? StartButtonPressed; + #region "Основное меню" + + + private void ExitToolStripMenuItem_Click(object sender, EventArgs e) + { + Close(); + } + + private void EntityViewWindowToolStripMenuItem_Click(object sender, EventArgs e) + { + ApplicationStateChange?.Invoke(this, ApplicationStage.Viewer); + } + + private void StartToolStripMenuItem_Click(object sender, EventArgs e) + { + ApplicationStateChange?.Invoke(this, ApplicationStage.ImportProcess); + } + + public event ImportApplication.ApplicationStateChangeDelegate? ApplicationStateChange; #endregion } diff --git a/Pf2eDataScrapper/Forms/StartForm.resx b/Pf2eDataScrapper/Forms/StartForm.resx index 8b2ff64..fc37e2c 100644 --- a/Pf2eDataScrapper/Forms/StartForm.resx +++ b/Pf2eDataScrapper/Forms/StartForm.resx @@ -117,4 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + 17, 17 + \ No newline at end of file diff --git a/Pf2eDataScrapper/Helpers/DatabaseHelper.cs b/Pf2eDataScrapper/Helpers/DatabaseHelper.cs index 85bebfa..bcf30e0 100644 --- a/Pf2eDataScrapper/Helpers/DatabaseHelper.cs +++ b/Pf2eDataScrapper/Helpers/DatabaseHelper.cs @@ -29,7 +29,7 @@ public class DatabaseHelper /// public int NextEntityId => ++_id; - private DatabaseHelper(Pf2eDbContext context, SlugHelper slugHelper) + public DatabaseHelper(Pf2eDbContext context, SlugHelper slugHelper) { Context = context; Slugger = slugHelper; diff --git a/Pf2eDataScrapper/Helpers/EntityView.cs b/Pf2eDataScrapper/Helpers/EntityView.cs new file mode 100644 index 0000000..cd440b2 --- /dev/null +++ b/Pf2eDataScrapper/Helpers/EntityView.cs @@ -0,0 +1,138 @@ +using Microsoft.EntityFrameworkCore; +using Pf2eModel; +using Pf2eModel.Model; +using Pf2eModel.Model.Base; +using Pf2eModel.Model.Enum; +using Pf2eModel.Model.Structure; + +namespace Pf2eViewer; + +/// +/// Класс, обрабатывающий запросы к сущностям и подключающий необходимые данные. +/// +public class EntityView +{ + /// + /// Корневая сущность. + /// + public Entity? Entity { get; } + + /// + /// Основная сущность. + /// + public IEntity? SubEntity { get; } + + /// + /// Конструктор класса, который получает сущность из базы данных и заполняет её данными. + /// + /// Контекст базы данных. + /// Тип запрашиваемой сущности. + /// Идентификатор запрашиваемой сущности. + public EntityView(Pf2eDbContext context, EntityType type, int id) + { + Getter? getter = _getters.FirstOrDefault(g => g.Type == type); + + Entity fullEntity = + context + .Entities.Where(e => e.Id == id && e.Type == type) + .Include(e => e.Sources) + .ThenInclude(s => s.Source) + .Include(e => e.Keywords) + .Include(e => e.Externals) + .ThenInclude(x => x.Target) + .Include(e => e.Traits) + .ThenInclude(t => t.Entity) + .FirstOrDefault() ?? throw new("Entity is null"); + + IEntity? subEntity = + (getter != null) + ? getter.Get(id, context) + : (IEntity?)context.Find(EntityOfTypeAttribute.Resolve(fullEntity), id); + + Entity = fullEntity; + SubEntity = subEntity; + } + + /// + /// Словарь, содержащий методы получения сущностей из базы данных. + /// + private readonly ICollection _getters = + [ + new Getter( + EntityType.Ancestry, + context => context.Ancestries.Include(a => a.AncestryBoosts) + ), + new Getter( + EntityType.Archetype, + context => + context + .Archetypes.Include(a => a.DedicationFeat) + .ThenInclude(f => f.Entity) + .Include(a => a.Feats) + .ThenInclude(f => f.Entity) + ), + new Getter( + EntityType.Background, + context => + context + .Backgrounds.Include(b => b.BackgroundBoosts) + .Include(b => b.BackgroundSkills) + .ThenInclude(t => t.Training) + .Include(b => b.BackgroundItems) + ), + new Getter( + EntityType.Class, + context => + context + .Classes.Include(c => c.ClassKeyAbilities) + .Include(c => c.ClassTrainings) + .ThenInclude(t => t.Training) + .Include(c => c.ClassProgressions) + ), + new Getter( + EntityType.Feat, + context => + context.Feats.Include(f => f.Archetypes).ThenInclude(a => a.Entity).AsQueryable() + ), + new Getter( + EntityType.Spell, + context => context.Spells.Include(s => s.SpellAreas).Include(s => s.SpellDefenses) + ), + new Getter( + EntityType.Ritual, + context => + context + .Rituals.Include(r => r.Area) + .Include(r => r.Checks) + .ThenInclude(c => c.Training) + .AsQueryable() + ), + ]; + + /// + /// Абстрактный класс, который определяет метод получения сущности из базы данных. + /// + /// Связанный тип сущности. + private abstract class Getter(EntityType type) + { + public readonly EntityType Type = type; + + public abstract IEntity? Get(int id, Pf2eDbContext context); + } + + /// + /// Класс, определяющий метод получения сущности из базы данных. + /// + /// Тип данных основной сущности. + /// Связанный тип сущности. + /// Метод получения сущности из базы данных. + private class Getter(EntityType type, Func> method) + : Getter(type) + where T : IEntity + { + public override IEntity? Get(int id, Pf2eDbContext context) + { + return method.Invoke(context).FirstOrDefault(c => c.Id == id); + } + } +} diff --git a/Pf2eDataScrapper/Helpers/ILogger.cs b/Pf2eDataScrapper/Helpers/ILogger.cs new file mode 100644 index 0000000..adb1550 --- /dev/null +++ b/Pf2eDataScrapper/Helpers/ILogger.cs @@ -0,0 +1,22 @@ +namespace Pf2eDataScrapper.Helpers; + +internal abstract class AbstractLogger +{ + public abstract void Message(LogLevel level, string message); + + public void Important(string message) => Message(LogLevel.Important, message); + + public void Warning(string message) => Message(LogLevel.Warning, message); + + public void Information(string message) => Message(LogLevel.Information, message); + + public void Progress(string message) => Message(LogLevel.Progress, message); +} + +internal enum LogLevel +{ + Important, + Warning, + Information, + Progress, +} diff --git a/Pf2eDataScrapper/Helpers/ProcessProgress.cs b/Pf2eDataScrapper/Helpers/ProcessProgress.cs index ec0e608..168fbb9 100644 --- a/Pf2eDataScrapper/Helpers/ProcessProgress.cs +++ b/Pf2eDataScrapper/Helpers/ProcessProgress.cs @@ -2,7 +2,7 @@ namespace Pf2eDataScrapper.Helpers; -internal class ProcessProgress +internal class ProcessProgress : AbstractLogger { /// /// Прогресс обработки обработчиков . @@ -31,15 +31,19 @@ internal class ProcessProgress ProgressUpdate?.Invoke(this); } - public void Message(string message) + public override void Message(LogLevel level, string message) { - ProgressMessage?.Invoke(this, message); + ProgressMessage?.Invoke(this, level, message); } public delegate void ProgressUpdateDelegate(ProcessProgress sender); public event ProgressUpdateDelegate? ProgressUpdate; - public delegate void ProgressMessageDelegate(ProcessProgress sender, string message); + public delegate void ProgressMessageDelegate( + ProcessProgress sender, + LogLevel level, + string message + ); public event ProgressMessageDelegate? ProgressMessage; /// diff --git a/Pf2eDataScrapper/ImportApplication.cs b/Pf2eDataScrapper/ImportApplication.cs index aaccccc..23d7dbc 100644 --- a/Pf2eDataScrapper/ImportApplication.cs +++ b/Pf2eDataScrapper/ImportApplication.cs @@ -4,6 +4,7 @@ using Pf2eDataScrapper.Forms; using Pf2eDataScrapper.Helpers; using Pf2eDataScrapper.Processors; using Pf2eModel; +using Pf2eViewer; using Slugify; namespace Pf2eDataScrapper; @@ -50,10 +51,26 @@ internal sealed class ImportApplication : ApplicationContext private ProgressForm? _progressForm; + private EntityViewForm? _entityViewForm; + #endregion #region "Жизненный цикл" + private Form? CurrentForm { get; set; } = null; + + public delegate void ApplicationStateChangeDelegate(object sender, ApplicationStage nextStage); + + public void ApplicationStateChange(object sender, ApplicationStage nextStage) + { + if (sender != CurrentForm) + { + return; + } + + CurrentStage = nextStage; + } + private void StartStage(ApplicationStage stage) { switch (stage) @@ -66,15 +83,14 @@ internal sealed class ImportApplication : ApplicationContext case ApplicationStage.InitialSettings: _startForm = ActivatorUtilities.CreateInstance(ServiceProvider); _startForm.StartPosition = FormStartPosition.CenterParent; - _startForm.StartButtonPressed += () => - { - CurrentStage = ApplicationStage.ProcessorsWorking; - }; + _startForm.ApplicationStateChange += ApplicationStateChange; _startForm.FormClosing += HandleFormClosing; _startForm.Show(); + + CurrentForm = _startForm; break; - case ApplicationStage.ProcessorsWorking: + case ApplicationStage.ImportProcess: ProcessorExecutor executor = ActivatorUtilities.CreateInstance( ServiceProvider ); @@ -84,10 +100,21 @@ internal sealed class ImportApplication : ApplicationContext _progressForm.FormClosing += HandleFormClosing; _progressForm.Show(); + + CurrentForm = _progressForm; executor.Start(); break; case ApplicationStage.Viewer: + _entityViewForm = ActivatorUtilities.CreateInstance( + ServiceProvider + ); + _entityViewForm.StartPosition = FormStartPosition.CenterParent; + _entityViewForm.ApplicationStateChange += ApplicationStateChange; + _entityViewForm.FormClosing += HandleFormClosing; + + _entityViewForm.Show(); + CurrentForm = _entityViewForm; break; default: @@ -99,6 +126,7 @@ internal sealed class ImportApplication : ApplicationContext private void CloseStage(ApplicationStage stage) { + CurrentForm = null; switch (stage) { case ApplicationStage.BootUp: @@ -111,10 +139,13 @@ internal sealed class ImportApplication : ApplicationContext _startForm = null; break; - case ApplicationStage.ProcessorsWorking: + case ApplicationStage.ImportProcess: break; case ApplicationStage.Viewer: + _entityViewForm!.Hide(); + _entityViewForm!.Close(); + _entityViewForm = null; break; default: @@ -126,16 +157,13 @@ internal sealed class ImportApplication : ApplicationContext private void HandleFormClosing(object? sender, FormClosingEventArgs e) { - switch (sender) + if (sender != CurrentForm) { - case StartForm: - e.Cancel = CurrentStage != ApplicationStage.InitialSettings; - if (!e.Cancel) - { - ExitThread(); - } - return; + e.Cancel = true; + return; } + + ExitThread(); } #endregion @@ -177,6 +205,7 @@ internal enum ApplicationStage { BootUp, InitialSettings, - ProcessorsWorking, + ImportProcess, Viewer, + Rules, } diff --git a/Pf2eDataScrapper/Importers/Implementations/Aonprd/AonprdImporter.cs b/Pf2eDataScrapper/Importers/Implementations/Aonprd/AonprdImporter.cs index 84ea587..e6b7095 100644 --- a/Pf2eDataScrapper/Importers/Implementations/Aonprd/AonprdImporter.cs +++ b/Pf2eDataScrapper/Importers/Implementations/Aonprd/AonprdImporter.cs @@ -33,6 +33,8 @@ internal class AonprdImporter : AbstractImporter services.AddSingleton(); services.AddSingleton(); services.AddTransient(); + + AonprdProcessorInputExtension.RegisterServices(services); } #endregion diff --git a/Pf2eDataScrapper/Importers/Implementations/Aonprd/AonprdRepository.cs b/Pf2eDataScrapper/Importers/Implementations/Aonprd/AonprdRepository.cs index 9e1a631..7569127 100644 --- a/Pf2eDataScrapper/Importers/Implementations/Aonprd/AonprdRepository.cs +++ b/Pf2eDataScrapper/Importers/Implementations/Aonprd/AonprdRepository.cs @@ -43,11 +43,11 @@ internal class AonprdRepository(AonprdImporter importer) : AbstractImporterRepos if (CheckRevision()) { progress.Local.Complete(); - progress.Message($"Данные Aonprd актуальны, текущая ревизия {Revision}."); + progress.Information($"Данные Aonprd актуальны, текущая ревизия {Revision}."); return; } - progress.Message( + progress.Information( $"Данные Aonprd устарели, обновляем {RepositoryDirPath} до ревизии {Revision}..." ); diff --git a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Compendium/AonprdCompendium.cs b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Compendium/AonprdCompendium.cs index 3980c48..24adc29 100644 --- a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Compendium/AonprdCompendium.cs +++ b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Compendium/AonprdCompendium.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json.Linq; +using System.Collections.Concurrent; +using Newtonsoft.Json.Linq; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium; @@ -9,33 +10,10 @@ internal sealed class AonprdCompendium(AonprdRepository repository) : AbstractCo { public override AonprdRepository Repository { get; } = repository; - /// - /// Загрузка данных компендиума. - /// - public void Load() - { - using StreamReader indexFile = new( - Path.Combine(Repository.RepositoryDirPath, "index.json") - ); - JObject indexJson = JObject.Parse(indexFile.ReadToEnd()); - - foreach ((string file, JToken? value) in indexJson) - { - JArray items = (JArray)value!; - - foreach (JToken item in items) - { - AonprdCompendiumItem compendiumItem = new(item.ToString(), file); - - Items.Add(compendiumItem); - } - } - } - /// /// Коллекция всех записей компендиума. /// - public List Items { get; } = []; + public ConcurrentBag Items { get; } = []; /// /// Возвращает количество элементов в компендиуме. @@ -50,20 +28,6 @@ internal sealed class AonprdCompendium(AonprdRepository repository) : AbstractCo /// Количество элементов. public override int Count(string type) => Items.Count(i => i.Type == type); - /// - /// Возвращает список всех файлов. - /// - /// Список искомых файлов. - private IEnumerable Files() => Items.Select(t => t.File).Distinct(); - - /// - /// Возвращает список файлов, в которых есть сущности указанного типа данных. - /// - /// Тип данных. - /// Список искомых файлов. - private IEnumerable Files(string type) => - Items.Where(t => t.Type == type).Select(t => t.File).Distinct(); - /// /// Получение итератора сущностей. Если указан тип - то только указанного типа. /// @@ -71,7 +35,11 @@ internal sealed class AonprdCompendium(AonprdRepository repository) : AbstractCo /// Итератор сущностей. public override IEnumerable> Iterate(string? type = null) { - foreach (string file in type is null ? Files() : Files(type)) + IEnumerable files = type is null + ? Items.Select(t => t.File).Distinct() + : Items.Where(t => t.Type == type).Select(t => t.File).Distinct(); + + foreach (string file in files) { Dictionary fileReferences = Items .Where(i => i.File == file) diff --git a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Helpers/AonprdBuilderGlobals.cs b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Helpers/AonprdBuilderGlobals.cs index f065e5b..9fe0ceb 100644 --- a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Helpers/AonprdBuilderGlobals.cs +++ b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Helpers/AonprdBuilderGlobals.cs @@ -16,10 +16,5 @@ internal class AonprdBuilderGlobals /// /// Конструктор класса . /// - private AonprdBuilderGlobals() { } - - /// - /// Экземпляр класса . - /// - public static readonly AonprdBuilderGlobals Instance = new(); + public AonprdBuilderGlobals() { } } diff --git a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Helpers/AonprdEnumResolver.cs b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Helpers/AonprdEnumResolver.cs deleted file mode 100644 index 1f43999..0000000 --- a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Helpers/AonprdEnumResolver.cs +++ /dev/null @@ -1,134 +0,0 @@ -using Pf2eModel.Model.Enum; - -namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Helpers; - -/// -/// Класс, помогающий разрешить строковые значения с Архивов Нефиса в перечисления. -/// -public static class AonprdEnumResolver -{ - /// - /// Коллекция классов, разрешающих строковые значения в перечисления. - /// - private static readonly ICollection Solvers = - [ - new Solver( - new() - { - { "Rulebooks", SourceCategory.Rulebooks }, - { "Adventure Paths", SourceCategory.AdventurePaths }, - { "Lost Omens", SourceCategory.LostOmens }, - { "Adventures", SourceCategory.Adventures }, - { "Comics", SourceCategory.Comics }, - { "Blog Posts", SourceCategory.BlogPosts }, - { "Society", SourceCategory.Society }, - } - ), - new Solver( - new() - { - { "Low-Light VisionEnum", VisionEnum.LowLightVision }, - { "Darkvision", VisionEnum.DarkVision }, - } - ), - new Solver( - new() - { - { "Consitution", AttributeValues.Constitution }, - { "Other", AttributeValues.Free }, - { "other", AttributeValues.Free }, - } - ), - new Solver( - new() - { - { "Single Action", ActionEnum.SingleAction }, - { "Two Actions", ActionEnum.TwoActions }, - { "Three Actions", ActionEnum.ThreeActions }, - { "Reaction", ActionEnum.Reaction }, - { "Free Action", ActionEnum.FreeAction }, - { "Single Action to Three Actions", ActionEnum.SingleActionToThreeActions }, - { "Single Action or more Actions", ActionEnum.SingleActionToThreeActions }, - { "Single Action or Two Actions", ActionEnum.SingleActionToTwoActions }, - { "Single Action to Two Actions", ActionEnum.SingleActionToTwoActions }, - { "Two Actions or Three Actions", ActionEnum.TwoActionsToThreeActions }, - { "Two Actions to 2 rounds", ActionEnum.TwoActionsToTwoRounds }, - { "Reaction or One Action", ActionEnum.ReactionOrOneAction }, - { "Reaction or Two Actions", ActionEnum.ReactionOrTwoAction }, - { "Reaction or Three Actions", ActionEnum.ReactionOrThreeAction }, - { "Free Action or One Action", ActionEnum.FreeActionOrOneAction }, - { "1 Minute", ActionEnum.OneMinute }, - { "1 minute", ActionEnum.OneMinute }, - { "5 minutes", ActionEnum.FiveMinutes }, - { "10 minutes", ActionEnum.TenMinutes }, - { "30 minutes", ActionEnum.ThirtyMinutes }, - { "1 hour", ActionEnum.OneHour }, - { "Nethys Note: No cast time was printed", ActionEnum.Unknown }, - } - ), - ]; - - /// - /// Разрешить строковое значение с Архивов Нефиса в перечисление. - /// - /// Тип искомого перечисления. - /// Обрабатываемое строковоее значение. - /// Значение искомого перечисления. - /// - /// Исключение, если для указанного типа перечисления не указан класс, разрешающий строковые значения. - /// - /// - /// Исключение, если указанное значение не получилось преобразовать искомое значение. - /// - public static T Resolve(string value) - where T : Enum - { - Type type = typeof(T); - - Solver? solver = Solvers.FirstOrDefault(s => s.Type == type); - - if (solver is null || solver.GetType() != typeof(Solver)) - { - throw new EnumResolverEnumUnknownException(type); - } - - Solver tSolver = (Solver)solver; - - return tSolver.Data.TryGetValue(value, out T? data) - ? data - : throw new EnumResolverValueUnknownException(type, value); - } - - /// - /// Класс, разрешающий строковые значения в перечисления. - /// - private abstract class Solver - { - public abstract Type Type { get; } - } - - /// - /// Абстрактный общий класс, разрешающий строковые значения в перечисления. - /// - private class Solver(Dictionary data) : Solver - where T : Enum - { - public override Type Type { get; } = typeof(T); - public Dictionary Data { get; } = data; - } - - /// - /// Исключение, указывающее, что для указанного типа перечисления не указан класс, разрешающий строковые значения. - /// - /// Тип искомого перечисления. - public class EnumResolverEnumUnknownException(Type t) - : Exception($"Can't resolve type '{t.FullName}': Solver not defined"); - - /// - /// Исключение, указывающее, что указанное значение не получилось преобразовать искомое значение. - /// - /// Тип искомого перечисления. - /// Искомое значение. - public class EnumResolverValueUnknownException(Type t, string value) - : Exception($"Can't resolve type '{t.FullName}': Value '{value}' is not matched"); -} diff --git a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Helpers/AonprdProcessorInputExtension.cs b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Helpers/AonprdProcessorInputExtension.cs index ed5f530..b29847a 100644 --- a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Helpers/AonprdProcessorInputExtension.cs +++ b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Helpers/AonprdProcessorInputExtension.cs @@ -1,25 +1,15 @@ -using Pf2eDataScrapper.Processors; +using Microsoft.Extensions.DependencyInjection; +using Pf2eDataScrapper.Processors; +using Pf2eModel.Model.Enum; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Helpers; internal static class AonprdProcessorInputExtension { - public static T Enum(this ProcessorInput input, string value) - where T : Enum - { - try - { - return AonprdEnumResolver.Resolve(value); - } - catch - { - return input.Enum(value); - } - } - /// /// Обработать строку, содержащую информацию о количестве действий. /// + /// /// Входная строка с данными. /// Структура, хранящая информацию о количетсве действий. public static ActionStruct ResolveActionString(this ProcessorInput _, string input) @@ -129,4 +119,78 @@ internal static class AonprdProcessorInputExtension /// public bool TwoRounds { get; set; } } + + public class SourceCategorySolver : EnumSolver + { + public override Dictionary Data { get; } = + new() + { + { "Rulebooks", SourceCategory.Rulebooks }, + { "Adventure Paths", SourceCategory.AdventurePaths }, + { "Lost Omens", SourceCategory.LostOmens }, + { "Adventures", SourceCategory.Adventures }, + { "Comics", SourceCategory.Comics }, + { "Blog Posts", SourceCategory.BlogPosts }, + { "Society", SourceCategory.Society }, + }; + } + + public class VisionEnumSolver : EnumSolver + { + public override Dictionary Data { get; } = + new() + { + { "Low-Light VisionEnum", VisionEnum.LowLightVision }, + { "Darkvision", VisionEnum.DarkVision }, + }; + } + + public class AttributeValuesSolver : EnumSolver + { + public override Dictionary Data { get; } = + new() + { + { "Consitution", AttributeValues.Constitution }, + { "Other", AttributeValues.Free }, + { "other", AttributeValues.Free }, + }; + } + + public class ActionEnumSolver : EnumSolver + { + public override Dictionary Data { get; } = + new() + { + { "Single Action", ActionEnum.SingleAction }, + { "Two Actions", ActionEnum.TwoActions }, + { "Three Actions", ActionEnum.ThreeActions }, + { "Reaction", ActionEnum.Reaction }, + { "Free Action", ActionEnum.FreeAction }, + { "Single Action to Three Actions", ActionEnum.SingleActionToThreeActions }, + { "Single Action or more Actions", ActionEnum.SingleActionToThreeActions }, + { "Single Action or Two Actions", ActionEnum.SingleActionToTwoActions }, + { "Single Action to Two Actions", ActionEnum.SingleActionToTwoActions }, + { "Two Actions or Three Actions", ActionEnum.TwoActionsToThreeActions }, + { "Two Actions to 2 rounds", ActionEnum.TwoActionsToTwoRounds }, + { "Reaction or One Action", ActionEnum.ReactionOrOneAction }, + { "Reaction or Two Actions", ActionEnum.ReactionOrTwoAction }, + { "Reaction or Three Actions", ActionEnum.ReactionOrThreeAction }, + { "Free Action or One Action", ActionEnum.FreeActionOrOneAction }, + { "1 Minute", ActionEnum.OneMinute }, + { "1 minute", ActionEnum.OneMinute }, + { "5 minutes", ActionEnum.FiveMinutes }, + { "10 minutes", ActionEnum.TenMinutes }, + { "30 minutes", ActionEnum.ThirtyMinutes }, + { "1 hour", ActionEnum.OneHour }, + { "Nethys Note: No cast time was printed", ActionEnum.Unknown }, + }; + } + + public static void RegisterServices(IServiceCollection serviceCollection) + { + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + } } diff --git a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Helpers/StringHelper.cs b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Helpers/StringHelper.cs index 7682c74..12e25b0 100644 --- a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Helpers/StringHelper.cs +++ b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Helpers/StringHelper.cs @@ -11,7 +11,7 @@ internal partial class StringHelper /// /// Конструктор класса /// - private StringHelper() { } + public StringHelper() { } /// /// Разбиение строки по нестабильным строковым разделителям (например, ", ", " or " и т.д.). @@ -67,11 +67,6 @@ internal partial class StringHelper return match.Captures.Count > 0 ? match.Captures[0].Value : ""; } - /// - /// Экземпляр класса . - /// - public static StringHelper Instance { get; } = new(); - /// /// Регулярное выражение для извлечения источника из строки с Markdown-ссылкой. /// diff --git a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/AbstractEntityProcessor.cs b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/AbstractEntityProcessor.cs index 442eec0..5bfbe31 100644 --- a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/AbstractEntityProcessor.cs +++ b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/AbstractEntityProcessor.cs @@ -1,4 +1,6 @@ -using System.Security.Cryptography; +using System.Collections.Concurrent; +using System.Linq; +using System.Security.Cryptography; using System.Text; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; @@ -62,12 +64,7 @@ internal abstract class AbstractEntityProcessor( /// public override void Process() { - if (Input.GetOptional("remaster_id") is not null) - { - LegacyEntityHandler handler = new(this); - LegacyHandlers.Add(handler); - throw new LegacyItemException(handler.LegacyName); - } + ProcessRemaster(); AddEntity(); AddSource(); @@ -199,7 +196,8 @@ internal abstract class AbstractEntityProcessor( { if (!traits.Any(t => t.Entity.Name.Contains(name))) { - throw new Exception($"Trait '{name}' is not found!!!"); + //TODO + //throw new Exception($"Trait '{name}' is not found!!!"); } } @@ -225,6 +223,11 @@ internal abstract class AbstractEntityProcessor( return; } + if (legacyIdArray.Count > 1) + { + throw new("Несколько отметок ремастер-сущности на устаревшие!"); + } + string[] legacyIds = legacyIdArray.ToArray().Select(t => t.ToString()).ToArray(); if (legacyIds.Length == 0) @@ -250,6 +253,40 @@ internal abstract class AbstractEntityProcessor( } } + protected virtual void ProcessRemaster() + { + JArray? remasterIdArray = Input.GetOptional("remaster_id"); + + if (remasterIdArray is null) + { + return; + } + + if (remasterIdArray.Count > 1) + { + throw new("Несколько отметок ремастер-сущности на устаревшие!"); + } + + string[] remasterIds = remasterIdArray.ToArray().Select(t => t.ToString()).ToArray(); + + foreach (string remasterId in remasterIds) + { + LegacyEntityHandler? handler = null; + //LegacyHandlers.FirstOrDefault(h => + // h.RemasterEntities.Contains(remasterId) + //); + + if (handler is null) + { + handler = new(this); + LegacyHandlers.Add(handler); + } + else { } + } + + throw new LegacyItemException(Input.GetRequired("name")); + } + /// /// Получение списка источников сущности. /// @@ -322,20 +359,5 @@ internal abstract class AbstractEntityProcessor( return isLowercase ? hash.ToLower() : hash; } - protected static readonly List LegacyHandlers = []; + protected static readonly ConcurrentBag LegacyHandlers = []; } - -/// -/// Исключение, показывающее, что обработка сущности пропускается как устаревшая. -/// -/// Название сущности. -public class LegacyItemException(string name) - : Exception($"Item '{name}' is linked with newer version, skipping.") { } - -/// -/// Исключение, показывающее, что обработка сущности пропускается. -/// -/// Название сущности. -/// Причина пропуска. -public class SkipItemException(string name, string reason) - : Exception($"Item '{name}' is skipped, reason: {reason}.") { } diff --git a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Common/ExternalProcessor.cs b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Common/ExternalProcessor.cs index c5f99cf..7b8f8ee 100644 --- a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Common/ExternalProcessor.cs +++ b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Common/ExternalProcessor.cs @@ -1,4 +1,5 @@ using Pf2eDataScrapper.App; +using Pf2eDataScrapper.Importers.Implementations.Aonprd.Helpers; using Pf2eDataScrapper.Processors; using Pf2eModel; using Pf2eModel.Model.Base.Targets; @@ -11,9 +12,11 @@ namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Common; Group = ProcessorGroupEnum.AonprdPreparation )] [ProcessorDependencies(["aonprd:repository"])] -internal class ExternalProcessor(Pf2eDbContext context) : AbstractProcessor +internal class ExternalProcessor(Pf2eDbContext context, AonprdBuilderGlobals globals) + : AbstractProcessor { public Pf2eDbContext Context { get; } = context; + public AonprdBuilderGlobals Globals { get; } = globals; public ExternalTarget AonprdTarget = null!; @@ -31,7 +34,6 @@ internal class ExternalProcessor(Pf2eDbContext context) : AbstractProcessor { Context.ExternalTargets.Add(AonprdTarget); - // TODO - // Globals.AonprdTarget = AonprdTarget; + Globals.AonprdTarget = AonprdTarget; } } diff --git a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Common/SkillProcessor.cs b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Common/SkillProcessor.cs index 09815a2..9c84231 100644 --- a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Common/SkillProcessor.cs +++ b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Common/SkillProcessor.cs @@ -13,6 +13,7 @@ namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Common; Description = "Навыки", Group = ProcessorGroupEnum.AonprdPreparation )] +[ProcessorDependencies(["aonprd:compendium"])] internal class SkillProcessor(ProcessorInput input, Pf2eDbContext context) : AbstractInputProcessor(input) { diff --git a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Common/SourceProcessor.cs b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Common/SourceProcessor.cs index c369af6..c20c2c0 100644 --- a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Common/SourceProcessor.cs +++ b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Common/SourceProcessor.cs @@ -12,7 +12,7 @@ namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Common; Description = "Источники", Group = ProcessorGroupEnum.AonprdPreparation )] -[ProcessorDependencies(["aonprd:repository"])] +[ProcessorDependencies(["aonprd:compendium"])] internal class SourceProcessor(ProcessorInput input, Pf2eDbContext context) : AbstractInputProcessor(input) { diff --git a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Preparation/CompendiumLoadProcessor.cs b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Preparation/CompendiumLoadProcessor.cs new file mode 100644 index 0000000..09e1885 --- /dev/null +++ b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Preparation/CompendiumLoadProcessor.cs @@ -0,0 +1,54 @@ +using Newtonsoft.Json.Linq; +using Pf2eDataScrapper.App; +using Pf2eDataScrapper.Helpers; +using Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium; +using Pf2eDataScrapper.Processors; + +namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Preparation; + +[Processor( + "aonprd:compendium", + Description = "Загрузка компендиума", + Group = ProcessorGroupEnum.AonprdPreparation +)] +[ProcessorDependencies(["aonprd:repository"])] +internal class CompendiumLoadProcessor( + AonprdRepository repository, + AonprdCompendium compendium, + ProcessProgress progress +) : IProcessor +{ + public AonprdRepository Repository { get; } = repository; + public AonprdCompendium Compendium { get; } = compendium; + public ProcessProgress Progress { get; } = progress; + + public void Process() + { + using StreamReader indexFile = new( + Path.Combine(Repository.RepositoryDirPath, "index.json") + ); + JObject indexJson = JObject.Parse(indexFile.ReadToEnd()); + + Progress.Local.Reset(indexJson.Count); + + foreach ((string file, JToken? value) in indexJson) + { + Progress.Local.Start(file); + + JArray items = (JArray)value!; + + foreach (JToken item in items) + { + AonprdCompendiumItem compendiumItem = new(item.ToString(), file); + + Compendium.Items.Add(compendiumItem); + } + + Progress.Local.End(); + } + + Progress.Local.Complete(); + } + + public void Save() { } +} diff --git a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Common/RepositoryUpdateProcessor.cs b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Preparation/RepositoryUpdateProcessor.cs similarity index 59% rename from Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Common/RepositoryUpdateProcessor.cs rename to Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Preparation/RepositoryUpdateProcessor.cs index 9e7d9c1..ce5d764 100644 --- a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Common/RepositoryUpdateProcessor.cs +++ b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Preparation/RepositoryUpdateProcessor.cs @@ -1,23 +1,18 @@ using Pf2eDataScrapper.App; using Pf2eDataScrapper.Helpers; -using Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium; using Pf2eDataScrapper.Processors; -namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Common; +namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Preparation; [Processor( "aonprd:repository", Description = "Обновление репозитория", Group = ProcessorGroupEnum.AonprdPreparation )] -internal class RepositoryUpdateProcessor( - AonprdRepository repository, - AonprdCompendium compendium, - ProcessProgress progress -) : IProcessor +internal class RepositoryUpdateProcessor(AonprdRepository repository, ProcessProgress progress) + : IProcessor { public AonprdRepository Repository { get; } = repository; - public AonprdCompendium Compendium { get; } = compendium; public ProcessProgress Progress { get; } = progress; public void Process() @@ -25,8 +20,5 @@ internal class RepositoryUpdateProcessor( Repository.Update(Progress); } - public void Save() - { - Compendium.Load(); - } + public void Save() { } } diff --git a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Common/SchemaBuildProcessor.cs b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Preparation/SchemaBuildProcessor.cs similarity index 96% rename from Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Common/SchemaBuildProcessor.cs rename to Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Preparation/SchemaBuildProcessor.cs index 82738ae..33ef7d2 100644 --- a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Common/SchemaBuildProcessor.cs +++ b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Preparation/SchemaBuildProcessor.cs @@ -3,12 +3,13 @@ using Pf2eDataScrapper.Helpers; using Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium; using Pf2eDataScrapper.SchemaBuilder; -namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Common; +namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Preparation; [Processor( "aonprd:schema", Description = "Построение схемы данных", - Group = ProcessorGroupEnum.AonprdPreparation + Group = ProcessorGroupEnum.AonprdPreparation, + Active = false )] [ProcessorDependencies(["aonprd:repository"])] internal class SchemaBuildProcessor( diff --git a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Types/TraitProcessor.cs b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Types/TraitProcessor.cs index 718a5f8..39a03c1 100644 --- a/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Types/TraitProcessor.cs +++ b/Pf2eDataScrapper/Importers/Implementations/Aonprd/Processors/Types/TraitProcessor.cs @@ -15,7 +15,7 @@ namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types; Description = "Признаки", Group = ProcessorGroupEnum.AonprdEntities )] -[ProcessorDependencies(["aonprd:external"])] +[ProcessorDependencies(["aonprd:external", "aonprd:source"])] internal class TraitProcessor( ProcessorInput input, Pf2eDbContext context, diff --git a/Pf2eDataScrapper/Importers/Implementations/FoundryEn/FoundryEnCompendium.cs b/Pf2eDataScrapper/Importers/Implementations/FoundryEn/FoundryEnCompendium.cs index bf99a45..6341c0c 100644 --- a/Pf2eDataScrapper/Importers/Implementations/FoundryEn/FoundryEnCompendium.cs +++ b/Pf2eDataScrapper/Importers/Implementations/FoundryEn/FoundryEnCompendium.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json.Linq; +using System.Collections.Concurrent; +using Newtonsoft.Json.Linq; using Pf2eDataScrapper.Importers.Implementations.FoundryEn.Compendium; namespace Pf2eDataScrapper.Importers.Implementations.FoundryEn; @@ -25,7 +26,7 @@ internal sealed class FoundryEnCompendium( /// /// Коллекция всех записей компендиума. /// - public List Items { get; } = []; + public ConcurrentBag Items { get; } = []; /// /// Возвращает количество элементов в компендиуме. diff --git a/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Entities/ActionProcessor.cs b/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Entities/ActionProcessor.cs index f5e860c..b07acf0 100644 --- a/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Entities/ActionProcessor.cs +++ b/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Entities/ActionProcessor.cs @@ -11,7 +11,8 @@ namespace Pf2eDataScrapper.Importers.Implementations.FoundryEn.Processors.Entiti "foundry-en:action", InnerType = "action", Description = "Действия", - Group = ProcessorGroupEnum.FoundryEnEntities + Group = ProcessorGroupEnum.FoundryEnEntities, + Active = false )] [ProcessorDependencies(["foundry-en:compendium"])] internal class ActionProcessor( diff --git a/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Entities/ConditionProcessor.cs b/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Entities/ConditionProcessor.cs index 89a3157..c967d8d 100644 --- a/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Entities/ConditionProcessor.cs +++ b/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Entities/ConditionProcessor.cs @@ -12,7 +12,8 @@ namespace Pf2eDataScrapper.Importers.Implementations.FoundryEn.Processors.Entiti "foundry-en:condition", InnerType = "Item.condition", Description = "Состояния", - Group = ProcessorGroupEnum.FoundryEnEntities + Group = ProcessorGroupEnum.FoundryEnEntities, + Active = false )] [ProcessorDependencies(["foundry-en:compendium"])] internal class ConditionProcessor( diff --git a/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Preparation/CompendiumFolderProcessor.cs b/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Preparation/CompendiumFolderProcessor.cs index a53b9a0..da6b757 100644 --- a/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Preparation/CompendiumFolderProcessor.cs +++ b/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Preparation/CompendiumFolderProcessor.cs @@ -8,7 +8,8 @@ namespace Pf2eDataScrapper.Importers.Implementations.FoundryEn.Processors.Prepar [Processor( "foundry-en:compendium-folders", Description = "Загрузка каталогов компендиума", - Group = ProcessorGroupEnum.FoundryEnPreparation + Group = ProcessorGroupEnum.FoundryEnPreparation, + Active = false )] [ProcessorDependencies(["foundry-en:repository"])] internal class CompendiumFolderProcessor( diff --git a/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Preparation/CompendiumItemsProcessor.cs b/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Preparation/CompendiumItemsProcessor.cs index 77e661f..7fd6260 100644 --- a/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Preparation/CompendiumItemsProcessor.cs +++ b/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Preparation/CompendiumItemsProcessor.cs @@ -9,7 +9,8 @@ namespace Pf2eDataScrapper.Importers.Implementations.FoundryEn.Processors.Prepar [Processor( "foundry-en:compendium", Description = "Загрузка компендиума", - Group = ProcessorGroupEnum.FoundryEnPreparation + Group = ProcessorGroupEnum.FoundryEnPreparation, + Active = false )] [ProcessorDependencies(["foundry-en:compendium-folders"])] internal class CompendiumItemsProcessor( diff --git a/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Preparation/RepositoryUpdateProcessor.cs b/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Preparation/RepositoryUpdateProcessor.cs index 1fcdbfa..2d25747 100644 --- a/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Preparation/RepositoryUpdateProcessor.cs +++ b/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Preparation/RepositoryUpdateProcessor.cs @@ -7,7 +7,8 @@ namespace Pf2eDataScrapper.Importers.Implementations.FoundryEn.Processors.Prepar [Processor( "foundry-en:repository", Description = "Обновление репозитория", - Group = ProcessorGroupEnum.FoundryEnPreparation + Group = ProcessorGroupEnum.FoundryEnPreparation, + Active = false )] internal class RepositoryUpdateProcessor(FoundryEnRepository repository, ProcessProgress progress) : IProcessor diff --git a/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Preparation/SchemaBuildProcessor.cs b/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Preparation/SchemaBuildProcessor.cs index 5f55eb5..905ea16 100644 --- a/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Preparation/SchemaBuildProcessor.cs +++ b/Pf2eDataScrapper/Importers/Implementations/FoundryEn/Processors/Preparation/SchemaBuildProcessor.cs @@ -7,7 +7,8 @@ namespace Pf2eDataScrapper.Importers.Implementations.FoundryEn.Processors.Prepar [Processor( "foundry-en:schema", Description = "Построение схемы данных", - Group = ProcessorGroupEnum.FoundryEnPreparation + Group = ProcessorGroupEnum.FoundryEnPreparation, + Active = false )] [ProcessorDependencies(["foundry-en:compendium"])] internal class SchemaBuildProcessor( diff --git a/Pf2eDataScrapper/Pf2eDataScrapper.csproj b/Pf2eDataScrapper/Pf2eDataScrapper.csproj index bb9ae8f..a299921 100644 --- a/Pf2eDataScrapper/Pf2eDataScrapper.csproj +++ b/Pf2eDataScrapper/Pf2eDataScrapper.csproj @@ -6,14 +6,19 @@ true enable + + + + + - + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -34,6 +39,17 @@ - + + Component + + + UserControl + + + UserControl + + + Form + diff --git a/Pf2eDataScrapper/Processors/ProcessorExecutor.cs b/Pf2eDataScrapper/Processors/ProcessorExecutor.cs index 1de918c..831d350 100644 --- a/Pf2eDataScrapper/Processors/ProcessorExecutor.cs +++ b/Pf2eDataScrapper/Processors/ProcessorExecutor.cs @@ -4,21 +4,27 @@ using Newtonsoft.Json.Linq; using Pf2eDataScrapper.App; using Pf2eDataScrapper.Helpers; using Pf2eDataScrapper.Importers; -using Pf2eDataScrapper.Importers.Implementations.Aonprd; using Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium; using Pf2eDataScrapper.Importers.Implementations.FoundryEn; +using Pf2eModel; namespace Pf2eDataScrapper.Processors; internal sealed class ProcessorExecutor( IServiceProvider serviceProvider, ProcessorCollection collection, - ProcessProgress progress + ProcessProgress progress, + Pf2eDbContext context, + AonprdCompendium aonprdCompendium, + FoundryEnCompendium foundryEnCompendium ) { public IServiceProvider ServiceProvider { get; } = serviceProvider; public ProcessorCollection Collection { get; } = collection; public ProcessProgress Progress { get; } = progress; + public Pf2eDbContext Context { get; } = context; + public AonprdCompendium AonprdCompendium { get; } = aonprdCompendium; + public FoundryEnCompendium FoundryEnCompendium { get; } = foundryEnCompendium; public void Start() { @@ -36,6 +42,7 @@ internal sealed class ProcessorExecutor( ProcessorCollectionItem item = Collection.Items[_currentIndex]; Progress.Processors.Start(item.Description); + Progress.Progress(Progress.Processors.ToString()); BackgroundWorker worker = new(); @@ -43,9 +50,17 @@ internal sealed class ProcessorExecutor( { foreach (IProcessor processor in IterateProcessors(item)) { - processor.Process(); - processor.Save(); + try + { + processor.Process(); + processor.Save(); + } + catch (SkipItemException exception) + { + Progress.Information(exception.Message); + } } + Context.SaveChanges(); }; worker.RunWorkerCompleted += Continue; @@ -86,19 +101,16 @@ internal sealed class ProcessorExecutor( ); } - Type importerType = item.Group switch + AbstractCompendium compendium = item.Group switch { ProcessorGroupEnum.AonprdPreparation or ProcessorGroupEnum.AonprdEntities - or ProcessorGroupEnum.AonprdSubclasses => typeof(AonprdCompendium), + or ProcessorGroupEnum.AonprdSubclasses => AonprdCompendium, ProcessorGroupEnum.FoundryEnPreparation or ProcessorGroupEnum.FoundryEnEntities => - typeof(FoundryEnCompendium), + FoundryEnCompendium, _ => throw new($"Не могу выбрать импортёр для группы {item.Group}"), }; - AonprdCompendium compendium = (AonprdCompendium) - ActivatorUtilities.CreateInstance(ServiceProvider, importerType); - Progress.Local.Reset(compendium.Count(item.InnerType!)); foreach ( @@ -109,12 +121,14 @@ internal sealed class ProcessorExecutor( { Progress.Local.Start(compendiumItem.Id); + ProcessorInput processorInput = ActivatorUtilities.CreateInstance( + ServiceProvider, + compendiumItem.Id, + json + ); + yield return (IProcessor) - ActivatorUtilities.CreateInstance( - ServiceProvider, - item.Type, - new ProcessorInput(compendiumItem.Id, json) - ); + ActivatorUtilities.CreateInstance(ServiceProvider, item.Type, processorInput); Progress.Local.End(); } @@ -122,3 +136,17 @@ internal sealed class ProcessorExecutor( Progress.Local.Complete(); } } + +/// +/// Исключение, показывающее, что обработка сущности пропускается как устаревшая. +/// +/// Название сущности. +public class LegacyItemException(string name) : SkipItemException(name, "Устаревший элемент") { } + +/// +/// Исключение, показывающее, что обработка сущности пропускается. +/// +/// Название сущности. +/// Причина пропуска. +public class SkipItemException(string name, string reason) + : Exception($"Пропускаю элемент '{name}', указана причина: {reason}.") { } diff --git a/Pf2eDataScrapper/Processors/ProcessorInput.cs b/Pf2eDataScrapper/Processors/ProcessorInput.cs index c6acd01..d1351da 100644 --- a/Pf2eDataScrapper/Processors/ProcessorInput.cs +++ b/Pf2eDataScrapper/Processors/ProcessorInput.cs @@ -9,7 +9,7 @@ namespace Pf2eDataScrapper.Processors; /// /// Название объекта, ассоциированного с вводом. /// JSON-объект, который будет использоваться для доступа к данным. -internal class ProcessorInput(string name, JObject input) +internal class ProcessorInput(string name, JObject input, IEnumerable enumSolvers) { /// /// Название объекта, ассоциированного с вводом. @@ -21,6 +21,11 @@ internal class ProcessorInput(string name, JObject input) /// public JObject Input { get; } = input; + /// + /// Интерфейсы, разрешающие значения перечислений. + /// + public IEnumerable EnumSolvers { get; } = enumSolvers; + /// /// Значение из JSON по заданному пути, которое обязательно должно существовать. /// @@ -141,6 +146,20 @@ internal class ProcessorInput(string name, JObject input) public virtual T Enum(string value) where T : Enum { + IEnumerable> solvers = EnumSolvers + .Where(solver => solver.GetType().IsAssignableTo(typeof(EnumSolver))) + .Cast>(); + + foreach (EnumSolver solver in solvers) + { + T? result = solver.SolveEnum(value); + + if (result is not null) + { + return result; + } + } + foreach (System.Reflection.FieldInfo field in typeof(T).GetFields()) { List variants = [field.Name]; @@ -170,3 +189,16 @@ internal class ProcessorInput(string name, JObject input) public class InvalidEnumStringException(Type type, string value) : Exception($"Invalid value '{value}' for enum type {type.FullName}.") { } } + +internal interface IEnumSolver { } + +public abstract class EnumSolver : IEnumSolver + where T : Enum +{ + public abstract Dictionary Data { get; } + + public T? SolveEnum(string input) + { + return Data.GetValueOrDefault(input); + } +} diff --git a/Pf2eModel/Migrations/20250510202833_1.Designer.cs b/Pf2eModel/Migrations/20250514084949_1.Designer.cs similarity index 99% rename from Pf2eModel/Migrations/20250510202833_1.Designer.cs rename to Pf2eModel/Migrations/20250514084949_1.Designer.cs index 396cb60..66ebb3f 100644 --- a/Pf2eModel/Migrations/20250510202833_1.Designer.cs +++ b/Pf2eModel/Migrations/20250514084949_1.Designer.cs @@ -13,7 +13,7 @@ using Pf2eModel.Model.Enum; namespace Pf2eModel.Migrations { [DbContext(typeof(Pf2eDbContext))] - [Migration("20250510202833_1")] + [Migration("20250514084949_1")] partial class _1 { /// diff --git a/Pf2eModel/Migrations/20250510202833_1.cs b/Pf2eModel/Migrations/20250514084949_1.cs similarity index 100% rename from Pf2eModel/Migrations/20250510202833_1.cs rename to Pf2eModel/Migrations/20250514084949_1.cs