diff --git a/Splitter-UI/App.axaml b/Splitter-UI/App.axaml new file mode 100644 index 0000000..f167199 --- /dev/null +++ b/Splitter-UI/App.axaml @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Splitter-UI/App.axaml.cs b/Splitter-UI/App.axaml.cs new file mode 100644 index 0000000..95bf3a3 --- /dev/null +++ b/Splitter-UI/App.axaml.cs @@ -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(); + + desktop.MainWindow = new MainWindow + { + DataContext = vm + }; + } + + base.OnFrameworkInitializationCompleted(); + } +} \ No newline at end of file diff --git a/Splitter-UI/Assets/avalonia-logo.ico b/Splitter-UI/Assets/avalonia-logo.ico new file mode 100644 index 0000000..f7da8bb Binary files /dev/null and b/Splitter-UI/Assets/avalonia-logo.ico differ diff --git a/Splitter-UI/GlobalUsing.cs b/Splitter-UI/GlobalUsing.cs new file mode 100644 index 0000000..4377859 --- /dev/null +++ b/Splitter-UI/GlobalUsing.cs @@ -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; diff --git a/Splitter-UI/Models/PreviewData.cs b/Splitter-UI/Models/PreviewData.cs new file mode 100644 index 0000000..23af86d --- /dev/null +++ b/Splitter-UI/Models/PreviewData.cs @@ -0,0 +1,11 @@ +using Avalonia; + +namespace Splitter_UI.Models; + +public sealed class PreviewData +{ + public Avalonia.Media.Imaging.Bitmap? Frame { get; init; } + public IReadOnlyList FaceBoxes { get; init; } = []; + public IReadOnlyList BodyBoxes { get; init; } = []; + public Rect? CropRect { get; init; } +} diff --git a/Splitter-UI/Models/ProgressInfo.cs b/Splitter-UI/Models/ProgressInfo.cs new file mode 100644 index 0000000..02cebc8 --- /dev/null +++ b/Splitter-UI/Models/ProgressInfo.cs @@ -0,0 +1,6 @@ +namespace Splitter_UI.Models; + +public class ProgressInfo +{ + public double Percent { get; set; } +} diff --git a/Splitter-UI/Program.cs b/Splitter-UI/Program.cs new file mode 100644 index 0000000..d435954 --- /dev/null +++ b/Splitter-UI/Program.cs @@ -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(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + // Domain services (your pipeline) + services.AddTransient(); + services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + + return services; + } + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp(ServiceProvider provider) + => AppBuilder.Configure(() => new App(provider)) + .UsePlatformDetect() +#if DEBUG + .WithDeveloperTools() +#endif + .WithInterFont() + .LogToTrace(); +} diff --git a/Splitter-UI/Services/AutoDecisionService.cs b/Splitter-UI/Services/AutoDecisionService.cs new file mode 100644 index 0000000..2887eb6 --- /dev/null +++ b/Splitter-UI/Services/AutoDecisionService.cs @@ -0,0 +1,8 @@ +namespace Splitter_UI.Services; + +public sealed class AutoDecisionService : IAutoDecisionService +{ + public void ApplyAutoDecisions(SingleJob job, VideoInfo probe) + { + } +} diff --git a/Splitter-UI/Services/FileJobFactory.cs b/Splitter-UI/Services/FileJobFactory.cs new file mode 100644 index 0000000..921a07c --- /dev/null +++ b/Splitter-UI/Services/FileJobFactory.cs @@ -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(_services, job); + } +} diff --git a/Splitter-UI/Services/FileProbeService.cs b/Splitter-UI/Services/FileProbeService.cs new file mode 100644 index 0000000..fbb88d0 --- /dev/null +++ b/Splitter-UI/Services/FileProbeService.cs @@ -0,0 +1,10 @@ +namespace Splitter_UI.Services; + +public sealed class FileProbeService : IFileProbeService +{ + public async Task ProbeAsync(SingleJob job) + { + var res = await Task.Run(() =>ProbeVideo.Probe(job)); + return res; + } +} diff --git a/Splitter-UI/Services/IAutoDecisionService.cs b/Splitter-UI/Services/IAutoDecisionService.cs new file mode 100644 index 0000000..5744321 --- /dev/null +++ b/Splitter-UI/Services/IAutoDecisionService.cs @@ -0,0 +1,6 @@ +namespace Splitter_UI.Services; + +public interface IAutoDecisionService +{ + void ApplyAutoDecisions(SingleJob job, VideoInfo probe); +} diff --git a/Splitter-UI/Services/IFileJobFactory.cs b/Splitter-UI/Services/IFileJobFactory.cs new file mode 100644 index 0000000..883cdb3 --- /dev/null +++ b/Splitter-UI/Services/IFileJobFactory.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Splitter_UI.Services; + +public interface IFileJobFactory +{ + FileJobViewModel Create(SingleJob job); +} diff --git a/Splitter-UI/Services/IFileProbeService.cs b/Splitter-UI/Services/IFileProbeService.cs new file mode 100644 index 0000000..33a284c --- /dev/null +++ b/Splitter-UI/Services/IFileProbeService.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Splitter_UI.Services; + +public interface IFileProbeService +{ + Task ProbeAsync(SingleJob job); +} diff --git a/Splitter-UI/Services/ILogService.cs b/Splitter-UI/Services/ILogService.cs new file mode 100644 index 0000000..048f375 --- /dev/null +++ b/Splitter-UI/Services/ILogService.cs @@ -0,0 +1,9 @@ + +namespace Splitter_UI.Services; + +public interface ILogService +{ + event Action? MessageLogged; + + void Write(string message); +} diff --git a/Splitter-UI/Services/IProcessingService.cs b/Splitter-UI/Services/IProcessingService.cs new file mode 100644 index 0000000..49f9e93 --- /dev/null +++ b/Splitter-UI/Services/IProcessingService.cs @@ -0,0 +1,11 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Splitter_UI.Services; + +public interface IProcessingService +{ + event Action? ProgressChanged; + + Task ProcessAsync(IEnumerable jobs, CancellationToken token); +} diff --git a/Splitter-UI/Services/IThumbnailService.cs b/Splitter-UI/Services/IThumbnailService.cs new file mode 100644 index 0000000..6c5ee75 --- /dev/null +++ b/Splitter-UI/Services/IThumbnailService.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; +using Avalonia.Media.Imaging; + +namespace Splitter_UI.Services; + +public interface IThumbnailService +{ + Task CreateThumbnailAsync(string file, VideoInfo probe); +} diff --git a/Splitter-UI/Services/LogService.cs b/Splitter-UI/Services/LogService.cs new file mode 100644 index 0000000..f29382a --- /dev/null +++ b/Splitter-UI/Services/LogService.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using Avalonia; + +namespace Splitter_UI.Services; + +public sealed class LogService : ILogService +{ + public event Action? MessageLogged; + + public void Write(string message) + { + MessageLogged?.Invoke(message); + } +} diff --git a/Splitter-UI/Services/ProcessingService.cs b/Splitter-UI/Services/ProcessingService.cs new file mode 100644 index 0000000..8fe779d --- /dev/null +++ b/Splitter-UI/Services/ProcessingService.cs @@ -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? ProgressChanged; + + public async Task ProcessAsync(IEnumerable 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); + } + } + } +} diff --git a/Splitter-UI/Services/ThumbnailService.cs b/Splitter-UI/Services/ThumbnailService.cs new file mode 100644 index 0000000..86443a8 --- /dev/null +++ b/Splitter-UI/Services/ThumbnailService.cs @@ -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 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 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); + } + } + +} diff --git a/Splitter-UI/Splitter-UI.csproj b/Splitter-UI/Splitter-UI.csproj new file mode 100644 index 0000000..60aaf87 --- /dev/null +++ b/Splitter-UI/Splitter-UI.csproj @@ -0,0 +1,35 @@ + + + WinExe + net10.0 + enable + enable + latest + true + x64 + win-x64 + app.manifest + true + + + + + + + + + + + + + None + All + + + + + + + + + diff --git a/Splitter-UI/ViewLocator.cs b/Splitter-UI/ViewLocator.cs new file mode 100644 index 0000000..ff90ea9 --- /dev/null +++ b/Splitter-UI/ViewLocator.cs @@ -0,0 +1,34 @@ +using System.Diagnostics.CodeAnalysis; +using Avalonia.Controls; +using Avalonia.Controls.Templates; + +namespace Splitter_UI; +/// +/// Given a view model, returns the corresponding view if possible. +/// +[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; + } +} diff --git a/Splitter-UI/ViewModels/FileJobViewModel.cs b/Splitter-UI/ViewModels/FileJobViewModel.cs new file mode 100644 index 0000000..cc8d896 --- /dev/null +++ b/Splitter-UI/ViewModels/FileJobViewModel.cs @@ -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); + } +} diff --git a/Splitter-UI/ViewModels/FileListViewModel.cs b/Splitter-UI/ViewModels/FileListViewModel.cs new file mode 100644 index 0000000..c67a227 --- /dev/null +++ b/Splitter-UI/ViewModels/FileListViewModel.cs @@ -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 Files { get; } = []; + + [ObservableProperty] + private FileJobViewModel? _selected; + + public event Action? SelectedFileChanged; + + public FileListViewModel(IFileJobFactory factory) + { + _factory = factory; + } + + partial void OnSelectedChanged(FileJobViewModel? value) + => SelectedFileChanged?.Invoke(value); + + [RelayCommand] + private void AddFiles(IEnumerable paths) + { + foreach (var path in paths) + { + // Probe + auto-detect + thumbnail + var job = new SingleJob { InputFile = path }; + var vm = _factory.Create(job); + Files.Add(vm); + } + } +} diff --git a/Splitter-UI/ViewModels/InspectorPaneViewModel.cs b/Splitter-UI/ViewModels/InspectorPaneViewModel.cs new file mode 100644 index 0000000..f8270cf --- /dev/null +++ b/Splitter-UI/ViewModels/InspectorPaneViewModel.cs @@ -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 DetectModes => + [ + "face", "body", "none" + ]; + + [RelayCommand] + private void ApplyOverrides() + { + if (Selected is null) + return; + + } +} diff --git a/Splitter-UI/ViewModels/LogPaneViewModel.cs b/Splitter-UI/ViewModels/LogPaneViewModel.cs new file mode 100644 index 0000000..e20f396 --- /dev/null +++ b/Splitter-UI/ViewModels/LogPaneViewModel.cs @@ -0,0 +1,16 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System.Collections.ObjectModel; + +namespace Splitter_UI.ViewModels; + +public partial class LogPaneViewModel : ObservableObject +{ + public ObservableCollection Logs { get; } = []; + + public void Add(string message) + { + Logs.Add(message); + if (Logs.Count > 5000) + Logs.RemoveAt(0); + } +} diff --git a/Splitter-UI/ViewModels/MainViewModel.cs b/Splitter-UI/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..432f695 --- /dev/null +++ b/Splitter-UI/ViewModels/MainViewModel.cs @@ -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"; + } +} diff --git a/Splitter-UI/ViewModels/PreviewPaneViewModel.cs b/Splitter-UI/ViewModels/PreviewPaneViewModel.cs new file mode 100644 index 0000000..464cc5b --- /dev/null +++ b/Splitter-UI/ViewModels/PreviewPaneViewModel.cs @@ -0,0 +1,13 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Splitter_UI.ViewModels; + +public partial class PreviewPaneViewModel : ObservableObject +{ + [ObservableProperty] + private FileJobViewModel? _selected; + + public PreviewPaneViewModel() + { + } +} diff --git a/Splitter-UI/ViewModels/StatusBarViewModel.cs b/Splitter-UI/ViewModels/StatusBarViewModel.cs new file mode 100644 index 0000000..0a8e3b3 --- /dev/null +++ b/Splitter-UI/ViewModels/StatusBarViewModel.cs @@ -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"; +} diff --git a/Splitter-UI/ViewModels/ViewModelBase.cs b/Splitter-UI/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..be3ad20 --- /dev/null +++ b/Splitter-UI/ViewModels/ViewModelBase.cs @@ -0,0 +1,7 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Splitter_UI.ViewModels; + +public abstract class ViewModelBase : ObservableObject +{ +} diff --git a/Splitter-UI/Views/FileListView.axaml b/Splitter-UI/Views/FileListView.axaml new file mode 100644 index 0000000..59e9c0e --- /dev/null +++ b/Splitter-UI/Views/FileListView.axaml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Splitter-UI/Views/FileListView.axaml.cs b/Splitter-UI/Views/FileListView.axaml.cs new file mode 100644 index 0000000..8b12827 --- /dev/null +++ b/Splitter-UI/Views/FileListView.axaml.cs @@ -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 IsDragActiveProperty = + AvaloniaProperty.Register(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() + .Select(f => f.Path.LocalPath) + .ToList(); + + if (paths.Count > 0) + vm.AddFilesCommand.Execute(paths); + } +} diff --git a/Splitter-UI/Views/InspectorPane.axaml b/Splitter-UI/Views/InspectorPane.axaml new file mode 100644 index 0000000..645ef85 --- /dev/null +++ b/Splitter-UI/Views/InspectorPane.axaml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + +