Compare commits

..

No commits in common. "main" and "codex-compact-settings-layout" have entirely different histories.

6 changed files with 80 additions and 117 deletions

View File

@ -1,40 +0,0 @@
using System.Windows;
using ICSharpCode.AvalonEdit;
namespace EmbyToolbox.Behaviors;
public static class AvalonEditTextBindingBehavior
{
public static readonly DependencyProperty BoundTextProperty =
DependencyProperty.RegisterAttached(
"BoundText",
typeof(string),
typeof(AvalonEditTextBindingBehavior),
new PropertyMetadata(string.Empty, OnBoundTextChanged));
public static string GetBoundText(DependencyObject obj)
{
return (string)obj.GetValue(BoundTextProperty);
}
public static void SetBoundText(DependencyObject obj, string value)
{
obj.SetValue(BoundTextProperty, value);
}
private static void OnBoundTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not TextEditor editor)
{
return;
}
var nextText = e.NewValue as string ?? string.Empty;
if (editor.Text == nextText)
{
return;
}
editor.Text = nextText;
}
}

View File

@ -29,8 +29,4 @@
<PackagePath>\</PackagePath> <PackagePath>\</PackagePath>
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="AvalonEdit" Version="6.3.1.120" />
</ItemGroup>
</Project> </Project>

View File

@ -7,7 +7,6 @@
xmlns:views="clr-namespace:EmbyToolbox.Views" xmlns:views="clr-namespace:EmbyToolbox.Views"
xmlns:viewModels="clr-namespace:EmbyToolbox.ViewModels" xmlns:viewModels="clr-namespace:EmbyToolbox.ViewModels"
xmlns:converters="clr-namespace:EmbyToolbox.Converters" xmlns:converters="clr-namespace:EmbyToolbox.Converters"
xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" mc:Ignorable="d"
@ -473,19 +472,21 @@
</TextBlock.Style> </TextBlock.Style>
</TextBlock> </TextBlock>
<avalonEdit:TextEditor Grid.Row="1" <ListBox Grid.Row="1"
behaviors:AvalonEditTextBindingBehavior.BoundText="{Binding FormattedJson, Mode=OneWay}" ItemsSource="{Binding FormattedJsonLines}"
Background="Transparent" Margin="0"
Foreground="{DynamicResource Ui.Brush.Text}" BorderThickness="0"
FontFamily="Consolas" Background="Transparent"
FontSize="13" Foreground="{DynamicResource Ui.Brush.Text}"
IsReadOnly="True" ScrollViewer.CanContentScroll="True"
ShowLineNumbers="False" ScrollViewer.HorizontalScrollBarVisibility="Auto"
WordWrap="False" ScrollViewer.VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto" VirtualizingPanel.IsVirtualizing="True"
VerticalScrollBarVisibility="Auto"> VirtualizingPanel.VirtualizationMode="Recycling"
<avalonEdit:TextEditor.Style> VirtualizingPanel.ScrollUnit="Pixel"
<Style TargetType="avalonEdit:TextEditor"> SelectionMode="Extended">
<ListBox.Style>
<Style TargetType="ListBox">
<Setter Property="Visibility" Value="Visible" /> <Setter Property="Visibility" Value="Visible" />
<Style.Triggers> <Style.Triggers>
<DataTrigger Binding="{Binding SelectedFilePath}" Value=""> <DataTrigger Binding="{Binding SelectedFilePath}" Value="">
@ -493,8 +494,36 @@
</DataTrigger> </DataTrigger>
</Style.Triggers> </Style.Triggers>
</Style> </Style>
</avalonEdit:TextEditor.Style> </ListBox.Style>
</avalonEdit:TextEditor> <ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="Focusable" Value="False" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Mode=OneWay}"
BorderThickness="0"
Background="Transparent"
Foreground="{DynamicResource Ui.Brush.Text}"
FontFamily="Consolas"
FontSize="13"
IsReadOnly="True"
IsUndoEnabled="False"
IsInactiveSelectionHighlightEnabled="True"
AcceptsReturn="False"
AcceptsTab="False"
TextWrapping="NoWrap"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Disabled" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid> </Grid>
</Border> </Border>
</Grid> </Grid>

View File

@ -18,12 +18,12 @@ public sealed class MediaAnalysisResult
public IReadOnlyList<MediaStreamInfo> AllStreams { get; init; } = Array.Empty<MediaStreamInfo>(); public IReadOnlyList<MediaStreamInfo> AllStreams { get; init; } = Array.Empty<MediaStreamInfo>();
public long? SourceVideoBitrateBps { get; init; } public long? SourceVideoBitrateBps { get; init; }
/// <summary>Основной видеопоток: default среди обычных video, затем самый крупный по площади кадра.</summary> /// <summary>Основной видеопоток: самый крупный по площади кадра (не первый подряд в JSON — иначе cover/mjpeg может оказаться «основным»).</summary>
public MediaStreamInfo? PrimaryVideo => public MediaStreamInfo? PrimaryVideo =>
VideoStreams VideoStreams
.Where(static v => !v.IsAttachedPicture) .Where(static v => !v.IsAttachedPicture)
.OrderByDescending(static v => v.IsDefault ? 1 : 0) .OrderByDescending(static v => ((long)(v.Width ?? 0)) * (v.Height ?? 0))
.ThenByDescending(static v => ((long)(v.Width ?? 0)) * (v.Height ?? 0)) .ThenByDescending(static v => v.IsDefault ? 1 : 0)
.FirstOrDefault(); .FirstOrDefault();
/// <summary>format.duration, иначе максимум duration среди streams (ffprobe).</summary> /// <summary>format.duration, иначе максимум duration среди streams (ffprobe).</summary>

View File

@ -1,5 +1,4 @@
using System.Globalization; using System.Globalization;
using System.IO;
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
using EmbyToolbox.Models; using EmbyToolbox.Models;
@ -70,8 +69,8 @@ public static class MediaAnalysisParser
var primaryVideoBitrate = streams var primaryVideoBitrate = streams
.Where(x => x.Kind == MediaStreamKind.Video) .Where(x => x.Kind == MediaStreamKind.Video)
.Where(x => !x.IsAttachedPicture) .Where(x => !x.IsAttachedPicture)
.OrderByDescending(static v => v.IsDefault ? 1 : 0) .OrderByDescending(static v => ((long)(v.Width ?? 0)) * (v.Height ?? 0))
.ThenByDescending(static v => ((long)(v.Width ?? 0)) * (v.Height ?? 0)) .ThenByDescending(static v => v.IsDefault ? 1 : 0)
.Select(static v => v.BitRateBps) .Select(static v => v.BitRateBps)
.FirstOrDefault(); .FirstOrDefault();
@ -101,7 +100,6 @@ public static class MediaAnalysisParser
var t = (ct.GetString() ?? string.Empty).ToLowerInvariant(); var t = (ct.GetString() ?? string.Empty).ToLowerInvariant();
if (t == "video") if (t == "video")
{ {
var isAttachedPicture = GetDisposition(s, "attached_pic", false) || IsCoverArtVideoStream(s);
return new MediaStreamInfo return new MediaStreamInfo
{ {
Index = GetInt(s, "index", -1), Index = GetInt(s, "index", -1),
@ -120,7 +118,7 @@ public static class MediaAnalysisParser
ColorSpace = GetStr(s, "color_space", null), ColorSpace = GetStr(s, "color_space", null),
ColorPrimaries = GetStr(s, "color_primaries", null), ColorPrimaries = GetStr(s, "color_primaries", null),
ColorTransfer = GetStr(s, "color_transfer", null), ColorTransfer = GetStr(s, "color_transfer", null),
IsAttachedPicture = isAttachedPicture, IsAttachedPicture = GetDisposition(s, "attached_pic", false),
BitRateBps = ParseLong(s, "bit_rate") ?? ParseLong(s, "max_bit_rate"), BitRateBps = ParseLong(s, "bit_rate") ?? ParseLong(s, "max_bit_rate"),
DurationSeconds = streamDuration DurationSeconds = streamDuration
}; };
@ -303,58 +301,12 @@ public static class MediaAnalysisParser
return null; return null;
} }
foreach (var tag in tags.EnumerateObject()) if (!tags.TryGetProperty(tagKey, out var val) || val.ValueKind != JsonValueKind.String)
{ {
if (string.Equals(tag.Name, tagKey, StringComparison.OrdinalIgnoreCase) && return null;
tag.Value.ValueKind == JsonValueKind.String)
{
return tag.Value.GetString();
}
} }
return null; return val.GetString();
}
private static bool IsCoverArtVideoStream(JsonElement stream)
{
var mimeType = GetTagExact(stream, "mimetype");
if (!string.IsNullOrWhiteSpace(mimeType) &&
mimeType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
{
return true;
}
var fileName = GetTagExact(stream, "filename");
if (string.IsNullOrWhiteSpace(fileName))
{
return false;
}
var codec = GetStr(stream, "codec_name", string.Empty) ?? string.Empty;
if (!IsStillImageCodec(codec))
{
return false;
}
var extension = Path.GetExtension(fileName);
return extension.Equals(".jpg", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".jpeg", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".png", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".webp", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".bmp", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".gif", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".tif", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".tiff", StringComparison.OrdinalIgnoreCase);
}
private static bool IsStillImageCodec(string codec)
{
return codec.Equals("mjpeg", StringComparison.OrdinalIgnoreCase)
|| codec.Equals("png", StringComparison.OrdinalIgnoreCase)
|| codec.Equals("webp", StringComparison.OrdinalIgnoreCase)
|| codec.Equals("bmp", StringComparison.OrdinalIgnoreCase)
|| codec.Equals("gif", StringComparison.OrdinalIgnoreCase)
|| codec.Equals("tiff", StringComparison.OrdinalIgnoreCase);
} }
private static bool GetDisposition(JsonElement s, string d, bool def) private static bool GetDisposition(JsonElement s, string d, bool def)

View File

@ -23,6 +23,7 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
private string _analysisStateText = string.Empty; private string _analysisStateText = string.Empty;
private string _errorMessage = string.Empty; private string _errorMessage = string.Empty;
private string _formattedJson = string.Empty; private string _formattedJson = string.Empty;
private IReadOnlyList<string> _formattedJsonLines = Array.Empty<string>();
private string _summaryText = string.Empty; private string _summaryText = string.Empty;
private bool _isBusy; private bool _isBusy;
private bool _isVideoInfoDropHighlight; private bool _isVideoInfoDropHighlight;
@ -109,6 +110,22 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
} }
_formattedJson = value; _formattedJson = value;
FormattedJsonLines = SplitTextLines(value);
OnPropertyChanged();
}
}
public IReadOnlyList<string> FormattedJsonLines
{
get => _formattedJsonLines;
private set
{
if (ReferenceEquals(_formattedJsonLines, value))
{
return;
}
_formattedJsonLines = value;
OnPropertyChanged(); OnPropertyChanged();
} }
} }
@ -479,5 +496,14 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
} }
} }
private static IReadOnlyList<string> SplitTextLines(string text)
{
if (string.IsNullOrEmpty(text))
{
return Array.Empty<string>();
}
return text.ReplaceLineEndings("\n").Split('\n');
}
} }