712 lines
22 KiB
C#
712 lines
22 KiB
C#
using System.Collections.ObjectModel;
|
||
using System.ComponentModel;
|
||
using System.Runtime.CompilerServices;
|
||
using System.Text.Json;
|
||
using System.Linq;
|
||
using System.Windows;
|
||
using System.Windows.Input;
|
||
using System.IO;
|
||
using EmbyToolbox.Models;
|
||
using EmbyToolbox.Services;
|
||
using Microsoft.Win32;
|
||
|
||
namespace EmbyToolbox.ViewModels;
|
||
|
||
public sealed class VideoInfoViewModel : INotifyPropertyChanged
|
||
{
|
||
private readonly FfprobeService _ffprobeService;
|
||
private readonly LoggingService _logging;
|
||
private readonly RecentPathService _recentPaths;
|
||
private readonly SidecarDiscoveryService _sidecarDiscoveryService;
|
||
private readonly VideoInfoSummaryService _summaryService;
|
||
|
||
private string _selectedFilePath = string.Empty;
|
||
private string _analysisStateText = string.Empty;
|
||
private string _errorMessage = string.Empty;
|
||
private string _rawJson = string.Empty;
|
||
private string _summaryText = string.Empty;
|
||
private bool _isBusy;
|
||
private bool _isVideoInfoDropHighlight;
|
||
private int _selectedSubTabIndex;
|
||
|
||
public VideoInfoViewModel(
|
||
FfprobeService ffprobeService,
|
||
LoggingService logging,
|
||
RecentPathService recentPaths,
|
||
SidecarDiscoveryService sidecarDiscoveryService,
|
||
VideoInfoSummaryService summaryService)
|
||
{
|
||
_ffprobeService = ffprobeService;
|
||
_logging = logging;
|
||
_recentPaths = recentPaths;
|
||
_sidecarDiscoveryService = sidecarDiscoveryService;
|
||
_summaryService = summaryService;
|
||
|
||
SelectFileCommand = new RelayCommand(ExecuteSelectFile);
|
||
SelectSummaryFilesCommand = new RelayCommand(ExecuteSelectSummaryFiles);
|
||
ExpandAllCommand = new RelayCommand(ExecuteExpandAll);
|
||
CollapseAllCommand = new RelayCommand(ExecuteCollapseAll);
|
||
CopyJsonCommand = new RelayCommand(ExecuteCopyJson, () => !string.IsNullOrWhiteSpace(_rawJson));
|
||
SaveJsonCommand = new RelayCommand(ExecuteSaveJson, () => !string.IsNullOrWhiteSpace(_rawJson));
|
||
CopySummaryCommand = new RelayCommand(ExecuteCopySummary, () => !string.IsNullOrWhiteSpace(SummaryText) && SummaryText != "Файл не проанализирован");
|
||
SaveSummaryCommand = new RelayCommand(ExecuteSaveSummary, () => !string.IsNullOrWhiteSpace(SummaryText) && SummaryText != "Файл не проанализирован");
|
||
CopyNodeValueCommand = new RelayCommand(ExecuteCopyNodeValue, CanCopyNodeValue);
|
||
CopyNodeLineCommand = new RelayCommand(ExecuteCopyNodeLine, CanCopyNodeLine);
|
||
CopyNodeWithChildrenCommand = new RelayCommand(ExecuteCopyNodeWithChildren, CanCopyNodeWithChildren);
|
||
CopyNodePathCommand = new RelayCommand(ExecuteCopyNodePath, CanCopyNodePath);
|
||
}
|
||
|
||
public ObservableCollection<JsonTreeNodeViewModel> JsonNodes { get; } = new();
|
||
|
||
public ICommand SelectFileCommand { get; }
|
||
public ICommand SelectSummaryFilesCommand { get; }
|
||
|
||
public ICommand ExpandAllCommand { get; }
|
||
|
||
public ICommand CollapseAllCommand { get; }
|
||
|
||
public ICommand CopyJsonCommand { get; }
|
||
|
||
public ICommand SaveJsonCommand { get; }
|
||
public ICommand CopySummaryCommand { get; }
|
||
public ICommand SaveSummaryCommand { get; }
|
||
|
||
public ICommand CopyNodeValueCommand { get; }
|
||
|
||
public ICommand CopyNodeLineCommand { get; }
|
||
|
||
public ICommand CopyNodeWithChildrenCommand { get; }
|
||
|
||
public ICommand CopyNodePathCommand { get; }
|
||
|
||
public string SelectedFilePath
|
||
{
|
||
get => _selectedFilePath;
|
||
private set
|
||
{
|
||
if (_selectedFilePath == value)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_selectedFilePath = value;
|
||
OnPropertyChanged();
|
||
}
|
||
}
|
||
|
||
public string AnalysisStateText
|
||
{
|
||
get => _analysisStateText;
|
||
private set
|
||
{
|
||
if (_analysisStateText == value)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_analysisStateText = value;
|
||
OnPropertyChanged();
|
||
}
|
||
}
|
||
|
||
public string ErrorMessage
|
||
{
|
||
get => _errorMessage;
|
||
private set
|
||
{
|
||
if (_errorMessage == value)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_errorMessage = value;
|
||
OnPropertyChanged();
|
||
}
|
||
}
|
||
|
||
public bool IsBusy
|
||
{
|
||
get => _isBusy;
|
||
private set
|
||
{
|
||
if (_isBusy == value)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_isBusy = value;
|
||
OnPropertyChanged();
|
||
}
|
||
}
|
||
|
||
public int SelectedSubTabIndex
|
||
{
|
||
get => _selectedSubTabIndex;
|
||
set
|
||
{
|
||
if (_selectedSubTabIndex == value)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_selectedSubTabIndex = value;
|
||
OnPropertyChanged();
|
||
}
|
||
}
|
||
|
||
public string SummaryText
|
||
{
|
||
get => _summaryText;
|
||
private set
|
||
{
|
||
if (_summaryText == value)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_summaryText = value;
|
||
OnPropertyChanged();
|
||
OnPropertyChanged(nameof(HasSummaryData));
|
||
}
|
||
}
|
||
|
||
public bool HasSummaryData => !string.IsNullOrWhiteSpace(_summaryText);
|
||
|
||
public bool IsVideoInfoDropHighlight
|
||
{
|
||
get => _isVideoInfoDropHighlight;
|
||
internal set
|
||
{
|
||
if (_isVideoInfoDropHighlight == value)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_isVideoInfoDropHighlight = value;
|
||
OnPropertyChanged();
|
||
}
|
||
}
|
||
|
||
/// <summary>На вкладке summary анализирует все поддерживаемые файлы; на detailed — только первый.</summary>
|
||
public void ApplyDroppedPathsAndAnalyze(string[]? paths)
|
||
{
|
||
if (paths is null || paths.Length == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
IsVideoInfoDropHighlight = false;
|
||
|
||
if (IsBusy)
|
||
{
|
||
_logging.Warning("video-info (drop): анализ уже выполняется — повторите после завершения", "video-info");
|
||
return;
|
||
}
|
||
|
||
_ = SelectedSubTabIndex == 0
|
||
? ApplyDroppedSummaryInternalAsync(paths)
|
||
: ApplyDroppedDetailedInternalAsync(paths);
|
||
}
|
||
|
||
public event PropertyChangedEventHandler? PropertyChanged;
|
||
|
||
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||
{
|
||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||
}
|
||
|
||
private async void ExecuteSelectFile()
|
||
{
|
||
var dialog = new OpenFileDialog
|
||
{
|
||
Title = "Выберите видеофайл",
|
||
Filter = SupportedVideoFormats.BuildOpenFileDialogFilter(),
|
||
InitialDirectory = _recentPaths.GetInitialDirectory(RecentPathScenario.VideoInfoOpenFile),
|
||
};
|
||
|
||
if (dialog.ShowDialog() != true)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!SupportedVideoFormats.IsSupportedVideoFile(dialog.FileName))
|
||
{
|
||
_logging.Warning($"video-info: формат не поддерживается: {dialog.FileName}", "video-info");
|
||
return;
|
||
}
|
||
|
||
_recentPaths.RememberChosenFiles(RecentPathScenario.VideoInfoOpenFile, [dialog.FileName]);
|
||
|
||
SelectedFilePath = dialog.FileName;
|
||
_logging.Info($"выбран файл: {dialog.FileName}", "video-info");
|
||
_logging.Debug("запуск ffprobe", "video-info.ffprobe");
|
||
await AnalyzeAsync();
|
||
}
|
||
|
||
private async void ExecuteSelectSummaryFiles()
|
||
{
|
||
var dialog = new OpenFileDialog
|
||
{
|
||
Title = "Выберите видеофайлы",
|
||
Filter = SupportedVideoFormats.BuildOpenFileDialogFilter(),
|
||
InitialDirectory = _recentPaths.GetInitialDirectory(RecentPathScenario.VideoInfoOpenFile),
|
||
Multiselect = true
|
||
};
|
||
|
||
if (dialog.ShowDialog() != true || dialog.FileNames.Length == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_recentPaths.RememberChosenFiles(RecentPathScenario.VideoInfoOpenFile, dialog.FileNames);
|
||
await AnalyzeSummaryFilesAsync(dialog.FileNames);
|
||
}
|
||
|
||
private async Task ApplyDroppedDetailedInternalAsync(string[] paths)
|
||
{
|
||
try
|
||
{
|
||
foreach (var raw in paths.OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
|
||
{
|
||
string full;
|
||
try
|
||
{
|
||
full = Path.GetFullPath(raw);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logging.Warning($"video-info (drop): путь «{raw}»: {ex.Message}", "video-info");
|
||
continue;
|
||
}
|
||
|
||
if (!File.Exists(full))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (!SupportedVideoFormats.IsSupportedVideoFile(full))
|
||
{
|
||
_logging.Warning($"video-info (drop): формат не поддерживается: {full}", "video-info");
|
||
continue;
|
||
}
|
||
|
||
_recentPaths.RememberChosenFiles(RecentPathScenario.VideoInfoOpenFile, [full]);
|
||
SelectedFilePath = full;
|
||
_logging.Info($"video-info (drop): запуск анализа {full}", "video-info");
|
||
await AnalyzeAsync();
|
||
return;
|
||
}
|
||
|
||
_logging.Warning("video-info (drop): ни один поддерживаемый видеофайл не найден", "video-info");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logging.Error($"video-info (drop): {ex.Message}", "video-info", ex);
|
||
}
|
||
}
|
||
|
||
private async Task AnalyzeAsync()
|
||
{
|
||
JsonNodes.Clear();
|
||
ErrorMessage = string.Empty;
|
||
_rawJson = string.Empty;
|
||
RaiseCommandStates();
|
||
|
||
if (string.IsNullOrWhiteSpace(SelectedFilePath))
|
||
{
|
||
AnalysisStateText = string.Empty;
|
||
return;
|
||
}
|
||
|
||
IsBusy = true;
|
||
AnalysisStateText = "Анализ файла...";
|
||
|
||
var result = await _ffprobeService.AnalyzeAsync(SelectedFilePath);
|
||
IsBusy = false;
|
||
|
||
if (!result.IsSuccess)
|
||
{
|
||
AnalysisStateText = "Ошибка анализа";
|
||
ErrorMessage = result.Error;
|
||
_logging.Error($"ffprobe: {result.Error}", "video-info.ffprobe", command: result.Command, stdout: result.StdOut, stderr: result.StdErr);
|
||
RaiseCommandStates();
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
_rawJson = result.Json;
|
||
BuildTree(_rawJson);
|
||
AnalysisStateText = "Готово";
|
||
RaiseCommandStates();
|
||
_logging.Info($"ffprobe завершен: {Path.GetFileName(SelectedFilePath)}", "video-info.ffprobe", command: result.Command, stdout: result.StdOut, stderr: result.StdErr);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
AnalysisStateText = "Ошибка анализа";
|
||
ErrorMessage = $"Не удалось разобрать JSON ffprobe: {ex.Message}";
|
||
_logging.Error($"parse json: {ex.Message}", "video-info.json", ex);
|
||
RaiseCommandStates();
|
||
}
|
||
}
|
||
|
||
private async Task ApplyDroppedSummaryInternalAsync(string[] paths)
|
||
{
|
||
try
|
||
{
|
||
await AnalyzeSummaryFilesAsync(paths);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logging.Error($"video-info summary (drop): {ex.Message}", "video-info", ex);
|
||
}
|
||
}
|
||
|
||
private async Task AnalyzeSummaryFilesAsync(IEnumerable<string> rawPaths)
|
||
{
|
||
SummaryText = string.Empty;
|
||
ErrorMessage = string.Empty;
|
||
AnalysisStateText = "Анализ файла...";
|
||
|
||
var supportedFiles = rawPaths
|
||
.Select(
|
||
p =>
|
||
{
|
||
try
|
||
{
|
||
return Path.GetFullPath(p);
|
||
}
|
||
catch
|
||
{
|
||
return string.Empty;
|
||
}
|
||
})
|
||
.Where(p => !string.IsNullOrWhiteSpace(p))
|
||
.Where(File.Exists)
|
||
.Where(SupportedVideoFormats.IsSupportedVideoFile)
|
||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||
.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)
|
||
.ToList();
|
||
|
||
if (supportedFiles.Count == 0)
|
||
{
|
||
AnalysisStateText = string.Empty;
|
||
return;
|
||
}
|
||
|
||
IsBusy = true;
|
||
var all = new List<string>(supportedFiles.Count * 8);
|
||
foreach (var file in supportedFiles)
|
||
{
|
||
var probe = await _ffprobeService.AnalyzeAsync(file);
|
||
if (!probe.IsSuccess)
|
||
{
|
||
all.Add($"{file}{Environment.NewLine}Ошибка анализа: {probe.Error}");
|
||
all.Add(string.Empty);
|
||
continue;
|
||
}
|
||
|
||
var media = MediaAnalysisParser.TryParse(probe.Json);
|
||
if (media is null)
|
||
{
|
||
all.Add($"{file}{Environment.NewLine}Ошибка разбора ffprobe JSON");
|
||
all.Add(string.Empty);
|
||
continue;
|
||
}
|
||
|
||
var sidecarResult = await _sidecarDiscoveryService.DiscoverAsync(file, _ffprobeService).ConfigureAwait(true);
|
||
var summarySidecars = new SidecarAnalysisResult(file, sidecarResult.Sidecars, sidecarResult.ExternalAudioFiles);
|
||
all.Add(file);
|
||
all.Add(_summaryService.BuildSummary(media, summarySidecars));
|
||
all.Add(string.Empty);
|
||
}
|
||
|
||
IsBusy = false;
|
||
SummaryText = string.Join(Environment.NewLine, all).Trim();
|
||
AnalysisStateText = "Готово";
|
||
RaiseCommandStates();
|
||
}
|
||
|
||
private void BuildTree(string json)
|
||
{
|
||
using var doc = JsonDocument.Parse(json);
|
||
JsonNodes.Clear();
|
||
|
||
if (doc.RootElement.ValueKind == JsonValueKind.Object)
|
||
{
|
||
foreach (var property in doc.RootElement.EnumerateObject())
|
||
{
|
||
JsonNodes.Add(CreateNode(property.Name, property.Value, null));
|
||
}
|
||
}
|
||
else
|
||
{
|
||
JsonNodes.Add(CreateNode("root", doc.RootElement, null));
|
||
}
|
||
}
|
||
|
||
private static JsonTreeNodeViewModel CreateNode(string name, JsonElement element, JsonTreeNodeViewModel? parent)
|
||
{
|
||
var node = new JsonTreeNodeViewModel(name, GetPreviewValue(element), element.GetRawText(), parent);
|
||
|
||
switch (element.ValueKind)
|
||
{
|
||
case JsonValueKind.Object:
|
||
foreach (var prop in element.EnumerateObject())
|
||
{
|
||
node.Children.Add(CreateNode(prop.Name, prop.Value, node));
|
||
}
|
||
break;
|
||
case JsonValueKind.Array:
|
||
var index = 0;
|
||
foreach (var item in element.EnumerateArray())
|
||
{
|
||
node.Children.Add(CreateNode($"[{index}]", item, node));
|
||
index++;
|
||
}
|
||
break;
|
||
}
|
||
|
||
return node;
|
||
}
|
||
|
||
private static string GetPreviewValue(JsonElement element)
|
||
{
|
||
return element.ValueKind switch
|
||
{
|
||
JsonValueKind.Object => "{...}",
|
||
JsonValueKind.Array => "[...]",
|
||
JsonValueKind.String => element.GetString() ?? string.Empty,
|
||
JsonValueKind.Number => element.GetRawText(),
|
||
JsonValueKind.True => "true",
|
||
JsonValueKind.False => "false",
|
||
JsonValueKind.Null => "null",
|
||
_ => element.GetRawText()
|
||
};
|
||
}
|
||
|
||
private void ExecuteExpandAll()
|
||
{
|
||
SetExpandedState(JsonNodes, true);
|
||
}
|
||
|
||
private void ExecuteCollapseAll()
|
||
{
|
||
SetExpandedState(JsonNodes, false);
|
||
}
|
||
|
||
private static void SetExpandedState(IEnumerable<JsonTreeNodeViewModel> nodes, bool isExpanded)
|
||
{
|
||
foreach (var node in nodes)
|
||
{
|
||
node.IsExpanded = isExpanded;
|
||
SetExpandedState(node.Children, isExpanded);
|
||
}
|
||
}
|
||
|
||
private void ExecuteCopyJson()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(_rawJson))
|
||
{
|
||
return;
|
||
}
|
||
|
||
Clipboard.SetText(_rawJson);
|
||
_logging.Info("JSON скопирован в буфер обмена", "video-info.copy");
|
||
}
|
||
|
||
private void ExecuteSaveJson()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(_rawJson))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var defaultName = string.IsNullOrWhiteSpace(SelectedFilePath)
|
||
? "ffprobe.json"
|
||
: $"{Path.GetFileNameWithoutExtension(SelectedFilePath)}.ffprobe.json";
|
||
|
||
var dialog = new SaveFileDialog
|
||
{
|
||
Title = "Сохранить JSON ffprobe",
|
||
Filter = "JSON (*.json)|*.json|Все файлы|*.*",
|
||
FileName = defaultName,
|
||
InitialDirectory = _recentPaths.GetInitialDirectory(RecentPathScenario.SettingsOutputFolder),
|
||
};
|
||
|
||
if (dialog.ShowDialog() != true)
|
||
{
|
||
return;
|
||
}
|
||
|
||
File.WriteAllText(dialog.FileName, _rawJson);
|
||
_recentPaths.RememberChosenFolder(
|
||
RecentPathScenario.SettingsOutputFolder,
|
||
Path.GetDirectoryName(dialog.FileName) ?? dialog.FileName);
|
||
_logging.Info($"JSON сохранен: {dialog.FileName}", "video-info.save");
|
||
}
|
||
|
||
private void ExecuteCopySummary()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(SummaryText) || SummaryText == "Файл не проанализирован")
|
||
{
|
||
return;
|
||
}
|
||
|
||
Clipboard.SetText(SummaryText);
|
||
_logging.Info("summary скопирован в буфер обмена", "video-info.copy");
|
||
}
|
||
|
||
private void ExecuteSaveSummary()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(SummaryText) || SummaryText == "Файл не проанализирован")
|
||
{
|
||
return;
|
||
}
|
||
|
||
var defaultName = string.IsNullOrWhiteSpace(SelectedFilePath)
|
||
? "video-summary.txt"
|
||
: $"{Path.GetFileNameWithoutExtension(SelectedFilePath)}.summary.txt";
|
||
|
||
var initialDir = string.IsNullOrWhiteSpace(SelectedFilePath)
|
||
? _recentPaths.GetInitialDirectory(RecentPathScenario.SettingsOutputFolder)
|
||
: Path.GetDirectoryName(SelectedFilePath) ?? _recentPaths.GetInitialDirectory(RecentPathScenario.SettingsOutputFolder);
|
||
|
||
var dialog = new SaveFileDialog
|
||
{
|
||
Title = "Сохранить summary",
|
||
Filter = "Text (*.txt)|*.txt|Все файлы|*.*",
|
||
FileName = defaultName,
|
||
InitialDirectory = initialDir
|
||
};
|
||
|
||
if (dialog.ShowDialog() != true)
|
||
{
|
||
return;
|
||
}
|
||
|
||
File.WriteAllText(dialog.FileName, SummaryText);
|
||
_recentPaths.RememberChosenFolder(
|
||
RecentPathScenario.SettingsOutputFolder,
|
||
Path.GetDirectoryName(dialog.FileName) ?? dialog.FileName);
|
||
_logging.Info($"summary сохранен: {dialog.FileName}", "video-info.save");
|
||
}
|
||
|
||
private void RaiseCommandStates()
|
||
{
|
||
(CopyJsonCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||
(SaveJsonCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||
(CopySummaryCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||
(SaveSummaryCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||
(CopyNodeValueCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||
(CopyNodeLineCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||
(CopyNodeWithChildrenCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||
(CopyNodePathCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||
}
|
||
|
||
private static JsonTreeNodeViewModel? AsNode(object? parameter)
|
||
{
|
||
return parameter as JsonTreeNodeViewModel;
|
||
}
|
||
|
||
private bool CanCopyNodeValue(object? parameter)
|
||
{
|
||
var node = AsNode(parameter);
|
||
return node is not null && node.Children.Count == 0;
|
||
}
|
||
|
||
private bool CanCopyNodeLine(object? parameter)
|
||
{
|
||
return AsNode(parameter) is not null;
|
||
}
|
||
|
||
private bool CanCopyNodeWithChildren(object? parameter)
|
||
{
|
||
return AsNode(parameter) is not null;
|
||
}
|
||
|
||
private bool CanCopyNodePath(object? parameter)
|
||
{
|
||
return AsNode(parameter) is not null;
|
||
}
|
||
|
||
private void ExecuteCopyNodeValue(object? parameter)
|
||
{
|
||
var node = AsNode(parameter);
|
||
if (node is null || node.Children.Count > 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Clipboard.SetText(node.Value);
|
||
_logging.Info("узел JSON скопирован", "video-info.copy");
|
||
}
|
||
|
||
private void ExecuteCopyNodeLine(object? parameter)
|
||
{
|
||
var node = AsNode(parameter);
|
||
if (node is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Clipboard.SetText($"{node.Name}: {node.Value}");
|
||
_logging.Info("узел JSON скопирован", "video-info.copy");
|
||
}
|
||
|
||
private void ExecuteCopyNodeWithChildren(object? parameter)
|
||
{
|
||
var node = AsNode(parameter);
|
||
if (node is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var formatted = FormatJsonOrFallback(node.SubtreeJson);
|
||
Clipboard.SetText(formatted);
|
||
_logging.Info("узел JSON скопирован", "video-info.copy");
|
||
}
|
||
|
||
private void ExecuteCopyNodePath(object? parameter)
|
||
{
|
||
var node = AsNode(parameter);
|
||
if (node is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Clipboard.SetText(BuildNodePath(node));
|
||
_logging.Info("узел JSON скопирован", "video-info.copy");
|
||
}
|
||
|
||
private static string BuildNodePath(JsonTreeNodeViewModel node)
|
||
{
|
||
if (node.Parent is null)
|
||
{
|
||
return node.Name;
|
||
}
|
||
|
||
var parentPath = BuildNodePath(node.Parent);
|
||
if (node.Name.StartsWith("[", StringComparison.Ordinal))
|
||
{
|
||
return $"{parentPath}{node.Name}";
|
||
}
|
||
|
||
return string.IsNullOrWhiteSpace(parentPath) ? node.Name : $"{parentPath}.{node.Name}";
|
||
}
|
||
|
||
private static string FormatJsonOrFallback(string json)
|
||
{
|
||
try
|
||
{
|
||
using var doc = JsonDocument.Parse(json);
|
||
return JsonSerializer.Serialize(doc.RootElement, new JsonSerializerOptions { WriteIndented = true });
|
||
}
|
||
catch
|
||
{
|
||
return json;
|
||
}
|
||
}
|
||
}
|
||
|