Update merge workflow behavior

This commit is contained in:
Emby Toolbox 2026-05-16 11:45:33 +05:00
parent 726408a44f
commit 787716e221
8 changed files with 118 additions and 24 deletions

View File

@ -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)

View File

@ -21,6 +21,7 @@ public sealed class MediaAnalysisResult
/// <summary>Основной видеопоток: самый крупный по площади кадра (не первый подряд в JSON — иначе cover/mjpeg может оказаться «основным»).</summary>
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();

View File

@ -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; }

View File

@ -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
};

View File

@ -56,6 +56,7 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
private string? _currentRunId;
private HashSet<ConversionQueueItem> _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();
}
}
/// <summary>Отображаемый общий прогресс (Floor от средних DisplayProgressPercent; без 100%, пока есть активные задачи).</summary>
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)

View File

@ -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<MergeFileItem>();
var deleteErrors = new List<string>();
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<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));

View File

@ -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">
<DataGrid.InputBindings>
<KeyBinding Key="Delete"

View File

@ -23,25 +23,16 @@
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0"
behaviors:MergeDropTargetBehavior.IsEnabled="True">
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="Background" Value="Transparent" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsMergeDropHighlight}" Value="True">
<Setter Property="Background" Value="{DynamicResource Ui.Brush.MergeDropOverlay}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<DataGrid x:Name="FilesGrid"
Grid.Column="0"
<Grid Grid.Column="0"
Background="Transparent"
behaviors:MergeDropTargetBehavior.IsEnabled="True">
<DataGrid x:Name="FilesGrid"
ItemsSource="{Binding Files}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
SelectionMode="Extended"
@ -76,7 +67,22 @@
<DataGridTextColumn Header="Размер, МБ" Binding="{Binding SizeMb, Mode=OneWay}" IsReadOnly="True" Width="90" />
<DataGridTextColumn Header="Имя части" Binding="{Binding PartName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="2*" MinWidth="200" />
</DataGrid.Columns>
</DataGrid>
</DataGrid>
<Border IsHitTestVisible="False">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsMergeDropHighlight}" Value="True">
<Setter Property="Background" Value="{DynamicResource Ui.Brush.MergeDropOverlay}" />
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
</Grid>
<StackPanel Grid.Column="1" Margin="8,0,0,0">
<Button MinWidth="150"