diff --git a/EmbyToolbox/Behaviors/DataGridAutoScrollSelectionBehavior.cs b/EmbyToolbox/Behaviors/DataGridAutoScrollSelectionBehavior.cs index e3c212b..b2a33ce 100644 --- a/EmbyToolbox/Behaviors/DataGridAutoScrollSelectionBehavior.cs +++ b/EmbyToolbox/Behaviors/DataGridAutoScrollSelectionBehavior.cs @@ -7,6 +7,12 @@ namespace EmbyToolbox.Behaviors; public static class DataGridAutoScrollSelectionBehavior { + public static readonly DependencyProperty ScrollIntoViewItemProperty = DependencyProperty.RegisterAttached( + "ScrollIntoViewItem", + typeof(object), + typeof(DataGridAutoScrollSelectionBehavior), + new PropertyMetadata(null, OnScrollIntoViewItemChanged)); + private static readonly DependencyProperty SuppressAutoScrollUntilProperty = DependencyProperty.RegisterAttached( "SuppressAutoScrollUntil", typeof(DateTime), @@ -22,6 +28,9 @@ public static class DataGridAutoScrollSelectionBehavior public static void SetIsEnabled(DependencyObject d, bool value) => d.SetValue(IsEnabledProperty, value); public static bool GetIsEnabled(DependencyObject d) => (bool)d.GetValue(IsEnabledProperty); + public static void SetScrollIntoViewItem(DependencyObject d, object? value) => d.SetValue(ScrollIntoViewItemProperty, value); + public static object? GetScrollIntoViewItem(DependencyObject d) => d.GetValue(ScrollIntoViewItemProperty); + private static void OnChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not DataGrid dg) @@ -46,6 +55,25 @@ public static class DataGridAutoScrollSelectionBehavior } } + private static void OnScrollIntoViewItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not DataGrid dg || e.NewValue is null) + { + return; + } + + dg.Dispatcher.BeginInvoke( + DispatcherPriority.Background, + () => + { + var item = GetScrollIntoViewItem(dg); + if (item is not null) + { + dg.ScrollIntoView(item); + } + }); + } + private static void OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (sender is not DataGrid dg) diff --git a/EmbyToolbox/Models/MediaAnalysisResult.cs b/EmbyToolbox/Models/MediaAnalysisResult.cs index dbe4377..e2178d7 100644 --- a/EmbyToolbox/Models/MediaAnalysisResult.cs +++ b/EmbyToolbox/Models/MediaAnalysisResult.cs @@ -21,6 +21,7 @@ public sealed class MediaAnalysisResult /// Основной видеопоток: самый крупный по площади кадра (не первый подряд в JSON — иначе cover/mjpeg может оказаться «основным»). public MediaStreamInfo? PrimaryVideo => VideoStreams + .Where(static v => !v.IsAttachedPicture) .OrderByDescending(static v => ((long)(v.Width ?? 0)) * (v.Height ?? 0)) .ThenByDescending(static v => v.IsDefault ? 1 : 0) .FirstOrDefault(); diff --git a/EmbyToolbox/Models/MediaStreamInfo.cs b/EmbyToolbox/Models/MediaStreamInfo.cs index c478301..49f6ddf 100644 --- a/EmbyToolbox/Models/MediaStreamInfo.cs +++ b/EmbyToolbox/Models/MediaStreamInfo.cs @@ -23,6 +23,7 @@ public sealed class MediaStreamInfo public string? ColorSpace { get; init; } public string? ColorPrimaries { get; init; } public string? ColorTransfer { get; init; } + public bool IsAttachedPicture { get; init; } public string? SubtitleFormat { get; init; } public string? FileNameTag { get; init; } public bool IsForcedByDisposition { get; init; } diff --git a/EmbyToolbox/Services/MediaAnalysisParser.cs b/EmbyToolbox/Services/MediaAnalysisParser.cs index 160b3fa..780463f 100644 --- a/EmbyToolbox/Services/MediaAnalysisParser.cs +++ b/EmbyToolbox/Services/MediaAnalysisParser.cs @@ -68,6 +68,7 @@ public static class MediaAnalysisParser var primaryVideoBitrate = streams .Where(x => x.Kind == MediaStreamKind.Video) + .Where(x => !x.IsAttachedPicture) .OrderByDescending(static v => ((long)(v.Width ?? 0)) * (v.Height ?? 0)) .ThenByDescending(static v => v.IsDefault ? 1 : 0) .Select(static v => v.BitRateBps) @@ -117,6 +118,7 @@ public static class MediaAnalysisParser ColorSpace = GetStr(s, "color_space", null), ColorPrimaries = GetStr(s, "color_primaries", null), ColorTransfer = GetStr(s, "color_transfer", null), + IsAttachedPicture = GetDisposition(s, "attached_pic", false), BitRateBps = ParseLong(s, "bit_rate") ?? ParseLong(s, "max_bit_rate"), DurationSeconds = streamDuration }; diff --git a/EmbyToolbox/ViewModels/ConversionViewModel.cs b/EmbyToolbox/ViewModels/ConversionViewModel.cs index 72f3f09..f62f72e 100644 --- a/EmbyToolbox/ViewModels/ConversionViewModel.cs +++ b/EmbyToolbox/ViewModels/ConversionViewModel.cs @@ -56,6 +56,7 @@ public sealed class ConversionViewModel : INotifyPropertyChanged private string? _currentRunId; private HashSet _currentRunItems = new(); private string _executionPhaseCaption = string.Empty; + private ConversionQueueItem? _queueItemToReveal; private bool _copyQueueItemErrorMenuVisible; private string _toastMessage = string.Empty; private bool _isToastVisible; @@ -232,6 +233,21 @@ public sealed class ConversionViewModel : INotifyPropertyChanged } } + public ConversionQueueItem? QueueItemToReveal + { + get => _queueItemToReveal; + private set + { + if (ReferenceEquals(_queueItemToReveal, value)) + { + return; + } + + _queueItemToReveal = value; + OnPropertyChanged(); + } + } + /// Отображаемый общий прогресс (Floor от средних DisplayProgressPercent; без 100%, пока есть активные задачи). public int OverallProgressPercent { @@ -549,6 +565,16 @@ public sealed class ConversionViewModel : INotifyPropertyChanged OpenBulkFileConversionSettingsCommand.RaiseCanExecuteChanged(); RecalculateOverallProgress(); } + + if (s is ConversionQueueItem changedItem + && e.PropertyName is nameof(ConversionQueueItem.Status) or nameof(ConversionQueueItem.ProcessedInCurrentRun) + && IsExecutionRunning + && string.Equals(changedItem.Status, ConversionQueueStatus.Done, StringComparison.Ordinal) + && changedItem.ProcessedInCurrentRun) + { + QueueItemToReveal = null; + QueueItemToReveal = changedItem; + } } private void OnTaskProfileChanged(ConversionQueueItem item) diff --git a/EmbyToolbox/ViewModels/MergeViewModel.cs b/EmbyToolbox/ViewModels/MergeViewModel.cs index df78be0..0277d8f 100644 --- a/EmbyToolbox/ViewModels/MergeViewModel.cs +++ b/EmbyToolbox/ViewModels/MergeViewModel.cs @@ -206,7 +206,7 @@ public sealed class MergeViewModel : INotifyPropertyChanged private void ExecuteSelectOutputFile() { - var suggestFile = "movies_merged.mkv"; + var suggestFile = "movies.mkv"; string? suggestDir = null; try { @@ -634,7 +634,7 @@ public sealed class MergeViewModel : INotifyPropertyChanged Number = i + 1, Status = "Готов", }; - item.SyncAutoPartName($"Часть {i + 1} - {Path.GetFileNameWithoutExtension(fileName)}"); + item.SyncAutoPartName(BuildDefaultPartName(i)); Files.Add(item); } @@ -664,12 +664,17 @@ public sealed class MergeViewModel : INotifyPropertyChanged : _recentPaths.GetInitialDirectory(RecentPathScenario.Merge); var fileName = parents.Count == 1 - ? $"{new DirectoryInfo(parents[0]).Name}_merged.mkv" - : "movies_merged.mkv"; + ? $"{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; @@ -686,8 +691,7 @@ public sealed class MergeViewModel : INotifyPropertyChanged for (var i = 0; i < Files.Count; i++) { Files[i].Number = i + 1; - var fn = Files[i].FileName; - Files[i].SyncAutoPartName($"Часть {i + 1} - {Path.GetFileNameWithoutExtension(fn)}"); + Files[i].SyncAutoPartName(BuildDefaultPartName(i)); } UpdateValidationState(); @@ -735,7 +739,7 @@ public sealed class MergeViewModel : INotifyPropertyChanged var name = Path.GetFileName(fullOutput); if (string.IsNullOrWhiteSpace(name)) { - error = "Укажите имя итогового файла (например, …\\movies_merged.mkv)."; + error = "Укажите имя итогового файла (например, …\\movies.mkv)."; } else if (name.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) { @@ -795,12 +799,15 @@ public sealed class MergeViewModel : INotifyPropertyChanged return; } + var deletedItems = new List(); var deleteErrors = new List(); - foreach (var source in ordered.Select(f => f.FullPath)) + foreach (var item in ordered) { + var source = item.FullPath; try { File.Delete(source); + deletedItems.Add(item); _logging.Info($"удален исходник после объединения: {source}", "merge"); } catch (Exception ex) @@ -810,6 +817,8 @@ public sealed class MergeViewModel : INotifyPropertyChanged } } + RemoveDeletedSourcesFromTable(deletedItems); + if (deleteErrors.Count == 0) { return; @@ -822,6 +831,27 @@ public sealed class MergeViewModel : INotifyPropertyChanged MessageBoxImage.Warning); } + private void RemoveDeletedSourcesFromTable(IReadOnlyList 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)); diff --git a/EmbyToolbox/Views/ConversionView.xaml b/EmbyToolbox/Views/ConversionView.xaml index aed868b..7ca1618 100644 --- a/EmbyToolbox/Views/ConversionView.xaml +++ b/EmbyToolbox/Views/ConversionView.xaml @@ -179,7 +179,7 @@ ScrollViewer.HorizontalScrollBarVisibility="Auto" behaviors:ConversionQueueDropTargetBehavior.IsDropTargetEnabled="True" behaviors:DataGridRowDoubleClickCommandBehavior.Command="{Binding OpenFileConversionSettingsCommand}" - behaviors:DataGridAutoScrollSelectionBehavior.IsEnabled="True" + behaviors:DataGridAutoScrollSelectionBehavior.ScrollIntoViewItem="{Binding QueueItemToReveal}" SelectionChanged="QueueDataGrid_SelectionChanged"> - - - - + - + - + + + + + + +