diff --git a/CHANGELOG.md b/CHANGELOG.md index 33a500d..f5e3c75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,39 @@ All notable changes to imajViewer are documented in this file. ## [Unreleased] +## [1.9.2] - 2026-10-31 + +### Changed +- Settings dialog is now tabbed (General / Size / Shortcuts) instead of one + long scrolling column; settings dialog panel has no outer margin, no rounded + corners and fills the window on small sizes — separated from the canvas by a + thin border. +- Tab bar spans the panel width; size grid cells stretch to fill the tab. +- Size grid: top-left cell is the `H:W` axis label; cells highlight on hover + with a pointer cursor. +- Sort labels are single words (Time / Name) so they never wrap on narrow + windows; section titles are bold. +- Shortcut key column uses the UI font (monospace removed) and is narrower. +- Footer tagline: "Simple, clean, light, fast image viewer." +- Language row (English / Türkçe / Français) added to the General tab as a + disabled placeholder — switching is not wired yet. +- Title bar: crop-save icon moved from the window controls to the left group, + right next to the settings gear. +- Hover highlight color is selectable in Settings > General (8-color palette, + default red) and applies to title bar buttons, navigation arrows and size + grid cells; stored as `hoverColor` in `window.json`. +- Title bar drag area: grab cursor and a faint hover tint while the pointer is + on the bar (buttons keep the click cursor and their own highlight). + +## [1.9.1] - 2026-10-31 + +### Changed +- Settings dialog: compact single top row — "Settings" title on the left, + print and close as icon buttons on the right (bottom action row removed). +- Settings dialog facelift: capped 520×560 centered dialog, M3 section cards, + segmented sort control, two-column shortcuts, fixed footer, size grid + cells 52×28. + ## [1.9.0] - 2026-10-31 ### Added diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e74fd42..af8b410 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,13 +1,49 @@ { "@@locale": "en", "settingsTitle": "Settings", + "tabGeneral": "General", + "language": "Language", "sortBy": "Sort by", - "sortByTime": "By time", - "sortByName": "By name", + "hoverColor": "Hover color", + "sortByTime": "Time", + "sortByName": "Name", "sizeSection": "Size", - "appDescription": "Ultra-lightweight and fast image viewer. Open images and use arrows or keyboard keys to navigate.", + "appDescription": "Simple, clean, light, fast image viewer.", "shortcuts": "Shortcuts", - "shortcutsList": "• Left/Right arrow — previous/next image\n• Ctrl+O — open file\n• Ctrl+S — save visible area (JPEG 95)\n• Ctrl+R — flip horizontally (clears effects, press again to undo)\n• Ctrl+Q — close window\n• Ctrl+Shift+Q — close all windows\n• F11 — toggle fullscreen\n• Esc — exit fullscreen\n• Scroll — zoom (10% per step)\n• Ctrl+scroll — fine zoom (1% per step)\n• Left-drag — pan\n• Double click — toggle fill/fit\n• Right double click — reset effects\n• Right-drag — contrast\n• Shift+right-drag — rotate\n• Ctrl+right-drag — saturation\n• Ctrl+Shift+right-drag — brightness", + "scNavKey": "Left/Right arrow", + "scNavAction": "previous/next image", + "scOpenKey": "Ctrl+O", + "scOpenAction": "open file", + "scSaveKey": "Ctrl+S", + "scSaveAction": "save visible area (JPEG 95)", + "scFlipKey": "Ctrl+R", + "scFlipAction": "flip horizontally (clears effects, press again to undo)", + "scCloseKey": "Ctrl+Q", + "scCloseAction": "close window", + "scCloseAllKey": "Ctrl+Shift+Q", + "scCloseAllAction": "close all windows", + "scFullscreenKey": "F11", + "scFullscreenAction": "toggle fullscreen", + "scExitFullscreenKey": "Esc", + "scExitFullscreenAction": "exit fullscreen", + "scZoomKey": "Scroll", + "scZoomAction": "zoom (10% per step)", + "scFineZoomKey": "Ctrl+scroll", + "scFineZoomAction": "fine zoom (1% per step)", + "scPanKey": "Left-drag", + "scPanAction": "pan", + "scFillKey": "Double click", + "scFillAction": "toggle fill/fit", + "scResetKey": "Right double click", + "scResetAction": "reset effects", + "scContrastKey": "Right-drag", + "scContrastAction": "contrast", + "scRotateKey": "Shift+right-drag", + "scRotateAction": "rotate", + "scSaturationKey": "Ctrl+right-drag", + "scSaturationAction": "saturation", + "scBrightnessKey": "Ctrl+Shift+right-drag", + "scBrightnessAction": "brightness", "print": "Print", "fileProperties": "File properties", "propResolution": "Resolution", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 73e256b..d61a255 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -104,22 +104,40 @@ abstract class AppLocalizations { /// **'Settings'** String get settingsTitle; + /// No description provided for @tabGeneral. + /// + /// In en, this message translates to: + /// **'General'** + String get tabGeneral; + + /// No description provided for @language. + /// + /// In en, this message translates to: + /// **'Language'** + String get language; + /// No description provided for @sortBy. /// /// In en, this message translates to: /// **'Sort by'** String get sortBy; + /// No description provided for @hoverColor. + /// + /// In en, this message translates to: + /// **'Hover color'** + String get hoverColor; + /// No description provided for @sortByTime. /// /// In en, this message translates to: - /// **'By time'** + /// **'Time'** String get sortByTime; /// No description provided for @sortByName. /// /// In en, this message translates to: - /// **'By name'** + /// **'Name'** String get sortByName; /// No description provided for @sizeSection. @@ -131,7 +149,7 @@ abstract class AppLocalizations { /// No description provided for @appDescription. /// /// In en, this message translates to: - /// **'Ultra-lightweight and fast image viewer. Open images and use arrows or keyboard keys to navigate.'** + /// **'Simple, clean, light, fast image viewer.'** String get appDescription; /// No description provided for @shortcuts. @@ -140,11 +158,209 @@ abstract class AppLocalizations { /// **'Shortcuts'** String get shortcuts; - /// No description provided for @shortcutsList. + /// No description provided for @scNavKey. /// /// In en, this message translates to: - /// **'• Left/Right arrow — previous/next image\n• Ctrl+O — open file\n• Ctrl+S — save visible area (JPEG 95)\n• Ctrl+R — flip horizontally (clears effects, press again to undo)\n• Ctrl+Q — close window\n• Ctrl+Shift+Q — close all windows\n• F11 — toggle fullscreen\n• Esc — exit fullscreen\n• Scroll — zoom (10% per step)\n• Ctrl+scroll — fine zoom (1% per step)\n• Left-drag — pan\n• Double click — toggle fill/fit\n• Right double click — reset effects\n• Right-drag — contrast\n• Shift+right-drag — rotate\n• Ctrl+right-drag — saturation\n• Ctrl+Shift+right-drag — brightness'** - String get shortcutsList; + /// **'Left/Right arrow'** + String get scNavKey; + + /// No description provided for @scNavAction. + /// + /// In en, this message translates to: + /// **'previous/next image'** + String get scNavAction; + + /// No description provided for @scOpenKey. + /// + /// In en, this message translates to: + /// **'Ctrl+O'** + String get scOpenKey; + + /// No description provided for @scOpenAction. + /// + /// In en, this message translates to: + /// **'open file'** + String get scOpenAction; + + /// No description provided for @scSaveKey. + /// + /// In en, this message translates to: + /// **'Ctrl+S'** + String get scSaveKey; + + /// No description provided for @scSaveAction. + /// + /// In en, this message translates to: + /// **'save visible area (JPEG 95)'** + String get scSaveAction; + + /// No description provided for @scFlipKey. + /// + /// In en, this message translates to: + /// **'Ctrl+R'** + String get scFlipKey; + + /// No description provided for @scFlipAction. + /// + /// In en, this message translates to: + /// **'flip horizontally (clears effects, press again to undo)'** + String get scFlipAction; + + /// No description provided for @scCloseKey. + /// + /// In en, this message translates to: + /// **'Ctrl+Q'** + String get scCloseKey; + + /// No description provided for @scCloseAction. + /// + /// In en, this message translates to: + /// **'close window'** + String get scCloseAction; + + /// No description provided for @scCloseAllKey. + /// + /// In en, this message translates to: + /// **'Ctrl+Shift+Q'** + String get scCloseAllKey; + + /// No description provided for @scCloseAllAction. + /// + /// In en, this message translates to: + /// **'close all windows'** + String get scCloseAllAction; + + /// No description provided for @scFullscreenKey. + /// + /// In en, this message translates to: + /// **'F11'** + String get scFullscreenKey; + + /// No description provided for @scFullscreenAction. + /// + /// In en, this message translates to: + /// **'toggle fullscreen'** + String get scFullscreenAction; + + /// No description provided for @scExitFullscreenKey. + /// + /// In en, this message translates to: + /// **'Esc'** + String get scExitFullscreenKey; + + /// No description provided for @scExitFullscreenAction. + /// + /// In en, this message translates to: + /// **'exit fullscreen'** + String get scExitFullscreenAction; + + /// No description provided for @scZoomKey. + /// + /// In en, this message translates to: + /// **'Scroll'** + String get scZoomKey; + + /// No description provided for @scZoomAction. + /// + /// In en, this message translates to: + /// **'zoom (10% per step)'** + String get scZoomAction; + + /// No description provided for @scFineZoomKey. + /// + /// In en, this message translates to: + /// **'Ctrl+scroll'** + String get scFineZoomKey; + + /// No description provided for @scFineZoomAction. + /// + /// In en, this message translates to: + /// **'fine zoom (1% per step)'** + String get scFineZoomAction; + + /// No description provided for @scPanKey. + /// + /// In en, this message translates to: + /// **'Left-drag'** + String get scPanKey; + + /// No description provided for @scPanAction. + /// + /// In en, this message translates to: + /// **'pan'** + String get scPanAction; + + /// No description provided for @scFillKey. + /// + /// In en, this message translates to: + /// **'Double click'** + String get scFillKey; + + /// No description provided for @scFillAction. + /// + /// In en, this message translates to: + /// **'toggle fill/fit'** + String get scFillAction; + + /// No description provided for @scResetKey. + /// + /// In en, this message translates to: + /// **'Right double click'** + String get scResetKey; + + /// No description provided for @scResetAction. + /// + /// In en, this message translates to: + /// **'reset effects'** + String get scResetAction; + + /// No description provided for @scContrastKey. + /// + /// In en, this message translates to: + /// **'Right-drag'** + String get scContrastKey; + + /// No description provided for @scContrastAction. + /// + /// In en, this message translates to: + /// **'contrast'** + String get scContrastAction; + + /// No description provided for @scRotateKey. + /// + /// In en, this message translates to: + /// **'Shift+right-drag'** + String get scRotateKey; + + /// No description provided for @scRotateAction. + /// + /// In en, this message translates to: + /// **'rotate'** + String get scRotateAction; + + /// No description provided for @scSaturationKey. + /// + /// In en, this message translates to: + /// **'Ctrl+right-drag'** + String get scSaturationKey; + + /// No description provided for @scSaturationAction. + /// + /// In en, this message translates to: + /// **'saturation'** + String get scSaturationAction; + + /// No description provided for @scBrightnessKey. + /// + /// In en, this message translates to: + /// **'Ctrl+Shift+right-drag'** + String get scBrightnessKey; + + /// No description provided for @scBrightnessAction. + /// + /// In en, this message translates to: + /// **'brightness'** + String get scBrightnessAction; /// No description provided for @print. /// diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index b06c0c3..fce97f8 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -11,28 +11,135 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settingsTitle => 'Settings'; + @override + String get tabGeneral => 'General'; + + @override + String get language => 'Language'; + @override String get sortBy => 'Sort by'; @override - String get sortByTime => 'By time'; + String get hoverColor => 'Hover color'; @override - String get sortByName => 'By name'; + String get sortByTime => 'Time'; + + @override + String get sortByName => 'Name'; @override String get sizeSection => 'Size'; @override - String get appDescription => - 'Ultra-lightweight and fast image viewer. Open images and use arrows or keyboard keys to navigate.'; + String get appDescription => 'Simple, clean, light, fast image viewer.'; @override String get shortcuts => 'Shortcuts'; @override - String get shortcutsList => - '• Left/Right arrow — previous/next image\n• Ctrl+O — open file\n• Ctrl+S — save visible area (JPEG 95)\n• Ctrl+R — flip horizontally (clears effects, press again to undo)\n• Ctrl+Q — close window\n• Ctrl+Shift+Q — close all windows\n• F11 — toggle fullscreen\n• Esc — exit fullscreen\n• Scroll — zoom (10% per step)\n• Ctrl+scroll — fine zoom (1% per step)\n• Left-drag — pan\n• Double click — toggle fill/fit\n• Right double click — reset effects\n• Right-drag — contrast\n• Shift+right-drag — rotate\n• Ctrl+right-drag — saturation\n• Ctrl+Shift+right-drag — brightness'; + String get scNavKey => 'Left/Right arrow'; + + @override + String get scNavAction => 'previous/next image'; + + @override + String get scOpenKey => 'Ctrl+O'; + + @override + String get scOpenAction => 'open file'; + + @override + String get scSaveKey => 'Ctrl+S'; + + @override + String get scSaveAction => 'save visible area (JPEG 95)'; + + @override + String get scFlipKey => 'Ctrl+R'; + + @override + String get scFlipAction => + 'flip horizontally (clears effects, press again to undo)'; + + @override + String get scCloseKey => 'Ctrl+Q'; + + @override + String get scCloseAction => 'close window'; + + @override + String get scCloseAllKey => 'Ctrl+Shift+Q'; + + @override + String get scCloseAllAction => 'close all windows'; + + @override + String get scFullscreenKey => 'F11'; + + @override + String get scFullscreenAction => 'toggle fullscreen'; + + @override + String get scExitFullscreenKey => 'Esc'; + + @override + String get scExitFullscreenAction => 'exit fullscreen'; + + @override + String get scZoomKey => 'Scroll'; + + @override + String get scZoomAction => 'zoom (10% per step)'; + + @override + String get scFineZoomKey => 'Ctrl+scroll'; + + @override + String get scFineZoomAction => 'fine zoom (1% per step)'; + + @override + String get scPanKey => 'Left-drag'; + + @override + String get scPanAction => 'pan'; + + @override + String get scFillKey => 'Double click'; + + @override + String get scFillAction => 'toggle fill/fit'; + + @override + String get scResetKey => 'Right double click'; + + @override + String get scResetAction => 'reset effects'; + + @override + String get scContrastKey => 'Right-drag'; + + @override + String get scContrastAction => 'contrast'; + + @override + String get scRotateKey => 'Shift+right-drag'; + + @override + String get scRotateAction => 'rotate'; + + @override + String get scSaturationKey => 'Ctrl+right-drag'; + + @override + String get scSaturationAction => 'saturation'; + + @override + String get scBrightnessKey => 'Ctrl+Shift+right-drag'; + + @override + String get scBrightnessAction => 'brightness'; @override String get print => 'Print'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 684cbca..241a16f 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -11,28 +11,136 @@ class AppLocalizationsTr extends AppLocalizations { @override String get settingsTitle => 'Ayarlar'; + @override + String get tabGeneral => 'Genel'; + + @override + String get language => 'Dil'; + @override String get sortBy => 'Sıralama'; @override - String get sortByTime => 'Zamana göre'; + String get hoverColor => 'Hover rengi'; @override - String get sortByName => 'İsme göre'; + String get sortByTime => 'Zaman'; + + @override + String get sortByName => 'İsim'; @override String get sizeSection => 'Boyut'; @override String get 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.'; + 'Basit, temiz, hafif, hızlı resim görüntüleyici.'; @override String get shortcuts => 'Kısayollar'; @override - String get shortcutsList => - '• Sol/Sağ ok — önceki/sonraki resim\n• Ctrl+O — dosya aç\n• Ctrl+S — görünen alanı kaydet (JPEG 95)\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• F11 — tam ekran / normal geçişi\n• Esc — tam ekrandan çık\n• Kaydırma — yakınlaştır (tık başına %10)\n• Ctrl+kaydırma — ince yakınlaştırma (tık başına %1)\n• Sol tık sürükle — kaydır\n• Çift tık — doldur/sığdır geçişi\n• Sağ çift tık — efektleri sıfırla\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'; + String get scNavKey => 'Sol/Sağ ok'; + + @override + String get scNavAction => 'önceki/sonraki resim'; + + @override + String get scOpenKey => 'Ctrl+O'; + + @override + String get scOpenAction => 'dosya aç'; + + @override + String get scSaveKey => 'Ctrl+S'; + + @override + String get scSaveAction => 'görünen alanı kaydet (JPEG 95)'; + + @override + String get scFlipKey => 'Ctrl+R'; + + @override + String get scFlipAction => + 'yatay aynala (efektleri sıfırlar, tekrar basınca geri alır)'; + + @override + String get scCloseKey => 'Ctrl+Q'; + + @override + String get scCloseAction => 'pencereyi kapat'; + + @override + String get scCloseAllKey => 'Ctrl+Shift+Q'; + + @override + String get scCloseAllAction => 'tüm pencereleri kapat'; + + @override + String get scFullscreenKey => 'F11'; + + @override + String get scFullscreenAction => 'tam ekran / normal geçişi'; + + @override + String get scExitFullscreenKey => 'Esc'; + + @override + String get scExitFullscreenAction => 'tam ekrandan çık'; + + @override + String get scZoomKey => 'Kaydırma'; + + @override + String get scZoomAction => 'yakınlaştır (tık başına %10)'; + + @override + String get scFineZoomKey => 'Ctrl+kaydırma'; + + @override + String get scFineZoomAction => 'ince yakınlaştırma (tık başına %1)'; + + @override + String get scPanKey => 'Sol tık sürükle'; + + @override + String get scPanAction => 'kaydır'; + + @override + String get scFillKey => 'Çift tık'; + + @override + String get scFillAction => 'doldur/sığdır geçişi'; + + @override + String get scResetKey => 'Sağ çift tık'; + + @override + String get scResetAction => 'efektleri sıfırla'; + + @override + String get scContrastKey => 'Sağ tık sürükle'; + + @override + String get scContrastAction => 'kontrast'; + + @override + String get scRotateKey => 'Shift+sağ tık'; + + @override + String get scRotateAction => 'döndür'; + + @override + String get scSaturationKey => 'Ctrl+sağ tık'; + + @override + String get scSaturationAction => 'doygunluk'; + + @override + String get scBrightnessKey => 'Ctrl+Shift+sağ tık'; + + @override + String get scBrightnessAction => 'parlaklık'; @override String get print => 'Yazdır'; diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index c0b409b..ca5f1ad 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -1,13 +1,49 @@ { "@@locale": "tr", "settingsTitle": "Ayarlar", + "tabGeneral": "Genel", + "language": "Dil", "sortBy": "Sıralama", - "sortByTime": "Zamana göre", - "sortByName": "İsme göre", + "hoverColor": "Hover rengi", + "sortByTime": "Zaman", + "sortByName": "İsim", "sizeSection": "Boyut", - "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.", + "appDescription": "Basit, temiz, hafif, hızlı resim görüntüleyici.", "shortcuts": "Kısayollar", - "shortcutsList": "• Sol/Sağ ok — önceki/sonraki resim\n• Ctrl+O — dosya aç\n• Ctrl+S — görünen alanı kaydet (JPEG 95)\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• F11 — tam ekran / normal geçişi\n• Esc — tam ekrandan çık\n• Kaydırma — yakınlaştır (tık başına %10)\n• Ctrl+kaydırma — ince yakınlaştırma (tık başına %1)\n• Sol tık sürükle — kaydır\n• Çift tık — doldur/sığdır geçişi\n• Sağ çift tık — efektleri sıfırla\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", + "scNavKey": "Sol/Sağ ok", + "scNavAction": "önceki/sonraki resim", + "scOpenKey": "Ctrl+O", + "scOpenAction": "dosya aç", + "scSaveKey": "Ctrl+S", + "scSaveAction": "görünen alanı kaydet (JPEG 95)", + "scFlipKey": "Ctrl+R", + "scFlipAction": "yatay aynala (efektleri sıfırlar, tekrar basınca geri alır)", + "scCloseKey": "Ctrl+Q", + "scCloseAction": "pencereyi kapat", + "scCloseAllKey": "Ctrl+Shift+Q", + "scCloseAllAction": "tüm pencereleri kapat", + "scFullscreenKey": "F11", + "scFullscreenAction": "tam ekran / normal geçişi", + "scExitFullscreenKey": "Esc", + "scExitFullscreenAction": "tam ekrandan çık", + "scZoomKey": "Kaydırma", + "scZoomAction": "yakınlaştır (tık başına %10)", + "scFineZoomKey": "Ctrl+kaydırma", + "scFineZoomAction": "ince yakınlaştırma (tık başına %1)", + "scPanKey": "Sol tık sürükle", + "scPanAction": "kaydır", + "scFillKey": "Çift tık", + "scFillAction": "doldur/sığdır geçişi", + "scResetKey": "Sağ çift tık", + "scResetAction": "efektleri sıfırla", + "scContrastKey": "Sağ tık sürükle", + "scContrastAction": "kontrast", + "scRotateKey": "Shift+sağ tık", + "scRotateAction": "döndür", + "scSaturationKey": "Ctrl+sağ tık", + "scSaturationAction": "doygunluk", + "scBrightnessKey": "Ctrl+Shift+sağ tık", + "scBrightnessAction": "parlaklık", "print": "Yazdır", "fileProperties": "Dosya özellikleri", "propResolution": "Çözünürlük", diff --git a/lib/screens/viewer_screen.dart b/lib/screens/viewer_screen.dart index 2c783b6..7dd5ea0 100644 --- a/lib/screens/viewer_screen.dart +++ b/lib/screens/viewer_screen.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:path/path.dart' as p; import 'package:window_manager/window_manager.dart'; @@ -11,6 +12,7 @@ import '../services/image_header.dart'; import '../services/image_manager.dart'; import '../services/file_handler.dart'; import '../services/crop_save_service.dart'; +import '../services/hover_color.dart'; import '../widgets/custom_title_bar.dart'; import '../widgets/image_canvas.dart'; import '../widgets/window_resize_zones.dart'; @@ -19,7 +21,7 @@ import '../shortcuts/app_shortcuts.dart'; import '../services/window_persistence.dart'; import '../l10n/app_localizations.dart'; -const appVersion = '1.9.0'; +const appVersion = '1.9.2'; class ViewerScreen extends StatefulWidget { final List initialFiles; @@ -114,113 +116,327 @@ class _ViewerScreenState extends State with WindowListener { } } + static const double _shortcutKeyColumnWidth = 126; + + int _settingsTab = 0; + void _showSettings() { showDialog( context: context, builder: (context) => StatefulBuilder( - builder: (context, setDialogState) => AlertDialog( - backgroundColor: const Color(0xFF2a2a2a), - title: Text(AppLocalizations.of(context)!.settingsTitle, - style: const TextStyle(color: Colors.white, fontSize: 18)), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (_hasImages) ...[ - Text(AppLocalizations.of(context)!.fileProperties, - style: const TextStyle(color: Colors.white70, fontSize: 13)), - const SizedBox(height: 4), - FutureBuilder>( - future: _fileProperties(ImageManager.instance.currentImagePath), - builder: (context, snap) { - if (!snap.hasData) return const SizedBox(height: 20); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: snap.data!.map((r) => Padding( - padding: const EdgeInsets.only(bottom: 2), - child: Text.rich( - TextSpan(children: [ - TextSpan(text: '${r.$1}: ', style: const TextStyle(color: Colors.white70, fontSize: 13)), - TextSpan(text: r.$2, style: const TextStyle(color: Colors.white, fontSize: 13)), - ]), + builder: (context, setDialogState) { + final l10n = AppLocalizations.of(context)!; + return Dialog( + insetPadding: EdgeInsets.zero, + backgroundColor: Colors.transparent, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), + child: LayoutBuilder( + builder: (context, c) => Container( + width: math.min(c.maxWidth, 520), + height: math.min(c.maxHeight, 560), + padding: const EdgeInsets.fromLTRB(14, 4, 14, 10), + decoration: BoxDecoration( + color: const Color(0xFF2a2a2a), + border: Border.all(color: Colors.white12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text(l10n.settingsTitle, + style: const TextStyle(color: Colors.white, fontSize: 18)), + const Spacer(), + IconButton( + tooltip: l10n.print, + icon: const Icon(Icons.print, color: Colors.white70, size: 18), + onPressed: () { + Navigator.pop(context); + _printImage(); + }, + ), + IconButton( + tooltip: l10n.close, + icon: const Icon(Icons.close, color: Colors.white70, size: 18), + onPressed: () => Navigator.pop(context), + ), + ], + ), + const SizedBox(height: 4), + Align( + alignment: Alignment.center, + child: SegmentedButton( + expandedInsets: EdgeInsets.zero, + segments: [ + ButtonSegment( + value: 0, + label: Text(l10n.tabGeneral, + style: const TextStyle(fontSize: 13)), ), - )).toList(), - ); - }, - ), - const SizedBox(height: 12), - const Divider(color: Colors.white24, height: 24), - ], - WindowSizeSelector( - workArea: _workArea, - onPick: _applyWindowSize, + ButtonSegment( + value: 1, + label: Text(l10n.sizeSection, + style: const TextStyle(fontSize: 13)), + ), + ButtonSegment( + value: 2, + label: Text(l10n.shortcuts, + style: const TextStyle(fontSize: 13)), + ), + ], + selected: {_settingsTab}, + showSelectedIcon: false, + style: _segmentedStyle, + onSelectionChanged: (selection) => + setDialogState(() => _settingsTab = selection.first), + ), + ), + const SizedBox(height: 10), + Expanded( + child: _settingsTabBody(l10n, setDialogState), + ), + const Divider(color: Colors.white10, height: 12), + Row( + children: [ + Text('imajViewer $appVersion', + style: const TextStyle(color: Colors.white54, fontSize: 11)), + const SizedBox(width: 16), + Expanded( + child: Text(l10n.appDescription, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.right, + style: const TextStyle(color: Colors.white54, fontSize: 11)), + ), + ], + ), + ], ), - const Divider(color: Colors.white24, height: 24), - Text(AppLocalizations.of(context)!.sortBy, - style: const TextStyle(color: Colors.white70, fontSize: 13)), - RadioListTile( - value: false, - groupValue: ImageManager.instance.sortByName, - dense: true, - activeColor: Colors.blueAccent, - title: Text(AppLocalizations.of(context)!.sortByTime, - style: const TextStyle(color: Colors.white, fontSize: 14)), - onChanged: (v) { - ImageManager.instance.setSortByName(v!); - setDialogState(() {}); - }, - ), - RadioListTile( - value: true, - groupValue: ImageManager.instance.sortByName, - dense: true, - activeColor: Colors.blueAccent, - title: Text(AppLocalizations.of(context)!.sortByName, - style: const TextStyle(color: Colors.white, fontSize: 14)), - onChanged: (v) { - ImageManager.instance.setSortByName(v!); - setDialogState(() {}); - }, - ), - Text('imajViewer $appVersion', - style: const TextStyle(color: Colors.white, fontSize: 15)), - const SizedBox(height: 4), - Text( - AppLocalizations.of(context)!.appDescription, - style: const TextStyle(color: Colors.white70, fontSize: 13, height: 1.5), - ), - const SizedBox(height: 12), - Text(AppLocalizations.of(context)!.shortcuts, - style: const TextStyle(color: Colors.white70, fontSize: 13)), - const SizedBox(height: 4), - Text( - AppLocalizations.of(context)!.shortcutsList, - style: const TextStyle(color: Colors.white70, fontSize: 13, height: 1.6), - ), - ], + ), ), - ), - actions: [ - TextButton.icon( - onPressed: () { - Navigator.pop(context); - _printImage(); - }, - icon: const Icon(Icons.print, color: Colors.white70, size: 18), - label: Text(AppLocalizations.of(context)!.print, - style: const TextStyle(color: Colors.white70)), - ), - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.close, style: const TextStyle(color: Colors.white70)), - ), - ], - ), + ); + }, ), ); } + Widget _settingsTabBody(AppLocalizations l10n, StateSetter setDialogState) { + switch (_settingsTab) { + case 1: + return WindowSizeSelector(workArea: _workArea, onPick: _applyWindowSize); + case 2: + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (final (key, action) in _shortcutRows(l10n)) + Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: _shortcutKeyColumnWidth, + child: Text(key, + style: const TextStyle(fontSize: 12, color: Colors.white)), + ), + const SizedBox(width: 8), + Expanded( + child: Text(action, + style: const TextStyle(color: Colors.white70, fontSize: 13)), + ), + ], + ), + ), + ], + ), + ); + default: + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_hasImages) ...[ + _settingsCard( + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionTitle(l10n.fileProperties), + const SizedBox(height: 4), + FutureBuilder>( + future: _fileProperties(ImageManager.instance.currentImagePath), + builder: (context, snap) { + if (!snap.hasData) return const SizedBox(height: 20); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: snap.data!.map((r) => Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Text.rich( + TextSpan(children: [ + TextSpan(text: '${r.$1}: ', + style: const TextStyle(color: Colors.white70, fontSize: 13)), + TextSpan(text: r.$2, + style: const TextStyle(color: Colors.white, fontSize: 13)), + ]), + ), + )).toList(), + ); + }, + ), + ], + ), + ), + const SizedBox(height: 12), + ], + _settingsCard( + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionTitle(l10n.sortBy), + const SizedBox(height: 4), + SegmentedButton( + segments: [ + ButtonSegment( + value: false, + label: Text(l10n.sortByTime, style: const TextStyle(fontSize: 13)), + ), + ButtonSegment( + value: true, + label: Text(l10n.sortByName, style: const TextStyle(fontSize: 13)), + ), + ], + selected: {ImageManager.instance.sortByName}, + showSelectedIcon: false, + style: _segmentedStyle, + onSelectionChanged: (selection) { + ImageManager.instance.setSortByName(selection.first); + setDialogState(() {}); + }, + ), + ], + ), + ), + const SizedBox(height: 12), + _settingsCard( + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionTitle(l10n.hoverColor), + const SizedBox(height: 8), + Wrap( + spacing: 10, + runSpacing: 10, + children: [ + for (final color in HoverColor.palette) + GestureDetector( + onTap: () { + HoverColor.set(color); + setDialogState(() {}); + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + border: Border.all( + color: HoverColor.notifier.value.toARGB32() == + color.toARGB32() + ? Colors.white + : Colors.white24, + width: 2, + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 12), + _settingsCard( + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionTitle(l10n.language), + const SizedBox(height: 6), + SegmentedButton( + segments: const [ + ButtonSegment( + value: 'en', + label: Text('English', style: TextStyle(fontSize: 13))), + ButtonSegment( + value: 'tr', + label: Text('Türkçe', style: TextStyle(fontSize: 13))), + ButtonSegment( + value: 'fr', + label: Text('Français', style: TextStyle(fontSize: 13))), + ], + selected: const {'tr'}, + showSelectedIcon: false, + ), + ], + ), + ), + ], + ), + ); + } + } + + Widget _sectionTitle(String text) { + return Text(text, + style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600)); + } + + ButtonStyle get _segmentedStyle => ButtonStyle( + backgroundColor: WidgetStateProperty.resolveWith( + (states) => states.contains(WidgetState.selected) + ? Colors.blueAccent.withValues(alpha: 0.25) + : Colors.white10, + ), + foregroundColor: const WidgetStatePropertyAll(Colors.white), + side: const WidgetStatePropertyAll(BorderSide(color: Colors.white24)), + ); + + Widget _settingsCard(Widget child) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(8), + ), + child: child, + ); + } + + List<(String, String)> _shortcutRows(AppLocalizations l10n) { + return [ + (l10n.scNavKey, l10n.scNavAction), + (l10n.scOpenKey, l10n.scOpenAction), + (l10n.scSaveKey, l10n.scSaveAction), + (l10n.scFlipKey, l10n.scFlipAction), + (l10n.scCloseKey, l10n.scCloseAction), + (l10n.scCloseAllKey, l10n.scCloseAllAction), + (l10n.scFullscreenKey, l10n.scFullscreenAction), + (l10n.scExitFullscreenKey, l10n.scExitFullscreenAction), + (l10n.scZoomKey, l10n.scZoomAction), + (l10n.scFineZoomKey, l10n.scFineZoomAction), + (l10n.scPanKey, l10n.scPanAction), + (l10n.scFillKey, l10n.scFillAction), + (l10n.scResetKey, l10n.scResetAction), + (l10n.scContrastKey, l10n.scContrastAction), + (l10n.scRotateKey, l10n.scRotateAction), + (l10n.scSaturationKey, l10n.scSaturationAction), + (l10n.scBrightnessKey, l10n.scBrightnessAction), + ]; + } + Future _printImage() async { final path = ImageManager.instance.currentImagePath; if (path.isEmpty) return; @@ -496,16 +712,19 @@ class _NavButtonState extends State<_NavButton> { cursor: SystemMouseCursors.click, onEnter: (_) => setState(() => _hovered = true), onExit: (_) => setState(() => _hovered = false), - child: Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: _hovered - ? Colors.red.withValues(alpha: 0.8) - : Colors.black.withValues(alpha: 0.35), - shape: BoxShape.circle, + child: ValueListenableBuilder( + valueListenable: HoverColor.notifier, + builder: (context, hover, _) => Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: _hovered + ? hover.withValues(alpha: 0.8) + : Colors.black.withValues(alpha: 0.35), + shape: BoxShape.circle, + ), + child: Icon(widget.icon, color: Colors.white, size: 22), ), - child: Icon(widget.icon, color: Colors.white, size: 22), ), ), ); diff --git a/lib/services/hover_color.dart b/lib/services/hover_color.dart new file mode 100644 index 0000000..da69c99 --- /dev/null +++ b/lib/services/hover_color.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +import 'window_persistence.dart'; + +/// Arayüzdeki tüm hover vurgularının rengi. Ayarlar > Genel'den seçilir, +/// window.json'da kalıcıdır. +class HoverColor { + HoverColor._(); + + static const List palette = [ + Color(0xFFF44336), // kırmızı + Color(0xFFFF9800), // turuncu + Color(0xFFFFD54F), // sarı + Color(0xFF4CAF50), // yeşil + Color(0xFF009688), // turkuaz + Color(0xFF2196F3), // mavi + Color(0xFF9C27B0), // mor + Color(0xFFE91E63), // pembe + ]; + + static final ValueNotifier notifier = + ValueNotifier(Color(WindowPersistence.hoverColor)); + + static void set(Color color) { + notifier.value = color; + WindowPersistence.saveHoverColor(color.toARGB32()); + } +} diff --git a/lib/services/window_persistence.dart b/lib/services/window_persistence.dart index d2bf59b..f4f8252 100644 --- a/lib/services/window_persistence.dart +++ b/lib/services/window_persistence.dart @@ -25,6 +25,11 @@ class WindowPersistence { /// Sadece modu günceller; x/y/w/h korunur. static void saveFilled(bool value) => _write({'filled': value}); + /// Tüm arayüzdeki hover vurgu rengi (ARGB). Anahtar yoksa varsayılan kırmızı. + static int get hoverColor => (_read()?['hoverColor'] as num?)?.toInt() ?? 0xFFF44336; + + static void saveHoverColor(int value) => _write({'hoverColor': value}); + /// Save window rect synchronously (safe to call before destroy). static void saveSync({ required Offset position, diff --git a/lib/widgets/custom_title_bar.dart b/lib/widgets/custom_title_bar.dart index 971109e..c46d8e5 100644 --- a/lib/widgets/custom_title_bar.dart +++ b/lib/widgets/custom_title_bar.dart @@ -1,13 +1,22 @@ import 'package:flutter/material.dart'; import 'package:window_manager/window_manager.dart'; -class CustomTitleBar extends StatelessWidget { +import '../services/hover_color.dart'; + +class CustomTitleBar extends StatefulWidget { final VoidCallback? onSettings; final VoidCallback? onSaveCrop; final bool visible; const CustomTitleBar({super.key, this.onSettings, this.onSaveCrop, this.visible = true}); + @override + State createState() => _CustomTitleBarState(); +} + +class _CustomTitleBarState extends State { + bool _barHovered = false; + @override Widget build(BuildContext context) { return Positioned( @@ -15,55 +24,66 @@ class CustomTitleBar extends StatelessWidget { left: 0, right: 0, child: AnimatedOpacity( - opacity: visible ? 1.0 : 0.0, + opacity: widget.visible ? 1.0 : 0.0, duration: const Duration(milliseconds: 150), - child: DragToMoveArea( - child: SizedBox( - height: 24, - child: Stack( - children: [ - if (onSettings != null) - Positioned( - left: 0, - top: 0, - bottom: 0, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - _BarButton( - icon: Icons.settings, - onTap: onSettings!, - ), - ], - ), - ), - Positioned( - right: 0, - top: 0, - bottom: 0, - child: Row( - mainAxisSize: MainAxisSize.min, + child: MouseRegion( + cursor: SystemMouseCursors.grab, + onEnter: (_) => setState(() => _barHovered = true), + onExit: (_) => setState(() => _barHovered = false), + child: AnimatedContainer( + duration: const Duration(milliseconds: 120), + color: _barHovered + ? Colors.white.withValues(alpha: 0.06) + : Colors.transparent, + child: DragToMoveArea( + child: SizedBox( + height: 24, + child: Stack( children: [ - if (onSaveCrop != null) - _BarButton( - icon: Icons.save_outlined, - onTap: onSaveCrop!, + Positioned( + left: 0, + top: 0, + bottom: 0, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.onSettings != null) + _BarButton( + icon: Icons.settings, + onTap: widget.onSettings!, + ), + if (widget.onSaveCrop != null) + _BarButton( + icon: Icons.save_outlined, + onTap: widget.onSaveCrop!, + ), + ], ), - _BarButton( - icon: Icons.horizontal_rule, - onTap: () => windowManager.minimize(), ), - _BarButton( - icon: Icons.close, - onTap: () => windowManager.close(), + Positioned( + right: 0, + top: 0, + bottom: 0, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _BarButton( + icon: Icons.horizontal_rule, + onTap: () => windowManager.minimize(), + ), + _BarButton( + icon: Icons.close, + onTap: () => windowManager.close(), + ), + ], + ), ), ], ), ), - ], + ), ), ), - ), ), ); } @@ -93,14 +113,17 @@ class _BarButtonState extends State<_BarButton> { cursor: SystemMouseCursors.click, onEnter: (_) => setState(() => _hovered = true), onExit: (_) => setState(() => _hovered = false), - child: Container( - width: 32, - height: 24, - color: _hovered ? Colors.red.withValues(alpha: 0.8) : Colors.transparent, - child: Icon( - widget.icon, - color: Colors.white, - size: 14, + child: ValueListenableBuilder( + valueListenable: HoverColor.notifier, + builder: (context, hover, _) => Container( + width: 32, + height: 24, + color: _hovered ? hover.withValues(alpha: 0.8) : Colors.transparent, + child: Icon( + widget.icon, + color: Colors.white, + size: 14, + ), ), ), ), diff --git a/lib/widgets/window_size_selector.dart b/lib/widgets/window_size_selector.dart index a077e91..55da93b 100644 --- a/lib/widgets/window_size_selector.dart +++ b/lib/widgets/window_size_selector.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; -import '../l10n/app_localizations.dart'; +import '../services/hover_color.dart'; /// Ekran oranı buton ızgarası: 31 buton `a:b` (a dikeyi, b yatayı böler; -/// hedef boyut `W/b × H/a`). Sol-üst hücre (1:1) boş placeholder — 1:1 yok -/// (title bar çift tık zaten tam ekran). +/// hedef boyut `W/b × H/a`). Sol-üst hücre `H:W` etiketi — buton değil +/// (1:1 yok: title bar çift tık zaten tam ekran). /// -/// Yerleşim: sabit 8 satır × 4 sütun (31-10: geniş/dar transpozesi -/// kaldırıldı — kullanıcı kararı). +/// Yerleşim: 8 satır × 4 sütun; hücreler mevcut alanı kaplayacak şekilde +/// esner (pencere/sekme alanı büyüdükçe butonlar büyür). class WindowSizeSelector extends StatelessWidget { final Future Function() workArea; final void Function(int rows, int cols, Size work) onPick; @@ -21,57 +21,89 @@ class WindowSizeSelector extends StatelessWidget { @override Widget build(BuildContext context) { return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(AppLocalizations.of(context)!.sizeSection, - style: const TextStyle(color: Colors.white70, fontSize: 13)), - const SizedBox(height: 4), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [for (var b = 1; b <= 8; b++) _row(b)], + for (var b = 1; b <= 8; b++) + Expanded( + child: Row( + children: [ + for (var a = 1; a <= 4; a++) Expanded(child: _cell(a, b)), + ], + ), ), - ), ], ); } /// Satır = `b` (1..8, yatay bölün); satır içinde `a` (1..4, dikey bölün). - Widget _row(int b) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - for (var a = 1; a <= 4; a++) _cell(a, b), - ], - ); - } - Widget _cell(int a, int b) { return Padding( - padding: const EdgeInsets.only(right: 4, bottom: 4), + padding: const EdgeInsets.only(right: 3, bottom: 3), child: a == 1 && b == 1 - ? const SizedBox(width: 46, height: 26) - : GestureDetector( + ? _gridLabel('H:W') + : _SizeCell( + a: a, + b: b, onTap: () async { final work = await workArea(); if (work != null) onPick(a, b, work); }, - child: Container( - width: 46, - height: 26, - decoration: BoxDecoration( - color: Colors.white10, - borderRadius: BorderRadius.circular(4), - ), - alignment: Alignment.center, - child: Text( - '$a:$b', - style: const TextStyle(color: Colors.white, fontSize: 11), - ), - ), ), ); } + + Widget _gridLabel(String text) { + return Container( + decoration: BoxDecoration( + color: Colors.white10, + borderRadius: BorderRadius.circular(4), + ), + alignment: Alignment.center, + child: Text( + text, + style: const TextStyle(color: Colors.white54, fontSize: 11), + ), + ); + } +} + +class _SizeCell extends StatefulWidget { + final int a; + final int b; + final VoidCallback onTap; + + const _SizeCell({required this.a, required this.b, required this.onTap}); + + @override + State<_SizeCell> createState() => _SizeCellState(); +} + +class _SizeCellState extends State<_SizeCell> { + bool _hover = false; + + @override + Widget build(BuildContext context) { + return MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) => setState(() => _hover = true), + onExit: (_) => setState(() => _hover = false), + child: GestureDetector( + onTap: widget.onTap, + child: ValueListenableBuilder( + valueListenable: HoverColor.notifier, + builder: (context, hover, _) => AnimatedContainer( + duration: const Duration(milliseconds: 100), + decoration: BoxDecoration( + color: _hover ? hover.withValues(alpha: 0.4) : Colors.white10, + borderRadius: BorderRadius.circular(4), + ), + alignment: Alignment.center, + child: Text( + '${widget.a}:${widget.b}', + style: const TextStyle(color: Colors.white, fontSize: 12), + ), + ), + ), + ), + ); + } } diff --git a/pubspec.yaml b/pubspec.yaml index 1c07bb4..d49bc7c 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.9.0+20 +version: 1.9.2+22 msix_config: display_name: ImajViewer