mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-21 16:12:01 +00:00
Avalonia UI work started
This commit is contained in:
parent
93de483bc6
commit
1f93eba839
15
Splitter-UI/App.axaml
Normal file
15
Splitter-UI/App.axaml
Normal 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
37
Splitter-UI/App.axaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
BIN
Splitter-UI/Assets/avalonia-logo.ico
Normal file
BIN
Splitter-UI/Assets/avalonia-logo.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
8
Splitter-UI/GlobalUsing.cs
Normal file
8
Splitter-UI/GlobalUsing.cs
Normal 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;
|
||||
11
Splitter-UI/Models/PreviewData.cs
Normal file
11
Splitter-UI/Models/PreviewData.cs
Normal 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; }
|
||||
}
|
||||
6
Splitter-UI/Models/ProgressInfo.cs
Normal file
6
Splitter-UI/Models/ProgressInfo.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Splitter_UI.Models;
|
||||
|
||||
public class ProgressInfo
|
||||
{
|
||||
public double Percent { get; set; }
|
||||
}
|
||||
54
Splitter-UI/Program.cs
Normal file
54
Splitter-UI/Program.cs
Normal 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();
|
||||
}
|
||||
8
Splitter-UI/Services/AutoDecisionService.cs
Normal file
8
Splitter-UI/Services/AutoDecisionService.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
public sealed class AutoDecisionService : IAutoDecisionService
|
||||
{
|
||||
public void ApplyAutoDecisions(SingleJob job, VideoInfo probe)
|
||||
{
|
||||
}
|
||||
}
|
||||
17
Splitter-UI/Services/FileJobFactory.cs
Normal file
17
Splitter-UI/Services/FileJobFactory.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
10
Splitter-UI/Services/FileProbeService.cs
Normal file
10
Splitter-UI/Services/FileProbeService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
6
Splitter-UI/Services/IAutoDecisionService.cs
Normal file
6
Splitter-UI/Services/IAutoDecisionService.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
public interface IAutoDecisionService
|
||||
{
|
||||
void ApplyAutoDecisions(SingleJob job, VideoInfo probe);
|
||||
}
|
||||
10
Splitter-UI/Services/IFileJobFactory.cs
Normal file
10
Splitter-UI/Services/IFileJobFactory.cs
Normal 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);
|
||||
}
|
||||
8
Splitter-UI/Services/IFileProbeService.cs
Normal file
8
Splitter-UI/Services/IFileProbeService.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
public interface IFileProbeService
|
||||
{
|
||||
Task<VideoInfo> ProbeAsync(SingleJob job);
|
||||
}
|
||||
9
Splitter-UI/Services/ILogService.cs
Normal file
9
Splitter-UI/Services/ILogService.cs
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
public interface ILogService
|
||||
{
|
||||
event Action<string>? MessageLogged;
|
||||
|
||||
void Write(string message);
|
||||
}
|
||||
11
Splitter-UI/Services/IProcessingService.cs
Normal file
11
Splitter-UI/Services/IProcessingService.cs
Normal 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);
|
||||
}
|
||||
9
Splitter-UI/Services/IThumbnailService.cs
Normal file
9
Splitter-UI/Services/IThumbnailService.cs
Normal 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);
|
||||
}
|
||||
14
Splitter-UI/Services/LogService.cs
Normal file
14
Splitter-UI/Services/LogService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
29
Splitter-UI/Services/ProcessingService.cs
Normal file
29
Splitter-UI/Services/ProcessingService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
120
Splitter-UI/Services/ThumbnailService.cs
Normal file
120
Splitter-UI/Services/ThumbnailService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
35
Splitter-UI/Splitter-UI.csproj
Normal file
35
Splitter-UI/Splitter-UI.csproj
Normal 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>
|
||||
34
Splitter-UI/ViewLocator.cs
Normal file
34
Splitter-UI/ViewLocator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
41
Splitter-UI/ViewModels/FileJobViewModel.cs
Normal file
41
Splitter-UI/ViewModels/FileJobViewModel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
36
Splitter-UI/ViewModels/FileListViewModel.cs
Normal file
36
Splitter-UI/ViewModels/FileListViewModel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Splitter-UI/ViewModels/InspectorPaneViewModel.cs
Normal file
25
Splitter-UI/ViewModels/InspectorPaneViewModel.cs
Normal 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;
|
||||
|
||||
}
|
||||
}
|
||||
16
Splitter-UI/ViewModels/LogPaneViewModel.cs
Normal file
16
Splitter-UI/ViewModels/LogPaneViewModel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
36
Splitter-UI/ViewModels/MainViewModel.cs
Normal file
36
Splitter-UI/ViewModels/MainViewModel.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
13
Splitter-UI/ViewModels/PreviewPaneViewModel.cs
Normal file
13
Splitter-UI/ViewModels/PreviewPaneViewModel.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Splitter_UI.ViewModels;
|
||||
|
||||
public partial class PreviewPaneViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
private FileJobViewModel? _selected;
|
||||
|
||||
public PreviewPaneViewModel()
|
||||
{
|
||||
}
|
||||
}
|
||||
15
Splitter-UI/ViewModels/StatusBarViewModel.cs
Normal file
15
Splitter-UI/ViewModels/StatusBarViewModel.cs
Normal 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";
|
||||
}
|
||||
7
Splitter-UI/ViewModels/ViewModelBase.cs
Normal file
7
Splitter-UI/ViewModels/ViewModelBase.cs
Normal file
@ -0,0 +1,7 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Splitter_UI.ViewModels;
|
||||
|
||||
public abstract class ViewModelBase : ObservableObject
|
||||
{
|
||||
}
|
||||
73
Splitter-UI/Views/FileListView.axaml
Normal file
73
Splitter-UI/Views/FileListView.axaml
Normal 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>
|
||||
72
Splitter-UI/Views/FileListView.axaml.cs
Normal file
72
Splitter-UI/Views/FileListView.axaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
42
Splitter-UI/Views/InspectorPane.axaml
Normal file
42
Splitter-UI/Views/InspectorPane.axaml
Normal 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>
|
||||
11
Splitter-UI/Views/InspectorPane.axaml.cs
Normal file
11
Splitter-UI/Views/InspectorPane.axaml.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Splitter_UI.Views;
|
||||
|
||||
public partial class InspectorPane : UserControl
|
||||
{
|
||||
public InspectorPane()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
19
Splitter-UI/Views/LogPane.axaml
Normal file
19
Splitter-UI/Views/LogPane.axaml
Normal 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>
|
||||
11
Splitter-UI/Views/LogPane.axaml.cs
Normal file
11
Splitter-UI/Views/LogPane.axaml.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Splitter_UI.Views;
|
||||
|
||||
public partial class LogPane : UserControl
|
||||
{
|
||||
public LogPane()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
41
Splitter-UI/Views/MainWindow.axaml
Normal file
41
Splitter-UI/Views/MainWindow.axaml
Normal 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>
|
||||
|
||||
12
Splitter-UI/Views/MainWindow.axaml.cs
Normal file
12
Splitter-UI/Views/MainWindow.axaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
24
Splitter-UI/Views/PreviewPane.axaml
Normal file
24
Splitter-UI/Views/PreviewPane.axaml
Normal 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>
|
||||
11
Splitter-UI/Views/PreviewPane.axaml.cs
Normal file
11
Splitter-UI/Views/PreviewPane.axaml.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Splitter_UI.Views;
|
||||
|
||||
public partial class PreviewPane : UserControl
|
||||
{
|
||||
public PreviewPane()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
24
Splitter-UI/Views/StatusBarView.axaml
Normal file
24
Splitter-UI/Views/StatusBarView.axaml
Normal 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>
|
||||
11
Splitter-UI/Views/StatusBarView.axaml.cs
Normal file
11
Splitter-UI/Views/StatusBarView.axaml.cs
Normal 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
18
Splitter-UI/app.manifest
Normal 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>
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user