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">
-
-
-
-
+
-
+
-
+
+
+
+
+
+
+