diff --git a/Splitter-UI/Converters/RotationAngleToIconConverter.cs b/Splitter-UI/Converters/RotationAngleToIconConverter.cs new file mode 100644 index 0000000..e8c65e6 --- /dev/null +++ b/Splitter-UI/Converters/RotationAngleToIconConverter.cs @@ -0,0 +1,21 @@ +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Splitter_UI.Converters; + +public sealed class RotationAngleToIconConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value switch + { + 90 => "\uf2f9", // FA7 (fa-rotate-left / fa-arrow-rotate-left / fa-undo) + 180 => "\uf2f1", // FA7 (fa-sync-alt) + 270 => "\uf2ea", // FA7 (fa-rotate-right / fa-arrow-rotate-right / fa-redo) + _ => null + }; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + => throw new NotSupportedException(); +} diff --git a/Splitter-UI/Converters/ZeroToBoolConverter.cs b/Splitter-UI/Converters/ZeroToBoolConverter.cs new file mode 100644 index 0000000..e40a6f5 --- /dev/null +++ b/Splitter-UI/Converters/ZeroToBoolConverter.cs @@ -0,0 +1,13 @@ +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Splitter_UI.Converters; + +public sealed class ZeroToBoolConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + => (value is int i && i == 0); + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + => throw new NotSupportedException(); +} diff --git a/Splitter-UI/Services/IThumbnailService.cs b/Splitter-UI/Services/IThumbnailService.cs index 84c1cb9..7429925 100644 --- a/Splitter-UI/Services/IThumbnailService.cs +++ b/Splitter-UI/Services/IThumbnailService.cs @@ -5,5 +5,5 @@ namespace Splitter_UI.Services; public interface IThumbnailService { - Task CreateThumbnailAsync(string file, VideoInfo probe, TimeSpan? skip = null, int? width = null, int? height = null); + Task CreateThumbnailAsync(string file, VideoInfo probe, TimeSpan? skip = null, int? width = null, int? height = null, int? rotateDegree = null); } diff --git a/Splitter-UI/Services/ThumbnailService.cs b/Splitter-UI/Services/ThumbnailService.cs index 427a077..d8883bf 100644 --- a/Splitter-UI/Services/ThumbnailService.cs +++ b/Splitter-UI/Services/ThumbnailService.cs @@ -13,7 +13,13 @@ public sealed class ThumbnailService : IThumbnailService private readonly byte [] _bgrBuffer = new byte[_thumbWidth * _thumbHeight * 3]; private readonly byte [] _bgraBuffer = new byte[_thumbWidth * _thumbHeight * 4]; - public async Task CreateThumbnailAsync(string file, VideoInfo probe, TimeSpan? skip = null, int? width = null, int? height = null) + public async Task CreateThumbnailAsync( + string file, + VideoInfo probe, + TimeSpan? skip = null, + int? width = null, + int? height = null, + int? rotateDegree = null) { width ??= _thumbWidth; height ??= _thumbHeight; @@ -29,7 +35,7 @@ public sealed class ThumbnailService : IThumbnailService var bgraBuffer = canUseStaticBuffers ? _bgraBuffer : new byte[width.Value * height.Value * 4]; // Decode a single frame using ffmpeg → raw BGR24 into _bgrBuffer - bool ok = await DecodeFrameAsync(bgrBuffer, file, skip.Value, width.Value, height.Value); + bool ok = await DecodeFrameAsync(bgrBuffer, file, skip.Value, width.Value, height.Value, rotateDegree); if (!ok) return null; @@ -37,17 +43,19 @@ public sealed class ThumbnailService : IThumbnailService ConvertBgrToBgra(bgrBuffer, bgraBuffer, width.Value, height.Value); // Create Avalonia Bitmap - return CreateBitmap(bgraBuffer, width.Value, height.Value); + return CreateBitmap(bgraBuffer, width.Value, height.Value, rotateDegree == 90 || rotateDegree == 270); } - private static async Task DecodeFrameAsync(byte [] bgrBuffer, string file, TimeSpan skip, int width, int height) + private static async Task DecodeFrameAsync(byte [] bgrBuffer, string file, TimeSpan skip, int width, int height, int? rotateDegree) { + var rotationStr = TrackingSplitter.GetRorationArg(rotateDegree); + // ffmpeg command: decode one frame, resize, output raw BGR24 var args = $"-ss {skip.TotalSeconds} -t 0.1 -i \"{file}\" " + "-an -sn " + $"-vf \"scale={width}:{height}:force_original_aspect_ratio=decrease," + - $"pad={width}:{height}:(ow-iw)/2:(oh-ih)/2,format=bgr24\" " + + $"pad={width}:{height}:(ow-iw)/2:(oh-ih)/2,format=bgr24{rotationStr}\" " + "-f rawvideo -"; var psi = new ProcessStartInfo @@ -107,8 +115,13 @@ public sealed class ThumbnailService : IThumbnailService } } - private static unsafe Bitmap CreateBitmap(byte[] bgra, int width, int height) + private static unsafe Bitmap CreateBitmap(byte[] bgra, int width, int height, bool isRotated) { + if (isRotated) + { + (height, width) = (width, height); + } + int stride = width * 4; fixed (byte* p = bgra) diff --git a/Splitter-UI/ViewModels/JobViewModel.cs b/Splitter-UI/ViewModels/JobViewModel.cs index 620acfa..6c37ad7 100644 --- a/Splitter-UI/ViewModels/JobViewModel.cs +++ b/Splitter-UI/ViewModels/JobViewModel.cs @@ -41,6 +41,12 @@ public partial class JobViewModel : ObservableObject public string FileName => Path.GetFileName(Job.InputFile); + public string TextDesc => Probe != null + ? $"{Probe.Width}x{Probe.Height}, {TimeSpan.FromSeconds(Probe.Duration).ToString(@"hh\:mm\:ss")}), FPS: {Probe.Fps:F2}, Bitrate: {Probe.Bitrate/1024/1024:F2} MB/s" + : ""; + + public override string ToString() => $"{FileName} - {TextDesc}"; + public ObservableCollection ParametersList { get; } = new(); @@ -105,6 +111,7 @@ public partial class JobViewModel : ObservableObject { Job.Rotate = value; OnPropertyChanged(); + Task.Run(CreatePreview); } } @@ -145,7 +152,7 @@ public partial class JobViewModel : ObservableObject private async Task LoadThumbnailAsync() { Probe = await _fileProbe.ProbeAsync(Job); - Thumbnail = await _thumbnails.CreateThumbnailAsync(Job.InputFile, Probe); + Thumbnail = await _thumbnails.CreateThumbnailAsync(Job.InputFile, Probe, rotateDegree: Job.Rotate); SuggestedAction = Probe.Rotation == 0 ? "crop" : "rotate"; await CreatePreview(); @@ -157,9 +164,8 @@ public partial class JobViewModel : ObservableObject return; try { - var frame = await _thumbnails.CreateThumbnailAsync(Job.InputFile, Probe, TimeSpan.FromSeconds(PositionSeconds), Probe.Width, Probe.Height); + var frame = await _thumbnails.CreateThumbnailAsync(Job.InputFile, Probe, TimeSpan.FromSeconds(PositionSeconds), Probe.Width, Probe.Height, Job.Rotate); Preview = new PreviewData(frame, [], null); - OnPropertyChanged(nameof(Preview)); } catch (Exception ex) { @@ -197,21 +203,23 @@ public partial class JobViewModel : ObservableObject private void StepForward() { - var step = 10.0; // seconds if (DurationSeconds <= 0) return; - PositionSeconds = Math.Min(DurationSeconds, PositionSeconds + step); + var step = DurationSeconds * 0.1; // 10% of total duration + + SliderLiveValue = Math.Min(DurationSeconds, SliderLiveValue + 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); + var step = DurationSeconds * 0.1; // 10% of total duration + + SliderLiveValue = Math.Max(0, SliderLiveValue - step); // trigger seek in your playback pipeline here } diff --git a/Splitter-UI/Views/FileListView.axaml b/Splitter-UI/Views/FileListView.axaml index a43ed8a..efb8e87 100644 --- a/Splitter-UI/Views/FileListView.axaml +++ b/Splitter-UI/Views/FileListView.axaml @@ -3,10 +3,15 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:Splitter_UI.ViewModels" xmlns:views="clr-namespace:Splitter_UI.Views" - xmlns:svg="clr-namespace:Avalonia.Svg.Skia;assembly=Avalonia.Svg.Skia" + xmlns:conv="clr-namespace:Splitter_UI.Converters" x:Class="Splitter_UI.Views.FileListView" x:DataType="vm:FileListViewModel"> + + + + + - - - - - - + - - - + + + + - - - + + + - + + + + - - - - - - - + + + + + + + + + + diff --git a/Splitter-UI/Views/InspectorPane.axaml b/Splitter-UI/Views/InspectorPane.axaml index 76973b2..c457207 100644 --- a/Splitter-UI/Views/InspectorPane.axaml +++ b/Splitter-UI/Views/InspectorPane.axaml @@ -12,8 +12,9 @@ x:DataType="vm:InspectorPaneViewModel"> - - + + + @@ -113,13 +114,22 @@ x:DataType="vm:InspectorPaneViewModel"> - + + + + + + + + diff --git a/Splitter-UI/Views/MainWindow.axaml b/Splitter-UI/Views/MainWindow.axaml index f27f108..d5faabd 100644 --- a/Splitter-UI/Views/MainWindow.axaml +++ b/Splitter-UI/Views/MainWindow.axaml @@ -6,7 +6,7 @@ x:Class="Splitter_UI.Views.MainWindow" x:DataType="vm:MainViewModel" Width="1400" - Height="900" + Height="950" Title="Splitter UI"> @@ -20,7 +20,7 @@ DataContext="{Binding LogPane}" /> - +