Avalonia UI work started

This commit is contained in:
Alexander Shabarshov 2026-05-20 08:34:54 +01:00
parent 93de483bc6
commit 1f93eba839
43 changed files with 1010 additions and 0 deletions

15
Splitter-UI/App.axaml Normal file
View File

@ -0,0 +1,15 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Splitter_UI.App"
xmlns:local="using:Splitter_UI"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

37
Splitter-UI/App.axaml.cs Normal file
View File

@ -0,0 +1,37 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
using Splitter_UI.Views;
namespace Splitter_UI;
public partial class App : Application
{
private readonly ServiceProvider _provider;
public App(ServiceProvider provider)
{
_provider = provider;
}
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
var vm = _provider.GetRequiredService<MainViewModel>();
desktop.MainWindow = new MainWindow
{
DataContext = vm
};
}
base.OnFrameworkInitializationCompleted();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

View File

@ -0,0 +1,8 @@
global using System;
global using System.Collections.Generic;
global using System.Threading.Tasks;
global using splitter;
global using Splitter_UI.Models;
global using Splitter_UI.Services;
global using Splitter_UI.ViewModels;

View File

@ -0,0 +1,11 @@
using Avalonia;
namespace Splitter_UI.Models;
public sealed class PreviewData
{
public Avalonia.Media.Imaging.Bitmap? Frame { get; init; }
public IReadOnlyList<Rect> FaceBoxes { get; init; } = [];
public IReadOnlyList<Rect> BodyBoxes { get; init; } = [];
public Rect? CropRect { get; init; }
}

View File

@ -0,0 +1,6 @@
namespace Splitter_UI.Models;
public class ProgressInfo
{
public double Percent { get; set; }
}

54
Splitter-UI/Program.cs Normal file
View File

@ -0,0 +1,54 @@
using Avalonia;
using Microsoft.Extensions.DependencyInjection;
namespace Splitter_UI;
internal sealed class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args)
{
var services = ConfigureServices();
var provider = services.BuildServiceProvider();
BuildAvaloniaApp(provider)
.StartWithClassicDesktopLifetime(args);
}
private static ServiceCollection ConfigureServices()
{
var services = new ServiceCollection();
// ViewModels
services.AddTransient<MainViewModel>();
services.AddTransient<FileListViewModel>();
services.AddTransient<PreviewPaneViewModel>();
services.AddTransient<InspectorPaneViewModel>();
services.AddTransient<StatusBarViewModel>();
services.AddTransient<LogPaneViewModel>();
// Domain services (your pipeline)
services.AddTransient<IFileProbeService, FileProbeService>();
services.AddTransient<IThumbnailService, ThumbnailService>();
services.AddSingleton<IAutoDecisionService, AutoDecisionService>();
services.AddSingleton<IProcessingService, ProcessingService>();
services.AddSingleton<ILogService, LogService>();
services.AddSingleton<IFileJobFactory, FileJobFactory>();
return services;
}
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp(ServiceProvider provider)
=> AppBuilder.Configure<App>(() => new App(provider))
.UsePlatformDetect()
#if DEBUG
.WithDeveloperTools()
#endif
.WithInterFont()
.LogToTrace();
}

View File

@ -0,0 +1,8 @@
namespace Splitter_UI.Services;
public sealed class AutoDecisionService : IAutoDecisionService
{
public void ApplyAutoDecisions(SingleJob job, VideoInfo probe)
{
}
}

View File

@ -0,0 +1,17 @@
using Microsoft.Extensions.DependencyInjection;
public sealed class FileJobFactory : IFileJobFactory
{
private readonly IServiceProvider _services;
public FileJobFactory(IServiceProvider services)
{
_services = services;
}
public FileJobViewModel Create(SingleJob job)
{
// Resolve a fresh VM + fresh services
return ActivatorUtilities.CreateInstance<FileJobViewModel>(_services, job);
}
}

View File

@ -0,0 +1,10 @@
namespace Splitter_UI.Services;
public sealed class FileProbeService : IFileProbeService
{
public async Task<VideoInfo> ProbeAsync(SingleJob job)
{
var res = await Task.Run(() =>ProbeVideo.Probe(job));
return res;
}
}

View File

@ -0,0 +1,6 @@
namespace Splitter_UI.Services;
public interface IAutoDecisionService
{
void ApplyAutoDecisions(SingleJob job, VideoInfo probe);
}

View File

@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Splitter_UI.Services;
public interface IFileJobFactory
{
FileJobViewModel Create(SingleJob job);
}

View File

@ -0,0 +1,8 @@
using System.Threading.Tasks;
namespace Splitter_UI.Services;
public interface IFileProbeService
{
Task<VideoInfo> ProbeAsync(SingleJob job);
}

View File

@ -0,0 +1,9 @@

namespace Splitter_UI.Services;
public interface ILogService
{
event Action<string>? MessageLogged;
void Write(string message);
}

View File

@ -0,0 +1,11 @@
using System.Threading;
using System.Threading.Tasks;
namespace Splitter_UI.Services;
public interface IProcessingService
{
event Action<string, ProgressInfo>? ProgressChanged;
Task ProcessAsync(IEnumerable<SingleJob> jobs, CancellationToken token);
}

View File

@ -0,0 +1,9 @@
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
namespace Splitter_UI.Services;
public interface IThumbnailService
{
Task<Bitmap?> CreateThumbnailAsync(string file, VideoInfo probe);
}

View File

@ -0,0 +1,14 @@
using System.Threading.Tasks;
using Avalonia;
namespace Splitter_UI.Services;
public sealed class LogService : ILogService
{
public event Action<string>? MessageLogged;
public void Write(string message)
{
MessageLogged?.Invoke(message);
}
}

View File

@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Splitter_UI.Services;
public sealed class ProcessingService : IProcessingService
{
public event Action<string, ProgressInfo>? ProgressChanged;
public async Task ProcessAsync(IEnumerable<SingleJob> jobs, CancellationToken token)
{
foreach (var job in jobs)
{
for (int i = 0; i <= 100; i += 20)
{
if (token.IsCancellationRequested)
return;
var progress = new ProgressInfo { Percent = i };
// Notify UI
ProgressChanged?.Invoke(job.InputFile, progress);
await Task.Delay(100, token);
}
}
}
}

View File

@ -0,0 +1,120 @@
using System.Diagnostics;
using Avalonia;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace Splitter_UI.Services;
public sealed class ThumbnailService : IThumbnailService
{
private readonly int _thumbWidth = 160;
private readonly int _thumbHeight = 90;
// Reusable buffer for BGR24 → 3 bytes per pixel
private readonly byte[] _bgrBuffer;
private readonly byte[] _bgraBuffer;
public ThumbnailService()
{
_bgrBuffer = new byte[_thumbWidth * _thumbHeight * 3];
_bgraBuffer = new byte[_thumbWidth * _thumbHeight * 4];
}
public async Task<Bitmap?> CreateThumbnailAsync(string file, VideoInfo probe)
{
// Decode a single frame using ffmpeg → raw BGR24 into _bgrBuffer
bool ok = await DecodeFrameAsync(file);
if (!ok)
return null;
// Convert BGR24 → BGRA32
ConvertBgrToBgra(_bgrBuffer, _bgraBuffer, _thumbWidth, _thumbHeight);
// Create Avalonia Bitmap
return CreateBitmap(_bgraBuffer, _thumbWidth, _thumbHeight);
}
private async Task<bool> DecodeFrameAsync(string file)
{
// ffmpeg command: decode one frame, resize, output raw BGR24
var args =
$"-ss 0 -t 0.1 -i \"{file}\" " +
"-an -sn " +
$"-vf \"scale={_thumbWidth}:{_thumbHeight}:force_original_aspect_ratio=decrease," +
$"pad={_thumbWidth}:{_thumbHeight}:(ow-iw)/2:(oh-ih)/2,format=bgr24\" " +
"-f rawvideo -";
var psi = new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = args,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
var p = new Process { StartInfo = psi };
p.Start();
int needed = _bgrBuffer.Length;
int read = 0;
using var stdout = p.StandardOutput.BaseStream;
while (read < needed)
{
int r = await stdout.ReadAsync(_bgrBuffer, read, needed - read);
if (r == 0)
{
TryKill(p);
return false;
}
read += r;
}
TryKill(p);
return true;
}
private static void TryKill(Process p)
{
try { p.Kill(); } catch { }
}
private static void ConvertBgrToBgra(byte[] bgr, byte[] bgra, int width, int height)
{
int si = 0;
int di = 0;
int totalPixels = width * height;
for (int i = 0; i < totalPixels; i++)
{
bgra[di + 0] = bgr[si + 0]; // B
bgra[di + 1] = bgr[si + 1]; // G
bgra[di + 2] = bgr[si + 2]; // R
bgra[di + 3] = 255; // A
si += 3;
di += 4;
}
}
private static unsafe Bitmap CreateBitmap(byte[] bgra, int width, int height)
{
int stride = width * 4;
fixed (byte* p = bgra)
{
return new Bitmap(
PixelFormat.Bgra8888,
AlphaFormat.Premul,
(nint)p,
new PixelSize(width, height),
new Vector(96, 96),
stride);
}
}
}

View File

@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PlatformTarget>x64</PlatformTarget>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="12.0.3" />
<PackageReference Include="Avalonia.Desktop" Version="12.0.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.3" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.0.3" />
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.1">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\splitter-cli\splitter.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,34 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
namespace Splitter_UI;
/// <summary>
/// Given a view model, returns the corresponding view if possible.
/// </summary>
[RequiresUnreferencedCode(
"Default implementation of ViewLocator involves reflection which may be trimmed away.",
Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")]
public class ViewLocator : IDataTemplate
{
public Control? Build(object? param)
{
if (param is null)
return null;
var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
var type = Type.GetType(name);
if (type != null)
{
return (Control)Activator.CreateInstance(type)!;
}
return new TextBlock { Text = "Not Found: " + name };
}
public bool Match(object? data)
{
return data is ViewModelBase;
}
}

View File

@ -0,0 +1,41 @@
using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Splitter_UI.ViewModels;
public partial class FileJobViewModel : ObservableObject
{
public SingleJob Job { get; }
public VideoInfo? Probe { get; set; }
public PreviewData? Preview { get; set; }
public ProgressInfo? Progress { get; set; }
[ObservableProperty]
private Bitmap? _thumbnail;
public string FileName { get; set; }
[ObservableProperty]
private string _suggestedAction = "";
private readonly IThumbnailService _thumbnails;
private readonly IFileProbeService _fileProbe;
public FileJobViewModel(SingleJob job, IThumbnailService thumbnails, IFileProbeService fileProbe)
{
Job = job;
_thumbnails = thumbnails;
_fileProbe = fileProbe;
FileName = Path.GetFileName(job.InputFile);
_ = Task.Run( LoadThumbnailAsync );
}
private async Task LoadThumbnailAsync()
{
Probe = await _fileProbe.ProbeAsync(Job);
Thumbnail = await _thumbnails.CreateThumbnailAsync(Job.InputFile, Probe);
}
}

View File

@ -0,0 +1,36 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Splitter_UI.ViewModels;
public partial class FileListViewModel : ObservableObject
{
private readonly IFileJobFactory _factory;
public ObservableCollection<FileJobViewModel> Files { get; } = [];
[ObservableProperty]
private FileJobViewModel? _selected;
public event Action<FileJobViewModel?>? SelectedFileChanged;
public FileListViewModel(IFileJobFactory factory)
{
_factory = factory;
}
partial void OnSelectedChanged(FileJobViewModel? value)
=> SelectedFileChanged?.Invoke(value);
[RelayCommand]
private void AddFiles(IEnumerable<string> paths)
{
foreach (var path in paths)
{
// Probe + auto-detect + thumbnail
var job = new SingleJob { InputFile = path };
var vm = _factory.Create(job);
Files.Add(vm);
}
}
}

View File

@ -0,0 +1,25 @@
using System.Collections.ObjectModel;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Splitter_UI.ViewModels;
public partial class InspectorPaneViewModel : ObservableObject
{
[ObservableProperty]
private FileJobViewModel? _selected;
public List<string> DetectModes =>
[
"face", "body", "none"
];
[RelayCommand]
private void ApplyOverrides()
{
if (Selected is null)
return;
}
}

View File

@ -0,0 +1,16 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.ObjectModel;
namespace Splitter_UI.ViewModels;
public partial class LogPaneViewModel : ObservableObject
{
public ObservableCollection<string> Logs { get; } = [];
public void Add(string message)
{
Logs.Add(message);
if (Logs.Count > 5000)
Logs.RemoveAt(0);
}
}

View File

@ -0,0 +1,36 @@
using CommunityToolkit.Mvvm.Input;
namespace Splitter_UI.ViewModels;
public partial class MainViewModel : ViewModelBase
{
public FileListViewModel FileList { get; }
public PreviewPaneViewModel Preview { get; } = new PreviewPaneViewModel();
public InspectorPaneViewModel Inspector { get; } = new InspectorPaneViewModel();
public StatusBarViewModel StatusBar { get; } = new StatusBarViewModel();
public LogPaneViewModel LogPane { get; } = new LogPaneViewModel();
public MainViewModel(IFileJobFactory fileJobFactory)
{
FileList = new FileListViewModel(fileJobFactory);
// Wire selection → preview + inspector
FileList.SelectedFileChanged += file =>
{
Preview.Selected = file;
Inspector.Selected = file;
};
}
[RelayCommand]
private void Start()
{
StatusBar.StatusText = "Processing…";
// call IProcessingService here
}
[RelayCommand]
private void Stop()
{
StatusBar.StatusText = "Stopped";
}
}

View File

@ -0,0 +1,13 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Splitter_UI.ViewModels;
public partial class PreviewPaneViewModel : ObservableObject
{
[ObservableProperty]
private FileJobViewModel? _selected;
public PreviewPaneViewModel()
{
}
}

View File

@ -0,0 +1,15 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Splitter_UI.ViewModels;
public partial class StatusBarViewModel : ObservableObject
{
[ObservableProperty]
private string _statusText = "Ready";
[ObservableProperty]
private double _percent;
[ObservableProperty]
private string _threadInfo = "Threads: 0/0";
}

View File

@ -0,0 +1,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Splitter_UI.ViewModels;
public abstract class ViewModelBase : ObservableObject
{
}

View File

@ -0,0 +1,73 @@
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
xmlns:views="clr-namespace:Splitter_UI.Views"
x:Class="Splitter_UI.Views.FileListView"
x:DataType="vm:FileListViewModel">
<UserControl.Styles>
<Style Selector="views|FileListView Border#DropZone">
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
<Style Selector="views|FileListView[IsDragActive=true] Border#DropZone">
<Setter Property="BorderBrush" Value="Red"/>
<Setter Property="BorderThickness" Value="2"/>
</Style>
</UserControl.Styles>
<Border x:Name="DropZone"
Background="#1E1E1E"
Padding="10"
DragDrop.AllowDrop="True"
DragDrop.Drop="OnDrop"
DragDrop.DragOver="OnDragOver"
DragDrop.DragEnter="OnDragEnter"
DragDrop.DragLeave="OnDragLeave">
<ScrollViewer>
<ItemsControl ItemsSource="{Binding Files}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:FileJobViewModel">
<Border Margin="6" Padding="6" Background="#2A2A2A" CornerRadius="4">
<StackPanel MinWidth="160" MaxWidth="160">
<Border Width="160" Height="90" ClipToBounds="True">
<Image Source="{Binding Thumbnail}"
Stretch="UniformToFill"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<TextBlock Text="{Binding FileName}"
TextWrapping="Wrap"
Margin="0,6,0,0"
FontSize="10"/>
<TextBlock Text="{Binding SuggestedAction}"
Foreground="LightGreen"
FontSize="10"/>
<ProgressBar
MinWidth="160"
MaxWidth="160"
Height="10"
Margin="0,4,0,0"
Value="{Binding Progress.Percent}" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
</UserControl>

View File

@ -0,0 +1,72 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using Splitter_UI.ViewModels;
namespace Splitter_UI.Views;
public partial class FileListView : UserControl
{
public static readonly StyledProperty<bool> IsDragActiveProperty =
AvaloniaProperty.Register<FileListView, bool>(nameof(IsDragActive));
public bool IsDragActive
{
get => GetValue(IsDragActiveProperty);
set => SetValue(IsDragActiveProperty, value);
}
public FileListView()
{
InitializeComponent();
}
private void OnDragEnter(object? sender, DragEventArgs e)
{
IsDragActive = true;
}
private void OnDragLeave(object? sender, DragEventArgs e)
{
IsDragActive = false;
}
private void OnDragOver(object? sender, DragEventArgs e)
{
// Avalonia 12:
// e.Data is IDataObject, but it has NO strongly typed formats.
if (e.DataTransfer.Contains(DataFormat.File))
e.DragEffects = DragDropEffects.Copy;
else
e.DragEffects = DragDropEffects.None;
e.Handled = true;
}
private async void OnDrop(object? sender, DragEventArgs e)
{
IsDragActive = false;
if (DataContext is not FileListViewModel vm)
return;
if (!e.DataTransfer.Contains(DataFormat.File))
return;
// Avalonia 12:
// This is the ONLY correct way to get dropped files.
var items = e.DataTransfer.TryGetFiles();
if (items is null)
return;
var paths = items
.OfType<IStorageFile>()
.Select(f => f.Path.LocalPath)
.ToList();
if (paths.Count > 0)
vm.AddFilesCommand.Execute(paths);
}
}

View File

@ -0,0 +1,42 @@
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Splitter_UI.Views.InspectorPane"
xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
x:DataType="vm:InspectorPaneViewModel">
<Border Background="#252525" Padding="12">
<StackPanel Spacing="8">
<TextBlock Text="Parameters" FontSize="18" Margin="0,0,0,10"/>
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="Rotate:" Width="100"/>
<NumericUpDown Value="{Binding Selected.Job.Rotate}" Width="120"/>
</StackPanel>
<!--
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="Crop:" Width="100"/>
<TextBox Text="{Binding Selected.CropText}" Width="200"/>
</StackPanel>
-->
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="Detect:" Width="100"/>
<ComboBox ItemsSource="{Binding DetectModes}"
SelectedItem="{Binding Selected.Job.Detect}"
Width="200"/>
</StackPanel>
<CheckBox Content="Force Fixed Duration"
IsChecked="{Binding Selected.Job.ForceFixed}"/>
<CheckBox Content="Debug Mode"
IsChecked="{Binding Selected.Job.Debug}"/>
<Button Content="Apply to Selected"
Command="{Binding ApplyOverridesCommand}"
Margin="0,10,0,0"/>
</StackPanel>
</Border>
</UserControl>

View File

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Splitter_UI.Views;
public partial class InspectorPane : UserControl
{
public InspectorPane()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,19 @@
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Splitter_UI.Views.LogPane"
xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
x:DataType="vm:LogPaneViewModel">
<Border Background="#111" Padding="8">
<ScrollViewer>
<ItemsControl ItemsSource="{Binding Logs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" FontFamily="Consolas" FontSize="12"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
</UserControl>

View File

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Splitter_UI.Views;
public partial class LogPane : UserControl
{
public LogPane()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,41 @@
<Window
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:Splitter_UI.Views"
xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
x:Class="Splitter_UI.Views.MainWindow"
x:DataType="vm:MainViewModel"
Width="1400"
Height="900"
Title="Splitter UI">
<DockPanel>
<!-- Status Bar -->
<views:StatusBarView DockPanel.Dock="Bottom"
DataContext="{Binding StatusBar}" />
<!-- Log Pane -->
<views:LogPane DockPanel.Dock="Bottom" Height="150"
DataContext="{Binding LogPane}" />
<!-- Main Content -->
<Grid ColumnDefinitions="2*,3*,2*">
<!-- File List -->
<views:FileListView Grid.Column="0"
DataContext="{Binding FileList}" />
<!-- Preview -->
<views:PreviewPane Grid.Column="1"
DataContext="{Binding Preview}" />
<!-- Inspector -->
<views:InspectorPane Grid.Column="2"
DataContext="{Binding Inspector}" />
</Grid>
</DockPanel>
</Window>

View File

@ -0,0 +1,12 @@
using Avalonia.Controls;
namespace Splitter_UI.Views;
public partial class MainWindow : Window
{
public MainViewModel Data { get; } = null!; // set by DI
public MainWindow()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,24 @@
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Splitter_UI.Views.PreviewPane"
xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
x:DataType="vm:PreviewPaneViewModel">
<Border Background="#202020" Padding="10">
<Grid>
<Image Source="{Binding Selected.Preview.Frame}" Stretch="Uniform"/>
<!-- 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>
</Border>
</UserControl>

View File

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Splitter_UI.Views;
public partial class PreviewPane : UserControl
{
public PreviewPane()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,24 @@
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Splitter_UI.Views.StatusBarView"
xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
x:DataType="vm:StatusBarViewModel">
<Border Padding="4" Background="{DynamicResource ThemeBackgroundBrush}">
<Grid ColumnDefinitions="*,Auto,Auto" ColumnSpacing="8">
<TextBlock Grid.Column="0"
VerticalAlignment="Center"
Text="{Binding StatusText}" />
<ProgressBar Grid.Column="1"
Width="200" Height="16"
VerticalAlignment="Center"
Value="{Binding Percent}" />
<TextBlock Grid.Column="2"
VerticalAlignment="Center"
Text="{Binding ThreadInfo}" />
</Grid>
</Border>
</UserControl>

View File

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Splitter_UI.Views;
public partial class StatusBarView : UserControl
{
public StatusBarView()
{
InitializeComponent();
}
}

18
Splitter-UI/app.manifest Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="Splitter_UI.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

View File

@ -6,4 +6,5 @@
<File Path="README.md" />
</Folder>
<Project Path="splitter-cli/splitter.csproj" />
<Project Path="Splitter-UI/Splitter-UI.csproj" Id="f80bfed3-d5f0-4292-92b2-909c21625ee3" />
</Solution>