860 lines
25 KiB
C#
860 lines
25 KiB
C#
using System.Collections.ObjectModel;
|
||
using System.Collections.Specialized;
|
||
using System.ComponentModel;
|
||
using System.IO;
|
||
using System.Runtime.CompilerServices;
|
||
using System.Text;
|
||
using System.Windows;
|
||
using Microsoft.Win32;
|
||
using EmbyToolbox.Models;
|
||
using EmbyToolbox.Services;
|
||
|
||
namespace EmbyToolbox.ViewModels;
|
||
|
||
public sealed class MergeViewModel : INotifyPropertyChanged
|
||
{
|
||
private readonly LoggingService _logging;
|
||
private readonly MergeService _mergeService;
|
||
private readonly RecentPathService _recentPaths;
|
||
private string _mergedOutputPath = string.Empty;
|
||
private bool _isRunning;
|
||
private int _progressPercent;
|
||
private string _progressText = "Готово";
|
||
private string _validationMessage = string.Empty;
|
||
private MergeFileItem? _selectedItem;
|
||
private CancellationTokenSource? _mergeCts;
|
||
private bool _isMergeDropHighlight;
|
||
private MergeCompletionKind _lastMergeCompletion = MergeCompletionKind.None;
|
||
private readonly List<MergeFileItem> _selectedItems = [];
|
||
|
||
public MergeViewModel(LoggingService logging, MergeService mergeService, RecentPathService recentPaths)
|
||
{
|
||
_logging = logging;
|
||
_mergeService = mergeService;
|
||
_recentPaths = recentPaths;
|
||
Files.CollectionChanged += OnFilesCollectionChanged;
|
||
|
||
SelectVideoFilesCommand = new RelayCommand(ExecuteSelectVideoFiles, () => !IsRunning);
|
||
SelectOutputFileCommand = new RelayCommand(ExecuteSelectOutputFile, () => !IsRunning);
|
||
MoveUpCommand = new RelayCommand(ExecuteMoveUp, CanMoveUp);
|
||
MoveDownCommand = new RelayCommand(ExecuteMoveDown, CanMoveDown);
|
||
RefreshCommand = new RelayCommand(ExecuteRefresh, () => !IsRunning && Files.Count > 0);
|
||
RemoveFromListCommand = new RelayCommand(ExecuteRemoveFromList, CanRemoveFromList);
|
||
ClearListCommand = new RelayCommand(ExecuteClearList, () => !IsRunning && Files.Count > 0);
|
||
MergeCommand = new RelayCommand(async () => await ExecuteMergeAsync(), CanMerge);
|
||
CancelOrClearCommand = new RelayCommand(ExecuteCancelOrClear);
|
||
}
|
||
|
||
public ObservableCollection<MergeFileItem> Files { get; } = new();
|
||
|
||
public RelayCommand SelectVideoFilesCommand { get; }
|
||
public RelayCommand SelectOutputFileCommand { get; }
|
||
|
||
public bool IsMergeDropHighlight
|
||
{
|
||
get => _isMergeDropHighlight;
|
||
internal set
|
||
{
|
||
if (_isMergeDropHighlight == value)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_isMergeDropHighlight = value;
|
||
OnPropertyChanged();
|
||
}
|
||
}
|
||
public RelayCommand MoveUpCommand { get; }
|
||
public RelayCommand MoveDownCommand { get; }
|
||
public RelayCommand RefreshCommand { get; }
|
||
public RelayCommand RemoveFromListCommand { get; }
|
||
public RelayCommand ClearListCommand { get; }
|
||
public RelayCommand MergeCommand { get; }
|
||
public RelayCommand CancelOrClearCommand { get; }
|
||
|
||
public string MergedOutputPath
|
||
{
|
||
get => _mergedOutputPath;
|
||
set
|
||
{
|
||
if (_mergedOutputPath == value)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_mergedOutputPath = value;
|
||
OnPropertyChanged();
|
||
UpdateValidationState();
|
||
RaiseCommandStates();
|
||
}
|
||
}
|
||
|
||
public bool IsRunning
|
||
{
|
||
get => _isRunning;
|
||
private set
|
||
{
|
||
if (_isRunning == value)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_isRunning = value;
|
||
OnPropertyChanged();
|
||
RaiseCommandStates();
|
||
}
|
||
}
|
||
|
||
public int ProgressPercent
|
||
{
|
||
get => _progressPercent;
|
||
private set
|
||
{
|
||
if (_progressPercent == value)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_progressPercent = value;
|
||
OnPropertyChanged();
|
||
}
|
||
}
|
||
|
||
public string ProgressText
|
||
{
|
||
get => _progressText;
|
||
private set
|
||
{
|
||
if (_progressText == value)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_progressText = value;
|
||
OnPropertyChanged();
|
||
}
|
||
}
|
||
|
||
public string ValidationMessage
|
||
{
|
||
get => _validationMessage;
|
||
private set
|
||
{
|
||
if (_validationMessage == value)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_validationMessage = value;
|
||
OnPropertyChanged();
|
||
OnPropertyChanged(nameof(HasValidationMessage));
|
||
}
|
||
}
|
||
|
||
public bool HasValidationMessage => !string.IsNullOrWhiteSpace(ValidationMessage);
|
||
|
||
/// <summary>Итог последней операции объединения (для строки состояния главного окна).</summary>
|
||
public MergeCompletionKind LastMergeCompletion => _lastMergeCompletion;
|
||
|
||
public MergeFileItem? SelectedItem
|
||
{
|
||
get => _selectedItem;
|
||
set
|
||
{
|
||
if (_selectedItem == value)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_selectedItem = value;
|
||
OnPropertyChanged();
|
||
RaiseCommandStates();
|
||
}
|
||
}
|
||
|
||
public event PropertyChangedEventHandler? PropertyChanged;
|
||
|
||
public void UpdateSelectedItems(IReadOnlyList<MergeFileItem> selectedItems)
|
||
{
|
||
_selectedItems.Clear();
|
||
_selectedItems.AddRange(selectedItems);
|
||
RaiseCommandStates();
|
||
}
|
||
|
||
private void ExecuteSelectVideoFiles()
|
||
{
|
||
var dialog = new OpenFileDialog
|
||
{
|
||
Title = "Выберите видеофайлы для объединения",
|
||
Filter = SupportedVideoFormats.BuildOpenFileDialogFilter(),
|
||
Multiselect = true,
|
||
InitialDirectory = _recentPaths.GetInitialDirectory(RecentPathScenario.Merge),
|
||
};
|
||
|
||
if (dialog.ShowDialog() != true || dialog.FileNames is not { Length: > 0 })
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (dialog.FileNames.Length > 0)
|
||
{
|
||
_recentPaths.RememberChosenFiles(RecentPathScenario.Merge, dialog.FileNames);
|
||
}
|
||
|
||
LoadFilesFromFilePicker(dialog.FileNames);
|
||
}
|
||
|
||
private void ExecuteSelectOutputFile()
|
||
{
|
||
var suggestFile = "movies.mkv";
|
||
string? suggestDir = null;
|
||
try
|
||
{
|
||
if (!string.IsNullOrWhiteSpace(MergedOutputPath))
|
||
{
|
||
var full = Path.GetFullPath(MergedOutputPath.Trim());
|
||
suggestDir = Path.GetDirectoryName(full);
|
||
var fn = Path.GetFileName(full);
|
||
if (!string.IsNullOrEmpty(fn))
|
||
{
|
||
suggestFile = fn;
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore malformed path
|
||
}
|
||
|
||
suggestDir ??= _recentPaths.GetInitialDirectory(RecentPathScenario.Merge);
|
||
|
||
var dlg = new SaveFileDialog
|
||
{
|
||
Title = "Итоговый файл",
|
||
Filter = "Matroska (*.mkv)|*.mkv",
|
||
DefaultExt = ".mkv",
|
||
AddExtension = true,
|
||
FileName = suggestFile,
|
||
};
|
||
|
||
if (!string.IsNullOrWhiteSpace(suggestDir) && Directory.Exists(suggestDir))
|
||
{
|
||
dlg.InitialDirectory = suggestDir;
|
||
}
|
||
|
||
if (dlg.ShowDialog() != true || string.IsNullOrWhiteSpace(dlg.FileName))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var chosen = Path.GetFullPath(dlg.FileName);
|
||
MergedOutputPath = chosen;
|
||
_recentPaths.RememberChosenFiles(RecentPathScenario.Merge, [chosen]);
|
||
}
|
||
|
||
/// <summary>Drag & drop: добавить файлы/каталог (только видео первого уровня) к списку без дублей.</summary>
|
||
public void ApplyDroppedPaths(IReadOnlyList<string> rawPaths)
|
||
{
|
||
if (IsRunning || rawPaths is null || rawPaths.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
IsMergeDropHighlight = false;
|
||
|
||
var discovered = new List<string>();
|
||
foreach (var raw in rawPaths)
|
||
{
|
||
try
|
||
{
|
||
var full = Path.GetFullPath(raw);
|
||
if (File.Exists(full))
|
||
{
|
||
if (SupportedVideoFormats.IsSupportedVideoFile(full))
|
||
{
|
||
discovered.Add(full);
|
||
}
|
||
else
|
||
{
|
||
_logging.Warning($"объединение (drop): не поддерживаемое расширение, пропуск: {full}", "merge");
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
if (Directory.Exists(full))
|
||
{
|
||
foreach (var top in EnumerateTopLevelVideoFilesOnly(full))
|
||
{
|
||
discovered.Add(top);
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logging.Warning($"объединение (drop): не удалось обработать «{raw}»: {ex.Message}", "merge");
|
||
}
|
||
}
|
||
|
||
if (discovered.Count == 0)
|
||
{
|
||
_logging.Warning("объединение (drop): нет подходящих видеофайлов", "merge");
|
||
return;
|
||
}
|
||
|
||
var combined = new HashSet<string>(Files.Select(f => f.FullPath), StringComparer.OrdinalIgnoreCase);
|
||
var added = 0;
|
||
foreach (var p in discovered)
|
||
{
|
||
if (combined.Add(p))
|
||
{
|
||
added++;
|
||
}
|
||
}
|
||
|
||
if (added == 0)
|
||
{
|
||
_logging.Warning("объединение (drop): все элементы уже в списке", "merge");
|
||
return;
|
||
}
|
||
|
||
var sorted = combined.OrderBy(p => p, FileDiscoveryService.QueuePathOrderComparer).ToList();
|
||
RebuildMergeItems(sorted);
|
||
AfterFilesChanged();
|
||
_logging.Info($"объединение (drop): добавлено файлов: {added}, всего в списке: {Files.Count}", "merge");
|
||
}
|
||
|
||
private static IEnumerable<string> EnumerateTopLevelVideoFilesOnly(string directoryFullPath)
|
||
{
|
||
foreach (var file in Directory.EnumerateFiles(directoryFullPath))
|
||
{
|
||
if (!SupportedVideoFormats.IsSupportedVideoFile(file))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
string normalized;
|
||
try
|
||
{
|
||
normalized = Path.GetFullPath(file);
|
||
}
|
||
catch
|
||
{
|
||
continue;
|
||
}
|
||
|
||
yield return normalized;
|
||
}
|
||
}
|
||
|
||
private void LoadFilesFromFilePicker(string[] fileNames)
|
||
{
|
||
var list = new List<string>();
|
||
foreach (var raw in fileNames)
|
||
{
|
||
try
|
||
{
|
||
var full = Path.GetFullPath(raw);
|
||
if (!File.Exists(full))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (!SupportedVideoFormats.IsSupportedVideoFile(full))
|
||
{
|
||
_logging.Warning($"объединение: файл не поддерживается и пропущен: {full}", "merge");
|
||
continue;
|
||
}
|
||
|
||
list.Add(full);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logging.Warning($"объединение: неверный путь «{raw}»: {ex.Message}", "merge");
|
||
}
|
||
}
|
||
|
||
var sorted = list.OrderBy(p => p, FileDiscoveryService.QueuePathOrderComparer).ToList();
|
||
RebuildMergeItems(sorted);
|
||
AfterFilesChanged();
|
||
_logging.Info($"объединение: выбрано файлов из диалога: {sorted.Count}", "merge");
|
||
}
|
||
|
||
private void ExecuteMoveUp()
|
||
{
|
||
if (SelectedItem is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var idx = Files.IndexOf(SelectedItem);
|
||
if (idx <= 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Files.Move(idx, idx - 1);
|
||
RenumberMergeRows();
|
||
}
|
||
|
||
private void ExecuteMoveDown()
|
||
{
|
||
if (SelectedItem is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var idx = Files.IndexOf(SelectedItem);
|
||
if (idx < 0 || idx >= Files.Count - 1)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Files.Move(idx, idx + 1);
|
||
RenumberMergeRows();
|
||
}
|
||
|
||
private void ExecuteRefresh()
|
||
{
|
||
if (Files.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var sorted = Files
|
||
.Select(f => f.FullPath)
|
||
.OrderBy(p => p, FileDiscoveryService.QueuePathOrderComparer)
|
||
.ToList();
|
||
RebuildMergeItems(sorted);
|
||
AfterFilesChanged();
|
||
_logging.Debug("объединение: порядок обновлён по алфавиту полных путей", "merge");
|
||
}
|
||
|
||
private void ExecuteRemoveFromList()
|
||
{
|
||
if (_selectedItems.Count == 0 && SelectedItem is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var targets = _selectedItems.Count > 0
|
||
? _selectedItems.ToList()
|
||
: [SelectedItem!];
|
||
|
||
foreach (var item in targets)
|
||
{
|
||
Files.Remove(item);
|
||
}
|
||
|
||
SelectedItem = null;
|
||
_selectedItems.Clear();
|
||
RenumberMergeRows();
|
||
}
|
||
|
||
private void ExecuteClearList()
|
||
{
|
||
if (Files.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Files.Clear();
|
||
_selectedItems.Clear();
|
||
SelectedItem = null;
|
||
MergedOutputPath = string.Empty;
|
||
ProgressPercent = 0;
|
||
ProgressText = "Готово";
|
||
ValidationMessage = string.Empty;
|
||
RaiseCommandStates();
|
||
}
|
||
|
||
private void SetLastMergeCompletion(MergeCompletionKind value)
|
||
{
|
||
if (_lastMergeCompletion == value)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_lastMergeCompletion = value;
|
||
OnPropertyChanged(nameof(LastMergeCompletion));
|
||
}
|
||
|
||
private async Task ExecuteMergeAsync()
|
||
{
|
||
if (!CanMerge())
|
||
{
|
||
return;
|
||
}
|
||
|
||
string outputPath;
|
||
try
|
||
{
|
||
outputPath = Path.GetFullPath(MergedOutputPath.Trim());
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
ProgressText = $"Ошибка пути: {ex.Message}";
|
||
_logging.Error($"объединение: некорректный путь выхода: {ex.Message}", "merge", ex);
|
||
return;
|
||
}
|
||
|
||
var outDir = Path.GetDirectoryName(outputPath);
|
||
if (!string.IsNullOrEmpty(outDir) && !Directory.Exists(outDir))
|
||
{
|
||
Directory.CreateDirectory(outDir);
|
||
}
|
||
|
||
var ordered = Files.ToList();
|
||
|
||
_mergeCts?.Dispose();
|
||
_mergeCts = new CancellationTokenSource();
|
||
|
||
try
|
||
{
|
||
SetLastMergeCompletion(MergeCompletionKind.None);
|
||
IsRunning = true;
|
||
ProgressPercent = 0;
|
||
ProgressText = "Объединение...";
|
||
foreach (var file in Files)
|
||
{
|
||
file.Status = "В очереди";
|
||
}
|
||
|
||
var progress = new Progress<int>(p =>
|
||
{
|
||
ProgressPercent = p;
|
||
ProgressText = $"Объединение... {p}%";
|
||
});
|
||
|
||
foreach (var file in ordered)
|
||
{
|
||
file.Status = "Обработка";
|
||
}
|
||
|
||
await _mergeService.MergeAsync(ordered, outputPath, progress, _mergeCts.Token);
|
||
|
||
foreach (var file in Files)
|
||
{
|
||
file.Status = "Готово";
|
||
}
|
||
|
||
TryDeleteSourcesAfterSuccess(ordered);
|
||
|
||
SetLastMergeCompletion(MergeCompletionKind.Success);
|
||
ProgressPercent = 100;
|
||
ProgressText = $"Готово: {Path.GetFileName(outputPath)}";
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
foreach (var file in Files.Where(f => f.Status == "Обработка"))
|
||
{
|
||
file.Status = "Отмена";
|
||
}
|
||
|
||
SetLastMergeCompletion(MergeCompletionKind.Cancelled);
|
||
ProgressText = "Операция отменена";
|
||
_logging.Warning("объединение отменено пользователем", "merge");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
foreach (var file in Files)
|
||
{
|
||
if (file.Status != "Готово")
|
||
{
|
||
file.Status = "Ошибка";
|
||
}
|
||
}
|
||
|
||
SetLastMergeCompletion(MergeCompletionKind.Error);
|
||
ProgressText = $"Ошибка: {ex.Message}";
|
||
_logging.Error($"ошибка объединения: {ex.Message}", "merge", ex);
|
||
}
|
||
finally
|
||
{
|
||
IsRunning = false;
|
||
}
|
||
}
|
||
|
||
private void ExecuteCancelOrClear()
|
||
{
|
||
if (IsRunning)
|
||
{
|
||
_mergeCts?.Cancel();
|
||
return;
|
||
}
|
||
|
||
SetLastMergeCompletion(MergeCompletionKind.None);
|
||
Files.Clear();
|
||
_selectedItems.Clear();
|
||
SelectedItem = null;
|
||
MergedOutputPath = string.Empty;
|
||
ProgressPercent = 0;
|
||
ProgressText = "Готово";
|
||
ValidationMessage = string.Empty;
|
||
}
|
||
|
||
private bool CanMoveUp()
|
||
{
|
||
if (IsRunning || SelectedItem is null || _selectedItems.Count > 1)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return Files.IndexOf(SelectedItem) > 0;
|
||
}
|
||
|
||
private bool CanMoveDown()
|
||
{
|
||
if (IsRunning || SelectedItem is null || _selectedItems.Count > 1)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var idx = Files.IndexOf(SelectedItem);
|
||
return idx >= 0 && idx < Files.Count - 1;
|
||
}
|
||
|
||
private bool CanMerge()
|
||
{
|
||
return !IsRunning && ValidateInputs(showMessage: false);
|
||
}
|
||
|
||
private void RebuildMergeItems(IReadOnlyList<string> sortedFullPaths)
|
||
{
|
||
Files.Clear();
|
||
for (var i = 0; i < sortedFullPaths.Count; i++)
|
||
{
|
||
var fullPath = sortedFullPaths[i];
|
||
var fileName = Path.GetFileName(fullPath);
|
||
var item = new MergeFileItem
|
||
{
|
||
FullPath = fullPath,
|
||
FileName = fileName,
|
||
SizeMb = (int)Math.Round(new FileInfo(fullPath).Length / (1024d * 1024d), MidpointRounding.AwayFromZero),
|
||
Number = i + 1,
|
||
Status = "Готов",
|
||
};
|
||
item.SyncAutoPartName(BuildDefaultPartName(i));
|
||
Files.Add(item);
|
||
}
|
||
|
||
UpdateDefaultMergedOutputPath(sortedFullPaths);
|
||
}
|
||
|
||
private void UpdateDefaultMergedOutputPath(IReadOnlyList<string> sortedFullPaths)
|
||
{
|
||
if (sortedFullPaths.Count == 0)
|
||
{
|
||
MergedOutputPath = string.Empty;
|
||
return;
|
||
}
|
||
|
||
MergedOutputPath = BuildDefaultMergedOutputFullPath(sortedFullPaths);
|
||
}
|
||
|
||
private string BuildDefaultMergedOutputFullPath(IReadOnlyList<string> sortedFullPaths)
|
||
{
|
||
var parents = sortedFullPaths
|
||
.Select(static p => Path.GetDirectoryName(p)!)
|
||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||
.ToList();
|
||
|
||
var dir = parents.Count == 1
|
||
? parents[0]
|
||
: _recentPaths.GetInitialDirectory(RecentPathScenario.Merge);
|
||
|
||
var fileName = parents.Count == 1
|
||
? $"{new DirectoryInfo(parents[0]).Name}.mkv"
|
||
: "movies.mkv";
|
||
|
||
return Path.Combine(dir, fileName);
|
||
}
|
||
|
||
private static string BuildDefaultPartName(int zeroBasedIndex)
|
||
{
|
||
return $"Часть {zeroBasedIndex + 1}";
|
||
}
|
||
|
||
private void AfterFilesChanged()
|
||
{
|
||
ProgressPercent = 0;
|
||
ProgressText = Files.Count < 2
|
||
? "Нужно минимум 2 файла для объединения"
|
||
: "Готово";
|
||
|
||
UpdateValidationState();
|
||
RaiseCommandStates();
|
||
}
|
||
|
||
private void RenumberMergeRows()
|
||
{
|
||
for (var i = 0; i < Files.Count; i++)
|
||
{
|
||
Files[i].Number = i + 1;
|
||
Files[i].SyncAutoPartName(BuildDefaultPartName(i));
|
||
}
|
||
|
||
UpdateValidationState();
|
||
RaiseCommandStates();
|
||
}
|
||
|
||
private void OnFilesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||
{
|
||
UpdateValidationState();
|
||
RaiseCommandStates();
|
||
}
|
||
|
||
private void UpdateValidationState()
|
||
{
|
||
ValidateInputs(showMessage: true);
|
||
}
|
||
|
||
private bool ValidateInputs(bool showMessage)
|
||
{
|
||
string? error = null;
|
||
|
||
if (Files.Count < 2)
|
||
{
|
||
error = "Нужно минимум 2 файла для объединения.";
|
||
}
|
||
else if (string.IsNullOrWhiteSpace(MergedOutputPath))
|
||
{
|
||
error = "Укажите полный путь итогового файла.";
|
||
}
|
||
else
|
||
{
|
||
string fullOutput;
|
||
try
|
||
{
|
||
fullOutput = Path.GetFullPath(MergedOutputPath.Trim());
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
error = $"Некорректный путь итогового файла: {ex.Message}";
|
||
fullOutput = string.Empty;
|
||
}
|
||
|
||
if (error is null)
|
||
{
|
||
var name = Path.GetFileName(fullOutput);
|
||
if (string.IsNullOrWhiteSpace(name))
|
||
{
|
||
error = "Укажите имя итогового файла (например, …\\movies.mkv).";
|
||
}
|
||
else if (name.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
|
||
{
|
||
error = "Имя итогового файла содержит недопустимые символы.";
|
||
}
|
||
}
|
||
}
|
||
|
||
if (showMessage)
|
||
{
|
||
ValidationMessage = error ?? string.Empty;
|
||
}
|
||
|
||
return string.IsNullOrWhiteSpace(error);
|
||
}
|
||
|
||
private void RaiseCommandStates()
|
||
{
|
||
SelectVideoFilesCommand.RaiseCanExecuteChanged();
|
||
SelectOutputFileCommand.RaiseCanExecuteChanged();
|
||
MoveUpCommand.RaiseCanExecuteChanged();
|
||
MoveDownCommand.RaiseCanExecuteChanged();
|
||
RefreshCommand.RaiseCanExecuteChanged();
|
||
RemoveFromListCommand.RaiseCanExecuteChanged();
|
||
ClearListCommand.RaiseCanExecuteChanged();
|
||
MergeCommand.RaiseCanExecuteChanged();
|
||
CancelOrClearCommand.RaiseCanExecuteChanged();
|
||
}
|
||
|
||
private bool CanRemoveFromList()
|
||
{
|
||
return !IsRunning && (_selectedItems.Count > 0 || SelectedItem is not null);
|
||
}
|
||
|
||
private void TryDeleteSourcesAfterSuccess(IReadOnlyList<MergeFileItem> ordered)
|
||
{
|
||
if (ordered.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var sourceList = string.Join(Environment.NewLine, ordered.Select(f => f.FullPath));
|
||
var question = new StringBuilder()
|
||
.AppendLine("Удалить исходные файлы?")
|
||
.AppendLine()
|
||
.AppendLine(sourceList)
|
||
.ToString();
|
||
|
||
var answer = MessageBox.Show(
|
||
question,
|
||
"Объединение завершено",
|
||
MessageBoxButton.YesNo,
|
||
MessageBoxImage.Question);
|
||
|
||
if (answer != MessageBoxResult.Yes)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var deletedItems = new List<MergeFileItem>();
|
||
var deleteErrors = new List<string>();
|
||
foreach (var item in ordered)
|
||
{
|
||
var source = item.FullPath;
|
||
try
|
||
{
|
||
File.Delete(source);
|
||
deletedItems.Add(item);
|
||
_logging.Info($"удален исходник после объединения: {source}", "merge");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
deleteErrors.Add($"{source} ({ex.Message})");
|
||
_logging.Warning($"не удалось удалить исходник: {source} ({ex.Message})", "merge", ex);
|
||
}
|
||
}
|
||
|
||
RemoveDeletedSourcesFromTable(deletedItems);
|
||
|
||
if (deleteErrors.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
MessageBox.Show(
|
||
"Не удалось удалить некоторые файлы:" + Environment.NewLine + Environment.NewLine + string.Join(Environment.NewLine, deleteErrors),
|
||
"Удаление исходников",
|
||
MessageBoxButton.OK,
|
||
MessageBoxImage.Warning);
|
||
}
|
||
|
||
private void RemoveDeletedSourcesFromTable(IReadOnlyList<MergeFileItem> deletedItems)
|
||
{
|
||
if (deletedItems.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
foreach (var item in deletedItems)
|
||
{
|
||
Files.Remove(item);
|
||
_selectedItems.Remove(item);
|
||
}
|
||
|
||
if (SelectedItem is not null && deletedItems.Contains(SelectedItem))
|
||
{
|
||
SelectedItem = null;
|
||
}
|
||
|
||
RenumberMergeRows();
|
||
}
|
||
|
||
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||
{
|
||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||
}
|
||
}
|