using System.Reactive.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Input; using Avalonia.Threading; using PicView.Avalonia.FileSystem; using PicView.Avalonia.Navigation; using PicView.Avalonia.Resizing; using PicView.Avalonia.UI; using PicView.Avalonia.ViewModels; using PicView.Core.ImageDecoding; using PicView.Core.Localization; using ReactiveUI; namespace PicView.Avalonia.Views; public partial class SingleImageResizeView : UserControl { private double _aspectRatio; private IDisposable? _imageUpdateSubscription; private bool _isKeepingAspectRatio = true; public SingleImageResizeView() { InitializeComponent(); Loaded += OnLoaded; Unloaded += OnUnloaded; } private void OnLoaded(object? sender, EventArgs e) { if (DataContext is not MainViewModel vm) { return; } _aspectRatio = (double)vm.PicViewer.PixelWidth / vm.PicViewer.PixelHeight; RegisterEventHandlers(vm); _imageUpdateSubscription = vm.PicViewer.WhenAnyValue(x => x.FileInfo) .ObserveOn(RxApp.MainThreadScheduler) .Select(x => x is not null) .Subscribe(_ => { UpdateQualitySliderState(); ShowCancelButton(); }); } private void OnUnloaded(object? sender, EventArgs e) { _imageUpdateSubscription?.Dispose(); } private void RegisterEventHandlers(MainViewModel vm) { UpdateQualitySliderState(); QualitySlider.ValueChanged += (_, _) => ShowResetButton(); SaveButton.Click += async (_, _) => await SaveImage(vm).ConfigureAwait(false); SaveAsButton.Click += async (_, _) => await SaveImageAs(vm).ConfigureAwait(false); PixelWidthTextBox.KeyDown += async (_, e) => await SaveImageOnEnter(e, vm); PixelHeightTextBox.KeyDown += async (_, e) => await SaveImageOnEnter(e, vm); PixelWidthTextBox.KeyUp += (_, _) => AdjustAspectRatio(PixelWidthTextBox); PixelHeightTextBox.KeyUp += (_, _) => AdjustAspectRatio(PixelHeightTextBox); ConversionComboBox.SelectionChanged += (_, _) => { UpdateQualitySliderState(); ShowResetButton(); }; ResetButton.Click += (_, _) => ResetSettings(vm); CancelButton.Click += (_, _) => (VisualRoot as Window)?.Close(); LinkChainButton.Click += (_, _) => ToggleAspectRatio(); } private void ShowResetButton() { CancelButton.IsVisible = false; ResetButton.IsVisible = true; } private void ShowCancelButton() { CancelButton.IsVisible = true; ResetButton.IsVisible = false; } private void AdjustAspectRatio(TextBox sender) { if (!_isKeepingAspectRatio) { return; } AspectRatioHelper.SetAspectRatioForTextBox( PixelWidthTextBox, PixelHeightTextBox, sender == PixelWidthTextBox, _aspectRatio, DataContext as MainViewModel); ShowResetButton(); } private void UpdateQualitySliderState() { if (DataContext is not MainViewModel vm) { return; } try { if (IsConversionToQualityFormat()) { QualitySlider.IsEnabled = true; QualitySlider.Value = 75; } else if (IsOriginalFileQualityFormat(vm.PicViewer.FileInfo.Extension)) { QualitySlider.IsEnabled = true; QualitySlider.Value = ImageAnalyzer.GetCompressionQuality(vm.PicViewer.FileInfo.FullName); } else { QualitySlider.IsEnabled = false; } } catch (Exception e) { #if DEBUG Console.WriteLine(e); #endif } } private bool IsConversionToQualityFormat() => JpgItem.IsSelected || PngItem.IsSelected; private static bool IsOriginalFileQualityFormat(string ext) => ext.Equals(".jpg", StringComparison.OrdinalIgnoreCase) || ext.Equals(".jpeg", StringComparison.OrdinalIgnoreCase) || ext.Equals(".png", StringComparison.OrdinalIgnoreCase); private async Task SaveImageOnEnter(KeyEventArgs e, MainViewModel vm) { if (e.Key == Key.Enter) { await SaveImage(vm).ConfigureAwait(false); } } private async Task SaveImageAs(MainViewModel vm) { if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop || desktop.MainWindow?.StorageProvider is not { } provider) { return; } var fileInfoFullName = vm.PicViewer.FileInfo.FullName; var ext = GetSelectedFileExtension(vm, ref fileInfoFullName); var file = await FilePicker.PickFileForSavingAsync(vm.PicViewer.FileInfo?.FullName, ext); if (file is null) { return; } await DoSaveImage(vm, file).ConfigureAwait(false); } private async Task SaveImage(MainViewModel vm) { await DoSaveImage(vm, vm.PicViewer.FileInfo.FullName).ConfigureAwait(false); } private async Task DoSaveImage(MainViewModel vm, string destination) { if (!uint.TryParse(PixelWidthTextBox.Text, out var width) || !uint.TryParse(PixelHeightTextBox.Text, out var height)) { return; } await Dispatcher.UIThread.InvokeAsync(() => SetLoadingState(true)); const int rotationAngle = 0; // TODO: Add rotation control var file = vm.PicViewer.FileInfo.FullName; var ext = GetSelectedFileExtension(vm, ref destination); destination = Path.ChangeExtension(destination, ext); var sameFile = file.Equals(destination, StringComparison.OrdinalIgnoreCase); var quality = GetQualityValue(ext, destination); var success = await SaveImageFileHelper.SaveImageAsync( null, file, sameFile ? null : destination, width, height, quality, ext, rotationAngle, null, _isKeepingAspectRatio).ConfigureAwait(false); await Dispatcher.UIThread.InvokeAsync(() => SetLoadingState(false)); if (!success) { await TooltipHelper.ShowTooltipMessageAsync(TranslationManager.Translation.SavingFileFailed); return; } await HandlePostSaveActions(vm, file, destination); if (Path.GetExtension(file) != ext) { await vm.PlatformService.DeleteFile(file, true); } } private void SetLoadingState(bool isLoading) { ParentContainer.Opacity = isLoading ? 0.1 : 1; ParentContainer.IsHitTestVisible = !isLoading; SpinWaiter.IsVisible = isLoading; } private string GetSelectedFileExtension(MainViewModel vm, ref string destination) { var ext = vm.PicViewer.FileInfo.Extension; if (NoConversion.IsSelected) { return ext; } ext = GetExtensionFromSelectedItem() ?? ext; destination = Path.ChangeExtension(destination, ext); return ext; } private string? GetExtensionFromSelectedItem() { if (PngItem.IsSelected) { return ".png"; } if (JpgItem.IsSelected) { return ".jpg"; } if (WebpItem.IsSelected) { return ".webp"; } if (AvifItem.IsSelected) { return ".avif"; } if (HeicItem.IsSelected) { return ".heic"; } if (JxlItem.IsSelected) { return ".jxl"; } return null; } private uint? GetQualityValue(string ext, string destination) { if (QualitySlider.IsEnabled && ( ext.Equals(".jpg", StringComparison.OrdinalIgnoreCase) || Path.GetExtension(destination).Equals(".jpg", StringComparison.OrdinalIgnoreCase) || Path.GetExtension(destination).Equals(".jpeg", StringComparison.OrdinalIgnoreCase))) { return (uint)QualitySlider.Value; } return null; } private static async Task HandlePostSaveActions(MainViewModel vm, string file, string destination) { if (destination == file) { await NavigationManager.QuickReload().ConfigureAwait(false); } else if (Path.GetDirectoryName(file) == Path.GetDirectoryName(destination)) { await NavigationManager.LoadPicFromFile(destination, vm).ConfigureAwait(false); } } private void ResetSettings(MainViewModel vm) { PixelWidthTextBox.Text = vm.PicViewer.PixelWidth.ToString(); PixelHeightTextBox.Text = vm.PicViewer.PixelHeight.ToString(); if (IsOriginalFileQualityFormat(vm.PicViewer.FileInfo.Extension)) { QualitySlider.IsEnabled = true; QualitySlider.Value = ImageAnalyzer.GetCompressionQuality(vm.PicViewer.FileInfo.FullName); } else { QualitySlider.IsEnabled = false; } ConversionComboBox.SelectedItem = NoConversion; _isKeepingAspectRatio = true; LinkChainImage.IsVisible = true; UnlinkChainImage.IsVisible = false; ShowCancelButton(); } private void ToggleAspectRatio() { _isKeepingAspectRatio = !_isKeepingAspectRatio; LinkChainImage.IsVisible = _isKeepingAspectRatio; UnlinkChainImage.IsVisible = !_isKeepingAspectRatio; if (_isKeepingAspectRatio) { AdjustAspectRatio(PixelWidthTextBox); } if (!_isKeepingAspectRatio) { ShowResetButton(); } } ~SingleImageResizeView() { _imageUpdateSubscription?.Dispose(); } }