335 lines
9.8 KiB
C#
335 lines
9.8 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.ComponentModel;
|
|
using System.IO;
|
|
using System.Runtime.CompilerServices;
|
|
using EmbyToolbox.Services;
|
|
using Microsoft.Win32;
|
|
|
|
namespace EmbyToolbox.ViewModels;
|
|
|
|
public sealed class SeriesRenamerViewModel : INotifyPropertyChanged
|
|
{
|
|
private readonly SeriesRenamerService _service;
|
|
private readonly LoggingService _logging;
|
|
private readonly RecentPathService _recentPaths;
|
|
|
|
private string _rootFolderPath = string.Empty;
|
|
private string _seriesName = string.Empty;
|
|
private string _unsupportedReason = string.Empty;
|
|
private bool _isPreviewSupported;
|
|
private bool _isRootTreeDragOver;
|
|
private bool _isSynchronizingTrees;
|
|
private readonly Dictionary<string, RenameTreeNodeViewModel> _currentByKey = new(StringComparer.Ordinal);
|
|
private readonly Dictionary<string, RenameTreeNodeViewModel> _previewByKey = new(StringComparer.Ordinal);
|
|
|
|
private SeriesRenamePreview _currentPreview = SeriesRenamePreview.Unsupported("Папка сериала не выбрана.");
|
|
|
|
public SeriesRenamerViewModel(SeriesRenamerService service, LoggingService logging, RecentPathService recentPaths)
|
|
{
|
|
_service = service;
|
|
_logging = logging;
|
|
_recentPaths = recentPaths;
|
|
|
|
SelectRootFolderCommand = new RelayCommand(ExecuteSelectRootFolder);
|
|
RefreshPreviewCommand = new RelayCommand(ExecuteRefreshPreview);
|
|
RunRenameCommand = new RelayCommand(ExecuteRunRename, () => IsPreviewSupported);
|
|
}
|
|
|
|
public ObservableCollection<RenameTreeNodeViewModel> CurrentTree { get; } = new();
|
|
public ObservableCollection<RenameTreeNodeViewModel> PreviewTree { get; } = new();
|
|
|
|
public RelayCommand SelectRootFolderCommand { get; }
|
|
public RelayCommand RefreshPreviewCommand { get; }
|
|
public RelayCommand RunRenameCommand { get; }
|
|
|
|
public bool IsRootTreeDragOver
|
|
{
|
|
get => _isRootTreeDragOver;
|
|
internal set
|
|
{
|
|
if (_isRootTreeDragOver == value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_isRootTreeDragOver = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>Перетаскивание в дерево текущей структуры: только один каталог как корень сериала.</summary>
|
|
public void ApplyDroppedPaths(IReadOnlyList<string> paths)
|
|
{
|
|
if (paths is null || paths.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
IsRootTreeDragOver = false;
|
|
|
|
foreach (var raw in paths)
|
|
{
|
|
try
|
|
{
|
|
var full = Path.GetFullPath(raw);
|
|
if (Directory.Exists(full))
|
|
{
|
|
ApplyRootFolder(full, fromDragDrop: true);
|
|
return;
|
|
}
|
|
|
|
if (File.Exists(full))
|
|
{
|
|
_logging.Warning(
|
|
$"переименование сериалов: ожидалась папка, файл пропущен: {full}",
|
|
"series-renamer");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logging.Warning(
|
|
$"переименование сериалов: неверный путь «{raw}»: {ex.Message}",
|
|
"series-renamer");
|
|
}
|
|
}
|
|
}
|
|
|
|
public string RootFolderPath
|
|
{
|
|
get => _rootFolderPath;
|
|
set
|
|
{
|
|
if (_rootFolderPath == value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_rootFolderPath = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public string SeriesName
|
|
{
|
|
get => _seriesName;
|
|
set
|
|
{
|
|
if (_seriesName == value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_seriesName = value;
|
|
OnPropertyChanged();
|
|
RebuildPreview();
|
|
}
|
|
}
|
|
|
|
public bool IsPreviewSupported
|
|
{
|
|
get => _isPreviewSupported;
|
|
private set
|
|
{
|
|
if (_isPreviewSupported == value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_isPreviewSupported = value;
|
|
OnPropertyChanged();
|
|
RunRenameCommand.RaiseCanExecuteChanged();
|
|
}
|
|
}
|
|
|
|
public string UnsupportedReason
|
|
{
|
|
get => _unsupportedReason;
|
|
private set
|
|
{
|
|
if (_unsupportedReason == value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_unsupportedReason = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
|
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
|
{
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
|
}
|
|
|
|
private void ExecuteSelectRootFolder()
|
|
{
|
|
var dialog = new OpenFolderDialog
|
|
{
|
|
Title = "Выберите корневую папку сериала",
|
|
InitialDirectory = _recentPaths.GetInitialDirectory(RecentPathScenario.SeriesRenamer),
|
|
};
|
|
|
|
if (dialog.ShowDialog() != true || string.IsNullOrWhiteSpace(dialog.FolderName))
|
|
{
|
|
return;
|
|
}
|
|
|
|
ApplyRootFolder(dialog.FolderName, fromDragDrop: false);
|
|
}
|
|
|
|
private void ApplyRootFolder(string directoryPath, bool fromDragDrop)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(directoryPath) || !Directory.Exists(directoryPath))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var full = Path.GetFullPath(directoryPath);
|
|
_recentPaths.RememberChosenFolder(RecentPathScenario.SeriesRenamer, full);
|
|
RootFolderPath = full;
|
|
SeriesName = Path.GetFileName(full.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
|
_logging.Info(
|
|
fromDragDrop
|
|
? $"папка сериала (drag & drop): {full}"
|
|
: $"выбрана папка сериала: {full}",
|
|
"series-renamer");
|
|
RebuildPreview();
|
|
}
|
|
|
|
private void ExecuteRefreshPreview()
|
|
{
|
|
RebuildPreview();
|
|
_logging.Debug("предпросмотр переименования обновлен вручную", "series-renamer");
|
|
}
|
|
|
|
private void RebuildPreview()
|
|
{
|
|
_currentPreview = _service.BuildPreview(RootFolderPath, SeriesName);
|
|
CurrentTree.Clear();
|
|
PreviewTree.Clear();
|
|
_currentByKey.Clear();
|
|
_previewByKey.Clear();
|
|
|
|
if (_currentPreview.CurrentTree is not null)
|
|
{
|
|
CurrentTree.Add(ConvertNode(_currentPreview.CurrentTree, RenameTreeSide.Current));
|
|
}
|
|
|
|
if (_currentPreview.IsSupported && _currentPreview.PreviewTree is not null)
|
|
{
|
|
PreviewTree.Add(ConvertNode(_currentPreview.PreviewTree, RenameTreeSide.Preview));
|
|
UnsupportedReason = string.Empty;
|
|
IsPreviewSupported = true;
|
|
}
|
|
else
|
|
{
|
|
UnsupportedReason = _currentPreview.UnsupportedReason ?? "Невозможно построить предпросмотр.";
|
|
IsPreviewSupported = false;
|
|
}
|
|
}
|
|
|
|
private void ExecuteRunRename()
|
|
{
|
|
if (!_currentPreview.IsSupported)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_logging.Info("запуск переименования сериала", "series-renamer");
|
|
var result = _service.ExecutePreview(_currentPreview, RootFolderPath);
|
|
if (result.IsSuccess)
|
|
{
|
|
if (result.RootWasRenamed)
|
|
{
|
|
RootFolderPath = result.NewRootPath;
|
|
}
|
|
|
|
_logging.Info("переименование завершено успешно", "series-renamer");
|
|
RebuildPreview();
|
|
}
|
|
else
|
|
{
|
|
_logging.Error($"ошибка переименования: {result.Error}", "series-renamer");
|
|
}
|
|
}
|
|
|
|
private RenameTreeNodeViewModel ConvertNode(SeriesNode node, RenameTreeSide side)
|
|
{
|
|
var vm = new RenameTreeNodeViewModel
|
|
{
|
|
Name = node.Name,
|
|
Kind = node.Kind,
|
|
IconGlyph = GetGlyph(node.Kind),
|
|
NodeKey = node.NodeKey,
|
|
Side = side,
|
|
IsExpanded = true
|
|
};
|
|
vm.PropertyChanged += OnTreeNodePropertyChanged;
|
|
|
|
if (side == RenameTreeSide.Current)
|
|
{
|
|
_currentByKey[node.NodeKey] = vm;
|
|
}
|
|
else
|
|
{
|
|
_previewByKey[node.NodeKey] = vm;
|
|
}
|
|
|
|
foreach (var child in node.Children)
|
|
{
|
|
vm.Children.Add(ConvertNode(child, side));
|
|
}
|
|
|
|
return vm;
|
|
}
|
|
|
|
private void OnTreeNodePropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
{
|
|
if (_isSynchronizingTrees || !IsPreviewSupported || sender is not RenameTreeNodeViewModel source)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (e.PropertyName is not nameof(RenameTreeNodeViewModel.IsExpanded) and not nameof(RenameTreeNodeViewModel.IsSelected))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var targetMap = source.Side == RenameTreeSide.Current ? _previewByKey : _currentByKey;
|
|
if (!targetMap.TryGetValue(source.NodeKey, out var target))
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
_isSynchronizingTrees = true;
|
|
if (e.PropertyName == nameof(RenameTreeNodeViewModel.IsExpanded))
|
|
{
|
|
target.IsExpanded = source.IsExpanded;
|
|
}
|
|
else if (e.PropertyName == nameof(RenameTreeNodeViewModel.IsSelected) && source.IsSelected)
|
|
{
|
|
target.IsSelected = true;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_isSynchronizingTrees = false;
|
|
}
|
|
}
|
|
|
|
private static string GetGlyph(string kind)
|
|
{
|
|
return kind switch
|
|
{
|
|
"Folder" => "\uE8B7",
|
|
"Video" => "\uEDA2",
|
|
"Sidecar" => "\uE8EA",
|
|
_ => "\uE8EA"
|
|
};
|
|
}
|
|
}
|