mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-22 00:22:01 +00:00
Detection preview UI.
This commit is contained in:
parent
e18d043b78
commit
e566bb6137
@ -2,10 +2,17 @@
|
|||||||
|
|
||||||
namespace Splitter_UI.Models;
|
namespace Splitter_UI.Models;
|
||||||
|
|
||||||
public sealed class PreviewData
|
public class PreviewData
|
||||||
{
|
{
|
||||||
public Avalonia.Media.Imaging.Bitmap? Frame { get; init; }
|
public Avalonia.Media.Imaging.Bitmap? Frame { get; }
|
||||||
public IReadOnlyList<Rect> FaceBoxes { get; init; } = [];
|
public IReadOnlyList<Rect> DetectedBoxes { get; }
|
||||||
public IReadOnlyList<Rect> BodyBoxes { get; init; } = [];
|
public Rect? CropRect { get; }
|
||||||
public Rect? CropRect { get; init; }
|
|
||||||
|
public PreviewData(Avalonia.Media.Imaging.Bitmap? frame, IReadOnlyList<Rect> boxes, Rect? crop)
|
||||||
|
{
|
||||||
|
Frame = frame;
|
||||||
|
DetectedBoxes = boxes;
|
||||||
|
CropRect = crop;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -5,5 +5,5 @@ namespace Splitter_UI.Services;
|
|||||||
|
|
||||||
public interface IThumbnailService
|
public interface IThumbnailService
|
||||||
{
|
{
|
||||||
Task<Bitmap?> CreateThumbnailAsync(string file, VideoInfo probe);
|
Task<Bitmap?> CreateThumbnailAsync(string file, VideoInfo probe, TimeSpan? skip = null, int? width = null, int? height = null);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,41 +7,47 @@ namespace Splitter_UI.Services;
|
|||||||
|
|
||||||
public sealed class ThumbnailService : IThumbnailService
|
public sealed class ThumbnailService : IThumbnailService
|
||||||
{
|
{
|
||||||
private readonly int _thumbWidth = 160;
|
private const int _thumbWidth = 160;
|
||||||
private readonly int _thumbHeight = 90;
|
private const int _thumbHeight = 90;
|
||||||
|
|
||||||
// Reusable buffer for BGR24 → 3 bytes per pixel
|
private readonly byte [] _bgrBuffer = new byte[_thumbWidth * _thumbHeight * 3];
|
||||||
private readonly byte[] _bgrBuffer;
|
private readonly byte [] _bgraBuffer = new byte[_thumbWidth * _thumbHeight * 4];
|
||||||
private readonly byte[] _bgraBuffer;
|
|
||||||
|
|
||||||
public ThumbnailService()
|
public async Task<Bitmap?> CreateThumbnailAsync(string file, VideoInfo probe, TimeSpan? skip = null, int? width = null, int? height = null)
|
||||||
{
|
{
|
||||||
_bgrBuffer = new byte[_thumbWidth * _thumbHeight * 3];
|
width ??= _thumbWidth;
|
||||||
_bgraBuffer = new byte[_thumbWidth * _thumbHeight * 4];
|
height ??= _thumbHeight;
|
||||||
}
|
skip ??= TimeSpan.Zero;
|
||||||
|
|
||||||
|
// buffer for BGR24 → 3 bytes per pixel
|
||||||
|
|
||||||
|
var canUseStaticBuffers =
|
||||||
|
width.Value == _thumbWidth &&
|
||||||
|
height.Value == _thumbHeight;
|
||||||
|
|
||||||
|
var bgrBuffer = canUseStaticBuffers ? _bgrBuffer : new byte[width.Value * height.Value * 3];
|
||||||
|
var bgraBuffer = canUseStaticBuffers ? _bgraBuffer : new byte[width.Value * height.Value * 4];
|
||||||
|
|
||||||
public async Task<Bitmap?> CreateThumbnailAsync(string file, VideoInfo probe)
|
|
||||||
{
|
|
||||||
// Decode a single frame using ffmpeg → raw BGR24 into _bgrBuffer
|
// Decode a single frame using ffmpeg → raw BGR24 into _bgrBuffer
|
||||||
bool ok = await DecodeFrameAsync(file);
|
bool ok = await DecodeFrameAsync(bgrBuffer, file, skip.Value, width.Value, height.Value);
|
||||||
if (!ok)
|
if (!ok)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// Convert BGR24 → BGRA32
|
// Convert BGR24 → BGRA32
|
||||||
ConvertBgrToBgra(_bgrBuffer, _bgraBuffer, _thumbWidth, _thumbHeight);
|
ConvertBgrToBgra(bgrBuffer, bgraBuffer, width.Value, height.Value);
|
||||||
|
|
||||||
// Create Avalonia Bitmap
|
// Create Avalonia Bitmap
|
||||||
return CreateBitmap(_bgraBuffer, _thumbWidth, _thumbHeight);
|
return CreateBitmap(bgraBuffer, width.Value, height.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> DecodeFrameAsync(string file)
|
private static async Task<bool> DecodeFrameAsync(byte [] bgrBuffer, string file, TimeSpan skip, int width, int height)
|
||||||
{
|
{
|
||||||
// ffmpeg command: decode one frame, resize, output raw BGR24
|
// ffmpeg command: decode one frame, resize, output raw BGR24
|
||||||
var args =
|
var args =
|
||||||
$"-ss 0 -t 0.1 -i \"{file}\" " +
|
$"-ss {skip.TotalSeconds} -t 0.1 -i \"{file}\" " +
|
||||||
"-an -sn " +
|
"-an -sn " +
|
||||||
$"-vf \"scale={_thumbWidth}:{_thumbHeight}:force_original_aspect_ratio=decrease," +
|
$"-vf \"scale={width}:{height}:force_original_aspect_ratio=decrease," +
|
||||||
$"pad={_thumbWidth}:{_thumbHeight}:(ow-iw)/2:(oh-ih)/2,format=bgr24\" " +
|
$"pad={width}:{height}:(ow-iw)/2:(oh-ih)/2,format=bgr24\" " +
|
||||||
"-f rawvideo -";
|
"-f rawvideo -";
|
||||||
|
|
||||||
var psi = new ProcessStartInfo
|
var psi = new ProcessStartInfo
|
||||||
@ -57,14 +63,14 @@ public sealed class ThumbnailService : IThumbnailService
|
|||||||
var p = new Process { StartInfo = psi };
|
var p = new Process { StartInfo = psi };
|
||||||
p.Start();
|
p.Start();
|
||||||
|
|
||||||
int needed = _bgrBuffer.Length;
|
int needed = bgrBuffer.Length;
|
||||||
int read = 0;
|
int read = 0;
|
||||||
|
|
||||||
using var stdout = p.StandardOutput.BaseStream;
|
using var stdout = p.StandardOutput.BaseStream;
|
||||||
|
|
||||||
while (read < needed)
|
while (read < needed)
|
||||||
{
|
{
|
||||||
int r = await stdout.ReadAsync(_bgrBuffer, read, needed - read);
|
int r = await stdout.ReadAsync(bgrBuffer, read, needed - read);
|
||||||
if (r == 0)
|
if (r == 0)
|
||||||
{
|
{
|
||||||
TryKill(p);
|
TryKill(p);
|
||||||
|
|||||||
@ -2,7 +2,9 @@
|
|||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
|
using Avalonia.Threading;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
||||||
namespace Splitter_UI.ViewModels;
|
namespace Splitter_UI.ViewModels;
|
||||||
|
|
||||||
@ -10,7 +12,8 @@ public partial class JobViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
public SingleJob Job { get; }
|
public SingleJob Job { get; }
|
||||||
public VideoInfo? Probe { get; set; }
|
public VideoInfo? Probe { get; set; }
|
||||||
public PreviewData? Preview { get; set; }
|
[ObservableProperty]
|
||||||
|
private PreviewData? _preview = new(null, [], null);
|
||||||
public ProgressInfo? Progress { get; set; }
|
public ProgressInfo? Progress { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
@ -19,8 +22,22 @@ public partial class JobViewModel : ObservableObject
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _suggestedAction = "";
|
private string _suggestedAction = "";
|
||||||
|
|
||||||
|
// This updates continuously
|
||||||
|
[ObservableProperty]
|
||||||
|
private double _sliderLiveValue;
|
||||||
|
|
||||||
|
// This updates only on release
|
||||||
|
[ObservableProperty]
|
||||||
|
private double _positionSeconds;
|
||||||
|
|
||||||
|
public double DurationSeconds => Probe?.Duration ?? 0;
|
||||||
|
|
||||||
|
public IRelayCommand StepForwardCommand { get; }
|
||||||
|
public IRelayCommand StepBackwardCommand { get; }
|
||||||
|
|
||||||
private readonly IThumbnailService _thumbnails;
|
private readonly IThumbnailService _thumbnails;
|
||||||
private readonly IFileProbeService _fileProbe;
|
private readonly IFileProbeService _fileProbe;
|
||||||
|
private readonly DispatcherTimer _debounceTimer;
|
||||||
|
|
||||||
public string FileName => Path.GetFileName(Job.InputFile);
|
public string FileName => Path.GetFileName(Job.InputFile);
|
||||||
|
|
||||||
@ -113,6 +130,14 @@ public partial class JobViewModel : ObservableObject
|
|||||||
|
|
||||||
ParametersList.CollectionChanged += OnParametersCollectionChanged;
|
ParametersList.CollectionChanged += OnParametersCollectionChanged;
|
||||||
|
|
||||||
|
StepForwardCommand = new RelayCommand(StepForward);
|
||||||
|
StepBackwardCommand = new RelayCommand(StepBackward);
|
||||||
|
|
||||||
|
_debounceTimer = new DispatcherTimer
|
||||||
|
{
|
||||||
|
Interval = TimeSpan.FromSeconds(1)
|
||||||
|
};
|
||||||
|
_debounceTimer.Tick += DebounceTimerTick;
|
||||||
|
|
||||||
_ = Task.Run( LoadThumbnailAsync );
|
_ = Task.Run( LoadThumbnailAsync );
|
||||||
}
|
}
|
||||||
@ -122,6 +147,23 @@ public partial class JobViewModel : ObservableObject
|
|||||||
Probe = await _fileProbe.ProbeAsync(Job);
|
Probe = await _fileProbe.ProbeAsync(Job);
|
||||||
Thumbnail = await _thumbnails.CreateThumbnailAsync(Job.InputFile, Probe);
|
Thumbnail = await _thumbnails.CreateThumbnailAsync(Job.InputFile, Probe);
|
||||||
SuggestedAction = Probe.Rotation == 0 ? "crop" : "rotate";
|
SuggestedAction = Probe.Rotation == 0 ? "crop" : "rotate";
|
||||||
|
|
||||||
|
await CreatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreatePreview()
|
||||||
|
{
|
||||||
|
if ( Probe == null)
|
||||||
|
return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var frame = await _thumbnails.CreateThumbnailAsync(Job.InputFile, Probe, TimeSpan.FromSeconds(PositionSeconds), Probe.Width, Probe.Height);
|
||||||
|
Preview = new PreviewData(frame, [], null);
|
||||||
|
OnPropertyChanged(nameof(Preview));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnParameterChanged(object? sender, PropertyChangedEventArgs e)
|
private void OnParameterChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
@ -153,4 +195,43 @@ public partial class JobViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void StepForward()
|
||||||
|
{
|
||||||
|
var step = 10.0; // seconds
|
||||||
|
if (DurationSeconds <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
PositionSeconds = Math.Min(DurationSeconds, PositionSeconds + step);
|
||||||
|
// trigger seek in your playback pipeline here
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StepBackward()
|
||||||
|
{
|
||||||
|
var step = 10.0; // seconds
|
||||||
|
if (DurationSeconds <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
PositionSeconds = Math.Max(0, PositionSeconds - step);
|
||||||
|
// trigger seek in your playback pipeline here
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSliderLiveValueChanged(double value)
|
||||||
|
{
|
||||||
|
// Restart debounce timer on every slider update
|
||||||
|
_debounceTimer.Stop();
|
||||||
|
_debounceTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DebounceTimerTick(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_debounceTimer.Stop();
|
||||||
|
|
||||||
|
// Commit the final value
|
||||||
|
PositionSeconds = SliderLiveValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnPositionSecondsChanged(double value)
|
||||||
|
{
|
||||||
|
Task.Run(CreatePreview);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
namespace Splitter_UI.ViewModels;
|
namespace Splitter_UI.ViewModels;
|
||||||
|
|
||||||
@ -7,7 +8,23 @@ public partial class PreviewPaneViewModel : ObservableObject
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private JobViewModel? _selected;
|
private JobViewModel? _selected;
|
||||||
|
|
||||||
public PreviewPaneViewModel()
|
public PreviewData? Preview => Selected?.Preview;
|
||||||
|
|
||||||
|
partial void OnSelectedChanged(JobViewModel? oldValue, JobViewModel? newValue)
|
||||||
{
|
{
|
||||||
|
if (oldValue != null)
|
||||||
|
oldValue.PropertyChanged -= SelectedPropertyChanged;
|
||||||
|
|
||||||
|
if (newValue != null)
|
||||||
|
newValue.PropertyChanged += SelectedPropertyChanged;
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(Preview));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SelectedPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(JobViewModel.Preview))
|
||||||
|
OnPropertyChanged(nameof(Preview));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
100
Splitter-UI/Views/PreviewCanvas.cs
Normal file
100
Splitter-UI/Views/PreviewCanvas.cs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
|
||||||
|
namespace Splitter_UI.Views;
|
||||||
|
|
||||||
|
public sealed class PreviewCanvas : Control
|
||||||
|
{
|
||||||
|
public static readonly StyledProperty<PreviewData?> PreviewProperty =
|
||||||
|
AvaloniaProperty.Register<PreviewCanvas, PreviewData?>(nameof(Preview));
|
||||||
|
|
||||||
|
public PreviewData? Preview
|
||||||
|
{
|
||||||
|
get => GetValue(PreviewProperty);
|
||||||
|
set => SetValue(PreviewProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PreviewCanvas()
|
||||||
|
{
|
||||||
|
PreviewProperty.Changed.AddClassHandler<PreviewCanvas>(
|
||||||
|
(canvas, args) =>
|
||||||
|
canvas.OnPreviewChanged(args.OldValue as PreviewData,
|
||||||
|
args.NewValue as PreviewData));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPreviewChanged(PreviewData? oldValue, PreviewData? newValue)
|
||||||
|
{
|
||||||
|
if (oldValue is INotifyPropertyChanged oldNotify)
|
||||||
|
oldNotify.PropertyChanged -= PreviewPropertyChanged;
|
||||||
|
|
||||||
|
if (newValue is INotifyPropertyChanged newNotify)
|
||||||
|
newNotify.PropertyChanged += PreviewPropertyChanged;
|
||||||
|
|
||||||
|
// Always marshal to UI thread
|
||||||
|
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Render);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PreviewPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(PreviewData.Frame) ||
|
||||||
|
e.PropertyName == nameof(PreviewData.DetectedBoxes) ||
|
||||||
|
e.PropertyName == nameof(PreviewData.CropRect))
|
||||||
|
{
|
||||||
|
// Always marshal to UI thread
|
||||||
|
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Render);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Size MeasureOverride(Size availableSize) => availableSize;
|
||||||
|
protected override Size ArrangeOverride(Size finalSize) => finalSize;
|
||||||
|
|
||||||
|
public override void Render(DrawingContext context)
|
||||||
|
{
|
||||||
|
var preview = Preview;
|
||||||
|
if (preview?.Frame is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var frame = preview.Frame;
|
||||||
|
var rawW = frame.PixelSize.Width;
|
||||||
|
var rawH = frame.PixelSize.Height;
|
||||||
|
|
||||||
|
var dispW = Bounds.Width;
|
||||||
|
var dispH = Bounds.Height;
|
||||||
|
|
||||||
|
if (dispW <= 0 || dispH <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var scale = Math.Min(dispW / rawW, dispH / rawH);
|
||||||
|
|
||||||
|
var scaledW = rawW * scale;
|
||||||
|
var scaledH = rawH * scale;
|
||||||
|
|
||||||
|
var offsetX = (dispW - scaledW) / 2;
|
||||||
|
var offsetY = (dispH - scaledH) / 2;
|
||||||
|
|
||||||
|
// draw frame
|
||||||
|
context.DrawImage(frame,
|
||||||
|
new Rect(0, 0, rawW, rawH),
|
||||||
|
new Rect(offsetX, offsetY, scaledW, scaledH));
|
||||||
|
|
||||||
|
// draw overlays
|
||||||
|
if (preview.DetectedBoxes is { Count: > 0 })
|
||||||
|
{
|
||||||
|
var pen = new Pen(Brushes.Lime, 2);
|
||||||
|
|
||||||
|
foreach (var r in preview.DetectedBoxes )
|
||||||
|
{
|
||||||
|
var rr = new Rect(
|
||||||
|
offsetX + r.X * scale,
|
||||||
|
offsetY + r.Y * scale,
|
||||||
|
r.Width * scale,
|
||||||
|
r.Height * scale);
|
||||||
|
|
||||||
|
context.DrawRectangle(null, pen, rr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,24 +1,57 @@
|
|||||||
<UserControl
|
<UserControl
|
||||||
xmlns="https://github.com/avaloniaui"
|
xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
x:Class="Splitter_UI.Views.PreviewPane"
|
|
||||||
xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
|
xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
|
||||||
|
xmlns:local="clr-namespace:Splitter_UI.Views"
|
||||||
|
x:Class="Splitter_UI.Views.PreviewPane"
|
||||||
x:DataType="vm:PreviewPaneViewModel">
|
x:DataType="vm:PreviewPaneViewModel">
|
||||||
|
|
||||||
<Border Background="#202020" Padding="10">
|
<Border Background="#202020" Padding="10">
|
||||||
<Grid>
|
<Grid RowDefinitions="*,Auto">
|
||||||
<Image Source="{Binding Selected.Preview.Frame}" Stretch="Uniform"/>
|
|
||||||
|
<local:PreviewCanvas
|
||||||
|
Grid.Row="0"
|
||||||
|
Preview="{Binding Preview}" />
|
||||||
|
|
||||||
|
<Grid Grid.Row="1"
|
||||||
|
ColumnDefinitions="Auto,*,Auto"
|
||||||
|
Margin="0,10,0,0">
|
||||||
|
|
||||||
|
<Button Grid.Column="0"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Width="24" Height="24"
|
||||||
|
Padding="0"
|
||||||
|
Margin="0,0,5,0"
|
||||||
|
Command="{Binding Selected.StepBackwardCommand}">
|
||||||
|
<TextBlock FontFamily="{StaticResource FontAwesome}"
|
||||||
|
Text=""
|
||||||
|
FontSize="12"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Center" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Slider Grid.Column="1"
|
||||||
|
Minimum="0"
|
||||||
|
Maximum="{Binding Selected.DurationSeconds}"
|
||||||
|
Value="{Binding Selected.SliderLiveValue, Mode=TwoWay}"
|
||||||
|
Margin="5,0,5,0" />
|
||||||
|
|
||||||
|
<Button Grid.Column="2"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Width="24" Height="24"
|
||||||
|
Padding="0"
|
||||||
|
Margin="5,0,0,0"
|
||||||
|
Command="{Binding Selected.StepForwardCommand}">
|
||||||
|
<TextBlock FontFamily="{StaticResource FontAwesome}"
|
||||||
|
Text=""
|
||||||
|
FontSize="12"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Center" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<!-- Optional overlays -->
|
|
||||||
<ItemsControl ItemsSource="{Binding Selected.Preview.FaceBoxes}">
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<Border BorderBrush="Lime" BorderThickness="2"
|
|
||||||
Width="{Binding Width}" Height="{Binding Height}"
|
|
||||||
Canvas.Left="{Binding X}" Canvas.Top="{Binding Y}"/>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.VisualTree;
|
||||||
|
|
||||||
namespace Splitter_UI.Views;
|
namespace Splitter_UI.Views;
|
||||||
|
|
||||||
@ -8,4 +12,5 @@ public partial class PreviewPane : UserControl
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user