diff --git a/.gitignore b/.gitignore index 2e1a675..cd2a080 100644 --- a/.gitignore +++ b/.gitignore @@ -49,7 +49,7 @@ app.*.map.json *.deb *.tar.gz *.exe -!dist/imajviewer_1.6.0_amd64.deb +!dist/imajviewer_1.6.1_amd64.deb !dist/imajviewer_setup_1.1.0.exe deb/usr/lib/imajviewer/ release/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a872bb..00223e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,18 @@ All notable changes to imajViewer are documented in this file. ## [Unreleased] +## [1.6.1] - 2026-10-14 + +### Fixed +- Horizontal flip shortcut (`Ctrl+R`) never fired when the keyboard layout resolves the R key to a + different logical key (reported with a Turkish/remote-desktop setup where the R key arrives as + logical `T`). The shortcut now matches the physical R key position as well. + ## [1.6.0] - 2026-10-14 ### Added -- Horizontal flip (`R` key): mirrors the image left-right about the viewport centre and clears - the active effects (same reset as double right click); pressing `R` again restores the +- Horizontal flip (`Ctrl+R`): mirrors the image left-right about the viewport centre and clears + the active effects (same reset as double right click); pressing `Ctrl+R` again restores the original orientation. Zoom/pan are kept. - Crop save (`Ctrl+S`) now bakes the flip into the output as well, so the saved JPEG matches the mirrored view (also in combination with rotation). diff --git a/README.md b/README.md index e4a23bf..89d5eb0 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,9 @@ Ultra-lightweight image viewer written in Flutter — Linux & Windows. - **Saturation:** Ctrl + Right drag + vertical move - **Brightness:** Ctrl+Shift + Right drag + vertical move - **Rotate:** Shift + Right drag + vertical move (~100px = 90°) -- **Flip horizontally:** `R` — mirrors the view and clears the active effects (same reset as - double right click); press `R` again to undo +- **Flip horizontally:** `Ctrl+R` — mirrors the view and clears the active effects (same reset as + double right click); press `Ctrl+R` again to undo. Also matches the physical R key position, so + keymaps that report the R key as a different character still work - **Reset (all effects):** Double right click - **Navigation arrows:** bottom corners, visible on hover - **Settings (gear):** sort by time/name, print, shortcut info @@ -88,7 +89,7 @@ folder including `imajviewer.exe` and all DLLs). | Ctrl + Right drag + vertical | Saturation (0.0 – 2.0) | | Ctrl+Shift + Right drag + vertical | Brightness (-100 – 100) | | Shift + Right drag + vertical | Rotate (free) | -| R | Flip horizontally (clears effects; press again to undo) | +| Ctrl + R | Flip horizontally (clears effects; press again to undo) | | Double right click | Reset (all effects) | | Left / Right arrow | Previous / next image | | Ctrl + S | Save visible area (JPEG 93, original resolution) | diff --git a/dist/imajviewer_1.6.0_amd64.deb b/dist/imajviewer_1.6.1_amd64.deb similarity index 60% rename from dist/imajviewer_1.6.0_amd64.deb rename to dist/imajviewer_1.6.1_amd64.deb index ec8fc13..21cc33f 100644 Binary files a/dist/imajviewer_1.6.0_amd64.deb and b/dist/imajviewer_1.6.1_amd64.deb differ diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 573cd01..955c991 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -6,7 +6,7 @@ "sortByName": "By name", "appDescription": "Ultra-lightweight and fast image viewer. Open images and use arrows or keyboard keys to navigate.", "shortcuts": "Shortcuts", - "shortcutsList": "• Left/Right arrow — previous/next image\n• Ctrl+S — save visible area (JPEG 93)\n• Ctrl+Q — close window\n• Ctrl+Shift+Q — close all windows\n• Double click — toggle fill/fit\n• Right-drag — contrast\n• Shift+right-drag — rotate\n• Ctrl+right-drag — saturation\n• Ctrl+Shift+right-drag — brightness\n• R — flip horizontally (resets effects, press again to undo)", + "shortcutsList": "• Left/Right arrow — previous/next image\n• Ctrl+S — save visible area (JPEG 93)\n• Ctrl+R — flip horizontally (clears effects, press again to undo)\n• Ctrl+Q — close window\n• Ctrl+Shift+Q — close all windows\n• Double click — toggle fill/fit\n• Right-drag — contrast\n• Shift+right-drag — rotate\n• Ctrl+right-drag — saturation\n• Ctrl+Shift+right-drag — brightness", "print": "Print", "close": "Close", "saveCrop": "Save visible area (JPEG 93)", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 9749391..4126311 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -137,7 +137,7 @@ abstract class AppLocalizations { /// No description provided for @shortcutsList. /// /// In en, this message translates to: - /// **'• Left/Right arrow — previous/next image\n• Ctrl+S — save visible area (JPEG 93)\n• Ctrl+Q — close window\n• Ctrl+Shift+Q — close all windows\n• Double click — toggle fill/fit\n• Right-drag — contrast\n• Shift+right-drag — rotate\n• Ctrl+right-drag — saturation\n• Ctrl+Shift+right-drag — brightness\n• R — flip horizontally (resets effects, press again to undo)'** + /// **'• Left/Right arrow — previous/next image\n• Ctrl+S — save visible area (JPEG 93)\n• Ctrl+R — flip horizontally (clears effects, press again to undo)\n• Ctrl+Q — close window\n• Ctrl+Shift+Q — close all windows\n• Double click — toggle fill/fit\n• Right-drag — contrast\n• Shift+right-drag — rotate\n• Ctrl+right-drag — saturation\n• Ctrl+Shift+right-drag — brightness'** String get shortcutsList; /// No description provided for @print. diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index aba75e3..6459ab3 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -29,7 +29,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get shortcutsList => - '• Left/Right arrow — previous/next image\n• Ctrl+S — save visible area (JPEG 93)\n• Ctrl+Q — close window\n• Ctrl+Shift+Q — close all windows\n• Double click — toggle fill/fit\n• Right-drag — contrast\n• Shift+right-drag — rotate\n• Ctrl+right-drag — saturation\n• Ctrl+Shift+right-drag — brightness\n• R — flip horizontally (resets effects, press again to undo)'; + '• Left/Right arrow — previous/next image\n• Ctrl+S — save visible area (JPEG 93)\n• Ctrl+R — flip horizontally (clears effects, press again to undo)\n• Ctrl+Q — close window\n• Ctrl+Shift+Q — close all windows\n• Double click — toggle fill/fit\n• Right-drag — contrast\n• Shift+right-drag — rotate\n• Ctrl+right-drag — saturation\n• Ctrl+Shift+right-drag — brightness'; @override String get print => 'Print'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index ab874ad..df82f3f 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -29,7 +29,7 @@ class AppLocalizationsTr extends AppLocalizations { @override String get shortcutsList => - '• Sol/Sağ ok — önceki/sonraki resim\n• Ctrl+S — görünen alanı kaydet (JPEG 93)\n• Ctrl+Q — pencereyi kapat\n• Ctrl+Shift+Q — tüm pencereleri kapat\n• Çift tık — doldur/sığdır geçişi\n• Sağ tık sürükle — kontrast\n• Shift+sağ tık — döndür\n• Ctrl+sağ tık — doygunluk\n• Ctrl+Shift+sağ tık — parlaklık\n• R — yatay aynala (efektleri sıfırlar, tekrar basınca geri alır)'; + '• Sol/Sağ ok — önceki/sonraki resim\n• Ctrl+S — görünen alanı kaydet (JPEG 93)\n• Ctrl+R — yatay aynala (efektleri sıfırlar, tekrar basınca geri alır)\n• Ctrl+Q — pencereyi kapat\n• Ctrl+Shift+Q — tüm pencereleri kapat\n• Çift tık — doldur/sığdır geçişi\n• Sağ tık sürükle — kontrast\n• Shift+sağ tık — döndür\n• Ctrl+sağ tık — doygunluk\n• Ctrl+Shift+sağ tık — parlaklık'; @override String get print => 'Yazdır'; diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 6dad7c9..5e152a2 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -6,7 +6,7 @@ "sortByName": "İsme göre", "appDescription": "Ultra hafif ve hızlı resim görüntüleyici. Görselleri açın, gezinti için okları veya klavye tuşlarını kullanın.", "shortcuts": "Kısayollar", - "shortcutsList": "• Sol/Sağ ok — önceki/sonraki resim\n• Ctrl+S — görünen alanı kaydet (JPEG 93)\n• Ctrl+Q — pencereyi kapat\n• Ctrl+Shift+Q — tüm pencereleri kapat\n• Çift tık — doldur/sığdır geçişi\n• Sağ tık sürükle — kontrast\n• Shift+sağ tık — döndür\n• Ctrl+sağ tık — doygunluk\n• Ctrl+Shift+sağ tık — parlaklık\n• R — yatay aynala (efektleri sıfırlar, tekrar basınca geri alır)", + "shortcutsList": "• Sol/Sağ ok — önceki/sonraki resim\n• Ctrl+S — görünen alanı kaydet (JPEG 93)\n• Ctrl+R — yatay aynala (efektleri sıfırlar, tekrar basınca geri alır)\n• Ctrl+Q — pencereyi kapat\n• Ctrl+Shift+Q — tüm pencereleri kapat\n• Çift tık — doldur/sığdır geçişi\n• Sağ tık sürükle — kontrast\n• Shift+sağ tık — döndür\n• Ctrl+sağ tık — doygunluk\n• Ctrl+Shift+sağ tık — parlaklık", "print": "Yazdır", "close": "Kapat", "saveCrop": "Görünen alanı kaydet (JPEG 93)", diff --git a/lib/screens/viewer_screen.dart b/lib/screens/viewer_screen.dart index e6686d9..7406cec 100644 --- a/lib/screens/viewer_screen.dart +++ b/lib/screens/viewer_screen.dart @@ -190,6 +190,8 @@ class _ViewerScreenState extends State with WindowListener { ); } + void _flipHorizontal() => _canvasKey.currentState?.toggleFlipH(); + Future _cropSave() async { final path = ImageManager.instance.currentImagePath; final state = _canvasKey.currentState; @@ -237,6 +239,7 @@ class _ViewerScreenState extends State with WindowListener { Widget build(BuildContext context) { return AppShortcuts( onCropSave: _hasImages ? _cropSave : null, + onFlipH: _hasImages ? _flipHorizontal : null, child: Scaffold( backgroundColor: Colors.transparent, body: WindowResizeZones( diff --git a/lib/shortcuts/app_shortcuts.dart b/lib/shortcuts/app_shortcuts.dart index eb98eca..a3fa2bc 100644 --- a/lib/shortcuts/app_shortcuts.dart +++ b/lib/shortcuts/app_shortcuts.dart @@ -8,8 +8,14 @@ import '../services/file_handler.dart'; class AppShortcuts extends StatelessWidget { final Widget child; final VoidCallback? onCropSave; + final VoidCallback? onFlipH; - const AppShortcuts({super.key, required this.child, this.onCropSave}); + const AppShortcuts({ + super.key, + required this.child, + this.onCropSave, + this.onFlipH, + }); @override Widget build(BuildContext context) { @@ -44,6 +50,17 @@ class AppShortcuts extends StatelessWidget { } } + // Ctrl+R: yatay aynala / geri al (fiziksel R: bazi klavye haritalarinda + // mantiksal tus T gibi farkli cozuluyor) + if (ctrl && + (event.logicalKey == LogicalKeyboardKey.keyR || + event.physicalKey == PhysicalKeyboardKey.keyR)) { + if (onFlipH != null) { + onFlipH!(); + return KeyEventResult.handled; + } + } + switch (event.logicalKey) { case LogicalKeyboardKey.arrowLeft: ImageManager.instance.previousImage(); diff --git a/lib/widgets/image_canvas.dart b/lib/widgets/image_canvas.dart index 45a090c..d0411e3 100644 --- a/lib/widgets/image_canvas.dart +++ b/lib/widgets/image_canvas.dart @@ -166,13 +166,6 @@ class ImageCanvasState extends State { bool _onHardwareKey(KeyEvent event) { final key = event.logicalKey; if (event is KeyDownEvent) { - if (key == LogicalKeyboardKey.keyR && - !HardwareKeyboard.instance.isControlPressed && - !HardwareKeyboard.instance.isShiftPressed && - !HardwareKeyboard.instance.isAltPressed) { - _toggleFlipH(); - return true; - } if (key == LogicalKeyboardKey.controlLeft || key == LogicalKeyboardKey.controlRight) { _ctrlPressed = true; @@ -224,8 +217,8 @@ class ImageCanvasState extends State { _markRenderDirty(); } - /// R: sağ çift tık gibi efektleri sıfırlar ve yatay aynalar; tekrar basınca geri alır. - void _toggleFlipH() { + /// Ctrl+R: sağ çift tık gibi efektleri sıfırlar ve yatay aynalar; tekrar basınca geri alır. + void toggleFlipH() { _flipH = !_flipH; _angle = 0; _contrast = 1.0; _saturation = 1.0; _brightness = 0; _interactionMode = 'none'; diff --git a/pubspec.yaml b/pubspec.yaml index 65c8659..ad91850 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: imajviewer description: "Ultra-lightweight image viewer for Linux and Windows." publish_to: 'none' -version: 1.6.0+12 +version: 1.6.1+13 msix_config: display_name: ImajViewer diff --git a/test/flip_test.dart b/test/flip_test.dart index 8519cdf..0ae6ae8 100644 --- a/test/flip_test.dart +++ b/test/flip_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image/image.dart' as img; +import 'package:imajviewer/shortcuts/app_shortcuts.dart'; import 'package:imajviewer/widgets/image_canvas.dart'; /// Aynalama uygulandiginda render zincirinde negatif x olcekli bir Transform bulunur. @@ -11,13 +12,37 @@ bool _mirrored(WidgetTester tester) => tester .widgetList(find.byType(Transform)) .any((t) => t.transform.entry(0, 0) < 0); -Future _pressR(WidgetTester tester) async { +Future _pressCtrlR(WidgetTester tester) async { + await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyR); + await tester.pump(); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyR); + await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); await tester.pump(); await tester.pump(); } +Future _pressCtrlRWrongLayout(WidgetTester tester) async { + await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); + await tester.sendKeyDownEvent(LogicalKeyboardKey.keyT, + physicalKey: PhysicalKeyboardKey.keyR); + await tester.pump(); + await tester.sendKeyUpEvent(LogicalKeyboardKey.keyT, + physicalKey: PhysicalKeyboardKey.keyR); + await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); + await tester.pump(); + await tester.pump(); +} + +Widget _app(GlobalKey key, String path, double w, double h) { + return MaterialApp( + home: AppShortcuts( + onFlipH: () => key.currentState?.toggleFlipH(), + child: SizedBox(width: w, height: h, child: ImageCanvas(key: key, filePath: path)), + ), + ); +} + void main() { late Directory tmp; late String pngPath; @@ -30,22 +55,39 @@ void main() { tearDownAll(() => tmp.deleteSync(recursive: true)); - testWidgets('R yatay aynalamayi acar, tekrar basinca kapatir', (tester) async { - await tester.pumpWidget(MaterialApp( - home: SizedBox( - width: 400, - height: 300, - child: ImageCanvas(filePath: pngPath), - ), - )); + testWidgets('Ctrl+R yatay aynalamayi acar, tekrar basinca kapatir', (tester) async { + final key = GlobalKey(); + await tester.pumpWidget(_app(key, pngPath, 400, 300)); await tester.pump(); expect(_mirrored(tester), isFalse, reason: 'baslangicta aynalama olmamali'); - await _pressR(tester); - expect(_mirrored(tester), isTrue, reason: 'R sonrasi aynalama acik olmali'); + await _pressCtrlR(tester); + expect(_mirrored(tester), isTrue, reason: 'Ctrl+R sonrasi aynalama acik olmali'); - await _pressR(tester); - expect(_mirrored(tester), isFalse, reason: 'ikinci R sonrasi eski haline donmeli'); + await _pressCtrlR(tester); + expect(_mirrored(tester), isFalse, reason: 'ikinci Ctrl+R sonrasi eski haline donmeli'); + }); + + testWidgets('Ctrl+R crop-save parametrelerine aynalamayi tasir', (tester) async { + final key = GlobalKey(); + await tester.pumpWidget(_app(key, pngPath, 100, 50)); + await tester.pump(); + await _pressCtrlR(tester); + final vp = key.currentState!.cropViewParams(4, 2); + expect(vp?.flipH, isTrue, reason: 'crop-save aynalamayi almali'); + }); + + testWidgets('fiziksel R tusu mantiksal T olarak gelse de Ctrl+R calisir', (tester) async { + final key = GlobalKey(); + await tester.pumpWidget(_app(key, pngPath, 400, 300)); + await tester.pump(); + + await _pressCtrlRWrongLayout(tester); + expect(_mirrored(tester), isTrue, + reason: 'klavye haritasi R tusunu T olarak cozse bile aynalama acilmali'); + + await _pressCtrlRWrongLayout(tester); + expect(_mirrored(tester), isFalse, reason: 'tekrar basinca geri donmeli'); }); }