diff --git a/src/Filee.App/Assets/i18n/ar.json b/src/Filee.App/Assets/i18n/ar.json index 3de1151e..9e35eec7 100644 --- a/src/Filee.App/Assets/i18n/ar.json +++ b/src/Filee.App/Assets/i18n/ar.json @@ -210,6 +210,9 @@ "crop.aspect_title": "نسبة الأبعاد:", "crop.aspect_free": "حرة", "crop.aspect_original": "الأصلية", + "crop.aspect_custom": "نسبة مخصّصة (اكتب العرض والارتفاع بجانب زر النسبة)", + "crop.custom_width": "العرض", + "crop.custom_height": "الارتفاع", "crop.page_counter": "{0} من {1}", "crop.save_next": "اقتصاص والتالي", "crop.swap": "تبديل الوضع الطولي والعرضي (X)", diff --git a/src/Filee.App/Assets/i18n/de.json b/src/Filee.App/Assets/i18n/de.json index fc742a82..d669c412 100644 --- a/src/Filee.App/Assets/i18n/de.json +++ b/src/Filee.App/Assets/i18n/de.json @@ -210,6 +210,9 @@ "crop.aspect_title": "Seitenverhältnis:", "crop.aspect_free": "Frei", "crop.aspect_original": "Original", + "crop.aspect_custom": "Eigenes Verhältnis (B und H neben der Verhältnis-Taste eingeben)", + "crop.custom_width": "Breite (B)", + "crop.custom_height": "Höhe (H)", "crop.page_counter": "{0} von {1}", "crop.save_next": "Zuschneiden & Weiter", "crop.swap": "Hoch- und Querformat tauschen (X)", diff --git a/src/Filee.App/Assets/i18n/en.json b/src/Filee.App/Assets/i18n/en.json index 75d0ee92..c412965d 100644 --- a/src/Filee.App/Assets/i18n/en.json +++ b/src/Filee.App/Assets/i18n/en.json @@ -219,6 +219,9 @@ "crop.aspect_title": "Aspect:", "crop.aspect_free": "Free", "crop.aspect_original": "Original", + "crop.aspect_custom": "Custom ratio (type W and H beside the ratio button)", + "crop.custom_width": "Width (W)", + "crop.custom_height": "Height (H)", "crop.page_counter": "{0} of {1}", "crop.save_next": "Crop & Next", "crop.swap": "Swap portrait and landscape (X)", diff --git a/src/Filee.App/Assets/i18n/es.json b/src/Filee.App/Assets/i18n/es.json index c074a17f..fcf60fc1 100644 --- a/src/Filee.App/Assets/i18n/es.json +++ b/src/Filee.App/Assets/i18n/es.json @@ -210,6 +210,9 @@ "crop.aspect_title": "Proporción:", "crop.aspect_free": "Libre", "crop.aspect_original": "Original", + "crop.aspect_custom": "Proporción personalizada (escribe An y Al junto al botón de proporción)", + "crop.custom_width": "Ancho (An)", + "crop.custom_height": "Alto (Al)", "crop.page_counter": "{0} de {1}", "crop.save_next": "Recortar y seguir", "crop.swap": "Cambiar entre vertical y horizontal (X)", diff --git a/src/Filee.App/Assets/i18n/fr.json b/src/Filee.App/Assets/i18n/fr.json index 68101164..f8e50731 100644 --- a/src/Filee.App/Assets/i18n/fr.json +++ b/src/Filee.App/Assets/i18n/fr.json @@ -219,6 +219,9 @@ "crop.aspect_title": "Proportions :", "crop.aspect_free": "Libre", "crop.aspect_original": "Original", + "crop.aspect_custom": "Rapport personnalisé (saisissez L et H à côté du bouton du rapport)", + "crop.custom_width": "Largeur (L)", + "crop.custom_height": "Hauteur (H)", "crop.page_counter": "{0} sur {1}", "crop.save_next": "Recadrer et suivant", "crop.swap": "Inverser portrait et paysage (X)", diff --git a/src/Filee.App/Assets/i18n/hi.json b/src/Filee.App/Assets/i18n/hi.json index 957153f4..dd79a945 100644 --- a/src/Filee.App/Assets/i18n/hi.json +++ b/src/Filee.App/Assets/i18n/hi.json @@ -210,6 +210,9 @@ "crop.aspect_title": "अनुपात:", "crop.aspect_free": "मुक्त", "crop.aspect_original": "मूल", + "crop.aspect_custom": "अपना अनुपात (अनुपात बटन के पास चौड़ाई और ऊँचाई लिखें)", + "crop.custom_width": "चौड़ाई", + "crop.custom_height": "ऊँचाई", "crop.page_counter": "{1} में से {0}", "crop.save_next": "क्रॉप करके अगला", "crop.swap": "खड़ा और आड़ा बदलें (X)", diff --git a/src/Filee.App/Assets/i18n/ja.json b/src/Filee.App/Assets/i18n/ja.json index 5dcc03f9..aa001ac5 100644 --- a/src/Filee.App/Assets/i18n/ja.json +++ b/src/Filee.App/Assets/i18n/ja.json @@ -210,6 +210,9 @@ "crop.aspect_title": "縦横比:", "crop.aspect_free": "自由", "crop.aspect_original": "元の比率", + "crop.aspect_custom": "カスタム比率 (比率ボタンの横に幅と高さを入力)", + "crop.custom_width": "幅", + "crop.custom_height": "高さ", "crop.page_counter": "{0}/{1}", "crop.save_next": "トリミングして次へ", "crop.swap": "縦向きと横向きを切り替え (X)", diff --git a/src/Filee.App/Assets/i18n/ko.json b/src/Filee.App/Assets/i18n/ko.json index 5e899b54..b4a5bc01 100644 --- a/src/Filee.App/Assets/i18n/ko.json +++ b/src/Filee.App/Assets/i18n/ko.json @@ -219,6 +219,9 @@ "crop.aspect_title": "비율:", "crop.aspect_free": "자유", "crop.aspect_original": "원본", + "crop.aspect_custom": "직접 입력 비율 (비율 버튼 옆에 너비와 높이를 입력)", + "crop.custom_width": "너비", + "crop.custom_height": "높이", "crop.page_counter": "{0}/{1}", "crop.save_next": "자르고 다음으로", "crop.swap": "세로/가로 바꾸기 (X)", diff --git a/src/Filee.App/Assets/i18n/ne.json b/src/Filee.App/Assets/i18n/ne.json index a9a85890..b79c62ea 100644 --- a/src/Filee.App/Assets/i18n/ne.json +++ b/src/Filee.App/Assets/i18n/ne.json @@ -210,6 +210,9 @@ "crop.aspect_title": "अनुपात:", "crop.aspect_free": "स्वतन्त्र", "crop.aspect_original": "मूल", + "crop.aspect_custom": "आफ्नै अनुपात (अनुपात बटनको छेउमा चौडाइ र उचाइ लेख्नुहोस्)", + "crop.custom_width": "चौडाइ", + "crop.custom_height": "उचाइ", "crop.page_counter": "{1} मध्ये {0}", "crop.save_next": "क्रप गरी अर्कोमा जानुहोस्", "crop.swap": "ठाडो र तेर्सो बदल्नुहोस् (X)", diff --git a/src/Filee.App/Assets/i18n/pl.json b/src/Filee.App/Assets/i18n/pl.json index 72f4aa02..ac5afdd3 100644 --- a/src/Filee.App/Assets/i18n/pl.json +++ b/src/Filee.App/Assets/i18n/pl.json @@ -219,6 +219,9 @@ "crop.aspect_title": "Proporcje:", "crop.aspect_free": "Dowolne", "crop.aspect_original": "Oryginalne", + "crop.aspect_custom": "Własne proporcje (wpisz S i W obok przycisku proporcji)", + "crop.custom_width": "Szerokość (S)", + "crop.custom_height": "Wysokość (W)", "crop.page_counter": "{0} z {1}", "crop.save_next": "Kadruj i dalej", "crop.swap": "Zamień pion i poziom (X)", diff --git a/src/Filee.App/Assets/i18n/pt-BR.json b/src/Filee.App/Assets/i18n/pt-BR.json index 75099813..c3c162f3 100644 --- a/src/Filee.App/Assets/i18n/pt-BR.json +++ b/src/Filee.App/Assets/i18n/pt-BR.json @@ -220,6 +220,9 @@ "crop.aspect_title": "Proporção:", "crop.aspect_free": "Livre", "crop.aspect_original": "Original", + "crop.aspect_custom": "Proporção personalizada (digite L e A ao lado do botão de proporção)", + "crop.custom_width": "Largura (L)", + "crop.custom_height": "Altura (A)", "crop.page_counter": "{0} de {1}", "crop.save_next": "Recortar e avançar", "crop.swap": "Trocar retrato e paisagem (X)", diff --git a/src/Filee.App/Assets/i18n/pt-PT.json b/src/Filee.App/Assets/i18n/pt-PT.json index e6ca7c15..f21ce773 100644 --- a/src/Filee.App/Assets/i18n/pt-PT.json +++ b/src/Filee.App/Assets/i18n/pt-PT.json @@ -219,6 +219,9 @@ "crop.aspect_title": "Proporção:", "crop.aspect_free": "Livre", "crop.aspect_original": "Original", + "crop.aspect_custom": "Proporção personalizada (escreva L e A ao lado do botão de proporção)", + "crop.custom_width": "Largura (L)", + "crop.custom_height": "Altura (A)", "crop.page_counter": "{0} de {1}", "crop.save_next": "Recortar e avançar", "crop.swap": "Alternar entre vertical e horizontal (X)", diff --git a/src/Filee.App/Assets/i18n/ru.json b/src/Filee.App/Assets/i18n/ru.json index 29acdd1e..84fae82b 100644 --- a/src/Filee.App/Assets/i18n/ru.json +++ b/src/Filee.App/Assets/i18n/ru.json @@ -219,6 +219,9 @@ "crop.aspect_title": "Пропорции:", "crop.aspect_free": "Произвольные", "crop.aspect_original": "Исходные", + "crop.aspect_custom": "Своё соотношение (введите Ш и В рядом с кнопкой соотношения)", + "crop.custom_width": "Ширина (Ш)", + "crop.custom_height": "Высота (В)", "crop.page_counter": "{0} из {1}", "crop.save_next": "Обрезать и дальше", "crop.swap": "Поменять книжную и альбомную (X)", diff --git a/src/Filee.App/Assets/i18n/th.json b/src/Filee.App/Assets/i18n/th.json index 8c419e6b..cc88d14f 100644 --- a/src/Filee.App/Assets/i18n/th.json +++ b/src/Filee.App/Assets/i18n/th.json @@ -210,6 +210,9 @@ "crop.aspect_title": "สัดส่วน:", "crop.aspect_free": "อิสระ", "crop.aspect_original": "ต้นฉบับ", + "crop.aspect_custom": "อัตราส่วนกำหนดเอง (พิมพ์กว้างและสูงข้างปุ่มอัตราส่วน)", + "crop.custom_width": "กว้าง", + "crop.custom_height": "สูง", "crop.page_counter": "{0} จาก {1}", "crop.save_next": "ครอบตัดและต่อไป", "crop.swap": "สลับแนวตั้งและแนวนอน (X)", diff --git a/src/Filee.App/Assets/i18n/tr.json b/src/Filee.App/Assets/i18n/tr.json index 62a8e5e1..99fa7e39 100644 --- a/src/Filee.App/Assets/i18n/tr.json +++ b/src/Filee.App/Assets/i18n/tr.json @@ -210,6 +210,9 @@ "crop.aspect_title": "Oran:", "crop.aspect_free": "Serbest", "crop.aspect_original": "Özgün", + "crop.aspect_custom": "Özel oran (oran düğmesinin yanına G ve Y yazın)", + "crop.custom_width": "Genişlik (G)", + "crop.custom_height": "Yükseklik (Y)", "crop.page_counter": "{0} / {1}", "crop.save_next": "Kırp ve ilerle", "crop.swap": "Dikey ve yatayı değiştir (X)", diff --git a/src/Filee.App/Assets/i18n/zh-CN.json b/src/Filee.App/Assets/i18n/zh-CN.json index 340de83b..75a970e0 100644 --- a/src/Filee.App/Assets/i18n/zh-CN.json +++ b/src/Filee.App/Assets/i18n/zh-CN.json @@ -219,6 +219,9 @@ "crop.aspect_title": "比例:", "crop.aspect_free": "自由", "crop.aspect_original": "原始比例", + "crop.aspect_custom": "自定义比例(在比例按钮旁输入宽和高)", + "crop.custom_width": "宽", + "crop.custom_height": "高", "crop.page_counter": "{0}/{1}", "crop.save_next": "裁剪并下一张", "crop.swap": "切换纵向与横向 (X)", diff --git a/src/Filee.App/Assets/i18n/zh-TW.json b/src/Filee.App/Assets/i18n/zh-TW.json index ee508a74..6e66cf97 100644 --- a/src/Filee.App/Assets/i18n/zh-TW.json +++ b/src/Filee.App/Assets/i18n/zh-TW.json @@ -219,6 +219,9 @@ "crop.aspect_title": "比例:", "crop.aspect_free": "自由", "crop.aspect_original": "原始比例", + "crop.aspect_custom": "自訂比例(在比例按鈕旁輸入寬和高)", + "crop.custom_width": "寬", + "crop.custom_height": "高", "crop.page_counter": "{0}/{1}", "crop.save_next": "裁剪並下一張", "crop.swap": "切換縱向與橫向 (X)", diff --git a/src/Filee.App/Controls/AspectDial.cs b/src/Filee.App/Controls/AspectDial.cs index 1d4819ad..acb5142e 100644 --- a/src/Filee.App/Controls/AspectDial.cs +++ b/src/Filee.App/Controls/AspectDial.cs @@ -17,7 +17,7 @@ namespace Filee.App.Controls; public sealed class AspectDial : Panel { public static readonly StyledProperty OuterRadiusProperty = - AvaloniaProperty.Register(nameof(OuterRadius), 118); + AvaloniaProperty.Register(nameof(OuterRadius), 132); // room for eight slices' names (#542) public static readonly StyledProperty InnerRadiusProperty = AvaloniaProperty.Register(nameof(InnerRadius), 52); @@ -36,12 +36,17 @@ public sealed class AspectDial : Panel public static readonly StyledProperty OriginalRatioProperty = AvaloniaProperty.Register(nameof(OriginalRatio)); + /// Width / height typed for the custom slice (#542): its outline. + public static readonly StyledProperty CustomRatioProperty = + AvaloniaProperty.Register(nameof(CustomRatio)); + public double OuterRadius { get => GetValue(OuterRadiusProperty); set => SetValue(OuterRadiusProperty, value); } public double InnerRadius { get => GetValue(InnerRadiusProperty); set => SetValue(InnerRadiusProperty, value); } public CornerRadius CornerRadius { get => GetValue(CornerRadiusProperty); set => SetValue(CornerRadiusProperty, value); } public string? SelectedMode { get => GetValue(SelectedModeProperty); set => SetValue(SelectedModeProperty, value); } public bool IsPortrait { get => GetValue(IsPortraitProperty); set => SetValue(IsPortraitProperty, value); } public double OriginalRatio { get => GetValue(OriginalRatioProperty); set => SetValue(OriginalRatioProperty, value); } + public double CustomRatio { get => GetValue(CustomRatioProperty); set => SetValue(CustomRatioProperty, value); } /// Gap between slices, as on the donut. public const double Gap = 4; @@ -49,7 +54,7 @@ public sealed class AspectDial : Panel static AspectDial() { AffectsArrange(OuterRadiusProperty, InnerRadiusProperty, CornerRadiusProperty, SelectedModeProperty, - IsPortraitProperty, OriginalRatioProperty); + IsPortraitProperty, OriginalRatioProperty, CustomRatioProperty); AffectsMeasure(OuterRadiusProperty); } @@ -93,7 +98,7 @@ protected override Size ArrangeOverride(Size finalSize) { var segment = segments[i]; segment.Place(center, i, segments.Count, InnerRadius, OuterRadius, Math.Clamp(CornerRadius.TopLeft, 0, 28) * 0.5); - segment.Show(SelectedMode == segment.Mode, IsPortrait, OriginalRatio); + segment.Show(SelectedMode == segment.Mode, IsPortrait, OriginalRatio, CustomRatio); segment.Arrange(new Rect(finalSize)); } @@ -141,6 +146,7 @@ public sealed class RingSegment : Button, ICustomHitTest private Geometry? _wedge; private bool _portrait; private double _originalRatio; + private double _customRatio; /// Whether this slice's ratio is the one in use. public bool IsSelected { get; private set; } @@ -170,12 +176,13 @@ internal void Place(Point center, int index, int count, double inner, double out InvalidateVisual(); } - internal void Show(bool selected, bool portrait, double originalRatio) + internal void Show(bool selected, bool portrait, double originalRatio, double customRatio = 0) { var label = Label; IsSelected = selected; _portrait = portrait; _originalRatio = originalRatio; + _customRatio = customRatio; PseudoClasses.Set(":selected", selected); if (label != Label || AutomationProperties.GetName(this) is null or "") AutomationProperties.SetName(this, Label); @@ -237,8 +244,8 @@ public override void Render(DrawingContext context) } /// - /// The smallest size a long one-word label shrinks to before it breaks. Ru's "Произвольные" needs 9.4 with seven - /// slices on a desktop and 8.7 on CI's runners, whose fonts differ, so there's room below that. + /// The smallest size a long one-word label shrinks to before it breaks. The dial draws in the embedded Noto Sans, where + /// Ru's "Произвольные" needs about 8.7 on seven slices; with eight, the dial is wider (OuterRadius) so it still fits. /// internal const double MinLabelSize = 8; @@ -251,7 +258,7 @@ private void DrawShape(DrawingContext context, IBrush? ink, Point center) { if (ink is null) return; - var ratio = Mode == "original" ? _originalRatio : CropMath.Ratio(Mode, _portrait, 0, 0); + var ratio = Mode == "original" ? _originalRatio : CropMath.Ratio(Mode, _portrait, 0, 0, _customRatio); const double box = 15; double w = box, h = box; if (ratio > 0) diff --git a/src/Filee.App/Controls/CropMath.cs b/src/Filee.App/Controls/CropMath.cs index 2ad1a721..55252f03 100644 --- a/src/Filee.App/Controls/CropMath.cs +++ b/src/Filee.App/Controls/CropMath.cs @@ -10,16 +10,22 @@ public static class CropMath /// The aspect dial's choices, clockwise from the top. public static IReadOnlyList AspectModes => Modes; - private static readonly string[] Modes = ["free", "original", "1:1", "5:4", "4:3", "3:2", "16:9"]; + private static readonly string[] Modes = ["free", "original", "1:1", "5:4", "4:3", "3:2", "16:9", "custom"]; /// Where the mode sits on the dial, -1 for none. public static int AspectIndex(string mode) => Array.IndexOf(Modes, mode); - /// A fixed ratio with a portrait and a landscape form (not free, the picture's own or square). - public static bool CanSwap(string mode) => mode is "5:4" or "4:3" or "3:2" or "16:9"; + /// + /// A ratio with a portrait and a landscape form (not free, the picture's own or square). "custom" turns too: its + /// W × H as typed, or H × W. + /// + public static bool CanSwap(string mode) => mode is "5:4" or "4:3" or "3:2" or "16:9" or "custom"; - /// Width / height of the mode, 0 for free. Portrait turns a swappable ratio around (4:3 → 3:4). - public static double Ratio(string mode, bool portrait, int originalWidth, int originalHeight) + /// + /// Width / height of the mode, 0 for free. Portrait turns a swappable ratio around (4:3 → 3:4). + /// is the typed W / H of "custom" (#542). + /// + public static double Ratio(string mode, bool portrait, int originalWidth, int originalHeight, double custom = 0) { var ratio = mode switch { @@ -29,15 +35,29 @@ public static double Ratio(string mode, bool portrait, int originalWidth, int or "4:3" => 4.0 / 3.0, "3:2" => 3.0 / 2.0, "16:9" => 16.0 / 9.0, + "custom" => custom, _ => 0, }; - return portrait && CanSwap(mode) ? 1 / ratio : ratio; + return portrait && CanSwap(mode) && ratio > 0 ? 1 / ratio : ratio; + } + + /// + /// The custom ratio's numbers as small as they go ("1080 × 1350" → "4:5"), turned around in portrait. Only the ratio + /// counts: resizing to exact pixels is a preset's job. + /// + public static string CustomNumbers(int width, int height, bool portrait) + { + var divisor = Gcd(Math.Max(1, width), Math.Max(1, height)); + var (w, h) = (Math.Max(1, width) / divisor, Math.Max(1, height) / divisor); + return portrait ? $"{h}:{w}" : $"{w}:{h}"; + + static int Gcd(int a, int b) => b == 0 ? a : Gcd(b, a % b); } - /// The numbers shown for a mode: "4:3", or "3:4" in portrait. Free and original have none. + /// The numbers shown for a mode: "4:3", or "3:4" in portrait. Free, original and custom have none here. public static string Numbers(string mode, bool portrait) { - if (!CanSwap(mode) || !portrait) + if (!CanSwap(mode) || !portrait || mode == "custom") return mode; var parts = mode.Split(':'); return $"{parts[1]}:{parts[0]}"; diff --git a/src/Filee.App/ViewModels/CropOverlayViewModel.cs b/src/Filee.App/ViewModels/CropOverlayViewModel.cs index da9e1993..20a0c8c6 100644 --- a/src/Filee.App/ViewModels/CropOverlayViewModel.cs +++ b/src/Filee.App/ViewModels/CropOverlayViewModel.cs @@ -60,15 +60,52 @@ public sealed partial class CropOverlayViewModel : ObservableObject, IDisposable [NotifyPropertyChangedFor(nameof(SelectedAspectIndex))] [NotifyPropertyChangedFor(nameof(CanSwapOrientation))] [NotifyPropertyChangedFor(nameof(IsAspectFree))] + [NotifyPropertyChangedFor(nameof(IsCustomAspect))] private string _selectedAspect = "free"; - /// The fixed ratios (5:4, 4:3, 3:2, 16:9) turned upright: 4:5, 3:4, 2:3, 9:16. + /// The fixed ratios (5:4, 4:3, 3:2, 16:9) turned upright: 4:5, 3:4, 2:3, 9:16; the custom one as H × W. [ObservableProperty] [NotifyPropertyChangedFor(nameof(AspectLabel))] [NotifyPropertyChangedFor(nameof(AspectToolTip))] [NotifyPropertyChangedFor(nameof(OrientationText))] + [NotifyPropertyChangedFor(nameof(CustomLabel))] private bool _isPortrait; + /// + /// The custom ratio's W and H (#542), typed beside the ratio button: 4 × 5 or 1080 × 1350, only the ratio counts. + /// Kept while the crop window is open, for the whole batch; no setting. + /// + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CustomRatio), nameof(CustomLabel), nameof(AspectLabel), nameof(AspectToolTip))] + private int _customWidth = 2; // a ratio no other slice has, so the slice reads as its own + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CustomRatio), nameof(CustomLabel), nameof(AspectLabel), nameof(AspectToolTip))] + private int _customHeight = 1; + + public double CustomRatio => (double)Math.Max(1, CustomWidth) / Math.Max(1, CustomHeight); + + /// The custom slice's name: its ratio as small as it goes ("4:5"), as the other ratio slices show theirs. + public string CustomLabel => CropMath.CustomNumbers(CustomWidth, CustomHeight, IsPortrait && IsCustomAspect); + + public bool IsCustomAspect => SelectedAspect == "custom"; + + partial void OnCustomWidthChanged(int value) => CustomChanged(); + + partial void OnCustomHeightChanged(int value) => CustomChanged(); + + private void CustomChanged() + { + if (!IsCustomAspect) + return; + ApplyAspect("custom"); + // Typed a digit at a time, the ratio passes through odd shapes (1080 × 5): the frame starts over as large as + // the new ratio fits, rather than shrinking along with each of them. + var frame = CropMath.Centered(AspectRatio, OriginalWidth, OriginalHeight); + UpdateDimensions(frame); + ResetRequested?.Invoke(frame); + } + [ObservableProperty] private bool _isAspectDialOpen; [ObservableProperty] @@ -126,6 +163,7 @@ public sealed partial class CropOverlayViewModel : ObservableObject, IDisposable { "free" => _loc["crop.aspect_free"], "original" => _loc["crop.aspect_original"], + "custom" => CustomLabel, _ => CropMath.Numbers(SelectedAspect, IsPortrait), }; @@ -201,7 +239,7 @@ public async Task LoadFileAsync(int index) SelectedAspect = mode; IsPortrait = CropMath.CanSwap(mode) ? _batchPortrait : origH > origW; IsAspectDialOpen = false; - AspectRatio = CropMath.Ratio(mode, IsPortrait, origW, origH); + AspectRatio = CropMath.Ratio(mode, IsPortrait, origW, origH, CustomRatio); AspectRatioChanged?.Invoke(AspectRatio); var frame = CropMath.Centered(AspectRatio, origW, origH); UpdateDimensions(frame); @@ -336,6 +374,9 @@ public void Cancel() public void SetAspect(string mode) { IsAspectDialOpen = false; + // The custom ratio is W × H as typed, whatever way the picture stands; the hub turns it. + if (mode == "custom" && !IsCustomAspect) + IsPortrait = false; ApplyAspect(mode); } @@ -354,6 +395,8 @@ public void CycleAspect(int step) { var modes = CropMath.AspectModes; var next = ((SelectedAspectIndex + step) % modes.Count + modes.Count) % modes.Count; + if (modes[next] == "custom") + IsPortrait = false; // as typed, see SetAspect ApplyAspect(modes[next]); } @@ -368,8 +411,9 @@ private void ApplyAspect(string mode) { (_batchAspect, _batchPortrait) = (mode == "free" ? null : mode, IsPortrait); SelectedAspect = mode; - AspectRatio = CropMath.Ratio(mode, IsPortrait, OriginalWidth, OriginalHeight); + AspectRatio = CropMath.Ratio(mode, IsPortrait, OriginalWidth, OriginalHeight, CustomRatio); AspectRatioChanged?.Invoke(AspectRatio); + OnPropertyChanged(nameof(CustomLabel)); } private void AddFailure(string path, string errorKey, string? detail) diff --git a/src/Filee.App/Views/CropOverlayWindow.axaml b/src/Filee.App/Views/CropOverlayWindow.axaml index 4c8c86a7..c52e3c47 100644 --- a/src/Filee.App/Views/CropOverlayWindow.axaml +++ b/src/Filee.App/Views/CropOverlayWindow.axaml @@ -309,6 +309,16 @@ + + + + + + @@ -403,7 +413,8 @@ + SelectedMode="{Binding SelectedAspect}" IsPortrait="{Binding IsPortrait}" OriginalRatio="{Binding OriginalRatio}" + CustomRatio="{Binding CustomRatio}"> @@ -411,6 +422,10 @@ + + +