3380 lines
144 KiB
C#
3380 lines
144 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.ComponentModel;
|
||
using System.Drawing;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Text;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using System.Windows.Forms;
|
||
using DCIT.App.Presentation;
|
||
using DCIT.Core.Models;
|
||
using DCIT.Core.Services;
|
||
using DCIT.Core.Utilities;
|
||
|
||
namespace DCIT.App.Forms
|
||
{
|
||
public sealed class MainForm : Form
|
||
{
|
||
private readonly ConfigurationService _configurationService;
|
||
private readonly LogService _logService;
|
||
private readonly FileScanService _fileScanService;
|
||
private readonly RuleService _ruleService;
|
||
private readonly AutoExtractService _autoExtractService;
|
||
private readonly IDocumentProcessingService _documentProcessingService;
|
||
private readonly IUserDialogService _dialogService;
|
||
private readonly BatchExecutionPlanner _batchExecutionPlanner = new BatchExecutionPlanner();
|
||
|
||
private readonly BindingList<FileScanItem> _replaceItems = new BindingList<FileScanItem>();
|
||
private readonly BindingList<FileScanItem> _restoreItems = new BindingList<FileScanItem>();
|
||
private readonly BindingList<RuleDefinition> _rules = new BindingList<RuleDefinition>();
|
||
|
||
private readonly TabControl _tabControl = new TabControl();
|
||
private readonly TabPage _replaceTab = new TabPage("替换");
|
||
private readonly TabPage _restoreTab = new TabPage("恢复");
|
||
private readonly TabPage _settingsTab = new TabPage("设置");
|
||
|
||
private readonly TextBox _txtReplaceSource = new TextBox();
|
||
private readonly TextBox _txtReplaceOutput = new TextBox();
|
||
private readonly TextBox _txtRuleFile = new TextBox();
|
||
private readonly TextBox _txtRestoreSource = new TextBox();
|
||
private readonly TextBox _txtRestoreOutput = new TextBox();
|
||
private readonly ComboBox _cmbRestoreDiffInput = new ComboBox();
|
||
|
||
private readonly DataGridView _gridReplace = new DataGridView();
|
||
private readonly DataGridView _gridRestore = new DataGridView();
|
||
private readonly DataGridView _gridRules = new DataGridView();
|
||
private SplitContainer _settingsRuleListSplitContainer;
|
||
private readonly RichTextBox _logBox = new RichTextBox();
|
||
private readonly ContextMenuStrip _logContextMenu = new ContextMenuStrip();
|
||
private readonly ContextMenuStrip _replaceGridContextMenu = new ContextMenuStrip();
|
||
private readonly ContextMenuStrip _restoreGridContextMenu = new ContextMenuStrip();
|
||
private readonly ContextMenuStrip _ruleGridContextMenu = new ContextMenuStrip();
|
||
|
||
private readonly Button _btnAutoExtract = new Button();
|
||
private readonly Button _btnStartReplace = new Button();
|
||
private readonly Button _btnStopReplace = new Button();
|
||
private readonly Button _btnStartRestore = new Button();
|
||
private readonly Button _btnStopRestore = new Button();
|
||
private readonly Button _btnBrowseReplaceSource = new Button();
|
||
private readonly Button _btnBrowseReplaceOutput = new Button();
|
||
private readonly Button _btnSelectAllReplace = new Button();
|
||
private readonly Button _btnClearReplace = new Button();
|
||
private readonly Button _btnBrowseRestoreSource = new Button();
|
||
private readonly Button _btnBrowseRestoreOutput = new Button();
|
||
private readonly Button _btnSelectAllRestore = new Button();
|
||
private readonly Button _btnClearRestore = new Button();
|
||
private readonly Button _btnUndoAutoExtract = new Button();
|
||
private readonly Button _btnSaveRules = new Button();
|
||
private readonly Button _btnOpenRules = new Button();
|
||
|
||
private readonly ProgressBar _replaceTotalProgress = new ProgressBar();
|
||
private readonly ProgressBar _replaceFileProgress = new ProgressBar();
|
||
private readonly ProgressBar _restoreTotalProgress = new ProgressBar();
|
||
private readonly ProgressBar _restoreFileProgress = new ProgressBar();
|
||
|
||
private readonly Label _lblReplaceRuntime = new Label();
|
||
private readonly Label _lblRestoreRuntime = new Label();
|
||
|
||
private readonly CheckBox _chkJsonDiffEncryption = new CheckBox();
|
||
private readonly CheckBox _chkEnableImageReplacement = new CheckBox();
|
||
private readonly CheckBox _chkUseFixedImageSeed = new CheckBox();
|
||
private readonly CheckBox _chkExtractKeywords = new CheckBox();
|
||
private readonly CheckBox _chkExtractWords = new CheckBox();
|
||
private readonly CheckBox _chkExtractSentences = new CheckBox();
|
||
private readonly CheckBox _chkKeywordFrequency = new CheckBox();
|
||
private readonly CheckBox _chkKeywordTfIdf = new CheckBox();
|
||
private readonly CheckBox _chkKeywordTextRank = new CheckBox();
|
||
private readonly CheckBox _chkChineseWords = new CheckBox();
|
||
private readonly CheckBox _chkEnglishWords = new CheckBox();
|
||
private readonly CheckBox _chkChineseSentences = new CheckBox();
|
||
private readonly CheckBox _chkEnglishSentences = new CheckBox();
|
||
private readonly CheckBox _chkExcludeEmails = new CheckBox();
|
||
private readonly NumericUpDown _numKeywordCount = new NumericUpDown();
|
||
private readonly NumericUpDown _numWordCount = new NumericUpDown();
|
||
private readonly NumericUpDown _numSentenceCount = new NumericUpDown();
|
||
private readonly NumericUpDown _numFixedImageSeed = new NumericUpDown();
|
||
private readonly NumericUpDown _numMinChineseLength = new NumericUpDown();
|
||
private readonly NumericUpDown _numMinEnglishLength = new NumericUpDown();
|
||
private readonly TextBox _txtSentenceDelimiters = new TextBox();
|
||
|
||
private AppConfig _config;
|
||
private ConfigurationLoadResult _configurationLoadResult;
|
||
private RuleFile _ruleFile = new RuleFile();
|
||
private List<string> _lastAutoExtractRuleIds = new List<string>();
|
||
|
||
private CancellationTokenSource _replaceScanCancellation;
|
||
private CancellationTokenSource _restoreScanCancellation;
|
||
private CancellationTokenSource _autoExtractCancellation;
|
||
private CancellationTokenSource _replaceExecutionCancellation;
|
||
private CancellationTokenSource _restoreExecutionCancellation;
|
||
private bool _isAutoExtractRunning;
|
||
private bool _isReplaceRunning;
|
||
private bool _isRestoreRunning;
|
||
private bool _skipConfigSaveOnClose;
|
||
private bool _settingsRuleListInitialWidthApplied;
|
||
private int _lastReplaceRuntimeFlushTick;
|
||
private int _lastRestoreRuntimeFlushTick;
|
||
|
||
private const int RuntimeUiRefreshIntervalMilliseconds = 100;
|
||
|
||
public MainForm(
|
||
ConfigurationService configurationService,
|
||
LogService logService,
|
||
FileScanService fileScanService,
|
||
RuleService ruleService,
|
||
AutoExtractService autoExtractService,
|
||
IDocumentProcessingService documentProcessingService,
|
||
IUserDialogService dialogService)
|
||
{
|
||
_configurationService = configurationService;
|
||
_logService = logService;
|
||
_fileScanService = fileScanService;
|
||
_ruleService = ruleService;
|
||
_autoExtractService = autoExtractService;
|
||
_documentProcessingService = documentProcessingService;
|
||
_dialogService = dialogService;
|
||
|
||
_configurationLoadResult = _configurationService.Load();
|
||
_config = _configurationLoadResult.Config;
|
||
|
||
Text = "WORD 2007 格式调整软件";
|
||
StartPosition = FormStartPosition.CenterScreen;
|
||
MinimumSize = new Size(1400, 900);
|
||
Width = 1600;
|
||
Height = 980;
|
||
Font = new Font("Microsoft YaHei UI", 9F);
|
||
|
||
InitializeLayout();
|
||
BindData();
|
||
ApplyConfigToUi();
|
||
|
||
_logService.EntryWritten += LogService_EntryWritten;
|
||
_logService.WriteFailed += LogService_WriteFailed;
|
||
FormClosing += MainForm_FormClosing;
|
||
Shown += MainForm_Shown;
|
||
}
|
||
|
||
private void InitializeLayout()
|
||
{
|
||
var splitContainer = new SplitContainer
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
Orientation = Orientation.Horizontal,
|
||
SplitterDistance = 720,
|
||
};
|
||
|
||
_tabControl.Dock = DockStyle.Fill;
|
||
_tabControl.TabPages.Add(_replaceTab);
|
||
_tabControl.TabPages.Add(_restoreTab);
|
||
_tabControl.TabPages.Add(_settingsTab);
|
||
|
||
splitContainer.Panel1.Controls.Add(_tabControl);
|
||
|
||
_logBox.Dock = DockStyle.Fill;
|
||
_logBox.ReadOnly = true;
|
||
_logBox.BackColor = Color.White;
|
||
_logBox.Font = new Font("Consolas", 10F);
|
||
splitContainer.Panel2.Controls.Add(_logBox);
|
||
|
||
Controls.Add(splitContainer);
|
||
|
||
BuildReplaceTab();
|
||
BuildRestoreTabV2();
|
||
BuildSettingsTab();
|
||
ConfigureContextMenus();
|
||
}
|
||
|
||
private void BuildReplaceTab()
|
||
{
|
||
var root = new TableLayoutPanel
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
ColumnCount = 1,
|
||
RowCount = 3,
|
||
};
|
||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
|
||
var pathPanel = new TableLayoutPanel
|
||
{
|
||
Dock = DockStyle.Top,
|
||
ColumnCount = 6,
|
||
AutoSize = true,
|
||
Padding = new Padding(8),
|
||
};
|
||
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F));
|
||
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
|
||
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F));
|
||
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F));
|
||
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
|
||
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F));
|
||
|
||
_btnBrowseReplaceSource.Text = "选择原始文件夹";
|
||
_btnBrowseReplaceSource.Dock = DockStyle.Fill;
|
||
_btnBrowseReplaceOutput.Text = "选择替换文件夹";
|
||
_btnBrowseReplaceOutput.Dock = DockStyle.Fill;
|
||
_btnSelectAllReplace.Text = "全选";
|
||
_btnSelectAllReplace.AutoSize = true;
|
||
_btnClearReplace.Text = "全不选";
|
||
_btnClearReplace.AutoSize = true;
|
||
_btnAutoExtract.Text = "自动提取";
|
||
_btnStartReplace.Text = "开始替换";
|
||
_btnStopReplace.Text = "停止";
|
||
_btnStopReplace.Enabled = false;
|
||
_lblReplaceRuntime.AutoSize = true;
|
||
_lblReplaceRuntime.Text = "当前文件 0/0 | 当前规则 - | 当前文件命中 0 | 累计命中 0";
|
||
|
||
AddLabeledRow(pathPanel, 0, "原始文件夹", _txtReplaceSource, _btnBrowseReplaceSource, "替换文件夹", _txtReplaceOutput, _btnBrowseReplaceOutput);
|
||
|
||
_btnBrowseReplaceSource.Click += async delegate { await BrowseFolderIntoTextBoxAsync(_txtReplaceSource, RefreshReplaceScanAsync); };
|
||
_btnBrowseReplaceOutput.Click += async delegate { await BrowseFolderIntoTextBoxAsync(_txtReplaceOutput, RefreshReplaceScanAsync); };
|
||
_txtReplaceSource.Leave += async delegate { await RefreshReplaceScanAsync(); };
|
||
_txtReplaceOutput.Leave += async delegate { await RefreshReplaceScanAsync(); };
|
||
_btnSelectAllReplace.Click += delegate { SetSelection(_replaceItems, true); };
|
||
_btnClearReplace.Click += delegate { SetSelection(_replaceItems, false); };
|
||
_btnAutoExtract.Click += async delegate { await RunAutoExtractAsync(); };
|
||
_btnStartReplace.Click += StartReplaceExecution_Click;
|
||
_btnStopReplace.Click += StopReplaceOrAutoExtract_Click;
|
||
|
||
ConfigureFileGrid(_gridReplace, isRestoreGrid: false);
|
||
|
||
var gridPanel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(8, 0, 8, 0) };
|
||
_gridReplace.Dock = DockStyle.Fill;
|
||
gridPanel.Controls.Add(_gridReplace);
|
||
|
||
var bottomPanel = new TableLayoutPanel
|
||
{
|
||
Dock = DockStyle.Top,
|
||
ColumnCount = 1,
|
||
AutoSize = true,
|
||
Padding = new Padding(8),
|
||
};
|
||
|
||
var buttonPanel = new FlowLayoutPanel
|
||
{
|
||
Dock = DockStyle.Top,
|
||
AutoSize = true,
|
||
FlowDirection = FlowDirection.LeftToRight,
|
||
};
|
||
buttonPanel.Controls.Add(_btnSelectAllReplace);
|
||
buttonPanel.Controls.Add(_btnClearReplace);
|
||
buttonPanel.Controls.Add(_btnAutoExtract);
|
||
buttonPanel.Controls.Add(_btnStartReplace);
|
||
buttonPanel.Controls.Add(_btnStopReplace);
|
||
|
||
bottomPanel.Controls.Add(buttonPanel);
|
||
bottomPanel.Controls.Add(CreateProgressLine("总进度", _replaceTotalProgress));
|
||
bottomPanel.Controls.Add(CreateProgressLine("单文件进度", _replaceFileProgress));
|
||
bottomPanel.Controls.Add(_lblReplaceRuntime);
|
||
|
||
root.Controls.Add(pathPanel, 0, 0);
|
||
root.Controls.Add(gridPanel, 0, 1);
|
||
root.Controls.Add(bottomPanel, 0, 2);
|
||
_replaceTab.Controls.Add(root);
|
||
}
|
||
|
||
private void BuildRestoreTab()
|
||
{
|
||
var root = new TableLayoutPanel
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
ColumnCount = 1,
|
||
RowCount = 3,
|
||
};
|
||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
|
||
var pathPanel = new TableLayoutPanel
|
||
{
|
||
Dock = DockStyle.Top,
|
||
ColumnCount = 6,
|
||
AutoSize = true,
|
||
Padding = new Padding(8),
|
||
};
|
||
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F));
|
||
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
|
||
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F));
|
||
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F));
|
||
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
|
||
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F));
|
||
|
||
var btnBrowseRestoreSource = new Button { Text = "选择替换文件夹", Dock = DockStyle.Fill };
|
||
var btnBrowseRestoreOutput = new Button { Text = "选择恢复文件夹", Dock = DockStyle.Fill };
|
||
var btnSelectAllRestore = new Button { Text = "全选", AutoSize = true };
|
||
var btnClearRestore = new Button { Text = "全不选", AutoSize = true };
|
||
_btnStartRestore.Text = "开始恢复";
|
||
_btnStopRestore.Text = "停止";
|
||
_btnStopRestore.Enabled = false;
|
||
_lblRestoreRuntime.AutoSize = true;
|
||
_lblRestoreRuntime.Text = "当前文件 0/0 | 当前规则 - | 当前文件命中 0 | 累计命中 0";
|
||
|
||
AddLabeledRow(pathPanel, 0, "替换文件夹", _txtRestoreSource, btnBrowseRestoreSource, "恢复文件夹", _txtRestoreOutput, btnBrowseRestoreOutput);
|
||
|
||
btnBrowseRestoreSource.Click += async delegate { await BrowseFolderIntoTextBoxAsync(_txtRestoreSource, RefreshRestoreScanAsync); };
|
||
btnBrowseRestoreOutput.Click += async delegate { await BrowseFolderIntoTextBoxAsync(_txtRestoreOutput, RefreshRestoreScanAsync); };
|
||
_txtRestoreSource.Leave += async delegate { await RefreshRestoreScanAsync(); };
|
||
_txtRestoreOutput.Leave += async delegate { await RefreshRestoreScanAsync(); };
|
||
btnSelectAllRestore.Click += delegate { SetSelection(_restoreItems, true); };
|
||
btnClearRestore.Click += delegate { SetSelection(_restoreItems, false); };
|
||
_btnStartRestore.Click += StartRestoreExecution_Click;
|
||
|
||
ConfigureFileGrid(_gridRestore, isRestoreGrid: true);
|
||
|
||
var gridPanel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(8, 0, 8, 0) };
|
||
_gridRestore.Dock = DockStyle.Fill;
|
||
gridPanel.Controls.Add(_gridRestore);
|
||
|
||
var bottomPanel = new TableLayoutPanel
|
||
{
|
||
Dock = DockStyle.Top,
|
||
ColumnCount = 1,
|
||
AutoSize = true,
|
||
Padding = new Padding(8),
|
||
};
|
||
|
||
var buttonPanel = new FlowLayoutPanel
|
||
{
|
||
Dock = DockStyle.Top,
|
||
AutoSize = true,
|
||
FlowDirection = FlowDirection.LeftToRight,
|
||
};
|
||
buttonPanel.Controls.Add(btnSelectAllRestore);
|
||
buttonPanel.Controls.Add(btnClearRestore);
|
||
buttonPanel.Controls.Add(_btnStartRestore);
|
||
buttonPanel.Controls.Add(_btnStopRestore);
|
||
|
||
bottomPanel.Controls.Add(buttonPanel);
|
||
bottomPanel.Controls.Add(CreateProgressLine("总进度", _restoreTotalProgress));
|
||
bottomPanel.Controls.Add(CreateProgressLine("单文件进度", _restoreFileProgress));
|
||
bottomPanel.Controls.Add(_lblRestoreRuntime);
|
||
|
||
root.Controls.Add(pathPanel, 0, 0);
|
||
root.Controls.Add(gridPanel, 0, 1);
|
||
root.Controls.Add(bottomPanel, 0, 2);
|
||
_restoreTab.Controls.Add(root);
|
||
}
|
||
|
||
private void BuildRestoreTabV2()
|
||
{
|
||
var root = new TableLayoutPanel
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
ColumnCount = 1,
|
||
RowCount = 3,
|
||
};
|
||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
|
||
var pathPanel = new TableLayoutPanel
|
||
{
|
||
Dock = DockStyle.Top,
|
||
ColumnCount = 6,
|
||
AutoSize = true,
|
||
Padding = new Padding(8),
|
||
};
|
||
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F));
|
||
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
|
||
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F));
|
||
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F));
|
||
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
|
||
pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F));
|
||
|
||
_btnBrowseRestoreSource.Text = "选择替换文件夹";
|
||
_btnBrowseRestoreSource.Dock = DockStyle.Fill;
|
||
_btnBrowseRestoreOutput.Text = "选择恢复文件夹";
|
||
_btnBrowseRestoreOutput.Dock = DockStyle.Fill;
|
||
_btnSelectAllRestore.Text = "全选";
|
||
_btnSelectAllRestore.AutoSize = true;
|
||
_btnClearRestore.Text = "全不选";
|
||
_btnClearRestore.AutoSize = true;
|
||
|
||
_btnStartRestore.Text = "开始恢复";
|
||
_btnStopRestore.Text = "停止";
|
||
_btnStopRestore.Enabled = false;
|
||
_lblRestoreRuntime.AutoSize = true;
|
||
_lblRestoreRuntime.Text = "当前文件 0/0 | 当前规则 - | 当前文件命中 0 | 累计命中 0";
|
||
|
||
_cmbRestoreDiffInput.DropDownStyle = ComboBoxStyle.DropDownList;
|
||
_cmbRestoreDiffInput.Items.Clear();
|
||
_cmbRestoreDiffInput.Items.Add(".diff");
|
||
_cmbRestoreDiffInput.Items.Add(".bmp");
|
||
_cmbRestoreDiffInput.SelectedIndex = 0;
|
||
|
||
AddLabeledRow(pathPanel, 0, "替换文件夹", _txtRestoreSource, _btnBrowseRestoreSource, "恢复文件夹", _txtRestoreOutput, _btnBrowseRestoreOutput);
|
||
pathPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
pathPanel.Controls.Add(new Label { Text = "差异文件格式", Anchor = AnchorStyles.Left, AutoSize = true }, 0, 1);
|
||
pathPanel.Controls.Add(_cmbRestoreDiffInput, 1, 1);
|
||
|
||
_btnBrowseRestoreSource.Click += async delegate { await BrowseFolderIntoTextBoxAsync(_txtRestoreSource, RefreshRestoreScanAsync); };
|
||
_btnBrowseRestoreOutput.Click += async delegate { await BrowseFolderIntoTextBoxAsync(_txtRestoreOutput, RefreshRestoreScanAsync); };
|
||
_txtRestoreSource.Leave += async delegate { await RefreshRestoreScanAsync(); };
|
||
_txtRestoreOutput.Leave += async delegate { await RefreshRestoreScanAsync(); };
|
||
_cmbRestoreDiffInput.SelectedIndexChanged += async delegate { await RefreshRestoreScanAsync(); };
|
||
_btnSelectAllRestore.Click += delegate { SetSelection(_restoreItems, true); };
|
||
_btnClearRestore.Click += delegate { SetSelection(_restoreItems, false); };
|
||
_btnStartRestore.Click += StartRestoreExecution_Click;
|
||
_btnStopRestore.Click += StopRestore_Click;
|
||
|
||
ConfigureFileGrid(_gridRestore, isRestoreGrid: true);
|
||
|
||
var gridPanel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(8, 0, 8, 0) };
|
||
_gridRestore.Dock = DockStyle.Fill;
|
||
gridPanel.Controls.Add(_gridRestore);
|
||
|
||
var bottomPanel = new TableLayoutPanel
|
||
{
|
||
Dock = DockStyle.Top,
|
||
ColumnCount = 1,
|
||
AutoSize = true,
|
||
Padding = new Padding(8),
|
||
};
|
||
|
||
var buttonPanel = new FlowLayoutPanel
|
||
{
|
||
Dock = DockStyle.Top,
|
||
AutoSize = true,
|
||
FlowDirection = FlowDirection.LeftToRight,
|
||
};
|
||
buttonPanel.Controls.Add(_btnSelectAllRestore);
|
||
buttonPanel.Controls.Add(_btnClearRestore);
|
||
buttonPanel.Controls.Add(_btnStartRestore);
|
||
buttonPanel.Controls.Add(_btnStopRestore);
|
||
|
||
bottomPanel.Controls.Add(buttonPanel);
|
||
bottomPanel.Controls.Add(CreateProgressLine("总进度", _restoreTotalProgress));
|
||
bottomPanel.Controls.Add(CreateProgressLine("单文件进度", _restoreFileProgress));
|
||
bottomPanel.Controls.Add(_lblRestoreRuntime);
|
||
|
||
root.Controls.Add(pathPanel, 0, 0);
|
||
root.Controls.Add(gridPanel, 0, 1);
|
||
root.Controls.Add(bottomPanel, 0, 2);
|
||
_restoreTab.Controls.Add(root);
|
||
}
|
||
|
||
private void BuildSettingsTab()
|
||
{
|
||
var root = new TableLayoutPanel
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
ColumnCount = 1,
|
||
RowCount = 3,
|
||
};
|
||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||
|
||
var topPanel = new TableLayoutPanel
|
||
{
|
||
Dock = DockStyle.Top,
|
||
ColumnCount = 5,
|
||
AutoSize = true,
|
||
Padding = new Padding(8),
|
||
};
|
||
topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F));
|
||
topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||
topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F));
|
||
topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F));
|
||
topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 140F));
|
||
|
||
_btnOpenRules.Text = "打开规则";
|
||
_btnSaveRules.Text = "保存规则";
|
||
_btnUndoAutoExtract.Text = "撤销自动提取";
|
||
_btnUndoAutoExtract.Enabled = false;
|
||
|
||
topPanel.Controls.Add(new Label { Text = "规则文件", Anchor = AnchorStyles.Left, AutoSize = true }, 0, 0);
|
||
_txtRuleFile.Dock = DockStyle.Fill;
|
||
topPanel.Controls.Add(_txtRuleFile, 1, 0);
|
||
topPanel.Controls.Add(_btnOpenRules, 2, 0);
|
||
topPanel.Controls.Add(_btnSaveRules, 3, 0);
|
||
topPanel.Controls.Add(_btnUndoAutoExtract, 4, 0);
|
||
|
||
_btnOpenRules.Click += OpenRules_Click;
|
||
_btnSaveRules.Click += SaveRules_Click;
|
||
_btnUndoAutoExtract.Click += UndoAutoExtract_Click;
|
||
|
||
var actionPanel = new FlowLayoutPanel
|
||
{
|
||
Dock = DockStyle.Top,
|
||
AutoSize = true,
|
||
Padding = new Padding(8, 0, 8, 8),
|
||
};
|
||
|
||
var btnAddRule = new Button { Text = "新增规则", AutoSize = true };
|
||
var btnDeleteRule = new Button { Text = "删除规则", AutoSize = true };
|
||
var btnMoveUp = new Button { Text = "上移", AutoSize = true };
|
||
var btnMoveDown = new Button { Text = "下移", AutoSize = true };
|
||
|
||
btnAddRule.Click += AddRule_Click;
|
||
btnDeleteRule.Click += DeleteRule_Click;
|
||
btnMoveUp.Click += MoveRuleUp_Click;
|
||
btnMoveDown.Click += MoveRuleDown_Click;
|
||
|
||
actionPanel.Controls.Add(btnAddRule);
|
||
actionPanel.Controls.Add(btnDeleteRule);
|
||
actionPanel.Controls.Add(btnMoveUp);
|
||
actionPanel.Controls.Add(btnMoveDown);
|
||
|
||
var settingsAndGrid = new SplitContainer
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
Orientation = Orientation.Vertical,
|
||
SplitterDistance = 350,
|
||
};
|
||
_settingsRuleListSplitContainer = settingsAndGrid;
|
||
settingsAndGrid.HandleCreated += delegate { ApplySettingsRuleListInitialWidth(settingsAndGrid); };
|
||
settingsAndGrid.SizeChanged += delegate { ApplySettingsRuleListInitialWidth(settingsAndGrid); };
|
||
settingsAndGrid.VisibleChanged += delegate { ApplySettingsRuleListInitialWidth(settingsAndGrid); };
|
||
_tabControl.SelectedIndexChanged += async delegate
|
||
{
|
||
ApplySettingsRuleListInitialWidth(settingsAndGrid);
|
||
await RefreshScanOnTabSwitchAsync();
|
||
};
|
||
|
||
settingsAndGrid.Panel1.Controls.Add(BuildAutoExtractSettingsGroup());
|
||
settingsAndGrid.Panel2.Controls.Add(BuildRulesGridPanel());
|
||
|
||
root.Controls.Add(topPanel, 0, 0);
|
||
root.Controls.Add(actionPanel, 0, 1);
|
||
root.Controls.Add(settingsAndGrid, 0, 2);
|
||
_settingsTab.Controls.Add(root);
|
||
}
|
||
|
||
private void ApplySettingsRuleListInitialWidth(SplitContainer splitContainer)
|
||
{
|
||
if (_settingsRuleListInitialWidthApplied || splitContainer == null || splitContainer.Orientation != Orientation.Vertical)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (splitContainer.ClientSize.Width < 600)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var availableWidth = splitContainer.ClientSize.Width - splitContainer.SplitterWidth;
|
||
if (availableWidth <= 0 || availableWidth < splitContainer.Panel1MinSize + splitContainer.Panel2MinSize)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var leftWidth = availableWidth / 3;
|
||
var maxLeftWidth = splitContainer.ClientSize.Width - splitContainer.SplitterWidth - splitContainer.Panel2MinSize;
|
||
if (maxLeftWidth < splitContainer.Panel1MinSize)
|
||
{
|
||
return;
|
||
}
|
||
|
||
leftWidth = Math.Max(splitContainer.Panel1MinSize, Math.Min(maxLeftWidth, leftWidth));
|
||
splitContainer.SplitterDistance = leftWidth;
|
||
_settingsRuleListInitialWidthApplied = true;
|
||
}
|
||
|
||
private void ConfigureContextMenus()
|
||
{
|
||
ConfigureLogContextMenu();
|
||
ConfigureFileGridContextMenu(_gridReplace, _replaceGridContextMenu);
|
||
ConfigureFileGridContextMenu(_gridRestore, _restoreGridContextMenu);
|
||
ConfigureRuleGridContextMenu();
|
||
}
|
||
|
||
private void ConfigureLogContextMenu()
|
||
{
|
||
_logContextMenu.Items.Clear();
|
||
_logContextMenu.Items.Add("复制全部内容", null, delegate { CopyAllLogs(); });
|
||
_logContextMenu.Items.Add("清空日志", null, delegate { ClearLogBox(); });
|
||
_logBox.ContextMenuStrip = _logContextMenu;
|
||
}
|
||
|
||
private void ConfigureFileGridContextMenu(DataGridView grid, ContextMenuStrip menu)
|
||
{
|
||
menu.Items.Clear();
|
||
menu.Items.Add("删除", null, delegate
|
||
{
|
||
if (ReferenceEquals(grid, _gridReplace))
|
||
{
|
||
DeleteSelectedFileItems(_replaceItems, grid);
|
||
}
|
||
else
|
||
{
|
||
DeleteSelectedFileItems(_restoreItems, grid);
|
||
}
|
||
});
|
||
menu.Items.Add("启用", null, delegate { SetSelectedFileItems(grid, true); });
|
||
menu.Items.Add("不启用", null, delegate { SetSelectedFileItems(grid, false); });
|
||
grid.ContextMenuStrip = menu;
|
||
}
|
||
|
||
private void ConfigureRuleGridContextMenu()
|
||
{
|
||
_ruleGridContextMenu.Items.Clear();
|
||
_ruleGridContextMenu.Items.Add("删除", null, delegate { DeleteSelectedRules(); });
|
||
_ruleGridContextMenu.Items.Add("启用", null, delegate { SetSelectedRulesEnabled(true); });
|
||
_ruleGridContextMenu.Items.Add("不启用", null, delegate { SetSelectedRulesEnabled(false); });
|
||
_gridRules.ContextMenuStrip = _ruleGridContextMenu;
|
||
}
|
||
|
||
private Control BuildAutoExtractSettingsGroup()
|
||
{
|
||
var group = new GroupBox
|
||
{
|
||
Text = "自动提取设置",
|
||
Dock = DockStyle.Fill,
|
||
Padding = new Padding(12),
|
||
};
|
||
|
||
var layout = new TableLayoutPanel
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
ColumnCount = 2,
|
||
AutoScroll = true,
|
||
};
|
||
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 55F));
|
||
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 45F));
|
||
|
||
_chkJsonDiffEncryption.Text = "JSON 差异文件加密";
|
||
_chkEnableImageReplacement.Text = "启用图片替换";
|
||
_chkUseFixedImageSeed.Text = "使用固定图像随机种子";
|
||
_chkExtractKeywords.Text = "提取关键字";
|
||
_chkExtractWords.Text = "提取高频词";
|
||
_chkExtractSentences.Text = "提取高频句";
|
||
_chkKeywordFrequency.Text = "关键字算法: 词频";
|
||
_chkKeywordTfIdf.Text = "关键字算法: TF-IDF";
|
||
_chkKeywordTextRank.Text = "关键字算法: TextRank";
|
||
_chkChineseWords.Text = "统计中文词";
|
||
_chkEnglishWords.Text = "统计英文词";
|
||
_chkChineseSentences.Text = "统计中文句";
|
||
_chkEnglishSentences.Text = "统计英文句";
|
||
_chkExcludeEmails.Text = "排除邮箱地址";
|
||
|
||
ConfigureNumeric(_numKeywordCount, 1, 1000);
|
||
ConfigureNumeric(_numWordCount, 1, 1000);
|
||
ConfigureNumeric(_numSentenceCount, 1, 1000);
|
||
ConfigureNumeric(_numFixedImageSeed, 0, int.MaxValue);
|
||
ConfigureNumeric(_numMinChineseLength, 1, 50);
|
||
ConfigureNumeric(_numMinEnglishLength, 1, 50);
|
||
|
||
AddSettingRow(layout, 0, _chkJsonDiffEncryption, new Label());
|
||
AddSettingRow(layout, 1, _chkEnableImageReplacement, new Label());
|
||
AddSettingRow(layout, 2, _chkUseFixedImageSeed, _numFixedImageSeed);
|
||
AddSettingRow(layout, 3, _chkExtractKeywords, _chkKeywordFrequency);
|
||
AddSettingRow(layout, 4, _chkExtractWords, _chkKeywordTfIdf);
|
||
AddSettingRow(layout, 5, _chkExtractSentences, _chkKeywordTextRank);
|
||
AddSettingRow(layout, 6, new Label { Text = "关键字最大数量", Anchor = AnchorStyles.Left, AutoSize = true }, _numKeywordCount);
|
||
AddSettingRow(layout, 7, new Label { Text = "高频词最大数量", Anchor = AnchorStyles.Left, AutoSize = true }, _numWordCount);
|
||
AddSettingRow(layout, 8, new Label { Text = "高频句最大数量", Anchor = AnchorStyles.Left, AutoSize = true }, _numSentenceCount);
|
||
AddSettingRow(layout, 9, _chkChineseWords, _chkEnglishWords);
|
||
AddSettingRow(layout, 10, _chkChineseSentences, _chkEnglishSentences);
|
||
AddSettingRow(layout, 11, new Label { Text = "中文最小长度", Anchor = AnchorStyles.Left, AutoSize = true }, _numMinChineseLength);
|
||
AddSettingRow(layout, 12, new Label { Text = "英文最小长度", Anchor = AnchorStyles.Left, AutoSize = true }, _numMinEnglishLength);
|
||
AddSettingRow(layout, 13, new Label { Text = "句子分隔符", Anchor = AnchorStyles.Left, AutoSize = true }, _txtSentenceDelimiters);
|
||
AddSettingRow(layout, 14, _chkExcludeEmails, new Label());
|
||
|
||
_chkUseFixedImageSeed.CheckedChanged += delegate
|
||
{
|
||
_numFixedImageSeed.Enabled = _chkUseFixedImageSeed.Checked;
|
||
};
|
||
|
||
group.Controls.Add(layout);
|
||
return group;
|
||
}
|
||
|
||
private Control BuildRulesGridPanel()
|
||
{
|
||
ConfigureRuleGrid(_gridRules);
|
||
var panel = new Panel
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
Padding = new Padding(8),
|
||
};
|
||
panel.Controls.Add(_gridRules);
|
||
_gridRules.Dock = DockStyle.Fill;
|
||
return panel;
|
||
}
|
||
|
||
private void BindData()
|
||
{
|
||
_gridReplace.DataSource = _replaceItems;
|
||
_gridRestore.DataSource = _restoreItems;
|
||
_gridRules.DataSource = _rules;
|
||
}
|
||
|
||
private void ApplyConfigToUi()
|
||
{
|
||
_txtReplaceSource.Text = _config.ReplacePage.SourceDirectory ?? string.Empty;
|
||
_txtReplaceOutput.Text = _config.ReplacePage.OutputDirectory ?? string.Empty;
|
||
_txtRuleFile.Text = _config.ReplacePage.RuleFilePath ?? string.Empty;
|
||
_txtRestoreSource.Text = _config.RestorePage.ReplaceDirectory ?? string.Empty;
|
||
_txtRestoreOutput.Text = _config.RestorePage.RestoreDirectory ?? string.Empty;
|
||
_cmbRestoreDiffInput.SelectedItem = string.Equals(_config.RestorePage.DiffInputFormat, ".bmp", StringComparison.OrdinalIgnoreCase)
|
||
? ".bmp"
|
||
: ".diff";
|
||
|
||
var auto = _config.Settings.AutoExtract;
|
||
_chkJsonDiffEncryption.Checked = _config.Settings.JsonDiffEncryption;
|
||
_chkEnableImageReplacement.Checked = _config.Settings.EnableImageReplacement;
|
||
_chkUseFixedImageSeed.Checked = _config.Settings.UseFixedImageSeed;
|
||
_numFixedImageSeed.Value = Math.Max(_numFixedImageSeed.Minimum, Math.Min(_numFixedImageSeed.Maximum, _config.Settings.FixedImageSeed));
|
||
_numFixedImageSeed.Enabled = _chkUseFixedImageSeed.Checked;
|
||
_chkExtractKeywords.Checked = auto.ExtractKeywords;
|
||
_chkExtractWords.Checked = auto.ExtractHighFrequencyWords;
|
||
_chkExtractSentences.Checked = auto.ExtractHighFrequencySentences;
|
||
_chkKeywordFrequency.Checked = auto.UseKeywordFrequency;
|
||
_chkKeywordTfIdf.Checked = auto.UseKeywordTfIdf;
|
||
_chkKeywordTextRank.Checked = auto.UseKeywordTextRank;
|
||
_chkChineseWords.Checked = auto.CountChineseWords;
|
||
_chkEnglishWords.Checked = auto.CountEnglishWords;
|
||
_chkChineseSentences.Checked = auto.CountChineseSentences;
|
||
_chkEnglishSentences.Checked = auto.CountEnglishSentences;
|
||
_chkExcludeEmails.Checked = auto.ExcludeEmailAddresses;
|
||
_numKeywordCount.Value = auto.MaxKeywordCount;
|
||
_numWordCount.Value = auto.MaxWordCount;
|
||
_numSentenceCount.Value = auto.MaxSentenceCount;
|
||
_numMinChineseLength.Value = auto.MinChineseWordLength;
|
||
_numMinEnglishLength.Value = auto.MinEnglishWordLength;
|
||
_txtSentenceDelimiters.Text = auto.SentenceDelimiters ?? string.Empty;
|
||
|
||
if (!string.IsNullOrWhiteSpace(_txtRuleFile.Text) && File.Exists(_txtRuleFile.Text))
|
||
{
|
||
LoadRuleFile(_txtRuleFile.Text);
|
||
}
|
||
}
|
||
|
||
private void CaptureUiToConfig()
|
||
{
|
||
_config.ReplacePage.SourceDirectory = _txtReplaceSource.Text.Trim();
|
||
_config.ReplacePage.OutputDirectory = _txtReplaceOutput.Text.Trim();
|
||
_config.ReplacePage.RuleFilePath = _txtRuleFile.Text.Trim();
|
||
_config.RestorePage.ReplaceDirectory = _txtRestoreSource.Text.Trim();
|
||
_config.RestorePage.RestoreDirectory = _txtRestoreOutput.Text.Trim();
|
||
_config.RestorePage.DiffInputFormat = GetSelectedRestoreDiffInputFormat();
|
||
|
||
var auto = _config.Settings.AutoExtract;
|
||
_config.Settings.JsonDiffEncryption = _chkJsonDiffEncryption.Checked;
|
||
_config.Settings.EnableImageReplacement = _chkEnableImageReplacement.Checked;
|
||
_config.Settings.UseFixedImageSeed = _chkUseFixedImageSeed.Checked;
|
||
_config.Settings.FixedImageSeed = Decimal.ToInt32(_numFixedImageSeed.Value);
|
||
auto.ExtractKeywords = _chkExtractKeywords.Checked;
|
||
auto.ExtractHighFrequencyWords = _chkExtractWords.Checked;
|
||
auto.ExtractHighFrequencySentences = _chkExtractSentences.Checked;
|
||
auto.UseKeywordFrequency = _chkKeywordFrequency.Checked;
|
||
auto.UseKeywordTfIdf = _chkKeywordTfIdf.Checked;
|
||
auto.UseKeywordTextRank = _chkKeywordTextRank.Checked;
|
||
auto.CountChineseWords = _chkChineseWords.Checked;
|
||
auto.CountEnglishWords = _chkEnglishWords.Checked;
|
||
auto.CountChineseSentences = _chkChineseSentences.Checked;
|
||
auto.CountEnglishSentences = _chkEnglishSentences.Checked;
|
||
auto.ExcludeEmailAddresses = _chkExcludeEmails.Checked;
|
||
auto.MaxKeywordCount = Decimal.ToInt32(_numKeywordCount.Value);
|
||
auto.MaxWordCount = Decimal.ToInt32(_numWordCount.Value);
|
||
auto.MaxSentenceCount = Decimal.ToInt32(_numSentenceCount.Value);
|
||
auto.MinChineseWordLength = Decimal.ToInt32(_numMinChineseLength.Value);
|
||
auto.MinEnglishWordLength = Decimal.ToInt32(_numMinEnglishLength.Value);
|
||
auto.SentenceDelimiters = _txtSentenceDelimiters.Text;
|
||
}
|
||
|
||
private async void MainForm_Shown(object sender, EventArgs e)
|
||
{
|
||
_logService.Info("APP_START", "程序启动。");
|
||
if (_configurationLoadResult.IsCorrupted)
|
||
{
|
||
var message = "config.json 已损坏或无法解析,当前禁止执行替换、恢复和自动提取。是否重建默认配置?"
|
||
+ Environment.NewLine
|
||
+ _configurationLoadResult.ErrorMessage;
|
||
var result = ShowMessage(message, "配置文件损坏", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
|
||
if (result == DialogResult.Yes)
|
||
{
|
||
_config = _configurationService.CreateDefault();
|
||
ApplyConfigToUi();
|
||
_configurationService.Save(_config);
|
||
_logService.Warning("CONFIG_RESET", "配置文件已重建为默认值。", _configurationLoadResult.ErrorMessage);
|
||
}
|
||
else
|
||
{
|
||
_logService.Error("CONFIG_BLOCK", "配置文件损坏且用户拒绝重建默认配置。", _configurationLoadResult.ErrorMessage);
|
||
_skipConfigSaveOnClose = true;
|
||
Close();
|
||
return;
|
||
}
|
||
}
|
||
else if (_configurationLoadResult.IsMissing)
|
||
{
|
||
_logService.Info("CONFIG_DEFAULT", "config.json 不存在,当前使用默认配置。");
|
||
}
|
||
|
||
await RefreshReplaceScanAsync();
|
||
await RefreshRestoreScanAsync();
|
||
UpdateActionStates();
|
||
}
|
||
|
||
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
|
||
{
|
||
if (_isAutoExtractRunning || _isReplaceRunning || _isRestoreRunning)
|
||
{
|
||
e.Cancel = true;
|
||
ShowMessage("当前仍有任务在执行,请先停止任务并等待当前文件处理完成。", "WORD 2007 格式调整软件", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
if (!_skipConfigSaveOnClose)
|
||
{
|
||
CaptureUiToConfig();
|
||
_configurationService.Save(_config);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
ShowMessage("保存配置失败: " + ex.Message, "WORD 2007 格式调整软件", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||
}
|
||
}
|
||
|
||
private void LogService_EntryWritten(object sender, LogEntry entry)
|
||
{
|
||
if (InvokeRequired)
|
||
{
|
||
BeginInvoke(new Action<object, LogEntry>(LogService_EntryWritten), sender, entry);
|
||
return;
|
||
}
|
||
|
||
var color = Color.Black;
|
||
switch (entry.Level)
|
||
{
|
||
case LogLevel.Info:
|
||
color = Color.Green;
|
||
break;
|
||
case LogLevel.Warning:
|
||
color = Color.DarkOrange;
|
||
break;
|
||
case LogLevel.Error:
|
||
color = Color.Red;
|
||
break;
|
||
}
|
||
|
||
_logBox.SelectionStart = _logBox.TextLength;
|
||
_logBox.SelectionColor = color;
|
||
_logBox.AppendText(string.Format("{0:HH:mm:ss} [{1}] {2} {3}", entry.Timestamp, entry.Level.ToString().ToUpperInvariant(), entry.EventCode, entry.Message) + Environment.NewLine);
|
||
if (!string.IsNullOrWhiteSpace(entry.Detail))
|
||
{
|
||
_logBox.SelectionColor = Color.Black;
|
||
_logBox.AppendText(IndentBlock(entry.Detail) + Environment.NewLine);
|
||
}
|
||
|
||
_logBox.SelectionStart = _logBox.TextLength;
|
||
_logBox.ScrollToCaret();
|
||
FlushLogBox();
|
||
}
|
||
|
||
private void LogService_WriteFailed(object sender, LogWriteFailureEventArgs e)
|
||
{
|
||
if (InvokeRequired)
|
||
{
|
||
BeginInvoke(new Action<object, LogWriteFailureEventArgs>(LogService_WriteFailed), sender, e);
|
||
return;
|
||
}
|
||
|
||
var detail = string.Format(
|
||
"日志文件写入失败,当前日志路径: {0}{1}{2}",
|
||
e.LogFilePath,
|
||
Environment.NewLine,
|
||
e.Exception.Message);
|
||
|
||
_logBox.SelectionStart = _logBox.TextLength;
|
||
_logBox.SelectionColor = Color.Red;
|
||
_logBox.AppendText(string.Format("{0:HH:mm:ss} [ERROR] LOG_WRITE_FAIL 日志文件写入失败", DateTime.Now) + Environment.NewLine);
|
||
_logBox.SelectionColor = Color.Black;
|
||
_logBox.AppendText(IndentBlock(detail) + Environment.NewLine);
|
||
_logBox.SelectionStart = _logBox.TextLength;
|
||
_logBox.ScrollToCaret();
|
||
FlushLogBox();
|
||
|
||
if (_isAutoExtractRunning && _autoExtractCancellation != null && !_autoExtractCancellation.IsCancellationRequested)
|
||
{
|
||
_autoExtractCancellation.Cancel();
|
||
}
|
||
|
||
if (_isReplaceRunning && _replaceExecutionCancellation != null && !_replaceExecutionCancellation.IsCancellationRequested)
|
||
{
|
||
_replaceExecutionCancellation.Cancel();
|
||
}
|
||
|
||
if (_isRestoreRunning && _restoreExecutionCancellation != null && !_restoreExecutionCancellation.IsCancellationRequested)
|
||
{
|
||
_restoreExecutionCancellation.Cancel();
|
||
}
|
||
|
||
ShowMessage(
|
||
"日志文件当前无法继续写入。当前正在处理的文件会在可控状态结束后停止,后续文件不会继续处理。"
|
||
+ Environment.NewLine
|
||
+ detail,
|
||
"日志异常",
|
||
MessageBoxButtons.OK,
|
||
MessageBoxIcon.Warning);
|
||
}
|
||
|
||
private void FlushLogBox()
|
||
{
|
||
FlushControl(_logBox, forceRefresh: false);
|
||
}
|
||
|
||
private string GetAllLogText()
|
||
{
|
||
return _logBox.Text;
|
||
}
|
||
|
||
private static string IndentBlock(string value)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(value))
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
var normalized = value.Replace("\r\n", "\n").Replace('\r', '\n');
|
||
return " " + normalized.Replace("\n", Environment.NewLine + " ");
|
||
}
|
||
|
||
private string BuildReplaceScanStartDetail(string sourceDirectory, string configuredOutputDirectory, string effectiveOutputDirectory)
|
||
{
|
||
var builder = new StringBuilder();
|
||
AppendKeyValue(builder, "原始文件夹", sourceDirectory);
|
||
AppendKeyValue(builder, "替换文件夹(输入)", configuredOutputDirectory);
|
||
AppendKeyValue(builder, "替换文件夹(生效)", effectiveOutputDirectory);
|
||
return builder.ToString().TrimEnd();
|
||
}
|
||
|
||
private string BuildReplaceScanDoneDetail(IList<FileScanItem> items)
|
||
{
|
||
var builder = new StringBuilder();
|
||
AppendKeyValue(builder, "候选数量", (items == null ? 0 : items.Count).ToString());
|
||
AppendScanItems(builder, items);
|
||
return builder.ToString().TrimEnd();
|
||
}
|
||
|
||
private string BuildRestoreScanStartDetail(string replaceDirectory, string configuredOutputDirectory, string effectiveOutputDirectory, string diffInputFormat)
|
||
{
|
||
var builder = new StringBuilder();
|
||
AppendKeyValue(builder, "替换文件夹", replaceDirectory);
|
||
AppendKeyValue(builder, "恢复文件夹(输入)", configuredOutputDirectory);
|
||
AppendKeyValue(builder, "恢复文件夹(生效)", effectiveOutputDirectory);
|
||
AppendKeyValue(builder, "差异文件格式", diffInputFormat);
|
||
return builder.ToString().TrimEnd();
|
||
}
|
||
|
||
private string BuildRestoreScanDoneDetail(IList<FileScanItem> items, string diffInputFormat)
|
||
{
|
||
var builder = new StringBuilder();
|
||
AppendKeyValue(builder, "候选数量", (items == null ? 0 : items.Count).ToString());
|
||
AppendKeyValue(builder, "差异文件格式", diffInputFormat);
|
||
AppendScanItems(builder, items);
|
||
return builder.ToString().TrimEnd();
|
||
}
|
||
|
||
private string BuildReplaceBatchPlanDetail(IList<FileScanItem> selectedItems, BatchExecutionPlan plan, RuleFile executionRuleFile, bool encryptDiff, int? fixedImageSeed)
|
||
{
|
||
var builder = new StringBuilder();
|
||
AppendKeyValue(builder, "原始文件夹", _txtReplaceSource.Text.Trim());
|
||
AppendKeyValue(builder, "替换文件夹", _txtReplaceOutput.Text.Trim());
|
||
AppendKeyValue(builder, "规则文件", _txtRuleFile.Text.Trim());
|
||
AppendKeyValue(builder, "差异 JSON 加密", encryptDiff ? "是" : "否");
|
||
AppendKeyValue(builder, "图片替换", _chkEnableImageReplacement.Checked ? "启用" : "禁用");
|
||
AppendKeyValue(builder, "固定图片种子", fixedImageSeed.HasValue ? fixedImageSeed.Value.ToString() : "否");
|
||
AppendKeyValue(builder, "规则总数", executionRuleFile == null || executionRuleFile.Rules == null ? "0" : executionRuleFile.Rules.Count.ToString());
|
||
AppendKeyValue(builder, "勾选文件数", selectedItems == null ? "0" : selectedItems.Count.ToString());
|
||
AppendKeyValue(builder, "实际执行数", plan == null ? "0" : plan.ExecutionItems.Count.ToString());
|
||
AppendKeyValue(builder, "预跳过数", plan == null ? "0" : plan.SkippedItems.Count.ToString());
|
||
AppendKeyValue(builder, "并发度", plan == null ? "0" : plan.DegreeOfParallelism.ToString());
|
||
if (plan != null && plan.SkippedItems.Count > 0)
|
||
{
|
||
builder.AppendLine("预跳过项目:");
|
||
foreach (var skipped in plan.SkippedItems)
|
||
{
|
||
builder.AppendLine("- " + NormalizeDetailValue(skipped.Item == null ? string.Empty : skipped.Item.SourcePath));
|
||
AppendKeyValue(builder, " 原因", skipped.Message);
|
||
AppendKeyValue(builder, " 冲突路径", skipped.ConflictingPath);
|
||
}
|
||
}
|
||
|
||
builder.AppendLine("执行项目:");
|
||
AppendScanItems(builder, plan == null ? null : plan.ExecutionItems.ToList());
|
||
return builder.ToString().TrimEnd();
|
||
}
|
||
|
||
private string BuildRestoreBatchPlanDetail(IList<FileScanItem> selectedItems, BatchExecutionPlan plan)
|
||
{
|
||
var builder = new StringBuilder();
|
||
AppendKeyValue(builder, "替换文件夹", _txtRestoreSource.Text.Trim());
|
||
AppendKeyValue(builder, "恢复文件夹", _txtRestoreOutput.Text.Trim());
|
||
AppendKeyValue(builder, "差异文件格式", GetSelectedRestoreDiffInputFormat());
|
||
AppendKeyValue(builder, "规则文件", _txtRuleFile.Text.Trim());
|
||
AppendKeyValue(builder, "勾选文件数", selectedItems == null ? "0" : selectedItems.Count.ToString());
|
||
AppendKeyValue(builder, "实际执行数", plan == null ? "0" : plan.ExecutionItems.Count.ToString());
|
||
AppendKeyValue(builder, "预跳过数", plan == null ? "0" : plan.SkippedItems.Count.ToString());
|
||
AppendKeyValue(builder, "并发度", plan == null ? "0" : plan.DegreeOfParallelism.ToString());
|
||
if (plan != null && plan.SkippedItems.Count > 0)
|
||
{
|
||
builder.AppendLine("预跳过项目:");
|
||
foreach (var skipped in plan.SkippedItems)
|
||
{
|
||
builder.AppendLine("- " + NormalizeDetailValue(skipped.Item == null ? string.Empty : skipped.Item.SourcePath));
|
||
AppendKeyValue(builder, " 原因", skipped.Message);
|
||
AppendKeyValue(builder, " 冲突路径", skipped.ConflictingPath);
|
||
}
|
||
}
|
||
|
||
builder.AppendLine("执行项目:");
|
||
AppendScanItems(builder, plan == null ? null : plan.ExecutionItems.ToList());
|
||
return builder.ToString().TrimEnd();
|
||
}
|
||
|
||
private string BuildReplaceFileStartDetail(FileScanItem item, int displayIndex, int totalCount)
|
||
{
|
||
return BuildFileExecutionDetail("替换", item, displayIndex, totalCount, includeResultFiles: false, textOperationCount: null, imageOperationCount: null, bmpPath: null, exception: null);
|
||
}
|
||
|
||
private string BuildReplaceFileDoneDetail(FileScanItem item, FileProcessResult result, int displayIndex, int totalCount)
|
||
{
|
||
return BuildFileExecutionDetail(
|
||
"替换",
|
||
item,
|
||
displayIndex,
|
||
totalCount,
|
||
includeResultFiles: true,
|
||
textOperationCount: result == null ? null : (int?)result.TextOperationCount,
|
||
imageOperationCount: result == null ? null : (int?)result.ImageOperationCount,
|
||
bmpPath: result == null ? null : result.BmpPath,
|
||
exception: null);
|
||
}
|
||
|
||
private string BuildReplaceFileFailDetail(FileScanItem item, Exception exception, int displayIndex, int totalCount)
|
||
{
|
||
return BuildFileExecutionDetail("替换", item, displayIndex, totalCount, includeResultFiles: true, textOperationCount: null, imageOperationCount: null, bmpPath: null, exception: exception);
|
||
}
|
||
|
||
private string BuildRestoreFileStartDetail(FileScanItem item, int displayIndex, int totalCount)
|
||
{
|
||
return BuildFileExecutionDetail("恢复", item, displayIndex, totalCount, includeResultFiles: false, textOperationCount: null, imageOperationCount: null, bmpPath: null, exception: null);
|
||
}
|
||
|
||
private string BuildRestoreFileDoneDetail(FileScanItem item, FileProcessResult result, int displayIndex, int totalCount)
|
||
{
|
||
return BuildFileExecutionDetail(
|
||
"恢复",
|
||
item,
|
||
displayIndex,
|
||
totalCount,
|
||
includeResultFiles: true,
|
||
textOperationCount: result == null ? null : (int?)result.TextOperationCount,
|
||
imageOperationCount: result == null ? null : (int?)result.ImageOperationCount,
|
||
bmpPath: result == null ? null : result.BmpPath,
|
||
exception: null);
|
||
}
|
||
|
||
private string BuildRestoreFileFailDetail(FileScanItem item, Exception exception, int displayIndex, int totalCount)
|
||
{
|
||
return BuildFileExecutionDetail("恢复", item, displayIndex, totalCount, includeResultFiles: true, textOperationCount: null, imageOperationCount: null, bmpPath: null, exception: exception);
|
||
}
|
||
|
||
private string BuildFileExecutionDetail(
|
||
string operationName,
|
||
FileScanItem item,
|
||
int displayIndex,
|
||
int totalCount,
|
||
bool includeResultFiles,
|
||
int? textOperationCount,
|
||
int? imageOperationCount,
|
||
string bmpPath,
|
||
Exception exception)
|
||
{
|
||
var builder = new StringBuilder();
|
||
AppendKeyValue(builder, "操作", operationName);
|
||
AppendKeyValue(builder, "序号", string.Format("{0}/{1}", displayIndex, totalCount));
|
||
AppendFileSnapshot(builder, "源文件", item == null ? null : item.SourcePath);
|
||
AppendKeyValue(builder, "目标输出", item == null ? null : item.OutputPath);
|
||
AppendKeyValue(builder, "差异文件", item == null ? null : item.DiffPath);
|
||
if (includeResultFiles)
|
||
{
|
||
AppendFileSnapshot(builder, "目标输出", item == null ? null : item.OutputPath);
|
||
AppendFileSnapshot(builder, "差异文件", item == null ? null : item.DiffPath);
|
||
AppendFileSnapshot(builder, "BMP 差异文件", bmpPath);
|
||
}
|
||
|
||
if (textOperationCount.HasValue)
|
||
{
|
||
AppendKeyValue(builder, "文本命中/恢复数", textOperationCount.Value.ToString());
|
||
}
|
||
|
||
if (imageOperationCount.HasValue)
|
||
{
|
||
AppendKeyValue(builder, "图片命中/恢复数", imageOperationCount.Value.ToString());
|
||
}
|
||
|
||
AppendExceptionDetail(builder, exception);
|
||
return builder.ToString().TrimEnd();
|
||
}
|
||
|
||
private static void AppendScanItems(StringBuilder builder, IList<FileScanItem> items)
|
||
{
|
||
if (builder == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (items == null || items.Count == 0)
|
||
{
|
||
builder.AppendLine("候选项: <empty>");
|
||
return;
|
||
}
|
||
|
||
var maxCount = Math.Min(items.Count, 20);
|
||
for (var index = 0; index < maxCount; index++)
|
||
{
|
||
var item = items[index];
|
||
builder.AppendLine(string.Format("[{0}]", index + 1));
|
||
AppendFileSnapshot(builder, " 源文件", item == null ? null : item.SourcePath);
|
||
AppendKeyValue(builder, " 目标输出", item == null ? null : item.OutputPath);
|
||
AppendKeyValue(builder, " 差异文件", item == null ? null : item.DiffPath);
|
||
AppendKeyValue(builder, " 勾选", item != null && item.IsSelected ? "是" : "否");
|
||
AppendKeyValue(builder, " 状态", item == null ? null : item.Status.ToString());
|
||
}
|
||
|
||
if (items.Count > maxCount)
|
||
{
|
||
builder.AppendLine(string.Format("... 其余 {0} 项省略", items.Count - maxCount));
|
||
}
|
||
}
|
||
|
||
private static void AppendFileSnapshot(StringBuilder builder, string label, string path)
|
||
{
|
||
if (builder == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
AppendKeyValue(builder, label + "路径", path);
|
||
if (string.IsNullOrWhiteSpace(path))
|
||
{
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
if (!File.Exists(path))
|
||
{
|
||
AppendKeyValue(builder, label + "存在", "否");
|
||
return;
|
||
}
|
||
|
||
var info = new FileInfo(path);
|
||
AppendKeyValue(builder, label + "存在", "是");
|
||
AppendKeyValue(builder, label + "大小(bytes)", info.Length.ToString());
|
||
AppendKeyValue(builder, label + "属性", info.Attributes.ToString());
|
||
AppendKeyValue(builder, label + "只读", info.IsReadOnly ? "是" : "否");
|
||
AppendKeyValue(builder, label + "最后写入时间", info.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss.fff"));
|
||
AppendKeyValue(builder, label + "最后写入时间(UTC)", info.LastWriteTimeUtc.ToString("yyyy-MM-dd HH:mm:ss.fff"));
|
||
AppendKeyValue(builder, label + "SHA256", SafeComputeFileFingerprint(path));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
AppendKeyValue(builder, label + "快照错误", ex.GetType().Name + ": " + ex.Message);
|
||
}
|
||
}
|
||
|
||
private static void AppendExceptionDetail(StringBuilder builder, Exception exception)
|
||
{
|
||
if (builder == null || exception == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
AppendKeyValue(builder, "异常类型", exception.GetType().FullName);
|
||
AppendKeyValue(builder, "异常消息", exception.Message);
|
||
if (exception.InnerException != null)
|
||
{
|
||
AppendKeyValue(builder, "内部异常", exception.InnerException.GetType().FullName + ": " + exception.InnerException.Message);
|
||
}
|
||
|
||
AppendKeyValue(builder, "异常堆栈", exception.ToString());
|
||
}
|
||
|
||
private static void AppendKeyValue(StringBuilder builder, string key, string value)
|
||
{
|
||
if (builder == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
builder.Append(key ?? string.Empty);
|
||
builder.Append(": ");
|
||
builder.AppendLine(NormalizeDetailValue(value));
|
||
}
|
||
|
||
private static string SafeComputeFileFingerprint(string path)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(path))
|
||
{
|
||
return "<empty>";
|
||
}
|
||
|
||
try
|
||
{
|
||
return HashUtility.ComputeFileSha256Base64(path);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return "<unavailable: " + ex.GetType().Name + ": " + ex.Message + ">";
|
||
}
|
||
}
|
||
|
||
private static string NormalizeDetailValue(string value)
|
||
{
|
||
return string.IsNullOrWhiteSpace(value) ? "<empty>" : value;
|
||
}
|
||
|
||
private void CopyAllLogs()
|
||
{
|
||
var text = GetAllLogText();
|
||
if (string.IsNullOrEmpty(text))
|
||
{
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
Clipboard.SetText(text);
|
||
}
|
||
catch
|
||
{
|
||
}
|
||
}
|
||
|
||
private void ClearLogBox()
|
||
{
|
||
_logBox.Clear();
|
||
}
|
||
|
||
private async Task RefreshScanOnTabSwitchAsync()
|
||
{
|
||
try
|
||
{
|
||
if (_tabControl.SelectedTab == _replaceTab)
|
||
{
|
||
await RefreshReplaceScanAsync();
|
||
}
|
||
else if (_tabControl.SelectedTab == _restoreTab)
|
||
{
|
||
await RefreshRestoreScanAsync();
|
||
}
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
}
|
||
}
|
||
|
||
private async Task RefreshReplaceScanAsync()
|
||
{
|
||
_replaceScanCancellation?.Cancel();
|
||
_replaceScanCancellation = new CancellationTokenSource();
|
||
var token = _replaceScanCancellation.Token;
|
||
|
||
try
|
||
{
|
||
var source = _txtReplaceSource.Text.Trim();
|
||
if (string.IsNullOrWhiteSpace(source) || !Directory.Exists(source))
|
||
{
|
||
_replaceItems.Clear();
|
||
_lblReplaceRuntime.Text = "当前文件 0/0 | 当前规则 - | 当前文件命中 0 | 累计命中 0";
|
||
FlushReplaceRuntimeUi(forceRefresh: true);
|
||
UpdateActionStates();
|
||
return;
|
||
}
|
||
|
||
var output = GetReplacePreviewOutputDirectory(source);
|
||
if (!string.IsNullOrWhiteSpace(_txtReplaceOutput.Text.Trim()) && string.IsNullOrWhiteSpace(output))
|
||
{
|
||
foreach (var message in _fileScanService.ValidateReplaceDirectories(source, _txtReplaceOutput.Text.Trim()))
|
||
{
|
||
_logService.Error("SCAN_BLOCK", message);
|
||
}
|
||
}
|
||
|
||
_logService.Info("SCAN_START", "开始扫描替换候选文件。", BuildReplaceScanStartDetail(source, _txtReplaceOutput.Text.Trim(), output));
|
||
var ruleFile = CreateExecutionRuleFile();
|
||
var items = await Task.Run(() => _fileScanService.ScanReplaceCandidates(source, output, ruleFile), token);
|
||
if (token.IsCancellationRequested)
|
||
{
|
||
return;
|
||
}
|
||
|
||
ReplaceBinding(_replaceItems, items);
|
||
_lblReplaceRuntime.Text = string.Format(
|
||
"当前文件 0/{0} | 当前规则 - | 当前文件命中 0 | 累计命中 0",
|
||
items.Count);
|
||
FlushReplaceRuntimeUi(forceRefresh: true);
|
||
_logService.Info("SCAN_DONE", string.Format("替换候选文件扫描完成,共 {0} 项。", items.Count), BuildReplaceScanDoneDetail(items));
|
||
UpdateActionStates();
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService.Error("SCAN_FAIL", "替换候选文件扫描失败。", ex.Message);
|
||
}
|
||
}
|
||
|
||
private async Task RefreshRestoreScanAsync()
|
||
{
|
||
_restoreScanCancellation?.Cancel();
|
||
_restoreScanCancellation = new CancellationTokenSource();
|
||
var token = _restoreScanCancellation.Token;
|
||
|
||
try
|
||
{
|
||
var source = _txtRestoreSource.Text.Trim();
|
||
if (string.IsNullOrWhiteSpace(source) || !Directory.Exists(source))
|
||
{
|
||
_restoreItems.Clear();
|
||
_lblRestoreRuntime.Text = "当前文件 0/0 | 当前规则 - | 当前文件命中 0 | 累计命中 0";
|
||
FlushRestoreRuntimeUi(forceRefresh: true);
|
||
UpdateActionStates();
|
||
return;
|
||
}
|
||
|
||
var output = GetRestorePreviewOutputDirectory(source);
|
||
var diffInputFormat = GetSelectedRestoreDiffInputFormat();
|
||
if (!string.IsNullOrWhiteSpace(_txtRestoreOutput.Text.Trim()) && string.IsNullOrWhiteSpace(output))
|
||
{
|
||
foreach (var message in _fileScanService.ValidateRestoreDirectories(source, _txtRestoreOutput.Text.Trim()))
|
||
{
|
||
_logService.Error("RESTORE_SCAN_BLOCK", message);
|
||
}
|
||
}
|
||
|
||
_logService.Info("RESTORE_SCAN_START", "开始扫描恢复候选文件。", BuildRestoreScanStartDetail(source, _txtRestoreOutput.Text.Trim(), output, diffInputFormat));
|
||
var items = await Task.Run(() => _fileScanService.ScanRestoreCandidates(source, output, diffInputFormat), token);
|
||
if (token.IsCancellationRequested)
|
||
{
|
||
return;
|
||
}
|
||
|
||
ReplaceBinding(_restoreItems, items);
|
||
_lblRestoreRuntime.Text = string.Format(
|
||
"当前文件 0/{0} | 当前规则 - | 当前文件命中 0 | 累计命中 0",
|
||
items.Count);
|
||
FlushRestoreRuntimeUi(forceRefresh: true);
|
||
_logService.Info("RESTORE_SCAN_DONE", string.Format("恢复候选文件扫描完成,共 {0} 项。", items.Count), BuildRestoreScanDoneDetail(items, diffInputFormat));
|
||
UpdateActionStates();
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService.Error("RESTORE_SCAN_FAIL", "恢复候选文件扫描失败。", ex.Message);
|
||
}
|
||
}
|
||
|
||
private async Task RunAutoExtractAsync()
|
||
{
|
||
var selectedFiles = _replaceItems.Where(item => item.IsSelected).Select(item => item.SourcePath).ToList();
|
||
if (selectedFiles.Count == 0)
|
||
{
|
||
ShowMessage("未勾选任何文件,无法执行自动提取。", "自动提取", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
CaptureUiToConfig();
|
||
_autoExtractCancellation = new CancellationTokenSource();
|
||
_isAutoExtractRunning = true;
|
||
SetRuleEditingEnabled(false);
|
||
_btnStopReplace.Enabled = true;
|
||
UpdateActionStates();
|
||
_replaceTotalProgress.Maximum = Math.Max(selectedFiles.Count, 1);
|
||
_replaceTotalProgress.Value = 0;
|
||
_replaceFileProgress.Value = 0;
|
||
|
||
_logService.Info("EXTRACT_START", string.Format("开始自动提取,勾选文件数={0}。", selectedFiles.Count));
|
||
try
|
||
{
|
||
var progress = new Progress<ExtractionProgress>(value =>
|
||
{
|
||
_replaceTotalProgress.Maximum = Math.Max(value.Total, 1);
|
||
_replaceTotalProgress.Value = Math.Min(value.Current, _replaceTotalProgress.Maximum);
|
||
_replaceFileProgress.Style = ProgressBarStyle.Marquee;
|
||
_lblReplaceRuntime.Text = value.Message;
|
||
});
|
||
|
||
var result = await _autoExtractService.ExtractAsync(
|
||
selectedFiles,
|
||
_config.Settings.AutoExtract,
|
||
_rules.ToList(),
|
||
progress,
|
||
_autoExtractCancellation.Token);
|
||
|
||
_replaceFileProgress.Style = ProgressBarStyle.Blocks;
|
||
_replaceFileProgress.Value = 100;
|
||
foreach (var warning in result.Warnings)
|
||
{
|
||
_logService.Warning("EXTRACT_SKIP", warning);
|
||
}
|
||
|
||
if (result.Candidates.Count == 0)
|
||
{
|
||
ShowMessage("未提取到可用项。", "自动提取", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
_logService.Info("EXTRACT_DONE", "自动提取完成,但未提取到可用项。");
|
||
return;
|
||
}
|
||
|
||
var preview = _dialogService.ShowAutoExtractPreview(this, result.Candidates);
|
||
if (preview.DialogResult != DialogResult.OK)
|
||
{
|
||
_logService.Info("EXTRACT_DONE", "自动提取预览已取消,未追加规则。");
|
||
return;
|
||
}
|
||
|
||
var selectedCandidates = preview.SelectedCandidates ?? Array.Empty<ExtractionCandidate>();
|
||
AppendAutoExtractRules(selectedCandidates);
|
||
var skippedCount = result.Candidates.Count - selectedCandidates.Count;
|
||
var summary = string.Format("自动提取完成。提取={0},追加={1},跳过={2}。", result.Candidates.Count, selectedCandidates.Count, skippedCount);
|
||
ShowMessage(summary, "自动提取", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
_logService.Info("EXTRACT_DONE", summary);
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
_logService.Warning("EXTRACT_CANCEL", "自动提取已停止。");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService.Error("EXTRACT_FAIL", "自动提取失败。", ex.ToString());
|
||
ShowMessage("自动提取失败: " + ex.Message, "自动提取", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
}
|
||
finally
|
||
{
|
||
_isAutoExtractRunning = false;
|
||
SetRuleEditingEnabled(true);
|
||
_btnStopReplace.Enabled = false;
|
||
_replaceFileProgress.Style = ProgressBarStyle.Blocks;
|
||
UpdateActionStates();
|
||
}
|
||
}
|
||
|
||
private void AppendAutoExtractRules(IList<ExtractionCandidate> candidates)
|
||
{
|
||
if (candidates.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var newRuleIds = new List<string>();
|
||
foreach (var candidate in candidates)
|
||
{
|
||
var rule = _ruleService.CreateAutoExtractRule(candidate, _rules.ToList());
|
||
_rules.Add(rule);
|
||
newRuleIds.Add(rule.Id);
|
||
}
|
||
|
||
_lastAutoExtractRuleIds = newRuleIds;
|
||
_btnUndoAutoExtract.Enabled = true;
|
||
|
||
var report = _ruleService.Validate(new RuleFile { Rules = _rules.ToList() });
|
||
if (report.Issues.Count > 0)
|
||
{
|
||
var text = string.Join(Environment.NewLine, report.Issues.Select(_ruleService.FormatIssue));
|
||
ShowMessage(text, "规则检查", MessageBoxButtons.OK, report.HasErrors ? MessageBoxIcon.Warning : MessageBoxIcon.Information);
|
||
foreach (var issue in report.Issues)
|
||
{
|
||
LogRuleIssue(issue);
|
||
}
|
||
}
|
||
|
||
_tabControl.SelectedTab = _settingsTab;
|
||
if (_rules.Count > 0)
|
||
{
|
||
var firstIndex = _rules.ToList().FindIndex(rule => newRuleIds.Contains(rule.Id));
|
||
if (firstIndex >= 0)
|
||
{
|
||
_gridRules.ClearSelection();
|
||
if (_gridRules.Rows[firstIndex].Cells.Count > 0)
|
||
{
|
||
_gridRules.CurrentCell = _gridRules.Rows[firstIndex].Cells[0];
|
||
}
|
||
|
||
_gridRules.Rows[firstIndex].Selected = true;
|
||
try
|
||
{
|
||
_gridRules.FirstDisplayedScrollingRowIndex = firstIndex;
|
||
}
|
||
catch (InvalidOperationException)
|
||
{
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private void OpenRules_Click(object sender, EventArgs e)
|
||
{
|
||
using (var dialog = new OpenFileDialog())
|
||
{
|
||
dialog.Filter = "规则文件 (*.rule)|*.rule|所有文件 (*.*)|*.*";
|
||
if (dialog.ShowDialog(this) != DialogResult.OK)
|
||
{
|
||
return;
|
||
}
|
||
|
||
LoadRuleFile(dialog.FileName);
|
||
_txtRuleFile.Text = dialog.FileName;
|
||
}
|
||
}
|
||
|
||
private void SaveRules_Click(object sender, EventArgs e)
|
||
{
|
||
try
|
||
{
|
||
var path = _txtRuleFile.Text.Trim();
|
||
if (string.IsNullOrWhiteSpace(path))
|
||
{
|
||
using (var dialog = new SaveFileDialog())
|
||
{
|
||
dialog.Filter = "规则文件 (*.rule)|*.rule";
|
||
dialog.DefaultExt = "rule";
|
||
if (dialog.ShowDialog(this) != DialogResult.OK)
|
||
{
|
||
return;
|
||
}
|
||
|
||
path = dialog.FileName;
|
||
_txtRuleFile.Text = path;
|
||
}
|
||
}
|
||
|
||
_ruleFile.Rules = _rules.ToList();
|
||
var report = _ruleService.Validate(_ruleFile);
|
||
foreach (var issue in report.Issues)
|
||
{
|
||
LogRuleIssue(issue);
|
||
}
|
||
|
||
if (report.Issues.Count > 0)
|
||
{
|
||
var summary = string.Join(Environment.NewLine, report.Issues.Select(_ruleService.FormatIssue));
|
||
ShowMessage(summary, "规则检查", MessageBoxButtons.OK, report.HasErrors ? MessageBoxIcon.Warning : MessageBoxIcon.Information);
|
||
if (report.HasErrors)
|
||
{
|
||
return;
|
||
}
|
||
}
|
||
|
||
_ruleService.Save(path, _ruleFile);
|
||
_logService.Info("RULE_SAVE", "规则文件已保存。", path);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService.Error("RULE_SAVE_FAIL", "保存规则文件失败。", ex.Message);
|
||
ShowMessage("保存规则文件失败: " + ex.Message, "规则文件", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
}
|
||
}
|
||
|
||
private void UndoAutoExtract_Click(object sender, EventArgs e)
|
||
{
|
||
if (_lastAutoExtractRuleIds.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
for (var index = _rules.Count - 1; index >= 0; index--)
|
||
{
|
||
if (_lastAutoExtractRuleIds.Contains(_rules[index].Id))
|
||
{
|
||
_rules.RemoveAt(index);
|
||
}
|
||
}
|
||
|
||
_logService.Info("EXTRACT_UNDO", string.Format("已撤销最近一次自动提取追加的 {0} 条规则。", _lastAutoExtractRuleIds.Count));
|
||
_lastAutoExtractRuleIds.Clear();
|
||
_btnUndoAutoExtract.Enabled = false;
|
||
}
|
||
|
||
private void AddRule_Click(object sender, EventArgs e)
|
||
{
|
||
var nextIndex = _rules.Count + 1;
|
||
_rules.Add(new RuleDefinition
|
||
{
|
||
Id = "MR" + nextIndex.ToString("0000"),
|
||
Name = "MANUAL-" + nextIndex.ToString("0000"),
|
||
IsEnabled = true,
|
||
MatchMode = RuleMatchMode.PlainText,
|
||
Target = RuleTarget.DocumentContent,
|
||
Note = string.Empty,
|
||
});
|
||
}
|
||
|
||
private void DeleteRule_Click(object sender, EventArgs e)
|
||
{
|
||
DeleteSelectedRules();
|
||
}
|
||
|
||
private void MoveRuleUp_Click(object sender, EventArgs e)
|
||
{
|
||
MoveCurrentRule(-1);
|
||
}
|
||
|
||
private void MoveRuleDown_Click(object sender, EventArgs e)
|
||
{
|
||
MoveCurrentRule(1);
|
||
}
|
||
|
||
private void MoveCurrentRule(int delta)
|
||
{
|
||
if (_gridRules.CurrentRow == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var currentIndex = _gridRules.CurrentRow.Index;
|
||
var nextIndex = currentIndex + delta;
|
||
if (currentIndex < 0 || nextIndex < 0 || nextIndex >= _rules.Count)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var item = _rules[currentIndex];
|
||
_rules.RemoveAt(currentIndex);
|
||
_rules.Insert(nextIndex, item);
|
||
_gridRules.ClearSelection();
|
||
_gridRules.Rows[nextIndex].Selected = true;
|
||
}
|
||
|
||
private async void StartReplaceExecution_Click(object sender, EventArgs e)
|
||
{
|
||
if (_isReplaceRunning || _isRestoreRunning)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!TryPersistConfigFromUi())
|
||
{
|
||
return;
|
||
}
|
||
|
||
var selectedItems = _replaceItems.Where(item => item.IsSelected).ToList();
|
||
if (selectedItems.Count == 0)
|
||
{
|
||
ShowMessage("未勾选任何替换文件。", "开始替换", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
var validationMessages = GetReplaceExecutionValidationMessages();
|
||
if (validationMessages.Count > 0)
|
||
{
|
||
foreach (var message in validationMessages)
|
||
{
|
||
_logService.Error("REPLACE_BLOCK", message);
|
||
}
|
||
|
||
ShowMessage(string.Join(Environment.NewLine, validationMessages), "开始替换", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||
return;
|
||
}
|
||
|
||
RuleFile executionRuleFile;
|
||
if (!PrepareRuleExecutionWithBlockingErrors(out executionRuleFile))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var emptyReplacementRules = _rules.Where(rule => rule.IsEnabled && string.IsNullOrEmpty(rule.ReplacementText)).ToList();
|
||
if (emptyReplacementRules.Count > 0)
|
||
{
|
||
var warning = string.Format("当前存在 {0} 条启用规则的替换文本为空,执行后会删除命中的原始内容。是否继续?", emptyReplacementRules.Count);
|
||
if (ShowMessage(warning, "强提示", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes)
|
||
{
|
||
return;
|
||
}
|
||
}
|
||
|
||
await ExecuteReplaceBatchAsync(selectedItems, executionRuleFile);
|
||
}
|
||
|
||
private async void StartRestoreExecution_Click(object sender, EventArgs e)
|
||
{
|
||
if (_isReplaceRunning || _isRestoreRunning)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!TryPersistConfigFromUi())
|
||
{
|
||
return;
|
||
}
|
||
|
||
var selectedItems = _restoreItems.Where(item => item.IsSelected).ToList();
|
||
if (selectedItems.Count == 0)
|
||
{
|
||
ShowMessage("未勾选任何恢复文件。", "开始恢复", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
var validationMessages = GetRestoreExecutionValidationMessages();
|
||
if (validationMessages.Count > 0)
|
||
{
|
||
foreach (var message in validationMessages)
|
||
{
|
||
_logService.Error("RESTORE_BLOCK", message);
|
||
}
|
||
|
||
ShowMessage(string.Join(Environment.NewLine, validationMessages), "开始恢复", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||
return;
|
||
}
|
||
|
||
await ExecuteRestoreBatchAsync(selectedItems);
|
||
}
|
||
|
||
private void StopReplaceOrAutoExtract_Click(object sender, EventArgs e)
|
||
{
|
||
if (_isAutoExtractRunning)
|
||
{
|
||
_autoExtractCancellation?.Cancel();
|
||
return;
|
||
}
|
||
|
||
if (_isReplaceRunning && _replaceExecutionCancellation != null && !_replaceExecutionCancellation.IsCancellationRequested)
|
||
{
|
||
_replaceExecutionCancellation.Cancel();
|
||
_logService.Warning("REPLACE_STOP", "已请求停止替换任务,当前文件完成后停止。");
|
||
}
|
||
}
|
||
|
||
private void StopRestore_Click(object sender, EventArgs e)
|
||
{
|
||
if (_isRestoreRunning && _restoreExecutionCancellation != null && !_restoreExecutionCancellation.IsCancellationRequested)
|
||
{
|
||
_restoreExecutionCancellation.Cancel();
|
||
_logService.Warning("RESTORE_STOP", "已请求停止恢复任务,当前文件完成后停止。");
|
||
}
|
||
}
|
||
|
||
private async Task ExecuteReplaceBatchAsync(IList<FileScanItem> selectedItems, RuleFile executionRuleFile)
|
||
{
|
||
_replaceExecutionCancellation?.Dispose();
|
||
_replaceExecutionCancellation = new CancellationTokenSource();
|
||
_isReplaceRunning = true;
|
||
SetRuleEditingEnabled(false);
|
||
var encryptDiff = _chkJsonDiffEncryption.Checked;
|
||
var fixedImageSeed = _chkUseFixedImageSeed.Checked ? Decimal.ToInt32(_numFixedImageSeed.Value) : (int?)null;
|
||
|
||
foreach (var item in selectedItems)
|
||
{
|
||
item.Status = ProcessingStatus.Pending;
|
||
item.TextOperationCount = 0;
|
||
item.ImageOperationCount = 0;
|
||
}
|
||
|
||
ResetReplaceProgress(selectedItems.Count);
|
||
UpdateActionStates();
|
||
|
||
var plan = _batchExecutionPlanner.CreatePlan(selectedItems, Environment.ProcessorCount);
|
||
var executionItems = plan.ExecutionItems.ToList();
|
||
var displayOrder = executionItems
|
||
.Select((item, index) => new { item, index = index + 1 })
|
||
.ToDictionary(entry => entry.item, entry => entry.index);
|
||
var successCount = 0;
|
||
var failedCount = 0;
|
||
var stoppedCount = 0;
|
||
var totalHits = 0;
|
||
var completedCount = 0;
|
||
var nextExecutionIndex = -1;
|
||
|
||
foreach (var skipped in plan.SkippedItems)
|
||
{
|
||
skipped.Item.Status = ProcessingStatus.Failed;
|
||
RefreshReplaceItemRow(skipped.Item, forceRefresh: true);
|
||
failedCount++;
|
||
completedCount++;
|
||
_replaceTotalProgress.Value = Math.Min(completedCount, _replaceTotalProgress.Maximum);
|
||
FlushReplaceRuntimeUi(forceRefresh: true);
|
||
_logService.Warning(
|
||
"REPLACE_BATCH_SKIP",
|
||
skipped.Message,
|
||
string.Format("源文件: {0}{1}冲突路径: {2}", skipped.Item.SourcePath, Environment.NewLine, skipped.ConflictingPath));
|
||
}
|
||
|
||
try
|
||
{
|
||
_logService.Info(
|
||
"REPLACE_BATCH_PLAN",
|
||
string.Format(
|
||
"替换批处理开始。勾选={0},执行={1},预跳过={2},并发度={3}{4}。",
|
||
selectedItems.Count,
|
||
executionItems.Count,
|
||
plan.SkippedItems.Count,
|
||
plan.DegreeOfParallelism,
|
||
plan.RequiresSerialExecution ? "(包含 .doc,已退回串行)" : string.Empty),
|
||
BuildReplaceBatchPlanDetail(selectedItems, plan, executionRuleFile, encryptDiff, fixedImageSeed));
|
||
|
||
async Task RunWorkerAsync()
|
||
{
|
||
while (true)
|
||
{
|
||
if (_replaceExecutionCancellation.IsCancellationRequested)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var executionIndex = Interlocked.Increment(ref nextExecutionIndex);
|
||
if (executionIndex >= executionItems.Count)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var item = executionItems[executionIndex];
|
||
var displayIndex = displayOrder[item];
|
||
item.Status = ProcessingStatus.InProgress;
|
||
RefreshReplaceItemRow(item, forceRefresh: true);
|
||
UpdateReplaceStatus(displayIndex, selectedItems.Count, "-", 0, totalHits, 0, forceRefresh: true);
|
||
|
||
_logService.Info("DEBUG_RULES", "执行规则列表。",
|
||
string.Format("总规则数={0}; 规则详情=[{1}]",
|
||
executionRuleFile.Rules.Count,
|
||
string.Join("; ", executionRuleFile.Rules.Select(r =>
|
||
string.Format("Id={0}, Name={1}, Enabled={2}, Target={3}, SearchText='{4}', ReplacementText='{5}'",
|
||
r.Id, r.Name, r.IsEnabled, r.Target, r.SearchText, r.ReplacementText)))));
|
||
|
||
var fileNameRules = executionRuleFile.Rules
|
||
.Where(r => r != null && r.IsEnabled && !string.IsNullOrEmpty(r.SearchText) && r.Target == RuleTarget.FileName)
|
||
.ToList();
|
||
if (fileNameRules.Count > 0)
|
||
{
|
||
var sourceFileName = Path.GetFileNameWithoutExtension(item.SourcePath);
|
||
var extension = Path.GetExtension(item.SourcePath);
|
||
_logService.Info("FILENAME_RULES", "应用文件名替换规则。",
|
||
string.Format("源文件名={0}; 规则数={1}; 规则详情=[{2}]",
|
||
sourceFileName,
|
||
fileNameRules.Count,
|
||
string.Join("; ", fileNameRules.Select(r =>
|
||
string.Format("Id={0}, SearchText='{1}', ReplacementText='{2}', MatchMode={3}, CaseSensitive={4}, WholeWord={5}",
|
||
r.Id, r.SearchText, r.ReplacementText, r.MatchMode, r.IsCaseSensitive, r.IsWholeWord)))));
|
||
var replacedName = FileScanService.ApplyRulesToFileName(sourceFileName, fileNameRules);
|
||
_logService.Info("FILENAME_REPLACED", "文件名替换结果。",
|
||
string.Format("原文件名={0}; 替换后={1}", sourceFileName, replacedName));
|
||
var outputDir = Path.GetDirectoryName(item.OutputPath) ?? string.Empty;
|
||
var diffDir = Path.GetDirectoryName(item.DiffPath) ?? string.Empty;
|
||
var datedSuffix = "-" + DateTime.Today.ToString("yyyyMMdd");
|
||
item.OutputPath = Path.Combine(outputDir, replacedName + datedSuffix + extension);
|
||
item.DiffPath = Path.Combine(diffDir, replacedName + datedSuffix + ".diff");
|
||
}
|
||
|
||
var startDetail = await Task.Run(() => BuildReplaceFileStartDetail(item, displayIndex, selectedItems.Count));
|
||
_logService.Info("REPLACE_FILE_START", "开始处理替换文件。", startDetail);
|
||
|
||
try
|
||
{
|
||
var progressThrottle = new RuntimeProgressThrottle();
|
||
var progress = new Progress<FileProcessProgress>(value =>
|
||
{
|
||
LogProcessProgressWarning("REPLACE_PROGRESS_WARN", value);
|
||
var forceRefresh = value.FilePercent >= 100;
|
||
if (!progressThrottle.ShouldApply(forceRefresh))
|
||
{
|
||
return;
|
||
}
|
||
|
||
UpdateReplaceStatus(
|
||
displayIndex,
|
||
selectedItems.Count,
|
||
value.CurrentRuleName ?? value.CurrentRuleId ?? "-",
|
||
value.CurrentFileTextCount,
|
||
totalHits + value.CurrentFileTextCount,
|
||
value.FilePercent,
|
||
forceRefresh);
|
||
});
|
||
|
||
var request = new ReplaceFileRequest
|
||
{
|
||
SourcePath = item.SourcePath,
|
||
OutputPath = item.OutputPath,
|
||
DiffPath = item.DiffPath,
|
||
SourceRootDirectory = _txtReplaceSource.Text.Trim(),
|
||
RuleFilePath = _txtRuleFile.Text.Trim(),
|
||
RuleFile = executionRuleFile,
|
||
EncryptDiff = encryptDiff,
|
||
EnableImageReplacement = _chkEnableImageReplacement.Checked,
|
||
FixedImageSeed = fixedImageSeed,
|
||
};
|
||
|
||
var result = await Task.Run(() => _documentProcessingService.Replace(request, progress, CancellationToken.None));
|
||
item.OutputPath = result.OutputPath;
|
||
item.DiffPath = result.DiffPath;
|
||
item.TextOperationCount = result.TextOperationCount;
|
||
item.ImageOperationCount = result.ImageOperationCount;
|
||
item.Status = ProcessingStatus.Success;
|
||
RefreshReplaceItemRow(item, forceRefresh: true);
|
||
successCount++;
|
||
totalHits += result.TextOperationCount + result.ImageOperationCount;
|
||
UpdateReplaceStatus(displayIndex, selectedItems.Count, "-", result.TextOperationCount + result.ImageOperationCount, totalHits, 100, forceRefresh: true);
|
||
var doneDetail = await Task.Run(() => BuildReplaceFileDoneDetail(item, result, displayIndex, selectedItems.Count));
|
||
_logService.Info(
|
||
"REPLACE_FILE_DONE",
|
||
string.Format("文件替换完成。文本 {0} 次,图片 {1} 次。", result.TextOperationCount, result.ImageOperationCount),
|
||
doneDetail);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
item.Status = ProcessingStatus.Failed;
|
||
RefreshReplaceItemRow(item, forceRefresh: true);
|
||
failedCount++;
|
||
_replaceFileProgress.Value = 0;
|
||
UpdateReplaceStatus(displayIndex, selectedItems.Count, "-", 0, totalHits, 0, forceRefresh: true);
|
||
var failDetail = await Task.Run(() => BuildReplaceFileFailDetail(item, ex, displayIndex, selectedItems.Count));
|
||
_logService.Error("REPLACE_FILE_FAIL", "替换文件处理失败。", failDetail);
|
||
}
|
||
|
||
completedCount++;
|
||
_replaceTotalProgress.Value = Math.Min(completedCount, _replaceTotalProgress.Maximum);
|
||
FlushReplaceRuntimeUi(forceRefresh: true);
|
||
}
|
||
}
|
||
|
||
var workers = Enumerable.Range(0, Math.Max(plan.DegreeOfParallelism, 1))
|
||
.Select(_ => RunWorkerAsync())
|
||
.ToArray();
|
||
await Task.WhenAll(workers);
|
||
|
||
if (_replaceExecutionCancellation.IsCancellationRequested)
|
||
{
|
||
stoppedCount += MarkRemainingItemsStopped(selectedItems.Where(item => item.Status == ProcessingStatus.Pending || item.Status == ProcessingStatus.InProgress));
|
||
completedCount = Math.Min(selectedItems.Count, successCount + failedCount + stoppedCount);
|
||
_replaceTotalProgress.Value = Math.Min(completedCount, _replaceTotalProgress.Maximum);
|
||
FlushControl(_gridReplace, forceRefresh: true);
|
||
FlushReplaceRuntimeUi(forceRefresh: true);
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
_isReplaceRunning = false;
|
||
_replaceFileProgress.Style = ProgressBarStyle.Blocks;
|
||
_btnStopReplace.Enabled = false;
|
||
SetRuleEditingEnabled(true);
|
||
UpdateActionStates();
|
||
}
|
||
|
||
// 批次里每个文档的 DiffPayload(含所有图片 base64)是大块瞬态分配,GC 不保证即时回收。
|
||
// 在弹完成对话框、触发自动恢复扫描之前主动回收一次,把 ~4GB 还给系统,避免残留内存压力拖慢后续 UI。
|
||
await Task.Run(() =>
|
||
{
|
||
GC.Collect();
|
||
GC.WaitForPendingFinalizers();
|
||
});
|
||
|
||
CompleteReplaceRuntimeStatus(successCount + failedCount + stoppedCount, selectedItems.Count, totalHits);
|
||
var summary = string.Format("替换完成。成功 {0},失败 {1},停止 {2},累计命中 {3}。", successCount, failedCount, stoppedCount, totalHits);
|
||
_logService.Info("REPLACE_DONE", summary);
|
||
ShowMessage(summary, "开始替换", MessageBoxButtons.OK, failedCount > 0 ? MessageBoxIcon.Warning : MessageBoxIcon.Information);
|
||
await RefreshRestoreScanAsync();
|
||
}
|
||
|
||
private async Task ExecuteRestoreBatchAsync(IList<FileScanItem> selectedItems)
|
||
{
|
||
_restoreExecutionCancellation?.Dispose();
|
||
_restoreExecutionCancellation = new CancellationTokenSource();
|
||
_isRestoreRunning = true;
|
||
SetRuleEditingEnabled(false);
|
||
|
||
foreach (var item in selectedItems)
|
||
{
|
||
item.Status = ProcessingStatus.Pending;
|
||
item.TextOperationCount = 0;
|
||
item.ImageOperationCount = 0;
|
||
}
|
||
|
||
ResetRestoreProgress(selectedItems.Count);
|
||
UpdateActionStates();
|
||
|
||
var plan = _batchExecutionPlanner.CreatePlan(selectedItems, Environment.ProcessorCount);
|
||
var executionItems = plan.ExecutionItems.ToList();
|
||
var displayOrder = executionItems
|
||
.Select((item, index) => new { item, index = index + 1 })
|
||
.ToDictionary(entry => entry.item, entry => entry.index);
|
||
var successCount = 0;
|
||
var failedCount = 0;
|
||
var stoppedCount = 0;
|
||
var totalHits = 0;
|
||
var completedCount = 0;
|
||
var nextExecutionIndex = -1;
|
||
|
||
foreach (var skipped in plan.SkippedItems)
|
||
{
|
||
skipped.Item.Status = ProcessingStatus.Failed;
|
||
RefreshRestoreItemRow(skipped.Item, forceRefresh: true);
|
||
failedCount++;
|
||
completedCount++;
|
||
_restoreTotalProgress.Value = Math.Min(completedCount, _restoreTotalProgress.Maximum);
|
||
FlushRestoreRuntimeUi(forceRefresh: true);
|
||
_logService.Warning(
|
||
"RESTORE_BATCH_SKIP",
|
||
skipped.Message,
|
||
string.Format("源文件: {0}{1}冲突路径: {2}", skipped.Item.SourcePath, Environment.NewLine, skipped.ConflictingPath));
|
||
}
|
||
|
||
try
|
||
{
|
||
_logService.Info(
|
||
"RESTORE_BATCH_PLAN",
|
||
string.Format(
|
||
"恢复批处理开始。勾选={0},执行={1},预跳过={2},并发度={3}{4}。",
|
||
selectedItems.Count,
|
||
executionItems.Count,
|
||
plan.SkippedItems.Count,
|
||
plan.DegreeOfParallelism,
|
||
plan.RequiresSerialExecution ? "(包含 .doc,已退回串行)" : string.Empty),
|
||
BuildRestoreBatchPlanDetail(selectedItems, plan));
|
||
|
||
async Task RunWorkerAsync()
|
||
{
|
||
while (true)
|
||
{
|
||
if (_restoreExecutionCancellation.IsCancellationRequested)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var executionIndex = Interlocked.Increment(ref nextExecutionIndex);
|
||
if (executionIndex >= executionItems.Count)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var item = executionItems[executionIndex];
|
||
var displayIndex = displayOrder[item];
|
||
item.Status = ProcessingStatus.InProgress;
|
||
RefreshRestoreItemRow(item, forceRefresh: true);
|
||
UpdateRestoreStatus(displayIndex, selectedItems.Count, "-", 0, totalHits, 0, forceRefresh: true);
|
||
var startDetail = await Task.Run(() => BuildRestoreFileStartDetail(item, displayIndex, selectedItems.Count));
|
||
_logService.Info("RESTORE_FILE_START", "开始处理恢复文件。", startDetail);
|
||
|
||
try
|
||
{
|
||
var progressThrottle = new RuntimeProgressThrottle();
|
||
var progress = new Progress<FileProcessProgress>(value =>
|
||
{
|
||
LogProcessProgressWarning("RESTORE_PROGRESS_WARN", value);
|
||
var forceRefresh = value.FilePercent >= 100;
|
||
if (!progressThrottle.ShouldApply(forceRefresh))
|
||
{
|
||
return;
|
||
}
|
||
|
||
UpdateRestoreStatus(
|
||
displayIndex,
|
||
selectedItems.Count,
|
||
value.CurrentRuleName ?? value.CurrentRuleId ?? "-",
|
||
value.CurrentFileTextCount,
|
||
totalHits + value.CurrentFileTextCount,
|
||
value.FilePercent,
|
||
forceRefresh);
|
||
});
|
||
|
||
var request = new RestoreFileRequest
|
||
{
|
||
ReplacedPath = item.SourcePath,
|
||
DiffPath = item.DiffPath,
|
||
OutputPath = item.OutputPath,
|
||
ReplaceRootDirectory = _txtRestoreSource.Text.Trim(),
|
||
CurrentRuleFilePath = _txtRuleFile.Text.Trim(),
|
||
};
|
||
|
||
var result = await Task.Run(() => _documentProcessingService.Restore(request, progress, CancellationToken.None));
|
||
item.OutputPath = result.OutputPath;
|
||
item.TextOperationCount = result.TextOperationCount;
|
||
item.ImageOperationCount = result.ImageOperationCount;
|
||
item.Status = ProcessingStatus.Success;
|
||
RefreshRestoreItemRow(item, forceRefresh: true);
|
||
successCount++;
|
||
totalHits += result.TextOperationCount + result.ImageOperationCount;
|
||
UpdateRestoreStatus(displayIndex, selectedItems.Count, "-", result.TextOperationCount + result.ImageOperationCount, totalHits, 100, forceRefresh: true);
|
||
var doneDetail = await Task.Run(() => BuildRestoreFileDoneDetail(item, result, displayIndex, selectedItems.Count));
|
||
_logService.Info(
|
||
"RESTORE_FILE_DONE",
|
||
string.Format("文件恢复完成。文本 {0} 次,图片 {1} 次。", result.TextOperationCount, result.ImageOperationCount),
|
||
doneDetail);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
item.Status = ProcessingStatus.Failed;
|
||
RefreshRestoreItemRow(item, forceRefresh: true);
|
||
failedCount++;
|
||
_restoreFileProgress.Value = 0;
|
||
UpdateRestoreStatus(displayIndex, selectedItems.Count, "-", 0, totalHits, 0, forceRefresh: true);
|
||
var failDetail = await Task.Run(() => BuildRestoreFileFailDetail(item, ex, displayIndex, selectedItems.Count));
|
||
_logService.Error("RESTORE_FILE_FAIL", "恢复文件处理失败。", failDetail);
|
||
}
|
||
|
||
completedCount++;
|
||
_restoreTotalProgress.Value = Math.Min(completedCount, _restoreTotalProgress.Maximum);
|
||
FlushRestoreRuntimeUi(forceRefresh: true);
|
||
}
|
||
}
|
||
|
||
var workers = Enumerable.Range(0, Math.Max(plan.DegreeOfParallelism, 1))
|
||
.Select(_ => RunWorkerAsync())
|
||
.ToArray();
|
||
await Task.WhenAll(workers);
|
||
|
||
if (_restoreExecutionCancellation.IsCancellationRequested)
|
||
{
|
||
stoppedCount += MarkRemainingItemsStopped(selectedItems.Where(item => item.Status == ProcessingStatus.Pending || item.Status == ProcessingStatus.InProgress));
|
||
completedCount = Math.Min(selectedItems.Count, successCount + failedCount + stoppedCount);
|
||
_restoreTotalProgress.Value = Math.Min(completedCount, _restoreTotalProgress.Maximum);
|
||
FlushControl(_gridRestore, forceRefresh: true);
|
||
FlushRestoreRuntimeUi(forceRefresh: true);
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
_isRestoreRunning = false;
|
||
_restoreFileProgress.Style = ProgressBarStyle.Blocks;
|
||
_btnStopRestore.Enabled = false;
|
||
SetRuleEditingEnabled(true);
|
||
UpdateActionStates();
|
||
}
|
||
|
||
// 恢复批次同样释放掉 DiffPayload 载荷,避免残留内存影响后续交互。
|
||
await Task.Run(() =>
|
||
{
|
||
GC.Collect();
|
||
GC.WaitForPendingFinalizers();
|
||
});
|
||
|
||
CompleteRestoreRuntimeStatus(successCount + failedCount + stoppedCount, selectedItems.Count, totalHits);
|
||
var summary = string.Format("恢复完成。成功 {0},失败 {1},停止 {2},累计命中 {3}。", successCount, failedCount, stoppedCount, totalHits);
|
||
_logService.Info("RESTORE_DONE", summary);
|
||
ShowMessage(summary, "开始恢复", MessageBoxButtons.OK, failedCount > 0 ? MessageBoxIcon.Warning : MessageBoxIcon.Information);
|
||
}
|
||
|
||
private bool PrepareRuleExecution(out RuleFile executionRuleFile)
|
||
{
|
||
executionRuleFile = CreateExecutionRuleFile();
|
||
var report = _ruleService.Validate(executionRuleFile);
|
||
foreach (var issue in report.Issues)
|
||
{
|
||
LogRuleIssue(issue);
|
||
}
|
||
|
||
if (report.Issues.Count == 0)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
var summary = string.Join(Environment.NewLine, report.Issues.Select(_ruleService.FormatIssue));
|
||
var prompt = summary + Environment.NewLine + Environment.NewLine + "是否继续执行?";
|
||
return ShowMessage(
|
||
prompt,
|
||
"规则执行检查",
|
||
MessageBoxButtons.YesNo,
|
||
report.HasErrors ? MessageBoxIcon.Warning : MessageBoxIcon.Information) == DialogResult.Yes;
|
||
}
|
||
|
||
private bool PrepareRuleExecutionWithBlockingErrors(out RuleFile executionRuleFile)
|
||
{
|
||
executionRuleFile = CreateExecutionRuleFile();
|
||
var report = _ruleService.Validate(executionRuleFile);
|
||
foreach (var issue in report.Issues)
|
||
{
|
||
LogRuleIssue(issue);
|
||
}
|
||
|
||
if (report.Issues.Count == 0)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
var summary = string.Join(Environment.NewLine, report.Issues.Select(_ruleService.FormatIssue));
|
||
if (report.HasErrors)
|
||
{
|
||
ShowMessage(
|
||
summary + Environment.NewLine + Environment.NewLine + "存在错误,必须先修复后才能执行。",
|
||
"规则执行检查",
|
||
MessageBoxButtons.OK,
|
||
MessageBoxIcon.Warning);
|
||
return false;
|
||
}
|
||
|
||
var prompt = summary + Environment.NewLine + Environment.NewLine + "是否继续执行?";
|
||
return ShowMessage(
|
||
prompt,
|
||
"规则执行检查",
|
||
MessageBoxButtons.YesNo,
|
||
MessageBoxIcon.Information) == DialogResult.Yes;
|
||
}
|
||
|
||
private RuleFile CreateExecutionRuleFile()
|
||
{
|
||
if (_gridRules.IsHandleCreated)
|
||
{
|
||
_gridRules.EndEdit();
|
||
}
|
||
|
||
var result = new RuleFile
|
||
{
|
||
Format = _ruleFile.Format,
|
||
Version = _ruleFile.Version,
|
||
};
|
||
|
||
foreach (var rule in _rules)
|
||
{
|
||
result.Rules.Add(new RuleDefinition
|
||
{
|
||
Id = rule.Id,
|
||
Name = rule.Name,
|
||
IsEnabled = rule.IsEnabled,
|
||
MatchMode = rule.MatchMode,
|
||
Target = rule.Target,
|
||
IsCaseSensitive = rule.IsCaseSensitive,
|
||
IsWholeWord = rule.IsWholeWord,
|
||
SearchText = rule.SearchText,
|
||
ReplacementText = rule.ReplacementText,
|
||
Note = rule.Note,
|
||
});
|
||
}
|
||
|
||
_ruleFile = result;
|
||
return result;
|
||
}
|
||
|
||
private bool TryPersistConfigFromUi()
|
||
{
|
||
try
|
||
{
|
||
CaptureUiToConfig();
|
||
_configurationService.Save(_config);
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService.Error("CONFIG_SAVE_FAIL", "保存当前配置失败。", ex.Message);
|
||
ShowMessage("保存当前配置失败: " + ex.Message, "WORD 2007 格式调整软件", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private void ResetReplaceProgress(int totalCount)
|
||
{
|
||
_replaceTotalProgress.Style = ProgressBarStyle.Blocks;
|
||
_replaceTotalProgress.Maximum = Math.Max(totalCount, 1);
|
||
_replaceTotalProgress.Value = 0;
|
||
_replaceFileProgress.Style = ProgressBarStyle.Blocks;
|
||
_replaceFileProgress.Value = 0;
|
||
_lblReplaceRuntime.Text = "当前文件 0/0 | 当前规则 - | 当前文件命中 0 | 累计命中 0";
|
||
FlushReplaceRuntimeUi(forceRefresh: true);
|
||
}
|
||
|
||
private void ResetRestoreProgress(int totalCount)
|
||
{
|
||
_restoreTotalProgress.Style = ProgressBarStyle.Blocks;
|
||
_restoreTotalProgress.Maximum = Math.Max(totalCount, 1);
|
||
_restoreTotalProgress.Value = 0;
|
||
_restoreFileProgress.Style = ProgressBarStyle.Blocks;
|
||
_restoreFileProgress.Value = 0;
|
||
_lblRestoreRuntime.Text = "当前文件 0/0 | 当前规则 - | 当前文件命中 0 | 累计命中 0";
|
||
FlushRestoreRuntimeUi(forceRefresh: true);
|
||
}
|
||
|
||
private void LogProcessProgressWarning(string eventCode, FileProcessProgress value)
|
||
{
|
||
if (value == null || !value.IsWarning || string.IsNullOrWhiteSpace(value.Message))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var detail = string.IsNullOrWhiteSpace(value.FilePath) ? null : "文件: " + value.FilePath;
|
||
_logService.Warning(eventCode, value.Message, detail);
|
||
}
|
||
|
||
private void UpdateReplaceStatus(int currentFile, int totalFiles, string currentRule, int currentFileHits, int totalHits, int filePercent, bool forceRefresh = false)
|
||
{
|
||
_replaceFileProgress.Value = Math.Max(0, Math.Min(filePercent, 100));
|
||
_lblReplaceRuntime.Text = string.Format(
|
||
"当前文件 {0}/{1} | 当前规则 {2} | 当前文件命中 {3} | 累计命中 {4}",
|
||
currentFile,
|
||
totalFiles,
|
||
string.IsNullOrWhiteSpace(currentRule) ? "-" : currentRule,
|
||
currentFileHits,
|
||
totalHits);
|
||
FlushReplaceRuntimeUi(forceRefresh);
|
||
}
|
||
|
||
private void CompleteReplaceRuntimeStatus(int completedFiles, int totalFiles, int totalHits)
|
||
{
|
||
var safeTotal = Math.Max(0, totalFiles);
|
||
var safeCompleted = Math.Max(0, Math.Min(completedFiles, safeTotal));
|
||
_replaceTotalProgress.Value = Math.Min(safeCompleted, _replaceTotalProgress.Maximum);
|
||
UpdateReplaceStatus(
|
||
safeCompleted,
|
||
safeTotal,
|
||
"-",
|
||
0,
|
||
totalHits,
|
||
safeTotal > 0 && safeCompleted >= safeTotal ? 100 : 0,
|
||
forceRefresh: true);
|
||
}
|
||
|
||
private void UpdateRestoreStatus(int currentFile, int totalFiles, string currentRule, int currentFileHits, int totalHits, int filePercent, bool forceRefresh = false)
|
||
{
|
||
_restoreFileProgress.Value = Math.Max(0, Math.Min(filePercent, 100));
|
||
_lblRestoreRuntime.Text = string.Format(
|
||
"当前文件 {0}/{1} | 当前规则 {2} | 当前文件命中 {3} | 累计命中 {4}",
|
||
currentFile,
|
||
totalFiles,
|
||
string.IsNullOrWhiteSpace(currentRule) ? "-" : currentRule,
|
||
currentFileHits,
|
||
totalHits);
|
||
FlushRestoreRuntimeUi(forceRefresh);
|
||
}
|
||
|
||
private void CompleteRestoreRuntimeStatus(int completedFiles, int totalFiles, int totalHits)
|
||
{
|
||
var safeTotal = Math.Max(0, totalFiles);
|
||
var safeCompleted = Math.Max(0, Math.Min(completedFiles, safeTotal));
|
||
_restoreTotalProgress.Value = Math.Min(safeCompleted, _restoreTotalProgress.Maximum);
|
||
UpdateRestoreStatus(
|
||
safeCompleted,
|
||
safeTotal,
|
||
"-",
|
||
0,
|
||
totalHits,
|
||
safeTotal > 0 && safeCompleted >= safeTotal ? 100 : 0,
|
||
forceRefresh: true);
|
||
}
|
||
|
||
private void FlushReplaceRuntimeUi(bool forceRefresh)
|
||
{
|
||
if (!forceRefresh && !ShouldRefreshRuntimeUi(ref _lastReplaceRuntimeFlushTick))
|
||
{
|
||
return;
|
||
}
|
||
|
||
FlushControl(_replaceFileProgress, forceRefresh);
|
||
FlushControl(_replaceTotalProgress, forceRefresh);
|
||
FlushControl(_lblReplaceRuntime, forceRefresh);
|
||
}
|
||
|
||
private void FlushRestoreRuntimeUi(bool forceRefresh)
|
||
{
|
||
if (!forceRefresh && !ShouldRefreshRuntimeUi(ref _lastRestoreRuntimeFlushTick))
|
||
{
|
||
return;
|
||
}
|
||
|
||
FlushControl(_restoreFileProgress, forceRefresh);
|
||
FlushControl(_restoreTotalProgress, forceRefresh);
|
||
FlushControl(_lblRestoreRuntime, forceRefresh);
|
||
}
|
||
|
||
private static bool ShouldRefreshRuntimeUi(ref int lastFlushTick)
|
||
{
|
||
var now = Environment.TickCount & int.MaxValue;
|
||
if (lastFlushTick != 0 && now >= lastFlushTick && now - lastFlushTick < RuntimeUiRefreshIntervalMilliseconds)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
lastFlushTick = now;
|
||
return true;
|
||
}
|
||
|
||
private static bool ShouldApplyProgressUpdate(ref int lastProgressTick, bool forceRefresh)
|
||
{
|
||
if (forceRefresh)
|
||
{
|
||
lastProgressTick = Environment.TickCount & int.MaxValue;
|
||
return true;
|
||
}
|
||
|
||
return ShouldRefreshRuntimeUi(ref lastProgressTick);
|
||
}
|
||
|
||
private static void FlushControl(Control control, bool forceRefresh)
|
||
{
|
||
if (control == null || control.IsDisposed || !control.IsHandleCreated)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (forceRefresh)
|
||
{
|
||
control.Refresh();
|
||
return;
|
||
}
|
||
|
||
control.Update();
|
||
}
|
||
|
||
private void RefreshReplaceItemRow(FileScanItem item, bool forceRefresh = false)
|
||
{
|
||
RefreshFileItemRow(_gridReplace, _replaceItems, item, forceRefresh);
|
||
}
|
||
|
||
private void RefreshRestoreItemRow(FileScanItem item, bool forceRefresh = false)
|
||
{
|
||
RefreshFileItemRow(_gridRestore, _restoreItems, item, forceRefresh);
|
||
}
|
||
|
||
private static void RefreshFileItemRow(DataGridView grid, BindingList<FileScanItem> items, FileScanItem item, bool forceRefresh)
|
||
{
|
||
if (grid == null || items == null || item == null || grid.IsDisposed || !grid.IsHandleCreated)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var rowIndex = items.IndexOf(item);
|
||
if (rowIndex >= 0 && rowIndex < grid.Rows.Count)
|
||
{
|
||
grid.InvalidateRow(rowIndex);
|
||
}
|
||
|
||
FlushControl(grid, forceRefresh);
|
||
}
|
||
|
||
private sealed class RuntimeProgressThrottle
|
||
{
|
||
private int _lastProgressTick;
|
||
|
||
public bool ShouldApply(bool forceRefresh)
|
||
{
|
||
return ShouldApplyProgressUpdate(ref _lastProgressTick, forceRefresh);
|
||
}
|
||
}
|
||
|
||
private static int MarkRemainingItemsStopped(IEnumerable<FileScanItem> items)
|
||
{
|
||
var count = 0;
|
||
foreach (var item in items)
|
||
{
|
||
if (item.Status == ProcessingStatus.Pending || item.Status == ProcessingStatus.InProgress)
|
||
{
|
||
item.Status = ProcessingStatus.Stopped;
|
||
count++;
|
||
}
|
||
}
|
||
|
||
return count;
|
||
}
|
||
|
||
private string GetSelectedRestoreDiffInputFormat()
|
||
{
|
||
return string.Equals(_cmbRestoreDiffInput.SelectedItem as string, ".bmp", StringComparison.OrdinalIgnoreCase)
|
||
? ".bmp"
|
||
: ".diff";
|
||
}
|
||
|
||
private void SetRestoreDiffInputFormat(string diffInputFormat)
|
||
{
|
||
var normalized = string.Equals(diffInputFormat, ".bmp", StringComparison.OrdinalIgnoreCase)
|
||
? ".bmp"
|
||
: ".diff";
|
||
_cmbRestoreDiffInput.SelectedItem = normalized;
|
||
}
|
||
|
||
private void LoadRuleFile(string filePath)
|
||
{
|
||
try
|
||
{
|
||
_ruleFile = _ruleService.Load(filePath);
|
||
ReplaceBinding(_rules, _ruleFile.Rules);
|
||
_logService.Info("RULE_LOAD", "规则文件已加载。", filePath);
|
||
UpdateActionStates();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logService.Error("RULE_LOAD_FAIL", "加载规则文件失败。", ex.Message);
|
||
ShowMessage("加载规则文件失败: " + ex.Message, "规则文件", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
}
|
||
}
|
||
|
||
private async Task BrowseFolderIntoTextBoxAsync(TextBox textBox, Func<Task> refreshAsync)
|
||
{
|
||
string selectedPath;
|
||
if (!ModernFolderPicker.TrySelectFolder(this, textBox.Text, out selectedPath))
|
||
{
|
||
return;
|
||
}
|
||
|
||
await ApplySelectedFolderPathAsync(textBox, selectedPath, refreshAsync);
|
||
}
|
||
|
||
private async Task ApplySelectedFolderPathAsync(TextBox textBox, string selectedPath, Func<Task> refreshAsync)
|
||
{
|
||
textBox.Text = selectedPath ?? string.Empty;
|
||
if (refreshAsync != null)
|
||
{
|
||
await refreshAsync();
|
||
}
|
||
}
|
||
|
||
private string GetReplacePreviewOutputDirectory(string sourceDirectory)
|
||
{
|
||
var output = _txtReplaceOutput.Text.Trim();
|
||
if (string.IsNullOrWhiteSpace(output))
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
var validationMessages = _fileScanService.ValidateReplaceDirectories(sourceDirectory, output);
|
||
return validationMessages.Count == 0 ? output : string.Empty;
|
||
}
|
||
|
||
private string GetRestorePreviewOutputDirectory(string replaceDirectory)
|
||
{
|
||
var output = _txtRestoreOutput.Text.Trim();
|
||
if (string.IsNullOrWhiteSpace(output))
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
var validationMessages = _fileScanService.ValidateRestoreDirectories(replaceDirectory, output);
|
||
return validationMessages.Count == 0 ? output : string.Empty;
|
||
}
|
||
|
||
private IList<string> GetReplaceExecutionValidationMessages()
|
||
{
|
||
var source = _txtReplaceSource.Text.Trim();
|
||
var output = _txtReplaceOutput.Text.Trim();
|
||
var messages = new List<string>();
|
||
|
||
if (string.IsNullOrWhiteSpace(source))
|
||
{
|
||
messages.Add("未选择原始文件夹。");
|
||
}
|
||
else if (!Directory.Exists(source))
|
||
{
|
||
messages.Add("原始文件夹不存在。");
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(output))
|
||
{
|
||
messages.Add("未选择替换文件夹。");
|
||
}
|
||
|
||
messages.AddRange(_fileScanService.ValidateReplaceDirectories(source, output));
|
||
return messages;
|
||
}
|
||
|
||
private IList<string> GetRestoreExecutionValidationMessages()
|
||
{
|
||
var source = _txtRestoreSource.Text.Trim();
|
||
var output = _txtRestoreOutput.Text.Trim();
|
||
var messages = new List<string>();
|
||
|
||
if (string.IsNullOrWhiteSpace(source))
|
||
{
|
||
messages.Add("未选择替换文件夹。");
|
||
}
|
||
else if (!Directory.Exists(source))
|
||
{
|
||
messages.Add("替换文件夹不存在。");
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(output))
|
||
{
|
||
messages.Add("未选择恢复文件夹。");
|
||
}
|
||
|
||
messages.AddRange(_fileScanService.ValidateRestoreDirectories(source, output));
|
||
return messages;
|
||
}
|
||
|
||
private void UpdateActionStates()
|
||
{
|
||
var hasReplaceSelection = _replaceItems.Any(item => item.IsSelected);
|
||
var hasRestoreSelection = _restoreItems.Any(item => item.IsSelected);
|
||
var busy = _isAutoExtractRunning || _isReplaceRunning || _isRestoreRunning;
|
||
var canEditReplacePage = !busy;
|
||
var canEditRestorePage = !busy;
|
||
var canEditRules = !busy && _gridRules.Enabled;
|
||
var canExecuteReplace = !busy && hasReplaceSelection && GetReplaceExecutionValidationMessages().Count == 0;
|
||
var canExecuteRestore = !busy && hasRestoreSelection && GetRestoreExecutionValidationMessages().Count == 0;
|
||
|
||
_btnAutoExtract.Enabled = !_isAutoExtractRunning && !_isReplaceRunning && !_isRestoreRunning && hasReplaceSelection;
|
||
_btnStartReplace.Enabled = canExecuteReplace;
|
||
_btnStopReplace.Enabled = _isAutoExtractRunning || _isReplaceRunning;
|
||
_btnStartRestore.Enabled = canExecuteRestore;
|
||
_btnStopRestore.Enabled = _isRestoreRunning;
|
||
|
||
_txtReplaceSource.Enabled = canEditReplacePage;
|
||
_txtReplaceOutput.Enabled = canEditReplacePage;
|
||
_btnBrowseReplaceSource.Enabled = canEditReplacePage;
|
||
_btnBrowseReplaceOutput.Enabled = canEditReplacePage;
|
||
_btnSelectAllReplace.Enabled = canEditReplacePage && _replaceItems.Count > 0;
|
||
_btnClearReplace.Enabled = canEditReplacePage && _replaceItems.Count > 0;
|
||
_gridReplace.ReadOnly = busy;
|
||
|
||
_txtRestoreSource.Enabled = canEditRestorePage;
|
||
_txtRestoreOutput.Enabled = canEditRestorePage;
|
||
_cmbRestoreDiffInput.Enabled = canEditRestorePage;
|
||
_btnBrowseRestoreSource.Enabled = canEditRestorePage;
|
||
_btnBrowseRestoreOutput.Enabled = canEditRestorePage;
|
||
_btnSelectAllRestore.Enabled = canEditRestorePage && _restoreItems.Count > 0;
|
||
_btnClearRestore.Enabled = canEditRestorePage && _restoreItems.Count > 0;
|
||
_gridRestore.ReadOnly = busy;
|
||
|
||
_gridRules.Enabled = canEditRules;
|
||
_txtRuleFile.Enabled = canEditRules;
|
||
_btnOpenRules.Enabled = canEditRules;
|
||
_btnSaveRules.Enabled = canEditRules;
|
||
_btnUndoAutoExtract.Enabled = canEditRules && _lastAutoExtractRuleIds.Count > 0;
|
||
}
|
||
|
||
private void SetRuleEditingEnabled(bool enabled)
|
||
{
|
||
_gridRules.Enabled = enabled;
|
||
_txtRuleFile.Enabled = enabled;
|
||
UpdateActionStates();
|
||
}
|
||
|
||
private void LogRuleIssue(RuleValidationIssue issue)
|
||
{
|
||
var formatted = _ruleService.FormatIssue(issue);
|
||
if (issue.Severity == RuleValidationSeverity.Error)
|
||
{
|
||
_logService.Error("RULE_CHECK", formatted);
|
||
}
|
||
else if (issue.Severity == RuleValidationSeverity.Warning)
|
||
{
|
||
_logService.Warning("RULE_CHECK", formatted);
|
||
}
|
||
else
|
||
{
|
||
_logService.Info("RULE_CHECK", formatted);
|
||
}
|
||
}
|
||
|
||
private void SetSelection(IList<FileScanItem> items, bool isSelected)
|
||
{
|
||
foreach (var item in items)
|
||
{
|
||
item.IsSelected = isSelected;
|
||
}
|
||
|
||
UpdateActionStates();
|
||
}
|
||
|
||
private void SetSelectedFileItems(DataGridView grid, bool isSelected)
|
||
{
|
||
foreach (var item in GetSelectedDataBoundItems<FileScanItem>(grid))
|
||
{
|
||
item.IsSelected = isSelected;
|
||
}
|
||
|
||
grid.Refresh();
|
||
UpdateActionStates();
|
||
}
|
||
|
||
private void DeleteSelectedFileItems(BindingList<FileScanItem> items, DataGridView grid)
|
||
{
|
||
foreach (var item in GetSelectedDataBoundItems<FileScanItem>(grid))
|
||
{
|
||
items.Remove(item);
|
||
}
|
||
|
||
UpdateActionStates();
|
||
}
|
||
|
||
private void SetSelectedRulesEnabled(bool isEnabled)
|
||
{
|
||
foreach (var rule in GetSelectedDataBoundItems<RuleDefinition>(_gridRules))
|
||
{
|
||
rule.IsEnabled = isEnabled;
|
||
}
|
||
|
||
RefreshRuleState();
|
||
}
|
||
|
||
private void DeleteSelectedRules()
|
||
{
|
||
foreach (var rule in GetSelectedDataBoundItems<RuleDefinition>(_gridRules))
|
||
{
|
||
_rules.Remove(rule);
|
||
}
|
||
|
||
RefreshRuleState();
|
||
}
|
||
|
||
private void RefreshRuleState()
|
||
{
|
||
_ruleFile.Rules = _rules.ToList();
|
||
|
||
var existingRuleIds = new HashSet<string>(
|
||
_rules.Where(item => !string.IsNullOrWhiteSpace(item.Id)).Select(item => item.Id),
|
||
StringComparer.OrdinalIgnoreCase);
|
||
_lastAutoExtractRuleIds = _lastAutoExtractRuleIds.Where(existingRuleIds.Contains).ToList();
|
||
|
||
_gridRules.Refresh();
|
||
UpdateActionStates();
|
||
}
|
||
|
||
private static List<T> GetSelectedDataBoundItems<T>(DataGridView grid)
|
||
where T : class
|
||
{
|
||
return grid.SelectedRows
|
||
.Cast<DataGridViewRow>()
|
||
.OrderBy(row => row.Index)
|
||
.Select(row => row.DataBoundItem as T)
|
||
.Where(item => item != null)
|
||
.ToList();
|
||
}
|
||
|
||
private static List<int> NormalizeIndexes(IEnumerable<int> rowIndexes, int count)
|
||
{
|
||
return (rowIndexes ?? Array.Empty<int>())
|
||
.Where(index => index >= 0 && index < count)
|
||
.Distinct()
|
||
.OrderBy(index => index)
|
||
.ToList();
|
||
}
|
||
|
||
private static void ReplaceBinding<T>(BindingList<T> target, IEnumerable<T> source)
|
||
{
|
||
target.Clear();
|
||
foreach (var item in source)
|
||
{
|
||
target.Add(item);
|
||
}
|
||
}
|
||
|
||
private static void ConfigureNumeric(NumericUpDown numeric, int minimum, int maximum)
|
||
{
|
||
numeric.Minimum = minimum;
|
||
numeric.Maximum = maximum;
|
||
numeric.Width = 120;
|
||
}
|
||
|
||
private static Control CreateProgressLine(string labelText, ProgressBar progressBar)
|
||
{
|
||
var panel = new TableLayoutPanel
|
||
{
|
||
Dock = DockStyle.Top,
|
||
ColumnCount = 2,
|
||
AutoSize = true,
|
||
};
|
||
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 90F));
|
||
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||
panel.Controls.Add(new Label { Text = labelText, Anchor = AnchorStyles.Left, AutoSize = true }, 0, 0);
|
||
progressBar.Dock = DockStyle.Fill;
|
||
progressBar.Maximum = 100;
|
||
panel.Controls.Add(progressBar, 1, 0);
|
||
return panel;
|
||
}
|
||
|
||
private static void AddLabeledRow(
|
||
TableLayoutPanel layout,
|
||
int rowIndex,
|
||
string leftLabel,
|
||
Control leftTextBox,
|
||
Control leftButton,
|
||
string rightLabel,
|
||
Control rightTextBox,
|
||
Control rightButton)
|
||
{
|
||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
layout.Controls.Add(new Label { Text = leftLabel, Anchor = AnchorStyles.Left, AutoSize = true }, 0, rowIndex);
|
||
leftTextBox.Dock = DockStyle.Fill;
|
||
layout.Controls.Add(leftTextBox, 1, rowIndex);
|
||
layout.Controls.Add(leftButton, 2, rowIndex);
|
||
layout.Controls.Add(new Label { Text = rightLabel, Anchor = AnchorStyles.Left, AutoSize = true }, 3, rowIndex);
|
||
rightTextBox.Dock = DockStyle.Fill;
|
||
layout.Controls.Add(rightTextBox, 4, rowIndex);
|
||
layout.Controls.Add(rightButton, 5, rowIndex);
|
||
}
|
||
|
||
private static void AddSettingRow(TableLayoutPanel layout, int rowIndex, Control leftControl, Control rightControl)
|
||
{
|
||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
leftControl.Dock = DockStyle.Fill;
|
||
rightControl.Dock = DockStyle.Fill;
|
||
layout.Controls.Add(leftControl, 0, rowIndex);
|
||
layout.Controls.Add(rightControl, 1, rowIndex);
|
||
}
|
||
|
||
private void ConfigureFileGrid(DataGridView grid, bool isRestoreGrid)
|
||
{
|
||
grid.AllowUserToAddRows = false;
|
||
grid.AllowUserToDeleteRows = false;
|
||
grid.AutoGenerateColumns = false;
|
||
grid.MultiSelect = true;
|
||
grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
|
||
grid.EditMode = DataGridViewEditMode.EditOnEnter;
|
||
grid.Columns.Clear();
|
||
|
||
var checkColumn = new DataGridViewCheckBoxColumn
|
||
{
|
||
DataPropertyName = nameof(FileScanItem.IsSelected),
|
||
HeaderText = "勾选",
|
||
Width = 60,
|
||
};
|
||
grid.Columns.Add(checkColumn);
|
||
grid.Columns.Add(new DataGridViewTextBoxColumn
|
||
{
|
||
DataPropertyName = nameof(FileScanItem.SourcePath),
|
||
HeaderText = isRestoreGrid ? "替换后文件路径" : "原始文件路径",
|
||
Width = 360,
|
||
ReadOnly = true,
|
||
});
|
||
grid.Columns.Add(new DataGridViewTextBoxColumn
|
||
{
|
||
DataPropertyName = nameof(FileScanItem.OutputPath),
|
||
HeaderText = isRestoreGrid ? "恢复文件路径" : "替换后文件路径",
|
||
Width = 360,
|
||
ReadOnly = true,
|
||
});
|
||
grid.Columns.Add(new DataGridViewTextBoxColumn
|
||
{
|
||
DataPropertyName = nameof(FileScanItem.DiffPath),
|
||
HeaderText = "差异文件路径",
|
||
Width = 320,
|
||
ReadOnly = true,
|
||
});
|
||
grid.Columns.Add(new DataGridViewTextBoxColumn
|
||
{
|
||
DataPropertyName = nameof(FileScanItem.TextOperationCount),
|
||
HeaderText = isRestoreGrid ? "文本恢复次数" : "文本替换次数",
|
||
Width = 110,
|
||
});
|
||
grid.Columns.Add(new DataGridViewTextBoxColumn
|
||
{
|
||
DataPropertyName = nameof(FileScanItem.ImageOperationCount),
|
||
HeaderText = isRestoreGrid ? "图片恢复次数" : "图片替换次数",
|
||
Width = 110,
|
||
});
|
||
grid.Columns.Add(new DataGridViewTextBoxColumn
|
||
{
|
||
DataPropertyName = nameof(FileScanItem.Status),
|
||
HeaderText = "当前状态",
|
||
Width = 90,
|
||
ReadOnly = true,
|
||
});
|
||
|
||
grid.CurrentCellDirtyStateChanged += Grid_CurrentCellDirtyStateChanged;
|
||
grid.CellValueChanged += GridSelection_CellValueChanged;
|
||
grid.CellMouseDown += Grid_CellMouseDownSelectRow;
|
||
grid.RowPrePaint += Grid_RowPrePaint;
|
||
}
|
||
|
||
private void ConfigureRuleGrid(DataGridView grid)
|
||
{
|
||
grid.AllowUserToAddRows = false;
|
||
grid.AllowUserToDeleteRows = false;
|
||
grid.AutoGenerateColumns = false;
|
||
grid.MultiSelect = true;
|
||
grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
|
||
grid.EditMode = DataGridViewEditMode.EditOnEnter;
|
||
grid.Columns.Clear();
|
||
|
||
grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(RuleDefinition.Id), HeaderText = "规则 ID", Width = 110 });
|
||
grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(RuleDefinition.Name), HeaderText = "规则名称", Width = 140 });
|
||
grid.Columns.Add(new DataGridViewCheckBoxColumn { DataPropertyName = nameof(RuleDefinition.IsEnabled), HeaderText = "启用", Width = 60 });
|
||
grid.Columns.Add(new DataGridViewComboBoxColumn
|
||
{
|
||
DataPropertyName = nameof(RuleDefinition.MatchMode),
|
||
HeaderText = "匹配模式",
|
||
Width = 110,
|
||
DataSource = Enum.GetValues(typeof(RuleMatchMode)),
|
||
});
|
||
grid.Columns.Add(new DataGridViewComboBoxColumn
|
||
{
|
||
DataPropertyName = nameof(RuleDefinition.Target),
|
||
HeaderText = "目标类型",
|
||
Width = 100,
|
||
DataSource = Enum.GetValues(typeof(RuleTarget)),
|
||
ValueType = typeof(RuleTarget),
|
||
});
|
||
grid.Columns.Add(new DataGridViewCheckBoxColumn { DataPropertyName = nameof(RuleDefinition.IsCaseSensitive), HeaderText = "区分大小写", Width = 90 });
|
||
grid.Columns.Add(new DataGridViewCheckBoxColumn { DataPropertyName = nameof(RuleDefinition.IsWholeWord), HeaderText = "全字匹配", Width = 90 });
|
||
grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(RuleDefinition.SearchText), HeaderText = "原文本/表达式", Width = 200 });
|
||
grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(RuleDefinition.ReplacementText), HeaderText = "新文本/模板", Width = 180 });
|
||
grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(RuleDefinition.Note), HeaderText = "备注", Width = 320 });
|
||
|
||
grid.CurrentCellDirtyStateChanged += Grid_CurrentCellDirtyStateChanged;
|
||
grid.CellValueChanged += GridSelection_CellValueChanged;
|
||
grid.CellMouseDown += Grid_CellMouseDownSelectRow;
|
||
grid.DataError += delegate { };
|
||
}
|
||
|
||
private void Grid_CurrentCellDirtyStateChanged(object sender, EventArgs e)
|
||
{
|
||
var grid = sender as DataGridView;
|
||
if (grid != null && grid.IsCurrentCellDirty && grid.CurrentCell is DataGridViewCheckBoxCell)
|
||
{
|
||
grid.CommitEdit(DataGridViewDataErrorContexts.Commit);
|
||
}
|
||
}
|
||
|
||
private void GridSelection_CellValueChanged(object sender, DataGridViewCellEventArgs e)
|
||
{
|
||
UpdateActionStates();
|
||
}
|
||
|
||
private void Grid_CellMouseDownSelectRow(object sender, DataGridViewCellMouseEventArgs e)
|
||
{
|
||
if (e.Button != MouseButtons.Right || e.RowIndex < 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var grid = sender as DataGridView;
|
||
if (grid == null || e.RowIndex >= grid.Rows.Count)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var row = grid.Rows[e.RowIndex];
|
||
if (!row.Selected)
|
||
{
|
||
grid.ClearSelection();
|
||
row.Selected = true;
|
||
}
|
||
|
||
if (e.ColumnIndex >= 0 && e.ColumnIndex < row.Cells.Count)
|
||
{
|
||
grid.CurrentCell = row.Cells[e.ColumnIndex];
|
||
}
|
||
}
|
||
|
||
private void Grid_RowPrePaint(object sender, DataGridViewRowPrePaintEventArgs e)
|
||
{
|
||
var grid = sender as DataGridView;
|
||
if (grid == null || e.RowIndex < 0 || e.RowIndex >= grid.Rows.Count)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var item = grid.Rows[e.RowIndex].DataBoundItem as FileScanItem;
|
||
if (item == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var color = Color.Black;
|
||
switch (item.Status)
|
||
{
|
||
case ProcessingStatus.Success:
|
||
color = Color.Green;
|
||
break;
|
||
case ProcessingStatus.Failed:
|
||
color = Color.Red;
|
||
break;
|
||
case ProcessingStatus.InProgress:
|
||
color = Color.Blue;
|
||
break;
|
||
case ProcessingStatus.Stopped:
|
||
color = Color.Gray;
|
||
break;
|
||
}
|
||
|
||
grid.Rows[e.RowIndex].DefaultCellStyle.ForeColor = color;
|
||
}
|
||
|
||
private DialogResult ShowMessage(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon)
|
||
{
|
||
return _dialogService.ShowMessage(this, text, caption, buttons, icon);
|
||
}
|
||
|
||
internal Task ExecuteReplaceBatchForTestAsync(IList<FileScanItem> selectedItems, RuleFile executionRuleFile, string sourceRootDirectory, string ruleFilePath, bool encryptDiff, int? fixedImageSeed)
|
||
{
|
||
_txtReplaceSource.Text = sourceRootDirectory ?? string.Empty;
|
||
_txtRuleFile.Text = ruleFilePath ?? string.Empty;
|
||
_chkJsonDiffEncryption.Checked = encryptDiff;
|
||
_chkUseFixedImageSeed.Checked = fixedImageSeed.HasValue;
|
||
_numFixedImageSeed.Value = Math.Max(_numFixedImageSeed.Minimum, Math.Min(_numFixedImageSeed.Maximum, fixedImageSeed ?? 0));
|
||
return ExecuteReplaceBatchAsync(selectedItems, executionRuleFile);
|
||
}
|
||
|
||
internal Task ExecuteRestoreBatchForTestAsync(IList<FileScanItem> selectedItems, string replaceRootDirectory, string ruleFilePath)
|
||
{
|
||
_txtRestoreSource.Text = replaceRootDirectory ?? string.Empty;
|
||
_txtRuleFile.Text = ruleFilePath ?? string.Empty;
|
||
return ExecuteRestoreBatchAsync(selectedItems);
|
||
}
|
||
|
||
internal Task RefreshReplaceScanForTestAsync(string sourceDirectory, string outputDirectory)
|
||
{
|
||
_txtReplaceSource.Text = sourceDirectory ?? string.Empty;
|
||
_txtReplaceOutput.Text = outputDirectory ?? string.Empty;
|
||
return RefreshReplaceScanAsync();
|
||
}
|
||
|
||
internal Task ApplyReplaceSourceSelectionForTestAsync(string selectedPath)
|
||
{
|
||
return ApplySelectedFolderPathAsync(_txtReplaceSource, selectedPath, RefreshReplaceScanAsync);
|
||
}
|
||
|
||
internal Task ApplyReplaceOutputSelectionForTestAsync(string selectedPath)
|
||
{
|
||
return ApplySelectedFolderPathAsync(_txtReplaceOutput, selectedPath, RefreshReplaceScanAsync);
|
||
}
|
||
|
||
internal Task RefreshRestoreScanForTestAsync(string sourceDirectory, string outputDirectory, string diffInputFormat)
|
||
{
|
||
_txtRestoreSource.Text = sourceDirectory ?? string.Empty;
|
||
_txtRestoreOutput.Text = outputDirectory ?? string.Empty;
|
||
SetRestoreDiffInputFormat(diffInputFormat);
|
||
return RefreshRestoreScanAsync();
|
||
}
|
||
|
||
internal Task ApplyRestoreSourceSelectionForTestAsync(string selectedPath)
|
||
{
|
||
return ApplySelectedFolderPathAsync(_txtRestoreSource, selectedPath, RefreshRestoreScanAsync);
|
||
}
|
||
|
||
internal Task ApplyRestoreOutputSelectionForTestAsync(string selectedPath)
|
||
{
|
||
return ApplySelectedFolderPathAsync(_txtRestoreOutput, selectedPath, RefreshRestoreScanAsync);
|
||
}
|
||
|
||
internal Task SetRestoreDiffInputForTestAsync(string diffInputFormat)
|
||
{
|
||
SetRestoreDiffInputFormat(diffInputFormat);
|
||
return RefreshRestoreScanAsync();
|
||
}
|
||
|
||
internal Task RunAutoExtractForTestAsync(IList<FileScanItem> replaceItems, IList<RuleDefinition> existingRules, AutoExtractSettings settings)
|
||
{
|
||
ReplaceBinding(_replaceItems, replaceItems ?? Array.Empty<FileScanItem>());
|
||
ReplaceBinding(_rules, existingRules ?? Array.Empty<RuleDefinition>());
|
||
_ruleFile.Rules = _rules.ToList();
|
||
|
||
if (settings != null)
|
||
{
|
||
_config.Settings.AutoExtract = settings;
|
||
ApplyConfigToUi();
|
||
}
|
||
|
||
UpdateActionStates();
|
||
return RunAutoExtractAsync();
|
||
}
|
||
|
||
internal void RequestStopReplaceForTest()
|
||
{
|
||
if (_isReplaceRunning && _replaceExecutionCancellation != null && !_replaceExecutionCancellation.IsCancellationRequested)
|
||
{
|
||
_replaceExecutionCancellation.Cancel();
|
||
}
|
||
}
|
||
|
||
internal void RequestStopRestoreForTest()
|
||
{
|
||
if (_isRestoreRunning && _restoreExecutionCancellation != null && !_restoreExecutionCancellation.IsCancellationRequested)
|
||
{
|
||
_restoreExecutionCancellation.Cancel();
|
||
}
|
||
}
|
||
|
||
internal void RaiseLogWriteFailureForTest(LogWriteFailureEventArgs e)
|
||
{
|
||
LogService_WriteFailed(this, e);
|
||
}
|
||
|
||
internal void UndoAutoExtractForTest()
|
||
{
|
||
UndoAutoExtract_Click(this, EventArgs.Empty);
|
||
}
|
||
|
||
internal void LoadReplaceItemsForTest(IList<FileScanItem> items)
|
||
{
|
||
ReplaceBinding(_replaceItems, items ?? Array.Empty<FileScanItem>());
|
||
UpdateActionStates();
|
||
}
|
||
|
||
internal void LoadRestoreItemsForTest(IList<FileScanItem> items)
|
||
{
|
||
ReplaceBinding(_restoreItems, items ?? Array.Empty<FileScanItem>());
|
||
UpdateActionStates();
|
||
}
|
||
|
||
internal void LoadRulesForTest(IList<RuleDefinition> rules)
|
||
{
|
||
ReplaceBinding(_rules, rules ?? Array.Empty<RuleDefinition>());
|
||
RefreshRuleState();
|
||
}
|
||
|
||
internal IList<string> LogContextMenuItemsForTest
|
||
{
|
||
get { return GetContextMenuTexts(_logBox.ContextMenuStrip); }
|
||
}
|
||
|
||
internal IList<string> ReplaceContextMenuItemsForTest
|
||
{
|
||
get { return GetContextMenuTexts(_gridReplace.ContextMenuStrip); }
|
||
}
|
||
|
||
internal IList<string> RestoreContextMenuItemsForTest
|
||
{
|
||
get { return GetContextMenuTexts(_gridRestore.ContextMenuStrip); }
|
||
}
|
||
|
||
internal IList<string> RuleContextMenuItemsForTest
|
||
{
|
||
get { return GetContextMenuTexts(_gridRules.ContextMenuStrip); }
|
||
}
|
||
|
||
internal bool IsRuleEditingEnabledForTest
|
||
{
|
||
get { return _gridRules.Enabled; }
|
||
}
|
||
|
||
internal bool IsUndoAutoExtractEnabledForTest
|
||
{
|
||
get { return _btnUndoAutoExtract.Enabled; }
|
||
}
|
||
|
||
internal bool IsAutoExtractEnabledForTest
|
||
{
|
||
get { return _btnAutoExtract.Enabled; }
|
||
}
|
||
|
||
internal bool IsStartReplaceEnabledForTest
|
||
{
|
||
get { return _btnStartReplace.Enabled; }
|
||
}
|
||
|
||
internal bool IsStartRestoreEnabledForTest
|
||
{
|
||
get { return _btnStartRestore.Enabled; }
|
||
}
|
||
|
||
internal bool IsStopReplaceEnabledForTest
|
||
{
|
||
get { return _btnStopReplace.Enabled; }
|
||
}
|
||
|
||
internal bool IsStopRestoreEnabledForTest
|
||
{
|
||
get { return _btnStopRestore.Enabled; }
|
||
}
|
||
|
||
internal string ReplaceRuntimeTextForTest
|
||
{
|
||
get { return _lblReplaceRuntime.Text; }
|
||
}
|
||
|
||
internal string RestoreRuntimeTextForTest
|
||
{
|
||
get { return _lblRestoreRuntime.Text; }
|
||
}
|
||
|
||
internal string RestoreBrowseSourceButtonTextForTest
|
||
{
|
||
get { return _btnBrowseRestoreSource.Text; }
|
||
}
|
||
|
||
internal string RestoreBrowseOutputButtonTextForTest
|
||
{
|
||
get { return _btnBrowseRestoreOutput.Text; }
|
||
}
|
||
|
||
internal string RestoreSelectAllButtonTextForTest
|
||
{
|
||
get { return _btnSelectAllRestore.Text; }
|
||
}
|
||
|
||
internal string RestoreClearButtonTextForTest
|
||
{
|
||
get { return _btnClearRestore.Text; }
|
||
}
|
||
|
||
internal int SettingsRuleListWidthForTest
|
||
{
|
||
get
|
||
{
|
||
return _settingsRuleListSplitContainer == null
|
||
? 0
|
||
: _settingsRuleListSplitContainer.ClientSize.Width - _settingsRuleListSplitContainer.SplitterWidth - _settingsRuleListSplitContainer.SplitterDistance;
|
||
}
|
||
}
|
||
|
||
internal void ApplySettingsRuleListInitialWidthForTest(int width)
|
||
{
|
||
if (_settingsRuleListSplitContainer == null)
|
||
{
|
||
throw new InvalidOperationException("设置页规则列表分隔容器尚未初始化。");
|
||
}
|
||
|
||
_settingsRuleListInitialWidthApplied = false;
|
||
_settingsRuleListSplitContainer.Width = width;
|
||
ApplySettingsRuleListInitialWidth(_settingsRuleListSplitContainer);
|
||
}
|
||
|
||
internal string SelectedTabTextForTest
|
||
{
|
||
get { return _tabControl.SelectedTab == null ? string.Empty : _tabControl.SelectedTab.Text; }
|
||
}
|
||
|
||
internal int SelectedRuleIndexForTest
|
||
{
|
||
get
|
||
{
|
||
if (_gridRules.SelectedRows.Count > 0)
|
||
{
|
||
return _gridRules.SelectedRows[0].Index;
|
||
}
|
||
|
||
if (_gridRules.CurrentCell != null)
|
||
{
|
||
return _gridRules.CurrentCell.RowIndex;
|
||
}
|
||
|
||
return -1;
|
||
}
|
||
}
|
||
|
||
internal IList<RuleDefinition> RulesForTest
|
||
{
|
||
get { return _rules.Select(CloneRule).ToList(); }
|
||
}
|
||
|
||
internal IList<FileScanItem> ReplaceItemsForTest
|
||
{
|
||
get { return _replaceItems.Select(CloneFileScanItem).ToList(); }
|
||
}
|
||
|
||
internal IList<FileScanItem> RestoreItemsForTest
|
||
{
|
||
get { return _restoreItems.Select(CloneFileScanItem).ToList(); }
|
||
}
|
||
|
||
internal string LogTextForTest
|
||
{
|
||
get { return _logBox.Text; }
|
||
}
|
||
|
||
internal string LogCopyTextForTest
|
||
{
|
||
get { return GetAllLogText(); }
|
||
}
|
||
|
||
internal void AppendLogTextForTest(string text)
|
||
{
|
||
if (!string.IsNullOrEmpty(text))
|
||
{
|
||
_logBox.AppendText(text);
|
||
}
|
||
}
|
||
|
||
internal void ClearLogsForTest()
|
||
{
|
||
ClearLogBox();
|
||
}
|
||
|
||
internal void SetReplaceItemSelectionForTest(IEnumerable<int> rowIndexes, bool isSelected)
|
||
{
|
||
foreach (var index in NormalizeIndexes(rowIndexes, _replaceItems.Count))
|
||
{
|
||
_replaceItems[index].IsSelected = isSelected;
|
||
}
|
||
|
||
UpdateActionStates();
|
||
}
|
||
|
||
internal void DeleteReplaceItemsForTest(IEnumerable<int> rowIndexes)
|
||
{
|
||
foreach (var index in NormalizeIndexes(rowIndexes, _replaceItems.Count).OrderByDescending(item => item))
|
||
{
|
||
_replaceItems.RemoveAt(index);
|
||
}
|
||
|
||
UpdateActionStates();
|
||
}
|
||
|
||
internal void SetRestoreItemSelectionForTest(IEnumerable<int> rowIndexes, bool isSelected)
|
||
{
|
||
foreach (var index in NormalizeIndexes(rowIndexes, _restoreItems.Count))
|
||
{
|
||
_restoreItems[index].IsSelected = isSelected;
|
||
}
|
||
|
||
UpdateActionStates();
|
||
}
|
||
|
||
internal void DeleteRestoreItemsForTest(IEnumerable<int> rowIndexes)
|
||
{
|
||
foreach (var index in NormalizeIndexes(rowIndexes, _restoreItems.Count).OrderByDescending(item => item))
|
||
{
|
||
_restoreItems.RemoveAt(index);
|
||
}
|
||
|
||
UpdateActionStates();
|
||
}
|
||
|
||
internal void SetRuleEnabledForTest(IEnumerable<int> rowIndexes, bool isEnabled)
|
||
{
|
||
foreach (var index in NormalizeIndexes(rowIndexes, _rules.Count))
|
||
{
|
||
_rules[index].IsEnabled = isEnabled;
|
||
}
|
||
|
||
RefreshRuleState();
|
||
}
|
||
|
||
internal void DeleteRulesForTest(IEnumerable<int> rowIndexes)
|
||
{
|
||
foreach (var index in NormalizeIndexes(rowIndexes, _rules.Count).OrderByDescending(item => item))
|
||
{
|
||
_rules.RemoveAt(index);
|
||
}
|
||
|
||
RefreshRuleState();
|
||
}
|
||
|
||
private static FileScanItem CloneFileScanItem(FileScanItem source)
|
||
{
|
||
if (source == null)
|
||
{
|
||
return new FileScanItem();
|
||
}
|
||
|
||
return new FileScanItem
|
||
{
|
||
IsSelected = source.IsSelected,
|
||
SourcePath = source.SourcePath,
|
||
OutputPath = source.OutputPath,
|
||
DiffPath = source.DiffPath,
|
||
TextOperationCount = source.TextOperationCount,
|
||
ImageOperationCount = source.ImageOperationCount,
|
||
Status = source.Status,
|
||
};
|
||
}
|
||
|
||
private static RuleDefinition CloneRule(RuleDefinition source)
|
||
{
|
||
if (source == null)
|
||
{
|
||
return new RuleDefinition();
|
||
}
|
||
|
||
return new RuleDefinition
|
||
{
|
||
Id = source.Id,
|
||
Name = source.Name,
|
||
IsEnabled = source.IsEnabled,
|
||
MatchMode = source.MatchMode,
|
||
IsCaseSensitive = source.IsCaseSensitive,
|
||
IsWholeWord = source.IsWholeWord,
|
||
SearchText = source.SearchText,
|
||
ReplacementText = source.ReplacementText,
|
||
Note = source.Note,
|
||
};
|
||
}
|
||
|
||
private static List<string> GetContextMenuTexts(ContextMenuStrip contextMenu)
|
||
{
|
||
if (contextMenu == null)
|
||
{
|
||
return new List<string>();
|
||
}
|
||
|
||
return contextMenu.Items.Cast<ToolStripItem>().Select(item => item.Text).ToList();
|
||
}
|
||
}
|
||
} |