diff --git a/.gitignore b/.gitignore index 58a0f54..31715ae 100644 --- a/.gitignore +++ b/.gitignore @@ -45,11 +45,17 @@ app.*.map.json /android/app/release # Distribution binaries — release assets, not tracked in git +# (only the latest build of each type stays tracked for direct download) *.deb *.tar.gz *.exe +!dist/imajviewer_1.3.1_amd64.deb +!dist/imajviewer_setup_1.1.0.exe deb/usr/lib/imajviewer/ release/ # Local backup snapshots (see soul.md §5) -docs/.bak/ \ No newline at end of file +docs/.bak/ + +# Internal dev docs stay local, not in the public repo +/docs/ \ No newline at end of file diff --git a/dist/imajviewer_1.0.1_amd64.deb b/dist/imajviewer_1.0.1_amd64.deb deleted file mode 100644 index b1cad00..0000000 Binary files a/dist/imajviewer_1.0.1_amd64.deb and /dev/null differ diff --git a/dist/imajviewer_1.1.0_amd64.deb b/dist/imajviewer_1.1.0_amd64.deb deleted file mode 100644 index 53a72c3..0000000 Binary files a/dist/imajviewer_1.1.0_amd64.deb and /dev/null differ diff --git a/dist/imajviewer_1.2.0_amd64.deb b/dist/imajviewer_1.2.0_amd64.deb deleted file mode 100644 index 7ba38a8..0000000 Binary files a/dist/imajviewer_1.2.0_amd64.deb and /dev/null differ diff --git a/dist/imajviewer_1.3.0_amd64.deb b/dist/imajviewer_1.3.0_amd64.deb deleted file mode 100644 index c2a9bd9..0000000 Binary files a/dist/imajviewer_1.3.0_amd64.deb and /dev/null differ diff --git a/docs/Implementation.md b/docs/Implementation.md deleted file mode 100644 index 4158aff..0000000 --- a/docs/Implementation.md +++ /dev/null @@ -1,209 +0,0 @@ -# Implementation — imajViewer - -> Teknik uygulama dokümanı -> Versiyon: 1.0 - ---- - -## 1. Mimari Genel Bakış - -``` -┌──────────────────────────────────────────────────┐ -│ Flutter App │ -│ ┌────────────┐ ┌────────────────────────────┐ │ -│ │ window_mgr │ │ ViewerScreen │ │ -│ │ (frameless)│ │ ┌──────────────────────┐ │ │ -│ └────────────┘ │ │ CustomTitleBar │ │ │ -│ │ │ (minimize, close) │ │ │ -│ │ ├──────────────────────┤ │ │ -│ │ │ ImageCanvas │ │ │ -│ │ │ (InteractiveViewer) │ │ │ -│ │ │ - Scroll → Zoom │ │ │ -│ │ │ - Drag → Pan │ │ │ -│ │ └──────────────────────┘ │ │ -│ └────────────────────────────┘ │ -│ ┌────────────────────────────────────────────┐ │ -│ │ ImageCacheManager │ │ -│ │ - PrecacheImage │ │ -│ │ - LRU eviction (> N images) │ │ -│ │ - File → Memory → GPU Texture │ │ -│ └────────────────────────────────────────────┘ │ -└──────────────────────────────────────────────────┘ -``` - -## 2. Bileşen Ağacı (Widget Tree) - -``` -MaterialApp (dark theme) -└── ViewerScreen (StatefulWidget) - ├── Stack - │ ├── InteractiveViewer (zoom + pan) - │ │ └── RawImage (resim) - │ ├── CustomTitleBar (Positioned: top-right) - │ │ ├── IconButton (minimize) - │ │ └── IconButton (close) - │ └── (koyu arka plan) - └── DragTarget (sürükle-bırak dosya alanı) -``` - -## 3. Data Flow - -``` -Kullanıcı dosya bırakır / açar - │ - ▼ -ViewerScreen.dragResult / openFileDialog - │ - ▼ -ImageList (List filePaths) - │ - ▼ -preloadImages() → ImageCache'e ekle - │ - ▼ -Indexed, currentIndex değişince - │ - ▼ -InteractiveViewer.builder → Image.file(path) - │ - ▼ -Flutter ImageCache → GPU Texture → Render -``` - -## 4. Adım Adım Uygulama - -### Adım 1: Proje İskeleti -- `flutter create imajViewer` -- `--platforms=linux` ile oluştur -- Temele dark tema, frameless window ekle - -**Dosyalar:** `main.dart`, `app.dart` - -### Adım 2: window_manager Kurulumu -- pubspec.yaml: `window_manager: ^0.4.3` -- `main()` içinde `windowManager.ensureInitialized()` -- `waitUntilReadyToShow` → frameless ayarları: - - `setTitleBarStyle(TitleBarStyle.hidden)` - - `setSize(Size(1280, 800))` - - `setMinimumSize(Size(800, 600))` - - `setBackgroundColor(Colors.transparent)` -- Linux'ta `linux/my_app.cc`'de window_manager için ek ayar gerekebilir - -**Dosyalar:** `lib/main.dart`, `pubspec.yaml` - -### Adım 3: CustomTitleBar Widget -- Sağ üst köşede, sabit konumda -- Siyah yarı saydam arka plan -- minimize (—) ve kapat (X) butonları -- **Püf nokta:** Butonların üzerine gelince InteractiveViewer zoom yapmamalı - - `AbsorbPointer` veya `IgnorePointer` ile InteractiveViewer etkileşimini engelle -- Pencere boyutlandırma ve sürükleme kenarlıkları (window_manager'in `setResizable` özelliği) - -**Dosya:** `lib/widgets/custom_title_bar.dart` - -### Adım 4: ImageCanvas (Ana Görüntüleme) -- `InteractiveViewer` widget'ı - - `boundaryMargin: EdgeInsets.all(double.infinity)` → sınırsız kaydırma - - `minScale: 0.1`, `maxScale: 10.0` - - `onInteractionUpdate` → zoom oranını göstermek için (ops.) - - `transformationController` ile programatik zoom kontrolü -- Mouse wheel event: `Listener` ile `onPointerSignal` yakala - - `PointerScrollEvent` → transformationController ile zoom - - Fare imlecinin resim üzerindeki konumuna göre zoom odağı - -**Püf nokta:** -`InteractiveViewer`'ın default scroll-to-zoom desteği vardır ama imleç odağında zoom -için `TransformationController`'a manuel matrix hesaplaması yapmak gerekir. - -**Dosya:** `lib/widgets/image_canvas.dart` - -### Adım 5: Resim Yönetimi (ImageManager) -- `ChangeNotifier` ile state yönetimi -- Dışarıdan dosya eklendiğinde `notifyListeners()` + `precacheImage()` -- `ImageCache.maximumSize` ve `maximumSizeBytes` ayarı -- **Performans stratejisi:** - - Sadece görünen resim bellekte tutulur - - 12+ resim için `ImageCache.maximumSizeBytes` = 512MB - - Resim değiştirince önceki resmin cache'ini düşürmeye gerek yok (Flutter LRU) -- Sürükle-bırak: `DragTarget>` widget'ı - -**Dosya:** `lib/services/image_manager.dart` - -### Adım 6: Dosya Açma (Dialog + DragDrop) -- `file_picker` paketi ile `Ctrl+O` kısayolu -- `DragTarget` ile sürükle-bırak -- Desteklenen format filtreleri: PNG, JPEG, WebP, BMP, GIF -- **Dikkat:** Flutter Linux'ta DragTarget çalışıyor — ayrıca `window_manager`'dan `onDragFile` event'i de var - -**Dosya:** `lib/services/file_handler.dart` - -### Adım 7: Klavye Kısayolları -- Flutter'daki `Shortcuts` + `Actions` sistemi -- `← →` → resim değiştir -- `Ctrl+O` → dosya aç -- `F11` → tam ekran (fullscreen toggle) -- `Esc` → tam ekrandan çık -- `Ctrl+Q` → çıkış - -**Dosya:** `lib/shortcuts/app_shortcuts.dart` - -### Adım 8: Birleştirme ve Test -- Ana ekranı `ViewerScreen`'de topla -- `Listener(onPointerSignal)` + `InteractiveViewer` entegrasyonu -- `flutter run -d linux` ile test -- Performance profiling: `flutter run --profile` - -### Adım 9: Derleme -- `flutter build linux --release` -- Binary: `build/linux/x64/release/bundle/imajViewer` - -## 5. pubspec.yaml Bağımlılıkları - -```yaml -dependencies: - flutter: - sdk: flutter - window_manager: ^0.4.3 - file_picker: ^8.0.0 - path: ^1.9.0 -``` - -## 6. Önemli Dikkat Noktaları - -### Zoom Odağı (Cursor-based Zoom) -InteractiveViewer varsayılan zoom odağı merkezdir. İmleç odağında zoom için -`TransformationController` ile manuel matrix çarpımı: - -``` -1. Scroll event yakala (PointerScrollEvent) -2. İmlecin resim üzerindeki koordinatını hesapla -3. Matrix4.translationValues(-txt) * Matrix4.diagonal3(values(scale)) * Matrix4.translationValues(txt) -``` - -Detaylı implementasyon `image_canvas.dart`'da yapılacak. - -### Frameless Window Sürükleme -Window_manager zaten varsayılan olarak fare ile pencerenin herhangi bir -yerinden sürüklenmesine izin vermez. Bunun için: -- CustomTitleBar alanını `DragToMoveArea` ile sarmak -- Veya `windowManager.startDragging()` çağırmak - -Biz `DragToMoveArea` widget'ını kullanacağız. - -### Performans -- Flutter'ın `ImageCache`'i varsayılan olarak 50 MB ve 1000 entry ile sınırlıdır -- 12 adet 4K resim için `ImageCache.maximumSizeBytes` 1 GB'a çıkarılmalı -- `RepaintBoundary` ile sadece değişen bölgelerin yeniden çizilmesi sağlanır -- `Texture` bazlı render ile GPU'da işleme devam eder - -## 7. Timeline - -| Adım | İş | Tahmini Süre | -|---|---|---| -| 1-2 | Proje iskeleti + window_manager | 15 dk | -| 3 | CustomTitleBar | 15 dk | -| 4 | ImageCanvas + zoom | 30 dk | -| 5-6 | Resim yönetimi + dosya açma | 30 dk | -| 7 | Kısayollar | 10 dk | -| 8-9 | Birleştirme + build | 20 dk | -| **Toplam** | | **~2 saat** | diff --git a/docs/PRD.md b/docs/PRD.md deleted file mode 100644 index 542442e..0000000 --- a/docs/PRD.md +++ /dev/null @@ -1,100 +0,0 @@ -# PRD — imajViewer - -> Product Requirements Document -> Versiyon: 1.0 -> Tarih: 2026-07-22 - ---- - -## 1. Ürün Özeti - -**imajViewer**, Flutter ile geliştirilmiş, Linux masaüstünde çalışan ultra hafif bir görüntü izleyici uygulamasıdır. -12'den fazla yüksek çözünürlüklü resmi aynı anda açabilir, fare tekerleği ile anlık zoom yapabilir ve -tüm pencere alanını resim göstermek için kullanır. - -## 2. Hedef Kitle - -- Linux kullanıcıları -- Fotoğrafçılar, tasarımcılar (hızlı önizleme ihtiyacı) -- Yüksek sayıda resim arasında hızlıca gezinmek isteyen kullanıcılar - -## 3. Fonksiyonel Gereksinimler - -### F-01: Resim Açma -- Kullanıcı dosya yöneticisinden sürükle-bırak ile resim ekleyebilir -- Aynı anda 12+ resim açılabilir -- Desteklenen formatlar: PNG, JPEG, WebP, BMP, GIF (statik) - -### F-02: Zoom -- Fare tekerleği (mouse scroll) ile kademesiz zoom -- Zoom odağı fare imlecinin bulunduğu nokta olmalı -- Zoom oranı: %10 - %1000 arası - -### F-03: Frameless Pencere -- Native title bar yok, sadece sağ üstte küçült (minimize) ve kapat (close) butonları -- Pencere sürüklenebilir (üst kısımdan) -- Pencere boyutlandırılabilir (kenarlardan/köşelerden) - -### F-04: Görüntüleme -- Scroll bar yok -- Resim pencereye sığacak şekilde başlangıçta yerleşir (fit) -- Siyah/zemin renkli arka plan -- Pencere yeniden boyutlandırılınca resim yeniden ortalanır - -### F-05: Navigasyon (ileri versiyon) -- Klavye kısayolları (← → ile resim değiştirme) -- Alt kısımda thumbnail strip - -## 4. Fonksiyonel Olmayan Gereksinimler - -### NF-01: Performans -- 12 adet 4K (3840x2160) resim aynı anda açıkken akıcı zoom (en az 60 FPS) -- Bellek kullanımı 2 GB'ı geçmemeli -- Resimler GPU'da işlenmeli, CPU yeniden ölçekleme yapılmamalı - -### NF-02: Başlangıç Süresi -- Uygulama 2 saniyeden kısa sürede açılmalı - -### NF-03: Binary Boyutu -- Derlenmiş uygulama 50 MB'ı geçmemeli - -### NF-04: Güvenilirlik -- Büyük resimler (10000x10000 pixel) uygulamayı çökertmemeli -- Bellek sınırı aşılınca en eski resim otomatik boşaltılmalı - -## 5. UI Tasarım İlkeleri - -- **Minimalist:** Sadece resim var, gereksiz hiçbir UI öğesi yok -- **Karanlık tema:** Siyah (#1a1a1a) zemin -- **Frameless:** Hiçbir pencere çerçevesi görünmez -- **Duyarlı:** Pencere boyutu değişince resim otomatik yeniden boyutlanır - -## 6. Teknik Yığın - -| Bileşen | Teknoloji | -|---|---| -| UI Framework | Flutter 3.44+ | -| Platform | Linux Desktop (GTK arkaplan) | -| Dil | Dart 3.12+ | -| Window Yönetimi | window_manager | -| State Yönetimi | ValueNotifier / ChangeNotifier | -| Resim İşleme | dart:ui Image + ImageCache | -| Render | Skia / Impeller (GPU) | - -## 7. Kullanıcı Akışı - -1. Uygulama açılır → boş siyah ekran -2. Kullanıcı dosyaları sürükler veya `Ctrl+O` ile açar -3. Resim(ler) pencereye sığacak şekilde görüntülenir -4. Fare tekerleği → zoom in/out (imleç odağında) -5. Fare sürükleme → resmi kaydırma (pan) -6. Pencere yeniden boyutlandırma → resim yeniden ortalanır -7. Sağ üst X → uygulama kapanır - -## 8. Başarı Kriterleri - -- [ ] 12 adet 4K resim açıkken zoom akıcı (60 FPS) -- [ ] Frameless pencere sorunsuz çalışıyor -- [ ] Scroll zoom imleç odağında çalışıyor -- [ ] Bellek kullanımı 2 GB altında -- [ ] Binary boyutu 50 MB altında diff --git a/docs/format-destegi-ek-formatlar.md b/docs/format-destegi-ek-formatlar.md deleted file mode 100644 index d922d04..0000000 --- a/docs/format-destegi-ek-formatlar.md +++ /dev/null @@ -1,59 +0,0 @@ -# Ek Görüntü Formatları Destek Planı - -> Tarih: 2026-08-03 · Değerlendirme + implementasyon planı -> Not: TIFF değerlendirildi ve **vazgeçildi** (kullanıcı kararı). Bu plan yalnızca mevcut mimariye sıfır maliyetle eklenebilen formatları kapsar. - -## 1. Hedef - -imajViewer'ın desteklediği format listesini, **ek decode katmanı gerektirmeden** (Flutter/Skia'nın zaten açabildiği) formatlarla genişletmek. - -## 2. Mevcut Durum - -### Desteklenen uzantılar (kod içi, 3 yerde) -| Dosya | Satır | Liste | -|---|---|---| -| `lib/services/file_handler.dart` | 9 | png, jpg, jpeg, webp, bmp, gif | -| `lib/services/image_manager.dart` | 24 | png, jpg, jpeg, webp, bmp, gif | -| `imajviewer.desktop` + `deb/.../imajviewer.desktop` | 9 | image/png;image/jpeg;image/webp;image/bmp;image/gif;image/jpg | -| `install.sh` | 77 | aynı MimeType listesi | - -### Flutter'ın yerleşik codec'leri (Skia) -Resmi dokümantasyona göre Flutter açabilir: **JPEG, PNG, GIF, WebP, BMP, WBMP** - -Yani Skia zaten açabiliyor ama listemizde olmayanlar: -- `.jfif` — JPEG/JFIF içerik (aslında JPEG, magic-byte aynı) -- `.jpe` — JPEG alternatif uzantısı -- `.wbmp` — Skia'da WBMP codec var -- `.avif` — **YOK**: Flutter issue #61229 açık, native plugin (`flutter_avif`/libavif) gerekir → kapsam dışı -- `.tif/.tiff` — **YOK**: image paketi gerekir → vazgeçildi - -## 3. Eklenecekler - -| Uzantı | MIME | Neden açılır | Maliyet | -|---|---|---|---| -| `.jfif` | image/jpeg | JPEG codec, aynı magic byte | sadece filtre | -| `.jpe` | image/jpeg | JPEG alternatif uzantısı | sadece filtre | -| `.wbmp` | image/vnd.wap.wbmp | Skia WBMP codec | sadece filtre | - -> `.avif` bilinçli olarak eklenmiyor: Flutter'ın yerleşik decoder'ı yok; native libavif bağımlılığı "ultra hafif" felsefeyi bozar. İstenirse ileride ayrı plan yapılır. - -## 4. Yapılacaklar (sıralı) - -1. [ ] `file_handler.dart` → allowedExtensions: `'jfif', 'jpe', 'wbmp'` ekle -2. [ ] `image_manager.dart` → supported: `'jfif', 'jpe', 'wbmp'` ekle -3. [ ] `imajviewer.desktop` → MimeType'a `image/jpeg` zaten var (jfif/jpe kapsar), `image/vnd.wap.wbmp;` ekle -4. [ ] `deb/usr/share/applications/imajviewer.desktop` → aynı -5. [ ] `install.sh` → aynı MimeType güncellemesi -6. [ ] Windows installer (`.iss`) dosya ilişkilendirme listesi kontrolü — varsa `image/jpeg` jfif/jpe'yi kapsar - -## 5. Riskler / Açık Sorular - -- **JFIF varyantı:** Bazı .jfif dosyaları CMYK-JPEG olabilir; Skia çoğunu açıyor ama tamamını değil → açılmayanlar `errorBuilder` ekranı gösterir (mevcut davranış, çökme yok) -- **WBMP nadir:** Çoğu kullanıcı hiç karşılaşmaz; eklemesi zararsız -- **Windows ilişkilendirme:** Inno Setup `.iss` dosyası `image/jpeg` kayıtlıysa .jfif/.jpe otomatik kapsanır; `.wbmp` için ayrı registry gerekebilir (opsiyonel) - -## 6. Kabul Kriterleri - -- [ ] `.jfif`, `.jpe`, `.wbmp` dosyaları dosya seçicide görünür -- [ ] JFIF dosyası pan/zoom/filtre ile görüntülenir -- [ ] Mevcut PNG/JPEG/WebP/GIF/BMP davranışı değişmez diff --git a/docs/galeri-klasor-tabanli.md b/docs/galeri-klasor-tabanli.md deleted file mode 100644 index 61cecd2..0000000 --- a/docs/galeri-klasor-tabanli.md +++ /dev/null @@ -1,65 +0,0 @@ -# Galeri: Klasör Tabanlı Gezinme — Rapor ve Plan - -Tarih: 2026-08-03 -Durum: Uygulandı (2026-08-03) - -## 1. Mevcut Sorunun Analizi - -### RAM şişmesi — `lib/services/image_manager.dart` -- `addImages()` her eklenen yol için `precacheImage(FileImage(...))` çağırıyor (satır 33-35). -- Sonuç: toplu sürüklemede tüm resimler tam çözünürlükte decode edilip ImageCache'e yazılıyor. -- Örnek: 12MP JPEG dekode ≈ 48MB RGBA; 30 resim ≈ 1.4GB. -- Decode UI thread'inde yapılıyor → pencere donması, "çok pencere açmış gibi" hissi. -- `configureCache()` tavanı: 1000 görsel / 1GB (satır 62-65). - -### Sağ tık efektlerinde çökme — `lib/widgets/image_canvas.dart` -- Her pointer move'da `setState` → `ColorFiltered` tüm görseli yeniden boyuyor (satır 224-240). -- Bellek zaten yüksekken + VM'de yazılımsal render → çökme en çok burada görülüyor. -- Kök neden RAM şişmesi; o çözülünce bu senaryo da hafifler. - -## 2. Alınan Kararlar (kullanıcı onayı) - -1. **Precache tamamen kaldırılacak** — hiçbir resim önceden decode edilmeyecek. -2. Görüntüleme anında decode: sıradaki resme geçişte yüklenene kadar beklenir. -3. **RAM kısıtlaması yok** — cache limitlerine dokunulmayacak, daraltılmayacak. -4. Önceki resim cache'ten düşebilir (zorunlu tutma yok). Precache olmadığı için - bellekte aynı anda yalnızca açık olan resim(ler) durur; kabarık galeri listesi - sorun yaratmaz. - -## 3. Yeni Özellik — Klasör Tabanlı Galeri - -Amaç: resimleri topluca seçip sürükleyerek galeri kurmak yerine, açılan herhangi -bir resmin bulunduğu dizin otomatik galeri olsun. - -Davranış: -- Bir resim açıldığında (çift tık, CLI argümanı, sürükle-bırak) bulunduğu dizin taranır. -- Dizindeki desteklenen imaj dosyaları (png/jpg/jpeg/jfif/jpe/webp/bmp/gif/wbmp) - galeri listesi olarak set edilir. -- Açılan resim listedeki konumunu bulur ve o an gösterilir. -- Ctrl+Tab / Ctrl+Shift+Tab ile klasör içinde ileri-geri gezilir. -- Mevcut pencere başına singleton yapısı korunur — her pencere ayrı process, - her biri kendi klasör galerisini kurar. - -### Sıralama -- Dosya yöneticisinin (Thunar/PCManFM/Nautilus) klasör sort'u OS seviyesinde - okunamaz — uygulama içi ayardır, dosya sisteminde saklanmaz. Bu seçenek pratikte yok. -- Bu yüzden **tarihe göre** sıralanacak (dosyanın değiştirilme zamanı). -- Yön: **en yeni önce, en eski sonra** (azalan). - -## 4. Uygulama (uygulandı) - -- `lib/services/image_manager.dart` — precache döngüsü ve `buildContext` kaldırıldı. - `addImages` yerine `setGalleryFromPaths(paths)` geldi: ilk geçerli resmin klasörünü - tarar, desteklenenleri filtreler, değiştirilme zamanına göre en yeni önce sıralar, - açılan resmi o anki konum yapar. (Aynı zaman damgasında dosya adına göre sabit sıra) -- `lib/services/file_handler.dart` — `addImages` çağrıları `setGalleryFromPaths` oldu - (Ctrl+O ve sürükle-bırak aynı akıştan geçiyor). -- `lib/screens/viewer_screen.dart` — `buildContext` ataması kaldırıldı (precache yok). -- RAM kısıtlaması eklenmedi — `configureCache` aynen duruyor. - -## 5. Açık Sorular - -Çözüldü: -1. ~~Tarih sıralama yönü~~ → **en yeni önce, en eski sonra** -2. ~~Pencereye birden fazla resim sürüklenirse~~ → **sürüklenen resmin klasörüne geçilir** - (galeri açıkken yeni resim gelirse de aynı: o resmin klasörü yeni galeri olur) diff --git a/docs/image-canvas-interactions.md b/docs/image-canvas-interactions.md deleted file mode 100644 index 58386ea..0000000 --- a/docs/image-canvas-interactions.md +++ /dev/null @@ -1,187 +0,0 @@ -# Image Canvas Interactions — Analiz ve İmplementasyon Planı - -**Tarih:** 2026-07-27 -**Kapsam:** `lib/widgets/image_canvas.dart` - ---- - -## 1. Mevcut Durum - -`ImageCanvas` widget'ı şu anda: - -- **Sol tık + sürükle:** Pan (imleç hareketiyle resmi kaydırma) — `GestureDetector.onPanUpdate` -- **Tekerlek:** Zoom (imleç pozisyonu merkezli) — `Listener.onPointerSignal → PointerScrollEvent` -- **Çift tık:** Fill/Fit mod geçişi — `GestureDetector.onDoubleTap` -- **Transform:** `Matrix4` ile sadece `translate(_tx, _ty)` + `scale(_sc)`, rotate yok -- **Renk/efekt filtresi:** Yok - -Sorun: `GestureDetector.onPanUpdate` hangi mouse tuşuna basıldığını ayırt etmez. Tüm tuşlarla yapılan sürüklemeyi `onPanUpdate` olarak yakalar. Sağ tık için ayrı bir işlem yapılamaz. - ---- - -## 2. Gereksinimler - -| # | Etkileşim | Davranış | -|---|---|---| -| R1 | **Sağ tık + Dikey sürükle** | Rotate. Yukarı sürükle → saat yönü, aşağı sürükle → saat tersi. Viewport merkezinden dönmeli. Hassasiyet: ~100px = 90° | -| R2 | **Ctrl + Sağ tık + Dikey sürükle** | Contrast ayarı. Yukarı sürükle → artır, aşağı sürükle → azalt. Aralık: 0.0 – 2.0 (default: 1.0) | -| R3 | **Shift + Sağ tık + Dikey sürükle** | Brightness ayarı. Yukarı sürükle → artır, aşağı sürükle → azalt. Aralık: -1.0 – 1.0 (default: 0.0) | -| R4 | **Sağ çift tık** | Reset: rotate, contrast, brightness sıfırlanır (0°, 1.0, 0.0) | -| R5 | **Sol tık + sürükle** | Mevcut pan davranışı korunacak | - ---- - -## 3. Teknik Tasarım - -### 3.1 Gesture Handling: `GestureDetector` → `Listener` - -Mevcut `GestureDetector` tuş ayrımı yapamadığı için, **`Listener` widget'ı** ile pointer event'leri manuel yakalayacağız: - -``` -Listener( - onPointerDown: _onPointerDown, // hangi tuş + modifier → mod belirle - onPointerMove: _onPointerMove, // moda göre pan / contrast / brightness - onPointerUp: _onPointerUp, // tıklama ise rotate uygula - onPointerSignal: _handleScroll, // zoom (mevcut) -) -``` - -**Mod State Machine:** - -``` -PointerDown: - if (sol tuş) → mod = pan - if (sağ tuş, no modifier) → mod = rotate, başlangıç açısını kaydet - if (sağ tuş + Ctrl) → mod = contrast, başlangıç değerini kaydet - if (sağ tuş + Shift) → mod = brightness, başlangıç değerini kaydet - -PointerMove: - pan: _tx += dx, _ty += dy - rotate: _angle += dy * sensitivity (clamp yok, serbest dönüş) - contrast: _contrast += dy * sensitivity, clamp(0.0, 2.0) - brightness: _brightness += dy * sensitivity, clamp(-1.0, 1.0) - -PointerUp: - modu temizle - -Çift tık (sağ tuş): - _angle = 0; _contrast = 1.0; _brightness = 0.0 -``` - -**Tuş tespiti:** `PointerEvent.buttons` bitmask'i: -- `kPrimaryMouseButton` (1) = sol -- `kSecondaryMouseButton` (2) = sağ -- `kMiddleMouseButton` (4) = orta - -**Modifier tespiti:** `HardwareKeyboard.instance.isControlPressed` / `isShiftPressed` - -### 3.2 Rotate — Matrix4 Güncellemesi - -Rotate, viewport'un **tam merkezi** etrafında dönmeli. Matrix4 işlem sırası (son yazılan ilk uygulanır): - -```dart -Matrix4.identity() - ..translate(vpCenterX, vpCenterY) // 4. pivot noktasına geri taşı - ..rotateZ(_angleRad) // 3. döndür - ..translate(-vpCenterX, -vpCenterY) // 2. pivot'u orijine taşı - ..translate(_tx, _ty) // 1. pan offset'i - ..scale(_sc); // 0. zoom -``` - -`_angleRad = _angle * pi / 180` - -### 3.3 Contrast ve Brightness — ColorFilter - -`Image` widget'ını `ColorFiltered` ile saracağız. Hem contrast hem brightness tek bir matrix'te birleştirilebilir: - -```dart -ColorFiltered( - colorFilter: ColorFilter.matrix(_buildColorMatrix()), - child: imageWidget, -) -``` - -**Birleşik matrix (contrast × brightness):** - -Önce brightness, sonra contrast uygulanır: - -``` -b = brightness (-1.0 .. 1.0) -c = contrast (0.0 .. 2.0) -t = (1.0 - c) / 2.0 - -Matrix: - c, 0, 0, 0, t + b, - 0, c, 0, 0, t + b, - 0, 0, c, 0, t + b, - 0, 0, 0, 1, 0, -``` - -Default değerler: `c = 1.0, b = 0.0` → birim matrix (görüntü değişmez). - -### 3.4 Sensitivity (Hassasiyet) - -- **Rotate:** `_angle += dy * 0.9` (100px sürükleme ≈ 90°) -- **Contrast:** `_contrast += dy * 0.005` (200px sürükleme = 1.0 birim) -- **Brightness:** `_brightness += dy * 0.005` (200px sürükleme = 1.0 birim) - -Değerler `setState` içinde clamp'lenir (rotate hariç, serbest dönüş). - -### 3.5 State Değişkenleri (Eklenecekler) - -```dart -double _angle = 0; // derece cinsinden, serbest -double _contrast = 1.0; // 0.0 – 2.0 -double _brightness = 0.0; // -1.0 – 1.0 - -// Gesture tracking -String _interactionMode = 'none'; // 'pan' | 'rotate' | 'contrast' | 'brightness' -Offset _pointerDownPos = Offset.zero; -double _startAngle = 0; -double _startContrast = 1.0; -double _startBrightness = 0.0; -``` - ---- - -## 4. Riskler ve Dikkat Edilecek Noktalar - -| Risk | Etki | Çözüm | -|---|---|---| -| Sağ tık + sürükleme, pan ile çakışabilir | Yanlış moda girme | `rotate_pending` modunda eşik kontrolü: 5px'den az hareket → rotate, fazla → pan | -| Rotate sonrası pan yönü bozulabilir | Kullanıcı deneyimi | Rotate, pan offset'lerinden (`_tx, _ty`) önce değil sonra uygulandığı için etkilenmez. Ama fit mode'da letterbox offset'i (`m, n`) rotate'den etkilenir — test edilecek | -| ColorFilter performansı | Büyük resimlerde takılma | Flutter'da `ColorFiltered` GPU'da çalışır, performans sorunu beklenmiyor | -| Ctrl/Shift + scroll zoom ile çakışma | Zoom yerine contrast değişebilir | Scroll event'leri `onPointerSignal` ile ayrı yakalanıyor, karışmaz | -| Fill/Fit mod geçişinde rotate sıfırlanmalı mı? | UX kararı | `_toggleFillFit()` içinde `_angle = 0` yapalım — fill ↔ fit geçişinde rotate sıfırlansın | - ---- - -## 5. İmplementasyon Planı - -### Adım 1: Listener tabanlı gesture sistemine geçiş -- `GestureDetector` yerine `Listener` kullan -- `onPointerDown`, `onPointerMove`, `onPointerUp` handler'larını yaz -- Sol tık pan'i koru, sağ tık modlarını ekle - -### Adım 2: Rotate -- `_angle` state değişkeni ekle -- `Matrix4`'e rotate transform'u ekle -- `_resetView()` ve `_toggleFillFit()` içinde `_angle = 0` - -### Adım 3: Contrast ve Brightness -- `_contrast`, `_brightness` state değişkenleri ekle -- `ColorFiltered` widget'ı ile image'ı sar -- `_buildColorMatrix()` metodunu yaz - -### Adım 4: Test ve clamp -- Tüm modlarda clamp/değer aralığı kontrolü -- Rotate + zoom + pan birlikte çalışıyor mu test et - ---- - -## 6. Etkilenen Dosyalar - -| Dosya | Değişiklik | -|---|---| -| `lib/widgets/image_canvas.dart` | Ana değişikliklerin tümü burada | -| (başka dosya yok) | Diğer widget'lar etkilenmez | diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 00b7955..0000000 --- a/docs/index.md +++ /dev/null @@ -1,40 +0,0 @@ -# imajViewer — Dokümantasyon İndeksi - -| Tarih | Dosya | Konu | -||---|---|---| -| 2026-08-06 | [rapor-github-acik-kaynak.md](rapor-github-acik-kaynak.md) | **Rapor** — GitHub açık kaynak hazırlığı (gereksinimler, riskler, plan) | -| 2026-10-11 | [plan-crop-save.md](plan-crop-save.md) | **Plan (1.4.0)** — Crop Save: görünen alanı orijinal çözünürlükte JPEG q90 kaydetme (uygulandı) | -| 2026-08-06 | [Implementation.md](Implementation.md) | Teknik uygulama dokümanı (kökten taşındı) | -| 2026-08-06 | [PRD.md](PRD.md) | Product Requirements Document (kökten taşındı) | -| 2026-08-06 | [update_plan_01.md](update_plan_01.md) | Pan sınırlama & zoom merkezi sorunu (kökten taşındı) | -| 2026-08-06 | [rapor-pan-kilitleme-nuks.md](rapor-pan-kilitleme-nuks.md) | **Rapor** — efekt uygulanmış görselde pan sırasında kilitlenme nüksü (analiz, açık sorular) | -| 2026-08-06 | [rapor-gezinme-ayarlar-print.md](rapor-gezinme-ayarlar-print.md) | **Rapor** — gezinme okları (hover), ayarlar/gear (sıralama), yazdırma (printing paketi) | -| 2026-08-06 | [rapor-sag-tik-donma-analizi.md](rapor-sag-tik-donma-analizi.md) | **Rapor** — sağ tuş efektlerinde donma analizi + slider UI değerlendirmesi | -| 2026-08-03 | [zoom-clamp-mekanizmasi.md](zoom-clamp-mekanizmasi.md) | **Düzeltme** — `_vpSize` sıfırlama bug'ı: çift tıklama sonrası zoom ölümü/pan serbestliği | -| 2026-08-03 | [performance_llvmpipe_issue.md](performance_llvmpipe_issue.md) | **Rapor** — llvmpipe donma analizi, kare hızı throttle, ölçüm sonuçları | -| 2026-08-03 | [rapor-brightness.md](rapor-brightness.md) | **Rapor** — brightness efekti (Ctrl+Shift+sağ tık) + 4x5 matris birleştirme | -| 2026-08-03 | [galeri-klasor-tabanli.md](galeri-klasor-tabanli.md) | **Rapor** — klasör tabanlı galeri + precache/RAM kararı | -| 2026-08-03 | [session-2026-08-03.md](session-2026-08-03.md) | **Session özeti** — jfif/jpe/wbmp, pencere cascade, konum kaydı düzeltmesi | -| 2026-08-03 | [format-destegi-ek-formatlar.md](format-destegi-ek-formatlar.md) | **Plan** — jfif/jpe/wbmp desteği (TIFF vazgeçildi) | -| 2026-08-03 | [pencere-cascade-yerlesim.md](pencere-cascade-yerlesim.md) | **Plan** — yeni pencereyi yan yana / alt satıra açma (screen_retriever) | -| 2026-07-27 | [session-windows-ci.md](session-windows-ci.md) | **Windows setup.exe yeniden derlendi, Gitea CI eklendi** | -| 2026-07-27 | [session-2026-07-27.md](session-2026-07-27.md) | **Session özeti** — rotate aktif, StartupWMClass ile taskbar gruplama | -| 2026-07-27 | [image-canvas-interactions.md](image-canvas-interactions.md) | Detaylı analiz: contrast, saturation, rotate implementasyon planı | -| 2026-08-10 | [windows-setup.md](windows-setup.md) | **Windows Development Ortam Kurulum Rehberi** — Flutter SDK, VS Build Tools, CMake, Inno Setup kurulum adımları | -| önceki | [zoom-clamp-mekanizmasi.md](zoom-clamp-mekanizmasi.md) | Zoom clamp mekanizması (letterbox-aware) | - -## Geliştirme Akışı — Otomatik Build ve Kurulum - -### Linux -1. Debug build: `flutter build linux --debug` -2. Sisteme kurulum: `./install.sh` (debug bundle tercih edilir → `~/.local/lib/imajviewer/`) - -### Windows -1. Ortam kurulumu: [windows-setup.md](windows-setup.md) rehberini takip edin -2. Build + installer: `.\build-windows.ps1` -3. Manuel kurulum (build çıktılarından): `.\install.ps1` -4. Kaldırma: `.\uninstall.ps1` - -Projede değişiklik yapıldıktan sonra otomatik yapılır (ayrıca istenmez): -1. Debug build: `flutter build linux --debug` -2. Sisteme kurulum: `./install.sh` (debug bundle tercih edilir → `~/.local/lib/imajviewer/`) diff --git a/docs/pencere-cascade-yerlesim.md b/docs/pencere-cascade-yerlesim.md deleted file mode 100644 index 59e556c..0000000 --- a/docs/pencere-cascade-yerlesim.md +++ /dev/null @@ -1,111 +0,0 @@ -# Pencere Cascade Yerleşimi Planı - -> Tarih: 2026-08-03 · Değerlendirme + implementasyon planı (henüz kod değişikliği yapılmadı) - -## 1. Sorun - -imajViewer'a yeni bir resim açıldığında pencere **hep aynı koordinat ve boyutta** açılıyor (önceki pencerenin üstüne biniyor). İstenen davranış: - -- Yeni pencere, önceki pencerenin **x + width** kadar sağına açılsın (yan yana) -- Ekrandan **taşacaksa** alt satıra, satır başından (x baştan) açılsın - -Bu, Windows'un klasik "cascade windows" davranışının basit hali. - -## 2. Mevcut Durum Analizi - -### Pencere açılış akışı (`lib/main.dart`) -``` -main(args) - → WindowPersistence.restore() // window.json'dan son konum/boyut - → windowOptions = WindowOptions(size: saved ?? 1280x800) - → waitUntilReadyToShow: - saved varsa → setPosition(saved.position) + setSize(saved.size) - saved yoksa → center() - → runApp(ImajViewerApp(initialFiles: args)) -``` - -### Konum kaydetme akışı (`lib/screens/viewer_screen.dart` + `lib/services/window_persistence.dart`) -- `_startPolling()`: 2 saniyede bir `windowManager.getBounds()` → `WindowPersistence.saveSync()` → `~/.config/imajviewer/window.json` (Linux) / `%APPDATA%/imajviewer/window.json` (Windows) -- `onWindowEvent` (move/resize/close) → debounce'lu `_doSave()` - -### Sorunun kökü -- Her yeni pencere `window.json`'daki **son kaydedilen** konumu okuyor → aynı yere açılıyor -- Cascade mantığı yok; ekran genişliği bilgisi hiç sorgulanmıyor - -## 3. Teknik Araştırma (doğrulandı) - -### window_manager 0.4.3 (mevcut bağımlılık) -- `setPosition`, `setSize`, `getBounds`, `getPosition`, `getSize`, `center()` → var -- **Ekran/display API'si → YOK** (getScreen/getPrimaryDisplay yok) - -### window_manager 0.5.2 (güncel) -- Ekran API'si yine yok — `screen_retriever`'ı bağımlılık olarak içeriyor ama expose etmiyor - -### screen_retriever 0.2.2 (pub.dev, window_manager'ın zaten bağımlısı) -- `ScreenRetriever.instance.getPrimaryDisplay()` → `Display` -- `getAllDisplays()` → `List` -- `Display` alanları: - - `size` (ekran tam boyutu, logical px) - - `visiblePosition` (Offset — çalışma alanı başlangıcı) - - `visibleSize` (Size — görev çubuğu hariç çalışma alanı) - - `scaleFactor` -- Linux (X11) + Windows desteği var - -**Karar: `screen_retriever` doğrudan bağımlılık olarak eklenir** (window_manager sürümü değişmez). - -## 4. Önerilen Algoritma - -`main.dart`'ta, `windowOptions` oluşturulmadan önce: - -``` -1. saved = WindowPersistence.restore() -2. display = await ScreenRetriever.instance.getPrimaryDisplay() - workArea = display.visiblePosition + display.visibleSize (yoksa display.size) -3. Eğer saved != null: - newX = saved.x + saved.width // önceki pencerenin sağına - newY = saved.y - - // Yatay taşma → alt satıra, satır başına - if (newX + pencereGenişliği > workArea.right): - newX = workArea.left - newY = saved.y + saved.height - - // Dikey taşma da varsa → en başa sar (0,0) - if (newY + pencereYüksekliği > workArea.bottom): - newX = workArea.left - newY = workArea.top - - position = Offset(newX, newY) -4. saved yoksa → center() (mevcut davranış) -``` - -### Örnek -- Ekran 1920×1080, pencere 1280×800, önceki pencere (100, 50) -- 1. yeni: (1380, 50) — yan yana ✓ -- 2. yeni: (100+1280*2 = hesaplanır) → 1380+1280=2660 > 1920 → (0, 850) — alt satır başı ✓ - -> Not: `saved` her 2 saniyede bir güncellendiği için "önceki pencere" = en son kaydedilmiş konum. Bu, en son açılan pencerenin konumu olur — istenen davranışla uyumlu. - -## 5. Yapılacaklar (sıralı) - -1. [ ] `pubspec.yaml` → `screen_retriever: ^0.2.2` ekle, `flutter pub get` -2. [ ] `lib/main.dart` → cascade hesabı ekle (yeni bir servis fonksiyonuna taşınabilir) -3. [ ] `lib/services/window_persistence.dart` → `restore()` dışında, cascade hesabı için yardımcı fonksiyon: - - `static Future cascadePosition({required Size windowSize})` (opsiyonel, temizlik için) -4. [ ] Ekran boyutu alınamazsa (hata / çoklu monitör) → mevcut davranışa düş (saved veya center) -5. [ ] Test: ardışık 3-4 resim aç → yan yana dizilim + taşınca alt satır - -## 6. Riskler / Açık Sorular - -- **Çoklu monitör:** `getPrimaryDisplay` birincil ekranı verir. İmleç hangi ekrandaysa oraya açılsın mı? (başlangıç: primary) -- **Pencere kapatılıp yenisi açılırsa:** `window.json` son kapanan pencerenin konumunu tutar; yeni pencere onun yanına açılır — boşluk oluşabilir, kabul edilebilir -- **Tam ekran / maximize durumu:** saved boyut maximize ise cascade hesabı yine de eski boyutla yapılır; `window.json` sadece normal durumda yazıldığı için risk düşük -- **Pencere ekrandan tamamen dışarıdaysa:** `restore()` zaten `x < -1000 || y < -1000` guard'ı var (window_persistence.dart:44) — yine de sağlamlaştırılabilir - -## 7. Kabul Kriterleri - -- [ ] Ardışık 2 pencere: ikincisi birincinin hemen sağında -- [ ] Yatay taşma: yeni pencere alt satıra, satır başından başlar -- [ ] İlk açılışta (kayıt yok) pencere yine ortalanır -- [ ] Pencere boyutu/konumu kaydetme davranışı bozulmaz -- [ ] Linux ve Windows'ta çalışır (screen_retriever her ikisini destekliyor) diff --git a/docs/performance_llvmpipe_issue.md b/docs/performance_llvmpipe_issue.md deleted file mode 100644 index ac4c851..0000000 --- a/docs/performance_llvmpipe_issue.md +++ /dev/null @@ -1,71 +0,0 @@ -# Performans: llvmpipe (Yazılımsal Render) Altında Efekt Donmaları - -Tarih: 2026-08-03 -Durum: Uygulandı (ölçüm sonuçları bölüm 3'te) - -## 1. Teşhis - -- VM'de donanım GPU'su yok: `glxinfo -B` → `Device: llvmpipe (LLVM 21.1.8, 128 bits)`, - `/dev/dri` yok. Tüm çizim 4 CPU çekirdeğinde LLVM ile yapılıyor. -- Efekt sürüklemesi (contrast/saturation/brightness/rotate): her `PointerMoveEvent` - doğrudan `setState` → tüm görsel katman `ColorFiltered` ile yeniden boyanıyor. -- Fare sürüklemesinde olay hızı 100-250+/sn; llvmpipe kare süresi 20-100ms+. - Üretim > tüketim → kuyruk birikmesi → UI thread %100 → donma. Kuyruk boşalınca - arayüz yakalıyor ("1 dk sonra normale döndü"). -- Rotate'in önce kilitlenmesi normal: `Transform` ikinci bir composite geçişi ekliyor. - Contrast aynı duvara galeri akışıyla (daha büyük görseller, dolu ImageCache) çarptı. - -## 2. Uygulanan Değişiklikler - -### Render izolasyonu ve kare hızı eşitleme — `lib/widgets/image_canvas.dart` - -- Pointer move / scroll / pan / efekt işleyicilerinden `setState` kaldırıldı. -- `ValueNotifier _renderTick` + `ValueListenableBuilder` — görsel katman yalnızca - tick ile yeniden kurulur. -- `SchedulerBinding.instance.scheduleFrameCallback` — saniyede en fazla 1 rebuild/kare - (ekran yenileme hızıyla sınırlı); olaylar kareye eşitlenir, kuyruk birikmez. -- `RepaintBoundary` — efekt repaint'i pencere/title bar/üst widget render'ını etkilemez. -- Not: per-efekt `ValueNotifier` yerine tek tick notifier kullanıldı. Nedeni: - `ValueListenableBuilder` her değer değişiminde yeniden kurulur (olay hızında kalır); - throttling için tek noktadan kare başına tick gerekli. - -### Image pipeline — `lib/widgets/image_canvas.dart` + `lib/services/image_manager.dart` - -- `ResizeImage.resizeIfNeeded(cacheWidth: viewport×2)` — yüksek çözünürlüklü görseller - görüntüleme boyutunun ~2 katıyla sınırlı decode edilir: decode süresi, bellek ve - llvmpipe CPU yükü düşer. (Zoom 2x'e kadar net; ötesi canlı önizleme için kabul edilen takas.) -- `configureCache`: 1000 görsel / 1GB → **16 görsel / 256MB**. -- Galeri/klasör geçişinde `imageCache.clear()` — canlı (gösterilen) görsel korunur, - eski klasörün decode edilmiş `ui.Image` nesneleri bellekten düşer. - -## 3. Ölçüm (llvmpipe) - -Ölçüm yöntemi: 4000×3000 PNG açıldı; kullanıcı elle contrast / saturation / -brightness sürüklemeleri yaptı. Kare süreleri `addTimingsCallback` ile (geçici -enstrümantasyon, sonra kaldırıldı), CPU `pidstat` ile örneklendi. - -| Metrik | Değer | -|---|---| -| Kare sayısı (contrast turu) | 76 | -| build ort / max | 3.9 ms / 78 ms | -| raster ort / max | 15.1 ms / 52 ms | -| Kare sayısı (sat/brightness turu) | 133 | -| build ort / max | 3.0 ms / 82 ms | -| raster ort / max | 14.1 ms / 48 ms | -| CPU — boştayken | %0-14 | -| CPU — aktif sürüklemede | %100-163 (1-1.6 çekirdek) | - -Değerlendirme: raster ort. ~14-15 ms (16.7 ms kare bütçesinin altında), sürükleme -sırasında maksimum ~50 ms → ara sıra kare atlanır ama **kuyruk birikmesi ve -dakikalarca süren donma yok**. CPU 4 çekirdeğin tamamını asla doyurmuyor -(önceki durumda sürükleme anında tam doygunluk + 1 dk donma vardı). - -## 4. VM Sistem Gereksinimleri (öneri — uygulama dışı) - -llvmpipe tamamen yazılımsal; kalıcı çözüm VM tarafında donanım hızlandırmasıdır: - -- **virtio-gpu + VirGL**: QEMU/libvirt'ta `-device virtio-vga-gl` / `virtio-gpu-gl` - (mesa'nın VirGL yoluyla hızlandırılmış GL'si). `/dev/dri` görünür hale gelir. -- **PCIe Passthrough**: gerçek GPU'yı VM'e bağlama (en yüksek performans, donanım gerektirir). -- Bu olmadan uygulama yukarıdaki yazılımsal önlemlerle çalışır, ancak büyük görsellerde - efekt sürüklemesi yine de kare atlayabilir. diff --git a/docs/plan-crop-save.md b/docs/plan-crop-save.md deleted file mode 100644 index 8abc8b3..0000000 --- a/docs/plan-crop-save.md +++ /dev/null @@ -1,98 +0,0 @@ -# Plan — Crop Save (görünen alanı kaydet) - -Tarih: 2026-10-11 · Versiyon: 1.4.0 · Durum: UYGULANDI (2026-10-11) — manual test + commit/push bekleniyor - -## Not — Uygulama Sırası -- `image` v3 kullanılamadı: `msix 3.18.0` `image ^4.0.0` gerektiriyor → v4 async API kullanıldı - (`decodeImage` Uint8List ile, `copyCrop(x:,y:,width:,height:)`, `JpegEncoder(quality:90)`, - `bakeOrientation`, pixel kanalları `r/g/b/a`). -- v4'te renk matrisi filtresi yok → matris `crop_save_service.dart:_applyMatrix` ile elle - uygulandı (aynı 0-255 alanı). -- Boyut okuma ayrı descriptor yerine tam decode (`load()`) üzerinden yapılıyor. - -## Gereksinim + Kararlar -- Pencerenin görünen kısmı (image canvas viewport'u) → **orijinal çözünürlükte** crop -- JPEG **quality 90** -- Dosya adı: orijinal ismin sonuna **saat only** eki → `resim.jpg` → `resim_143025.jpg` -- **Çakışma:** dosya zaten varsa yazı YAPILMAZ, uyarı gösterilir (mevcut dosya korunur) -- **Renk efektleri:** kayda **GÖMÜLÜR** (contrast/saturation/brightness, ekrandaki gibi) -- **Rotasyon aktifken:** yapılsın — görünür bölgeyi saran dikdörtgen (bounding box) kaydedilir -- Title bar: Save butonu minimize'in SOLUNDA (sıralama: **save · minimize · x**) -- Klavye: **Ctrl+S** de tetikler - -## Mimari Analiz - -### 1) Görünen alan → kaynak piksel eşlemesi -`image_canvas.dart` render zinciri: `Transform(translate(_tx,_ty) scale(_sc))` içinde -opsiyonel `Transform(rotate(_angle), vpCenter)` → `Image(fit: cover|contain)`. - -Canvas lokal koordinatındaki bir nokta `p` için geri dönüş: -1. `q = (p - (tx,ty)) / sc` -2. `_angle != 0` ise `r` = `q`'yi vpCenter ekseninde `-angle` kadar döndür (yoksa `r = q`) -3. **cover** (`_isFilled`): `k = max(vpW/iw, vpH/ih)`; **contain**: `k = min(vpW/iw, vpH/ih)`; - her ikisinde de `ox=(vpW-iw*k)/2`, `oy=(vpH-ih*k)/2` → `src = ((r.x-ox)/k, (r.y-oy)/k)` -4. Viewport'un 4 köşesi → `src` uzayda → min/max bounding box → `[0,iw]×[0,ih]`'a clamp → int -5. Rotasyonlu durumda bounding box, ekranda görünür bölgeyi kapsayan dikdörtgendir - (çıkış aspect/boyutu pencereden farklı olabilir — onaylı davranış) - -- `iw,ih` = **gerçek** dosya boyutu (descriptor ile). `_imageSize` downsample'lı (2048) - olabilir → eşlemede kullanılmaz. -- Görünen alan = **tamamı canvas viewport** (title bar 24px bindirilmiş overlay, görsel - arkasında görünüyor → kesime dahil). - -### 2) Encode zinciri (yeni bağımlılık: `image` ^4.x, saf Dart) -``` -File → img.decodeImage(bytes, keepOrientation: true) - → img.cropRect(rect) - → aktif renk matrisi var ise img.ColorMatrixFilter(mat4x5).applyTo(crop) - → img.encodeJpeg(crop, quality: 90) - → hedef yol var ise → uyarı (yazılmaz); yoksa File.writeAsBytes -``` -- **Renk matrisi:** `image_canvas.dart`'daki mevcut `_buildCombinedFilter()` 4x5 matrisi - (contrast×brightness×saturation) aynı zamanda `ColorFilter.matrix` olarak kullanılıyor. - State'e `List? activeColorMatrix` getter'ı eklenir (filtre identity ise null). - Flutter ve `image` paketi aynı 4x5 row-major / 0-255 alanı kullanır → matris aynen - taşınır, yeniden türetilmez. -- **Tuzak — EXIF yönü:** Flutter engine JPEG'lerde EXIF orientation uygular; - `keepOrientation: true` verilmezse 90° döndürülmüş fotoğraflarda crop yanlış yerden - kesilir. Test listesinde zorunlu. -- **WBMP:** `image` WBMP çözer (uygulama destekliyor); decode hatası → "desteklenmiyor" - mesajı, çökme yok. - -### 3) Bileşenler -| Dosya | Değişiklik | -|---|---| -| `pubspec.yaml` | `image: ^4.x`; versiyon `1.4.0+8` | -| `lib/services/crop_save_service.dart` | **YENİ** — `imageSize(path)` (descriptor), `saveCrop({srcPath, rect, matrix, outPath})`, ad üretimi (`_HHmmss`), çakışma kontrolü (exists → uyarı), hata mesajları | -| `lib/widgets/image_canvas.dart` | State → `ImageCanvasState` (public); `Rect? cropSourceRect(int w, int h)`; `List? activeColorMatrix` getter | -| `lib/screens/viewer_screen.dart` | `GlobalKey`, `_cropSave()`, sonuç geri bildirimi (kısa overlay/Snackbar: yol veya hata) | -| `lib/widgets/custom_title_bar.dart` | `onSaveCrop` param; save ikonu minimize'in solunda; mevcut `_BarButton` stili (hover kırmızı) | -| `lib/shortcuts/app_shortcuts.dart` | `Ctrl+S` → crop save (callback param ile) | -| `lib/l10n/app_{en,tr}.arb` | `saveCrop` (tooltip), `cropSaved`, `cropFailed`, `cropFileExists`, `cropUnsupported` + `shortcutsList` güncelleme; `flutter gen-l10n` | -| `CHANGELOG.md` | `[1.4.0]` | - -### 4) Etkileşim -- Buton yalnızca görsel yüklüyken aktif (yoksa null → pas geçilir). -- Başarıda: kaydedilen tam yol gösterilir; hata/çakışmada: hata metni. -- Tam decode kayıt anında, tek seferlik (downsample swap ile ilişkisi yok). - -## Sınır Durumları -1. Görsel yok → buton pasif, Ctrl+S no-op -2. Crop rect boş/degenerate → hata mesajı -3. Yazılamayan klasör → hata mesajı, çökme yok -4. **Çakışma: dosya var → yazılmaz, uyarı** (karar) -5. WBMP/decode hatası → `cropUnsupported` -6. Rotasyonlu bounding box (karar: kaydedilir) -7. Büyük görsellerde tam decode RAM'i — tek seferlik, kabul edilebilir - -## Doğrulama -1. `flutter analyze` (otomatik izinli) -2. `flutter build linux --debug` + `./install.sh` (otomatik izinli) -3. Kullanıcı manuel test: fit/cover, zoom, pan, rotasyon + kaydet, renk efektleri + kaydet, - EXIF 90° görsel, WBMP, çakışan isim (uyarı + dosya korunur), salt okunur dizin -4. Commit + push: yalnızca senin komutunla - -## Kapsam Dışı -- Kullanıcı alanı seçerek crop (her zaman "görünen bölge") -- PNG/özel format seçimi (sabit JPEG q90) -- Gitea CI release / tag (senin komutunla) diff --git a/docs/rapor-aspect-ratio-ram.md b/docs/rapor-aspect-ratio-ram.md deleted file mode 100644 index a77fd9e..0000000 --- a/docs/rapor-aspect-ratio-ram.md +++ /dev/null @@ -1,135 +0,0 @@ -# Analiz: Aspect Ratio Bozulması + RAM Büyümesi - -Tarih: 2026-08-14 -Sürüm: v1.3.0 sonrası regresyon - ---- - -## 1. Aspect Ratio Bug'ı (kök neden bulundu) - -### Belirti -- Next/prev ile resim değişince görüntü **en/boy oranı bozulmuş** (gerilmiş/sıkışmış) geliyor. -- Fazla zoom yapınca aspect düzeliyor. - -### Kök Neden — `ResizeImagePolicy.exact` - -`lib/widgets/image_canvas.dart:429`: - -```dart -ResizeImage.resizeIfNeeded(effectiveWidth, effectiveWidth, _cachedFileImage!) -``` - -Bu çağrı Flutter SDK içinde şuna dönüşür (`image_provider.dart:1302-1310`): - -```dart -static ImageProvider resizeIfNeeded(int? cacheWidth, int? cacheHeight, provider) { - if (cacheWidth != null || cacheHeight != null) { - return ResizeImage(provider, width: cacheWidth, height: cacheHeight); - } - return provider; -} -``` - -Ve `ResizeImage` constructor'ı (`image_provider.dart:1262-1268`): - -```dart -const ResizeImage(this.imageProvider, { this.width, this.height, - this.policy = ResizeImagePolicy.exact, // ← VARSAYILAN exact - this.allowUpscaling = false, -}); -``` - -**`ResizeImagePolicy.exact`**: genişlik ve yüksekliği **bağımsız** ölçekler, aspect ratio'yu -**koruMAZ**. Yani `width: 2048, height: 2048` demek → her resmi **2048×2048 kareye -zorla**. 4000×2000 bir resim 2048×2048'e gerilir (2:1 → 1:1), görüntü bozulur. - -Zoom yapınca `_checkFullResSwap()` → `_usingFullRes = true` → `_cachedProvider = -_cachedFileImage` (orijinal boyutlu `FileImage`, aspect doğru) → görüntü düzelir. -Bu, belirtiyle birebir örtüşüyor. - -### Düzeltme - -`resizeIfNeeded` yerine doğrudan `ResizeImage` + `policy: ResizeImagePolicy.fit`: - -```dart -_cachedProvider = effectiveWidth != null - ? ResizeImage( - _cachedFileImage!, - width: effectiveWidth, - height: effectiveWidth, - policy: ResizeImagePolicy.fit, - ) - : _cachedFileImage; -``` - -`fit` policy'si (SDK `image_provider.dart:1378-1409`) aspect ratio'yu koruyarak -2048×2048 kutusuna **sığdırır**: -- 4000×2000 → 2048×1024 (2:1 korunur) -- 800×20000 dikey → 82×2048 (HATA-2'nin istediği dikey sınırlama da korunur) -- 20000×800 panorama → 2048×82 - -> Not: `resizeIfNeeded` helper'ı `policy` parametresi **almaz**, bu yüzden `fit` -> için `ResizeImage`'ı doğrudan kurmak zorunlu. - ---- - -## 2. RAM Büyümesi Analizi - -### Gözlem -RAM 200MB'a kadar büyüdü. "Daha büyür mü?" — Evet, aşağıdaki modele göre büyüyebilir. - -### RAM modeli (mevcut) - -| Kaynak | Boyut | -|---|---| -| Downsample (fit 2048×2048, RGBA) | en fazla 2048×2048×4 = **16.7MB** | -| Full-res swap (zoom ≥ 1.5x) | resme bağlı: 12000px → **576MB**, 4000px → 64MB | -| `ImageCache.maximumSize` | 16 entry | -| `ImageCache.maximumSizeBytes` | 256MB | - -### Neden 200MB'a büyüdü? - -1. Kullanıcı zoom yaptı → `_checkFullResSwap` threshold 1.5 aşıldı → tam çözünürlük - decode edildi → RAM'e ~100MB+ girdi. -2. Full-res resim **canlı** olduğu için `maximumSizeBytes = 256MB` sınırı onu - **atamaz** (Flutter aktif kullanımdaki image'ı evict etmez). - -### Daha büyür mü? — Evet, iki senaryo - -- **Daha büyük resim:** 12000px panorama full-res'e geçince tek resim ~576MB. - `maximumSizeBytes = 256MB` canlı resmi korur, sınır aşılır. -- **Çok sayıda full-res swap:** her resme zoom yapıp geçince, `didUpdateWidget` - eski provider'ı `evict` eder ama evict **asenkron/gecikmeli** olabilir; ardışık - hızlı geçişte 2-3 full-res aynı anda RAM'de kalabilir. - -### Yapılacak iyileştirmeler - -1. **Full-res swap'ı kademeli yap** (şu an binary: 2048 → full). Orta seviye - (ör. 4096px) ekleyerek RAM'i sınırla. Ama bu kullanıcı isteği değilse yapma. -2. **`_checkFullResSwap` stretch eşiğini yükselt** (1.5 → 2.0) → daha az full-res - swap, daha az RAM. Trade-off: zoom'da biraz daha geç keskinleşme. -3. **`maximumSizeBytes`'ı düşür** (256MB → 128MB) → LRU daha agresif. Ama canlı - resmi yine korur, etkisi sınırlı. -4. **Full-res swap'ı dispose etme zamanlamasını netleştir** — şu an `didUpdateWidget` - evict yapıyor; `dispose()` içine `_cachedProvider?.evict()` de eklenebilir. - -### Karar önerisi - -Aspect bug'ı **kesin düzeltilmeli** (görsel regresyon, her resimde). RAM büyümesi -ise full-res swap'ın **beklenen davranışı** — 200MB makul, ama 576MB'lık dev -panoramalarda tek resimde patlayabilir. `fit` policy'ye geçmek downsample'ı da -düzelteceği için ortalama RAM'i de bir miktar düşürür (exact kare yerine doğru -orantılı küçük resim). - ---- - -## 3. Öncelik Sırası - -1. **Aspect bug fix** — `ResizeImagePolicy.fit` (tek satır, kritik görsel düzeltme) -2. **RAM** — şimdilik izle; 2. eşik/kademeli swap istersen ayrı yaparız. - -## 4. Doğrulama - -- `flutter analyze`: 0 error -- Release build: ✓ -- Manuel test: kullanıcı (aspect düzelmesi + RAM gözlemi) diff --git a/docs/rapor-brightness.md b/docs/rapor-brightness.md deleted file mode 100644 index ea3e27d..0000000 --- a/docs/rapor-brightness.md +++ /dev/null @@ -1,62 +0,0 @@ -# Rapor: Brightness Efekti + Sağ Tık Etkileşim Düzeni - -Tarih: 2026-08-03 -Durum: Rapor — uygulama için onay bekleniyor - -## 1. Gönderilen Araştırmanın Özeti - -- `ColorMatrixHelper`: Rec.709 luma katsayıları (0.2126 / 0.7152 / 0.0722) — projedekiyle aynı. -- Üç ayrı 4x5 renk matrisi: - - **Brightness:** RGB'ye offset ekler (`b` → -255..255) - - **Contrast:** `c` ile çarpar + `128*(1-c)` offset (0..2 arası, 1.0 orijinal) - - **Saturation:** Rec.709 ağırlıklı karışım (0..2 arası, 1.0 orijinal) -- Birleştirme: `M_final = M_contrast * M_brightness * M_saturation` — genel 4x5 matris - çarpımı; uygulama sırası saturation → brightness → contrast. -- Uygulama: `ColorFiltered` + `ColorFilter.matrix` ile GPU shader'ı — projedekiyle aynı mekanizma. -- Örnek widget (`ImageAdjusterWidget`) **slider tabanlı**; bizim uygulama jest tabanlı - olduğu için bu kısım direkt uygulanmayacak. Asıl değer matris hesabında. - -## 2. Projedeki Mevcut Durum — `lib/widgets/image_canvas.dart` - -- State: `_contrast` (1.0), `_saturation` (1.0) — brightness yok. -- `_buildCombinedFilter()`: saturation × contrast tek matriste elle birleştiriliyor - (contrast offset: `t = (1-c)/2` → 255 ölçeğinde 127.5*(1-c)). -- Cache: `_cachedFilter` + `_cachedSat` / `_cachedCon`. -- Etkileşim: sağ tık = contrast, Ctrl+sağ tık = saturation, Shift+sağ tık = rotate, - sağ çift tık = reset (tüm efektler). -- Modifier takibi: `_onHardwareKey` yalnızca ctrl ve shift'i izliyor. - -## 3. Karşılaştırma — Araştırma vs Mevcut Kod - -| Konu | Mevcut | Araştırma | Fark | -|---|---|---|---| -| Saturation matrisi | Rec.709, aynı formül | Rec.709, aynı formül | Matematik birebir aynı | -| Contrast offset | 127.5*(1-c) | 128*(1-c) | ~%0.4, görsel fark yok | -| Brightness | yok | `b` offset matrisi | **Eklenecek** | -| Birleştirme | elle 2 matris | genel 4x5 çarpım | 3 efekt için genel çarpım daha temiz | -| Arayüz | jest (sağ tık + sürükle) | slider | Widget değil, matris katmanı alınacak | - -## 4. Uygulama Planı - -1. `_brightness` state ekle (varsayılan 0.0, aralık -100..100). -2. `_onPointerDown`: öncelik sırası — **Ctrl+Shift → brightness**, Ctrl → saturation, - Shift → rotate, yoksa contrast. (Başlangıçta Alt denenmişti; XFCE Alt+klik'i - pencere hareketine kaptığı için **Ctrl+Shift**'e geçildi.) -3. `_onPointerMove`: brightness modunda `_brightness = _startBrightness + dy * 0.5` - (clamp -100..100; hassasiyet açık soru). -4. `_buildCombinedFilter`: araştırmadaki 4x5 çarpım mantığıyla 3 efekt birleştir - (saturation → brightness → contrast); cache anahtarına `_brightness` ekle. -5. Reset (sağ çift tık), `_resetView`, `_toggleFillFit`: brightness 0'a döner. -6. `hasFilter` koşuluna `_brightness != 0` eklenir (ColorFiltered yalnızca gerektiğinde). - -## 5. Dikkat Edilecekler - -- **Linux/XFCE:** Alt+klik WM tarafından pencere hareketine kaptırıldığı için - brightness **Ctrl+Shift+sağ tık**'a alındı (Alt kaldırıldı). -- **Uygulama sırası:** saturation → brightness → contrast sabit kalacak; farklı sıra farklı sonuç verir. -- Reset davranışı brightness'i de kapsar. - -## 6. Açık Sorular - -1. Brightness hassasiyeti: `dy * 0.5`, aralık -100..100 uygun mu? -2. Rotate Shift+sağ tık'ta kalıyor (değişiklik yok) — onay? diff --git a/docs/rapor-downsampling-swap.md b/docs/rapor-downsampling-swap.md deleted file mode 100644 index 1a6bbc8..0000000 --- a/docs/rapor-downsampling-swap.md +++ /dev/null @@ -1,58 +0,0 @@ -# Durum Raporu: Downsampling + Full-Res Swap + RAM Leak Fix - -Tarih: 2026-08-14 -Branch: `main` - -## Yapılan Değişiklikler - -Tek dosya: `lib/widgets/image_canvas.dart` - -### 1. Downsampling (ilk yüklemede RAM tasarrufu) - -- `_downsampleTarget = 2048` sabiti eklendi. -- `_buildImage()` artık ilk yüklemede `ResizeImage.resizeIfNeeded(2048, null, ...)` - kullanıyor. Büyük resimler (ör. 12000px) 2048px'e küçültülerek decode ediliyor. -- Önceki davranış: resim her zaman tam çözünürlükte decode ediliyordu - (`cacheWidth = longest * _sc * 2`), bu da dev resimlerde RAM'i dolduruyordu. - -### 2. Full-Res Swap (zoom'da kalite) - -- `_checkFullResSwap()`: downsampled resmin viewport'a oranı (`stretch`) 1.5x'i - geçince `_usingFullRes = true` yapılıyor ve doğrudan `FileImage` (tam çözünürlük) - kullanılıyor. -- `_handleScroll()` içinde `_sc` güncellendikten sonra çağrılıyor. -- Sonuç: küçük görünümde 2048px (RAM ~1.6MB), zoom ile yakınlaşınca orijinal. - -### 3. RAM Leak Fix (resim geçişlerinde sızıntı) - -- `didUpdateWidget()` içinde `old.filePath != widget.filePath` kontrolü: - - `_cachedFileImage?.evict()`, `_cachedProvider?.evict()` - - `PaintingBinding.instance.imageCache.evict(FileImage(File(old.filePath)))` - - Tüm cache alanları `null`'a çekiliyor. -- Önceki davranış: her yüklenen resim Flutter'ın global `ImageCache`'inde - birikiyor, RAM sürekli artıyordu. - -### 4. Görüntü boyutu takibi - -- `_imageSize`, `loadingBuilder` + `ImageStreamListener` ile decode edilen - gerçek piksel boyutundan dolduruluyor. `_checkFullResSwap()` bu değeri - stretch hesabında kullanıyor. - -## Temizlik - -- Tiled render kodunun tamamı kaldırıldı (Flutter codec API region decode - desteklemiyor). -- `dart:ui` ve `dart:typed_data` import'ları gereksiz kalınca silindi. -- Kullanılmayan `_uiImage`, `_fullResImage`, `_fullResLoading` alanları kaldırıldı. - -## Doğrulama - -- `flutter analyze --no-pub`: 0 error. -- `flutter build linux --debug`: başarılı. -- Kullanıcı testi: onaylandı ("herşey tamam gözüküyor"). - -## Açık Konular / Sonraki Adımlar - -- `_downsampleTarget` ve swap threshold (1.5x) değerleri tune edilebilir. -- LOD (progressive loading) seviyeleri — şimdilik tek kademe (2048 → full) yeterli. -- Android/iOS hedeflerinde `ResizeImage` davranışı ayrıca test edilmeli. diff --git a/docs/rapor-gezinme-ayarlar-print.md b/docs/rapor-gezinme-ayarlar-print.md deleted file mode 100644 index fb888ac..0000000 --- a/docs/rapor-gezinme-ayarlar-print.md +++ /dev/null @@ -1,80 +0,0 @@ -# Rapor — Gezinme Okları, Ayarlar (Sıralama) ve Yazdırma - -Tarih: 2026-08-06 -Durum: Uygulandı - -## Amaç - -Klavyede zaten çalışan sol/sağ ok tuşu gezinmesine görsel karşılık eklemek; -ayarlar (gear) penceresiyle klasör sıralamasını kullanıcıya bırakmak; resmi -yazıcıya göndermek. - -## Yapılan Değişiklikler - -### 1. Gezinme okları (görsel arayüz) - -- Sol/sağ alt köşeye sade yuvarlak ok butonları eklendi (`_NavButton`, - `lib/screens/viewer_screen.dart`). -- Normalde görünmez; mouse görüntü alanına girince belirir - (`MouseRegion` → `_controlsVisible`). -- Hover'da kırmızı arka plan (kapatma butonuyla aynı stil). -- Tooltip yok (kullanıcı istemedi). -- `ImageManager.instance.previousImage()/nextImage()` çağırır — klavye - kısayoluyla aynı yolu kullanır. - -### 2. PITFALL — MouseRegion flicker döngüsü (çözüldü) - -İlk uygulamada MouseRegion yalnızca resim alanını sarıyordu, butonlar -Stack'te üstte ayrı duruyordu. Mouse butonun üzerine gelince hit-test -butona gidiyor → MouseRegion `onExit` → butonlar kayboluyor → mouse tekrar -resim alanına düşüyor → `onEnter` → sonsuz yanıp sönme ve tıklanamama. - -Çözüm: `MouseRegion`'ı **tüm Stack'in** üzerine taşı (DropTarget'ın -çocuğu). Butonlar da Stack içinde kaldığı için onların üzerindeyken -bölgeden çıkılmaz; `onExit` yalnızca pencere alanı terk edilince tetiklenir. - -### 3. Gear (Ayarlar) butonu - -- İlk olarak görüntü alanının sol üstüne kondu; kullanıcı "title yüzünden - aşağıda çıkıyor" deyince **title bar'ın soluna** taşındı - (`CustomTitleBar(onSettings: ...)`, `lib/widgets/custom_title_bar.dart`). -- Title bar `visible` parametresiyle `_controlsVisible`'a bağlandı: mouse - resmin dışındayken title (gear dahil) saydam (AnimatedOpacity 0), mouse - girince görünür. -- Hover arka planı kırmızı (`_BarButton` varsayılanı). - -### 4. Ayarlar penceresi (`_showSettings`) - -Gear'a tıklayınca açılan AlertDialog: -- **Sıralama**: Zamana göre / İsme göre (RadioListTile). - `ImageManager.sortByName` — `setGalleryFromPaths` içinde sıralama - karşılaştırıcısı buna göre değişir; isim sıralaması `toLowerCase` - karşılaştırma ile (büyük/küçük harf duyarsız). Mod değişince mevcut - galeri `setGalleryFromPaths([_images[_currentIndex]])` ile yeniden - kurulur (açık resim korunur). -- Uygulamanın amacı (kısa paragraf). -- Kısayollar listesi (F11 satırı kaldırıldı — tam ekran özelliği yok). - -### 5. Yazdırma (`_printImage`) - -- Yeni bağımlılıklar: `printing: ^5.13.4`, `pdf: ^3.11.1` (Linux dahil - masaüstü destekli). -- Buton ayarlar penceresinin actions kısmında ("Yazdır"); ayrı köşe butonu - değil (kullanıcı istediği yerleşim). -- Akış: `File.readAsBytes` → `pw.MemoryImage` → `pw.Document` A4 sayfa, - `BoxFit.contain` → `Printing.layoutPdf` (sistem yazıcı dialogu). - -### 6. `_BarButton.hoverColor` kaldırıldı - -Gear title'a taşınıp tek hover rengi (kırmızı) kaldığı için opsiyonel -parametre gereksizleşti; analyze uyarısı (unused_element_parameter) -temizlendi. - -## Doğrulama - -- `flutter analyze`: 21 info (pre-existing baseline; yeni hata yok). -- `flutter build linux --release`: ✓ Built. -- `./install.sh` (debug bundle silinip release kuruldu): exit 0. -- `bash dist/rebuild-deb.sh`: `dist/imajviewer_1.0.1_amd64.deb` (11M) üretildi. -- Görsel davranış kullanıcı tarafından manuel test edildi (oklar, - gear/title saydamlığı, hover renkleri, sıralama, print dialogu). diff --git a/docs/rapor-github-acik-kaynak.md b/docs/rapor-github-acik-kaynak.md deleted file mode 100644 index 21f7346..0000000 --- a/docs/rapor-github-acik-kaynak.md +++ /dev/null @@ -1,88 +0,0 @@ -# Rapor — imajViewer'ı GitHub'da Açık Kaynak Yapmak - -> Tarih: 2026-08-06 -> Konu: GitHub'a açık kaynak repo koymak için gerekenler + mevcut repo durumu + geçmiş/silinme sorusu + "pro" hale getirme planı - -## 1. Sorunun cevabı: dosya silersem GitHub'a eski dosyalar gider mi? - -**Çalışma ağacından gider, geçmişten GİTMEZ.** - -- Aynı repo geçmişini (aynı commit zincirini) GitHub'a push edersen: sildiğin dosyalar - son sürümde (en son commit'te) görünmez, ama **git geçmişindeki eski commit'lerde - aynen durur**. GitHub'da eski commit'lere tıklayan herkes o dosyaları görür ve indirebilir. -- Üstelik bu geçmiş 391 MB binary taşıyor (aşağıda detay). GitHub'a geçmişle gitmek hem - bu yükü hem de "eski dosyalar hâlâ orada" sorununu getirir. -- **Öneri:** GitHub için geçmişsiz / tek commit'li temiz başlangıç yap. Ya `git init` ile - yeni repo (sıfır geçmiş), ya da squash ile tek commit. O zaman ne sildiysen tamamen gider. - -## 2. GitHub'a açık kaynak repo koymak için gerekenler (checklist) - -| Öğe | Durum | Not | -|---|---|---| -| LICENSE dosyası | ❌ YOK | README'de "MIT" yazıyor ama kökte LICENSE dosyası yok. GitHub lisansı bunu algılamaz | -| README | ⚠️ Var | Gitea URL'leri içeriyor, Türkçe, badge yok (detay: madde 4) | -| CONTRIBUTING.md | ❌ YOK | Katkı kuralları (kod stili, test, PR süreci) | -| CODE_OF_CONDUCT.md | ❌ YOK | GitHub topluluk standartları için beklenir | -| SECURITY.md | ❌ YOK | Güvenlik açığı bildirim yolu (private advisory) | -| CHANGELOG.md | ❌ YOK | 1.0.0 → 1.1.0 geçmişi yazılı değil | -| CI (GitHub Actions) | ❌ YOK | Sadece `.gitea/workflows/` var — GitHub bunu ÇALIŞTIRMAZ, `.github/workflows/` gerekir | -| Issue/PR template | ❌ YOK | `.github/ISSUE_TEMPLATE/` + `PULL_REQUEST_TEMPLATE.md` | -| .gitignore | ✅ Var | İyi durumda (build/, .dart_tool/ ignore ediliyor) | -| Repo metadata | ❌ | description + topics (image-viewer, flutter, linux, windows) + lisans seçimi | -| Releases | ⚠️ | v1.1.0 tag + .deb/.exe asset'leri GitHub Releases'e taşınmalı | -| FUNDING.yml | ❌ | Opsiyonel | - -## 3. KRİTİK: Sızıntı / hijyen riskleri - -1. **Gömülü token:** `git remote -v` çıktısında origin URL'si - `https://git_alhan:1254baa3...@git.softmediadesign.com/...` — token URL'e gömülü. - Bu dosya `.git/config`'te; push ile GitHub'a gitmez (config transfer edilmez). - Ama GitHub'a yeni remote eklerken bu URL'yi yanlışlıkla kopyalamamaya dikkat. - Token'ı Gitea'dan yenilemek en temizi. -2. **Kişisel dosyalar repoda:** `imajviewer.desktop` (yerel .desktop, mode 600), - `Implementation.md`, `PRD.md`, `update_plan_01.md`, `docs/session-*.md`, - `docs/rapor-*.md` — iç geliştirme notları. Açık kaynak repo'da hepsi durmamalı. -3. **Binary dosyalar git'te:** `deb/` (16 MB libflutter_linux_gtk.so vb. — build çıktısı) - ve `dist/` (.deb, .exe — sürüm paketleri) git ile takip ediliyor. Binary'ler git'e - girmez; release asset'i olur. `.gitignore`'da `*.deb`/`*.exe` var ama `!dist/*.deb` - istisnası ve `deb/` dizini bunları açıkta bırakıyor. -4. **Mutlak yol:** `pubspec.yaml` → `msix_config.logo_path: C:/projects/imajviewer/...` - — yerel Windows yolu. GitHub runner'ında msix adımı kırılır; göreli yol olmalı. -5. **Geçmiş 391 MB:** En büyük blob'lar: setup.exe 24 MB ×4 kopya, libflutter_linux_gtk.so - 16 MB, .deb'ler 8-11 MB ×8, tar.gz 9 MB. GitHub'da dosya limiti 100 MB — şu an - geçmiyor ama repo 1 GB'a yaklaşırsa GitHub uyarır, şişkinlik de kötü görünür. - -## 4. README düzeltmeleri - -- Klon/curl komutlarındaki `git.softmediadesign.com` URL'leri GitHub URL'si olmalı -- `.deb` kurulum adımındaki sürüm `1.0.0` yazılmış, güncel `1.1.0` -- Türkçe → açık kaynak için İngilizce (veya TR+EN iki dilli) önerilir -- Badge'ler (build status, license, release) + ekran görüntüsü (imajviewer.png var) -- "Gitea Actions" bölümü → "GitHub Actions" olarak yeniden yazılmalı - -## 5. "Pro" hale getirme planı (uygulama adımları, onay sonrası) - -1. **Kök temizliği:** `imajviewer.desktop` sil; iç dokümanları (Implementation/PRD/ - update_plan/docs) taşı ya da sil — karar gerekiyor (aşağıda). -2. **Binary'leri git'ten çıkar:** `git rm --cached deb/ dist/*.deb dist/*.exe`, - `.gitignore`'u düzelt (deb/ tamamen ignore; dist/'te sadece rebuild-deb.sh kalsın). - Not: `git rm --cached` sadece ileriye dönük temizler; geçmişteki 391 MB durur — - tam temizlik için ya filter-repo ya da temiz başlangıç gerekir. -3. **Lisans:** Köke `LICENSE` (MIT) ekle. -4. **README'yi yeniden yaz:** İngilizce, GitHub linkleri, badge, screenshot. -5. **Topluluk dosyaları:** CONTRIBUTING.md, CODE_OF_CONDUCT.md, SECURITY.md, - CHANGELOG.md, `.github/` (issue/PR template + workflows/build-windows.yml). -6. **CI'ı GitHub'a uyarla:** `.gitea/workflows/build-windows.yml` → `.github/workflows/`: - `GITEA_TOKEN` → `GITHUB_TOKEN` (otomatik), release adımı - `softprops/action-gh-release` ile GitHub Release + .deb/.exe asset. -7. **pubspec.yaml:** `logo_path`'i göreli yap (CI'da msix kırılmasın). -8. **GitHub'a aktarım:** Yeni boş GitHub repo → `git remote add github ` → - temiz tek commit (squash/orphan) → `git push github main`. Gitea ayrı kalır. - -## 6. Açık kararlar (senin onayın gerekiyor) - -1. README dili: İngilizce mi, TR mi, iki dilli mi? (açık kaynak görünürlüğü için İngilizce önerilir) -2. `docs/` ve kök dokümanlar: iç raporlar/session notları ne olacak? (yerel yedek alınır mı?) -3. Geçmiş: temiz tek commit mi, yoksa tüm geçmiş GitHub'a taşınsın mı? (öneri: temiz tek commit) -4. GitHub hesabı/org: repo hangi hesaba? Repo adı: `imajviewer` mi `imajViewer` mi? -5. Lisans: MIT kesin mi? (README'de MIT yazıyor, dosya yok) diff --git a/docs/rapor-kod-inceleme-performans.md b/docs/rapor-kod-inceleme-performans.md deleted file mode 100644 index 45920db..0000000 --- a/docs/rapor-kod-inceleme-performans.md +++ /dev/null @@ -1,139 +0,0 @@ -# Kod İnceleme + Performans Analizi - -Tarih: 2026-08-14 -Kapsam: Tüm `lib/` (13 dosya, 1799 satır) -Bağlam: Downsampling + full-res swap + RAM leak fix (v1.2.0) sonrası genel inceleme - ---- - -## 1. Tespit Edilen Hatalar - -### HATA-1 (Önemli): `_imageSize` resim değişiminde sıfırlanmıyor - -`lib/widgets/image_canvas.dart` - -`_imageSize`, `loadingBuilder` içinde `_imageSize.isEmpty` koşuluyla **yalnızca bir kez** -dolduruluyor. Ama `_resetView()` ve `didUpdateWidget()` bu alanı **sıfırlamıyor**. - -Sonuç: ikinci resme geçince `_imageSize` eski resmin boyutunu tutar, yeni resmin -boyutu hiç okunmaz. `_checkFullResSwap()` eski boyutla stretch hesabı yapar → -swap yanlış zamanda tetiklenir (ya hiç, ya da gereksiz erken). - -Örnek: 2000px bir resimden 12000px panoramaya geçince, stretch hesabı hâlâ 2000px -üzerinden yapılır; swap ya geç tetiklenir ya da downsample 2048px'te takılı kalır -(zoom'da kalite düşer). Ters yönde ise gereksiz erken full-res yükleme olur. - -Düzeltme: `_resetView()` içine `_imageSize = Size.zero;` ekle. - -### HATA-2 (Önemli): Downsample yalnızca genişliğe uygulanıyor - -`_buildImage()` içinde `ResizeImage.resizeIfNeeded(2048, null, ...)` çağrısı -`cacheHeight = null` geçiyor. Bu, **yalnızca genişlik** kısıtı demek. - -Sonuç: Uzun dikey resimler (ör. 800×20000 ekran görüntüsü) genişliği 2048'in -altında olduğu için **hiç küçültülmez**, tam çözünürlükte (20000px) decode edilir. -Tek resim yüzlerce MB RAM yer — downsampling'in amacı boşa çıkar. Aynı sorun -panoramik 20000×800 resimlerde genişlik kısıtıyla zaten yakalanır, ama dikeyde kaçar. - -Düzeltme: `ResizeImage.resizeIfNeeded(2048, 2048, ...)` (iki boyut da sınırla). -Flutter codec aspect oranını koruyarak sığdırır (fit). - -### HATA-3 (Performans): `loadingBuilder` içinde listener sızıntısı - -`loadingBuilder` her frame'de çağrılabilir. `_imageSize.isEmpty` olduğu sürece her -çağrıda `provider.resolve()` + `addListener()` yapılıyor; listener **hiç -remove edilmiyor**. `_imageSize` bir kez dolunca duruyor ama dolana kadar (ya da -HATA-1 yüzünden hiç dolmazsa) her rebuild'de yeni listener birikir. - -Ayrıca `resolve()` ikinci bir decode stream'i açar — `Image` widget'ı zaten kendi -decode'ini yaparken boyut öğrenmek için ikinci kez çözmek gereksiz iş. - -Düzeltme: `_imageSize`'i `loadingBuilder` yerine `initState`/`didUpdateWidget`'ta -tek sefer `resolve()` ile oku ve `ImageStreamListener`'ı `ImageStream.removeListener` -ile kaldır. Ya da `Image` widget'ının `ImageInfo`'sini `imageStream` üzerinden -tek listener ile dinle. - -### HATA-4 (Küçük): Full-res swap'ta eski downsample provider evict edilmiyor - -`_checkFullResSwap()` `_cachedProvider = _cachedFileImage` yapıyor ama eski -`ResizeImage` provider'ı `ImageCache`'ten evict etmiyor. Downsamlı `ui.Image` -(~16MB) cache'te gereksiz kalır. `maximumSizeBytes = 256MB` altında LRU temizler -ama tamamen gereksiz bir tutma. - -### HATA-5 (Yorum yanlışlığı): `imageCache.clear()` canlı görseli de siler - -`image_manager.dart:73` — `PaintingBinding.instance.imageCache.clear()` tüm cache'i -boşaltır, **ekrandaki mevcut görsel de dahil**. Üstündeki yorum "canlı görsel korunur" -diyor ama bu doğru değil. `clear()` o an render edilen image'ı da atar → galeri -kurulumunda kısa bir flaş/boşluk riski. - -Düzeltme: Yalnızca eski klasöre ait dosyaları `evict` et (tek tek) ya da bu satırı -kaldırıp `didUpdateWidget` evict mantığına bırak. - ---- - -## 2. RAM Modeli (mevcut davranış) - -- Downsample 2048px → ~16MB/resim (RGBA 2048×2048×4) -- Full-res swap sonrası → 100MB+ (büyük resimlerde) -- `ImageCache`: `maximumSize = 16` entry, `maximumSizeBytes = 256MB` - -**Kullanıcının "50 resim = 5GB" endişesi gerçekleşmez.** `maximumSizeBytes = 256MB` -LRU evict ile toplamı ~256MB–1.6GB arasında sınırlar: -- Tüm resimler downsample ise: 16 entry × ~16MB ≈ 256MB (tam limit) -- Full-res resimler varsa: 2–3 tane tutulur, gerisi LRU ile atılır - -Yani ölçülen "her resim ~100MB, artmıyor" davranışı doğru ve beklenen. - -**Kalan risk:** `maximumSize = 16` entry limiti, `maximumSizeBytes`'tan bağımsız -işler. Full-res ağırlıklı kullanımda 16 × 100MB = 1.6GB'a kadar tutulabilir. -`maximumSizeBytes`'ı düşürmek (ör. 128MB) ya da entry limitini küçültmek bu tavanı -düşürür. - ---- - -## 3. Performans İyileştirme Önerileri - -### ÖN-1: `dir.listSync()` → async/isolate -`image_manager.dart:43` — klasör `listSync()` ile senkron taranıyor. Büyük klasörde -(10k+ dosya) UI thread bloklanır, pencere donar. `dir.list()` (async) ya da -`Isolate.run` ile arka planda tarama önerilir. Sonuç `notifyListeners` ile gelir. - -### ÖN-2: `_printImage` tam dosyayı belleğe alıyor -`viewer_screen.dart:173` — `File(path).readAsBytes()` tüm dosyayı RAM'e yükler. -12000px resimde print sırasında +200MB spike. Nadir işlem ama downsample edilmiş -kopya üzerinden print etmek düşünülebilir. - -### ÖN-3: `_buildImage` içindeki `_imageKey` + `ResizeImage` memo cache -HATA-1 çözülünce `_imageSize` doğru çalışır; ayrıca `_checkFullResSwap`'ın -`setState` çağrısı scroll handler içinde — bu zaten `_markRenderDirty` ile çift -rebuild üretebilir. `_checkFullResSwap`'ın `setState` yerine `_markRenderDirty` -kullanması yeterli (ValueListenableBuilder zaten tick'i dinliyor). - -### ÖN-4: Warmup katmanı her açılışta -`_startWarmup` shader derlemesini her resim açılışında yapıyor; uygulama ömründe -bir kez yapılması yeterli olabilir (global static flag). Küçük kazanç. - ---- - -## 4. Doğrulama Matrisi - -| Madde | Durum | -|---|---| -| `flutter analyze` | 0 error (mevcut info/deprecated uyarıları hariç) | -| Release build | ✓ | -| RAM stabilitesi (kullanıcı testi) | ✓ "her resim ~100MB, artmıyor" | -| HATA-1 (`_imageSize` reset) | ✅ düzeltildi | -| HATA-2 (dikey downsample) | ✅ düzeltildi | -| HATA-3 (listener sızıntısı) | ✅ düzeltildi | -| HATA-4 (eski provider evict) | ✅ düzeltildi | -| HATA-5 (`clear()` canlı görsel) | ✅ düzeltildi | - ---- - -## 5. Öncelik Sırası - -1. **HATA-1 + HATA-2** — doğruluk + RAM (uzun dikey resimler hâlâ patlayabilir) -2. **HATA-3** — listener sızıntısı (uzun oturumlarda) -3. **ÖN-1** — büyük klasörde UI donması -4. HATA-4, HATA-5 — kozmetik/ince ayar diff --git a/docs/rapor-pan-kilitleme-nuks.md b/docs/rapor-pan-kilitleme-nuks.md deleted file mode 100644 index a9f9153..0000000 --- a/docs/rapor-pan-kilitleme-nuks.md +++ /dev/null @@ -1,199 +0,0 @@ -# Rapor — Efekt Uygulanmış Görselde Pan Sırasında Kilitlenme (Nüks) - -Tarih: 2026-08-06 -Durum: Analiz — hipotezler hazır, açık sorular kullanıcı yanıtını bekliyor - -## 1. Semptom (yeni bildirim) - -- Donma sorunu **yeniden ortaya çıktı** — bu kez **"diğer bilgisayarda"** (VM değilse - ortamı bilinmiyor; önceki testler Xubuntu VM + llvmpipe idi). -- Tetikleyici farklı: önceki nüksler **efekt sürüklemesi sırasında** idi - (contrast/saturation/brightness/rotate drag). Şimdi **efekt uygulanmış durumda - (değerler sabit) iken PAN yaparken** kilitlenme. -- "Olmadık zamanlarda" — rastgele zamanlarda, her pan'da değil. - -## 2. Önceki Teşhisler ve Çözümler (kayıt özeti) - -| Tarih | Teşhis | Çözüm | Durum | -|---|---|---|---| -| 03 Ağu | **Kuyruk birikmesi**: pointer event hızı (100-250/sn) > llvmpipe kare hızı → setState her event'te → UI thread %100, 30-60sn donma | `_markRenderDirty()` frame-throttle (kare başına max 1 rebuild) + `ResizeImage` (viewport×2) + provider memoize + cache 16/256MB + galeri geçişinde `imageCache.clear()` | Uygulandı, ölçüldü (raster ort ~15ms, max ~50ms, kuyruk birikmesi yok) | -| 06 Ağu | **İlk kullanım shader derlemesi (B1)**: process başına soğuk Skia/GL cache; ilk ColorFiltered/Transform kullanımı 15sn-1dk derleme | **Shader warmup**: açılışta gizli Opacity(0) katmanla filtre + rotate shader'larını derlet | Uygulandı (release build + kurulum 06 Ağu) | - -## 2b. Kullanıcı Yanıtları (2026-08-06, ikinci bildirim) - -- Ortam: **gerçek makine** — `Mesa Intel(R) HD Graphics 3000 (SNB GT2)` (Sandy - Bridge iGPU, 2011). mesa i965 driver GLSL'i **LLVM ile CPU'da derler** → karmaşık - shader varyantları 30sn'ye çıkabilir. 12GB RAM, son sürüm kurulu. -- Test sonuçları (kullanıcı): - - **Resim büyükse (dosya ~1MB+) oluyor**; küçük görselde olmuyor. - - **Fill modda olmuyor, fit modda oluyor.** - - **İlk açılışta fit moda geçip rotate edilirse oluyor** — tek seferlik, sonra - tekrar değil (kullanıcı "sanırım" ile ifade etti, teyit beklenecek). -- Bu bilgiler **Aday F'yi doğruluyor**: warmup 1x1 gizli katmanda yapılıyor, gerçek - kullanımın (büyük doku + fit BoxFit.contain + rotate + ColorFiltered) shader - varyantı warmup'ta derlenmiyor → ilk gerçek kullanımda 30sn derleme. -- Aday A (ColorFiltered CPU filtrelemesi) ve C (bellek) elendi — GPU var, RAM bol. -- **EKLENTİ (kullanıcı kurcalayınca):** Fill modda da oluyor; **"biraz zorlayınca - aynı resimde tekrar oldu"** — yani tek seferlik DEĞİL. Bu Aday F'yi (ilk kullanım - derlemesi, cache'li kalır) **zayıflatıyor**; varyant sayısı fazlaysa her yeni - kombinasyon yeni derleme olabilir (aşağıda Aday H/I). - -Her iki çözüm de mevcut kodda aktif: `image_canvas.dart` → `_markRenderDirty`, -provider memoize (`_cachedProvider`), `_buildWarmupLayer()`. - -## 3. Yeni Analiz — Neden "Efekt + Pan" Kilitlenir? - -Pan akışı: `_onPanUpdate` → `_tx/_ty += delta` → `_clamp()` → `_markRenderDirty()`. - -### Aday A — ColorFiltered her pan frame'inde tüm görseli CPU'da filtreliyor ⭐ - -- Efekt aktifken (`hasFilter == true`) her rebuild'te `ColorFiltered` → `Image` tüm - görsel 5x4 matrisle filtrelenir. Pan'da translate değişir → her frame'de yeniden - boyama + filtreleme. -- 4000×3000 görsel ≈ 12M piksel; llvmpipe'ta filtre + ölçekleme + translate ≈ - 20-60ms/kare. FPS 15-50'ye düşer → **"takılma" hissi**. -- `_markRenderDirty` kuyruk birikmesini engeller ama kare başına maliyet yüksekse - düşük FPS kalıcı olur. Büyük görsel + zayıf makinede bu "kilitlenme" gibi algılanabilir. -- **Rotate aktifse** (pan serbest, `_clamp` erken return) Transform + ColorFiltered - birlikte — en ağır kombinasyon. - -### Aday B — `_clamp()` pan event hızında `findRenderObject()` çağırıyor ⭐ - -- `_onPanUpdate` her event'te `_clamp()` çağırır; fit modda `_clamp()` içinde - `_imageKey.currentContext?.findRenderObject()` (satır 77) **pointer hızında** - (throttle'ın DIŞINDA) render tree sorgusu yapar. Pan event'leri 100-250/sn → - gereksiz layout/tree traversal yükü. Kilitlenme üretmez ama takılmayı artırır. - -### Aday C — Bellek/GC baskısı ("olmadık zamanlarda" rastgeleliğin en iyi adayı) - -- Rastgele kilitlenme imzası GC/swap'e işaret eder. "Diğer bilgisayar"da RAM azsa - (8GB altı) veya başka işler varsa: büyük görsel decode + 256MB cache + pan sırasında - GC pause → kısa ama algılanabilir donmalar. -- Pan sırasında yeni decode yok (memoize) → cache baskısı düşük; ama makine belleği - dar ise OS seviyesinde swap thrashing olabilir. - -### Aday D — Eski binary / farklı ortam ("diğer bilgisayar") - -- "Diğer bilgisayarda" kurulu sürüm bilinmiyor. Eski .deb (warmup/memoize öncesi) - kuruluysa sorun zaten açıklanır. Ayrıca o makinede donanım GPU'su VARSA teşhis - tamamen değişir (llvmpipe değil) — ColorFiltered donanımda ucuzdur, o zaman aday - C veya farklı bir sebep aranır. - -### Aday E — Pan sırasında yeni frame kuyruğu (zayıf) - -- `scheduleFrameCallback` kare başına 1 callback garanti eder; kare süresi uzarsa FPS - düşer ama kuyruk birikmez (03 Ağu ölçümü). Kilitlenme üretmez. - -### Aday F — Warmup varyant eksikliği: 1x1 katman gerçek kullanımı derlemiyor ⭐⭐⭐ - -- Warmup (`_buildWarmupLayer`) **1x1 boyutunda** gizli katmanla filtre + rotate - shader'larını derletiyor. Skia/GL'de shader varyantları **doku boyutu, sampling - modu, filtre kalitesi** gibi parametrelere göre ayrı derlenir. -- Gerçek kullanımda **büyük görsel** (4000x3000) + `FilterQuality.high` + - `BoxFit.cover/contain` + `ColorFiltered` + pan/zoom `Transform` — bu kombinasyonun - shader'ı 1x1 warmup'ta derlenenle **aynı varyant olmayabilir** → ilk gerçek - kullanımda derleme yine olur → 30sn kilitlenme. -- "Efekt uygulanmışken pan yaparken" tetikleyicisi: pan/zoom + ColorFiltered birlikte - ilk kez kullanıldığında varyant derlenir; sonrası cache'li → "olmadık zamanlarda" - ve "her pan'da değil" imzası birebir uyuyor. -- GPU'da shader derleme süresi genelde ms seviyesindedir; **zayıf/iç ekran kartı + - mesa/LLVM backend** ile 30sn'ye çıkabilir. "İyi kötü ekran kartı" ifadesi bu - olasılığı güçlendiriyor. -- **Warmup'ın gerçek boyutla yapılması gerekir**: gizli katman 1x1 yerine mevcut - görselin boyutunda (veya viewport boyutunda) olmalı; ayrıca pan/zoom Transform'u - da warmup'a dahil edilmeli. - -### Aday G — Gerçek GPU driver / doku sınırı - -- Zayıf GPU'da büyük doku (12MP RGBA ≈ 48MB) + ColorFiltered → driver'da aşırı - yavaş path veya GL sync beklemesi. `glxinfo -B` renderer string'i kritik - (NVIDIA nouveau / AMD radeon / Intel / llvmpipe?). - -### Aday H — Skia varyant cache dolması / her kombinasyon yeni derleme ⭐⭐ - -- "Aynı resimde tekrar oldu" → tek seferlik derleme değil. Skia'da her farklı - kombinasyon (zoom seviyesi × rotate açısı × efekt değeri × fit/fill × filtre - kalitesi) farklı shader varyantı üretebilir; varyant cache'i sınırlıysa (LRU) - eski varyant atılır → aynı kombinasyon tekrar kullanılınca YENİDEN derlenir → - 30sn. "Zorlayınca" = çok varyant üretmek = cache'i doldurmak = tekrar derleme. -- Bu, "ilk kullanımda" + "sonra tekrar" imzasını birlikte açıklayan hipotez. - -### Aday I — Intel HD 3000 GL sync / doku upload darboğazı ⭐⭐ - -- Sandy Bridge iGPU paylaşımlı bellek kullanır; büyük doku upload + ColorFiltered - + Transform her frame'de GL sync bekleyebilir. `_markRenderDirty` throttle frame - üretimini sınırlar ama **raster thread** yavaş kalırsa UI thread frame kuyruğunu - tüketemez → 30sn'lik görünür donma (olaylar işlenir, görsel gecikir — önceki - raporlardaki imza). -- Doku boyutuna duyarlılık (1MB+ görsel) + fill/fit farkı (BoxFit.cover kırpar → - daha küçük etkin doku; contain → tam doku işlenir) bu hipotezle uyumlu. -- Intel HD 3000'de `LIBGL_ALWAYS_SOFTWARE=1` (llvmpipe) ile karşılaştırma testi - ayırt edici: GL sync sorunu ise software'de farklı davranır. - -## 4. Hipotez Önceliklendirme - -| Aday | Açıkladığı | Gücü | -|---|---|---| -| A (ColorFiltered her frame) | Efekt+pan takılması, "kilitlenme" hissi | ⭐⭐ (efekt açıkken kaçınılmaz maliyet; azaltılabilir) | -| B (`_clamp` findRenderObject) | Ek pan yükü | ⭐ (kolay düzeltilir) | -| C (bellek/GC) | "Olmadık zamanlarda" rastgelelik | ⭐⭐ (makine belleği bilinmiyor) | -| D (ortam/eski binary) | Tamamen farklı kök | ⭐ (önce doğrulanmalı) | - -## 5. Önerilen Çözüm Yolları (rapor onayı sonrası) - -### Seçenek 1 — Pan sırasında filtre maliyetini azalt (Aday A) - -- Efekt aktifken pan/zoom sırasında `FilterQuality.high` → `FilterQuality.medium` - (görsel kalite farkı minimal, llvmpipe yükü düşer) — tek satır. -- Pan sırasında `cacheWidth`'i düşürme (memoize zaten sabit tutuyor) — gerek yok. -- **Alternatif:** pan sırasında geçici düşük çözünürlük önizlemesi (mevcut görseli - küçük boyutta filtrele) — karmaşık, önce basit yol denenir. - -### Seçenek 2 — `_clamp()` içindeki `findRenderObject` yükünü azalt (Aday B) - -- `_imageKey.currentContext?.findRenderObject()` yalnızca fit modunda lazım; pan'da - `_vpSize` değişmediği sürece tekrar sorgulamaya gerek yok — `_clamp`'in render - sorgusunu her pan event'inde değil, sadece layout değişince yap. (Dikkatli: - rotate varken `_clamp` zaten erken return ediyor; fit modunda kilitlenme riski yok.) - -### Seçenek 3 — Ortam doğrulaması (Aday D) — rapor öncesi şart - -- "Diğer bilgisayar"da: `glxinfo -B | grep -i renderer` (llvmpipe mi GPU mu), - `free -h`, kurulu sürüm (`.deb` tarihi / `imajviewer --version` yoksa binary - timestamp). Ayrıca aynı görselle **bu VM'de** tekrar dener misin — nüks VM'de - de mi, sadece diğer makinede mi? - -## 6. Kalan Açık Sorular - -1. "Diğer bilgisayar" hangisi — başka VM mi, gerçek makine mi? Donanım GPU'su var mı - (`glxinfo -B` çıktısı)? -2. O makinede kurulu sürüm: son `.deb` (2026-08-06 sonrası) mi, yoksa eski mi? -3. Kilitlenme süresi ne kadar — 1-2sn mi, 10sn+ mı, yoksa "kapatmak zorunda kaldım" mı? -4. Hangi efekt uygulanmışken pan yapıyorsun — contrast mi, rotate mi (rotate en ağırı)? -5. Görsel boyutu ne — büyük fotoğraf mı (4000px+), küçük ekran görüntüsü mü? -6. Aynı görselle bu VM'de (llvmpipe) pan dener misin — nüks VM'de de oluyor mu? - -## 7. Ölçüm Planı (onaylanırsa) - -Skill'deki `references/llvmpipe-perf-measurement.md` akışı: geçici -`addTimingsCallback` (FRAME build/raster süreleri) + `pidstat` CPU. Ölçüm sırasında -kullanıcı elle pan yapar ("sen aç ben yaparım"). Enstrümantasyon geçicidir ve -sonrası temizlenir. VM'de ölçüm yapılabilir; "diğer bilgisayar"da enstrümantasyonlu -build kurmak gerekir (kullanıcı onayı şart). - -## 8. Sonuç - -İki önceki çözüm (throttle + warmup) doğru teşhislere dayanıyordu ve ilk iki donma -imzasını çözdü. Yeni imza **farklı**: efekt SABİT iken pan. En güçlü açıklama -Aday A (ColorFiltered'in her pan frame'inde tam görseli CPU'da filtrelemesi) — -özellikle büyük görsel + zayıf makinede "kilitlenme" hissi yaratır. Rastgelelik için -Aday C (bellek/GC) ve Aday D (ortam) doğrulanmalı. Kullanıcı yanıtları gelince -rapor güncellenir ve çözüm yolları netleşir. - -## 9. Kapanış Notu (2026-08-06) - -Kullanıcı donma sırasında `top -b -d 1` ölçümü başlattıktan sonra **nüks olmadı** -(ne denediyse kilitlenme/takılma görülmedi). Nüks koşulları belirsiz; konu bu -raporla **askıda bırakıldı** — kullanıcı talep ederse yeniden açılır. Olası ipucu: -ölçüm sırasında sistem yükü değişince davranış değişti (Aday I — GL sync/GPU yükü -ile ilişkili olabilir; kesin teşhis yapılmadı). diff --git a/docs/rapor-sag-tik-donma-analizi.md b/docs/rapor-sag-tik-donma-analizi.md deleted file mode 100644 index 1dbc158..0000000 --- a/docs/rapor-sag-tik-donma-analizi.md +++ /dev/null @@ -1,127 +0,0 @@ -# Rapor — Sağ Tuş Efektlerinde Donma Analizi ve Slider UI Değerlendirmesi - -Tarih: 2026-08-06 -Durum: Analiz — B teşhisi netleşti, uygulama bekliyor - -## 1. Semptom - -- Sağ tuş ile kullanılan özelliklerde (contrast / saturation / brightness / rotate) uygulama **geçici olarak donuyor**. -- Donma süresi **15sn - 1dk arası** (kısa değil, ciddi bir tek seferlik maliyet). -- **Release build** kullanılıyor (.deb'den kurulu) → Dart JIT hipotezi elendi, donma release AOT'ta da var. -- Donma **kalıcı değil**: kullanıcı "kapatayım mı" diye soruyor, "bekle" deyince **kendiliğinden düzeliyor**. -- Donma sırasında yapılan işlemler (sağ tık + aşağı çekme, pan) donma açıldığında **yapılmış olarak görülüyor** — olaylar işleniyor, sadece görsel gecikiyor. -- **Sadece çok pencereli (multi-instance) kullanımda oluyor**, tek pencerede olmuyor. -- **Her pencere açılışında ilk efekt kullanımında bir kez oluyor; o donma açıldıktan sonra aynı pencerede tekrar olmuyor.** -- Eskiden sadece rotate'te olurdu; şimdi **tüm efektlerde** oluyor, en çok rotate'te. -- Donan pencere kilitli kalıyor; diğer pencereler normal çalışıyor. Task bardan kapatılabiliyor. -- Kullanıcı şüphesi: klavye tuşlarının (Ctrl/Shift) basılı tutulmasıyla ilgili olabilir — "farklı bir şey olma ihtimali yüksek". -- Soru: bu özellikleri **açılan kutu içinde slider UI** ile yapsak takılmalar çözülür mü? - -## 2. Mevcut Mimari (lib/widgets/image_canvas.dart) - -| Katman | Mekanizma | -|---|---| -| Mod seçimi | `_onPointerDown`: sağ tık + `_ctrlPressed`/`_shiftPressed` state'i → contrast (yok) / rotate (Shift) / saturation (Ctrl) / brightness (Ctrl+Shift) | -| Modifier takibi | `HardwareKeyboard.instance.addHandler(_onHardwareKey)` — global, `return false` (yutmaz) | -| Değer güncelleme | `_onPointerMove`: `dy * sensitivity` → `_markRenderDirty()` | -| Render eşitleme | `_renderTick` ValueNotifier + `scheduleFrameCallback` — kare başına max 1 rebuild | -| Image pipeline | `ResizeImage.resizeIfNeeded(cacheWidth: viewport×2)` + provider memoize (03 Ağu fix) | -| Efekt | `ColorFiltered` (birleşik 4x5 matrix, cache'li) + rotate için `Transform` | -| Varsayılan | `_isFilled = true` (BoxFit.cover), `FilterQuality.high` | - -## 3. Kök Neden Analizi — Geliştirilmiş Teşhis (kullanıcı geri bildirimiyle) - -Kullanıcının verdiği yeni bilgiler adayları eliyor: - -| Gözlem | Hangi adayı eler/güçlendirir | -|---|---| -| Donma geçici, kendiliğinden düzeliyor | Aday A (kalıcı frame üretimi durması) **elendi** → B güçlendi | -| Donma sırasında yapılan işlemler sonradan görülüyor | UI thread olayları **işliyor**; sadece render/raster geride → Aday A elendi, B güçlendi | -| Sadece çok pencerede oluyor | Process başına soğuk kaynak → B güçlendi | -| Her pencerede İLK efekt kullanımında bir kez, sonra tekrar olmuyor | **İlk kullanım maliyeti** — shader derleme / kaynak ısınması → B1 (aşağıda) | -| Eskiden sadece rotate, şimdi tüm efektlerde | Efekt başına ilk-kullanım maliyeti → B1 | -| En çok rotate'te | Rotate en pahalı (Transform + ColorFiltered birlikte) → B | - -### Teşhis: B1 — İlk Kullanım Render Maliyeti (shader derleme + kaynak ısınması) ⭐⭐ - -Semptom setinin tamamını açıklayan tek hipotez: - -- Her pencere **ayrı process** (multi-instance). Her process'in kendi Skia/GL kaynakları **soğuk** başlar. -- İlk kez `ColorFiltered` (color matrix shader) ve/veya `Transform` (rotate) kullanıldığında llvmpipe **shader'ı derler + ilgili render path'i kurar** — bu tek seferlik maliyet 1-10sn+ sürebilir (yazılımsal render, 4 CPU çekirdeği). -- **Sonraki kullanımlarda shader cache'li** → donma tekrar etmez. "Bir kere oluyor, açıldıktan sonra tekrar olmuyor" = tam olarak bu. -- **Tüm efektlerde olması**: brightness (03 Ağu'da eklendi, yeni birleşik 4x5 matrix) dahil her efekt kendi shader'ını ilk kez derliyor. Eskiden sadece rotate'te olması: o günlerde contrast/saturation zaten ısınmış/kullanılmıştı; rotate her oturumda en son eklenen ve en pahalı olandı. -- **En çok rotate'te**: rotate iki shader path'ini birleştirir (ColorFiltered matrix + Transform composite) → en uzun ilk derleme. -- **Çok pencerede olması**: her pencere soğuk başlar → her pencerede ilk efekt kullanımında ayrı donma. Tek pencerede donma tek seferlik olduğu için "sorun yok" gibi görünür. - -### Aday B — llvmpipe raster thread uzun kare (arka plan maliyeti) - -- Donma anında UI thread çalışıyor (işlemler işleniyor) ama görsel gecikiyor → raster/kompozisyon kuyruğu geride. -- llvmpipe'ta her kare 15-50ms (03 Ağu ölçümü); ilk shader derlemesi bu kareleri 100x şişirir. -- B1'in "ilk kullanım" tetikleyicisi, B'nin "uzun kare" mekanizması — ikisi aynı olayın iki yüzü: **soğuk kaynakla ilk efekt frame'i çok pahalı**. - -### Aday C — Bellek baskısı / swap thrashing - -- Çok pencerede olması C'yi güçlendirir (her process ayrı decode + doku). Ama "tekrar olmuyor" imzası C ile tam uymuyor (bellek baskısı her seferinde olur). İkincil faktör olabilir. - -### Aday D — `_clamp()` build sırasında `findRenderObject()` - -- Donma yapmaz (null döner), performans etkisi minimal. Kırmızı ringa. - -## 4. Klavye Hipotezi Değerlendirmesi (kullanıcının şüphesi) - -**Donmaya klavye neden olmuyor.** `_onHardwareKey` handler'ı sadece iki boolean set/clear ediyor — maliyeti ihmal edilebilir, render tetiklemiyor, event yutmuyor. Kullanıcının "farklı bir şey" sezgisi doğru. - -Ancak klavye **yanlış mod tetikleme** (takılma hissi) üretebilir: - -- X11'de Shift durumu pointer event sırasında güvenilmez (skill'de belgeli: `isShiftPressed` false dönebilir). `_onHardwareKey` ile takip doğru çözüm ama yine de key repeat / focus kaybı durumlarında state bozulabilir. -- Shift basılıyken sağ tık → kullanıcı farkında olmadan **rotate** modu açılır → en pahalı efekt + beklenmedik davranış → "takılıyor" hissi. -- Alt + tık XFCE WM tarafından yutulur (bilinen pitfall) — zaten Ctrl/Ctrl+Shift'e geçilmişti. - -## 5. Slider UI Değerlendirmesi (kullanıcının önerisi) - -**Kısa cevap: B1 (ilk kullanım shader derlemesi) teşhisi doğruysa, slider UI donmayı ÇÖZMEZ — çünkü donma render maliyetinden değil, ilk kullanımdaki tek seferlik shader derlemesinden kaynaklanıyor.** Slider ile contrast'ı ilk kez açtığın anda yine aynı shader derlenecek ve yine aynı donma olacak. Ama doğru tasarımla yine de değerli: - -| Bileşen | Slider UI etkisi | -|---|---| -| İlk kullanım shader derlemesi (B1) | **Çözmez** — shader yine o an derlenir. Çözüm warmup (aşağıda) | -| Olay hızı | **Aynı kalır** — slider da sürüklerken `onChanged` pointer hızında çağrılır. Ama ilk derleme bittikten sonra gerisi akıcıdır | -| Mod bilinirliği | **Çözer** — hangi modda olduğun görünür; gizli modifier mapping'i biter. "Takılıyor" hissinin büyük kısmı modun ne olduğunu bilmemektir | -| Klavye bağımlılığı | **Tamamen kaldırır** — `HardwareKeyboard` handler'ına, X11 Shift güvenilmezliğine, Alt yutma pitfall'ına gerek kalmaz | -| Render maliyeti (ilk derleme sonrası) | **Ancak tasarımla düşer** — `onChangeEnd`'de uygula + sürükleme sırasında düşük çözünürlüklü önizleme (küçük cacheWidth) yapılırsa render başına maliyet ciddi düşer | - -**Sonuç:** Slider UI tek başına "sihirli çözüm" değil. Donmanın asıl çözümü B1'e karşı: **shader warmup + ilk derlemeyi kullanıcı etkileşiminden önce yaptırmak**. - -## 6. Önerilen Çözüm Yolları (B1 teşhisine göre) - -### Seçenek 1 — Shader Warmup (ilk derlemeyi öne çek) ⭐ UYGULANDI (2026-08-06) - -Açılışta, kullanıcı daha sağ tıka basmadan efekt shader'ları derletilir: - -- `ImageCanvas.initState` → ilk frame sonrası `_startWarmup()`. -- Görünmez 1x1 gizli katman (`_buildWarmupLayer`): adım 1'de `ColorFiltered` (filtre shader'ı), adım 2'de `ColorFiltered` + `Transform` rotate (rotate shader'ı) render edilir; sonra `_warmupDone = true` ile katman kaldırılır. -- Kullanıcı ilk sağ tıka bastığında shader'lar hazırdır → donma hissi kaybolmalı. -- Maliyet: pencere açılışında 2-3 ekstra gizli frame (arka planda, etkileşim yokken). -- Not: çok pencerede her process kendi warmup'ını yapar — aynı mantık. -- Doğrulama: release build + kurulum yapıldı (2026-08-06). Kullanıcı test edecek — ilk sağ tık donması yoksa teşhis doğrulanır. - -### Seçenek 2 — Release build ile test (JIT yükü teyidi) - -- **GÜNCELLEME:** Kullanıcı zaten release build kullanıyor (.deb'den kurulu) — JIT hipotezi **elendi**. Donma release AOT'ta da oluyor. -- 15sn-1dk süre, llvmpipe'ta ilk render path kurulumunun (shader derleme + layer/doku hazırlığı) toplam maliyeti olabilir; "bir kez oluyor, tekrar olmuyor" imzası korunuyor. - -### Seçenek 3 — Rotate maliyetini düşür - -- Rotate aktifken `FilterQuality.high` → `FilterQuality.medium` (rotate sırasında görsel kalite farkı minimal, maliyet düşer). -- `Transform` + `ColorFiltered` aynı anda aktifken tek composite path önerilir (mevcut yapı zaten tek ColorFiltered kullanıyor). - -### Seçenek 4 — Slider UI (UX netliği, donma çözümü değil) - -- Donma B1'den kaynaklanıyorsa slider UI donmayı çözmez; ama mod bilinirliği + klavye bağımsızlığı sağlar. -- Warmup (Seçenek 1) uygulandıktan sonra slider UI istenirse eklenir — ikisi birlikte: donma yok + kullanım net. - -## 7. Kalan Açık Sorular - -1. Donma süresi yaklaşık ne kadar — 1-2sn mi, 10sn+ mi? (B1 shader derleme süresi tahminini doğrular) -2. Debug build mi kullanıyorsun (install.sh ile kurulan), yoksa release mi? → Seçenek 2'yi doğrular -3. Slider UI hâlâ isteniyor mu, yoksa önce donma çözümü mü? (Sıra: warmup → slider opsiyonel) -4. Warmup için "açılışta arkada 1-3sn hazırlık" kabul edilir mi? (Alternatif: ilk efekt kullanımında 1 kez donma kabul edilir mi?) diff --git a/docs/session-2026-07-27.md b/docs/session-2026-07-27.md deleted file mode 100644 index c13db0b..0000000 --- a/docs/session-2026-07-27.md +++ /dev/null @@ -1,79 +0,0 @@ -# Session Özeti — 2026-07-27 - -## Eklenen Özellikler - -### Çalışan -| Özellik | Tetikleyici | Durum | -|---|---|---| -| Pan (sürükleme) | Sol tık | ✓ | -| Zoom (mouse tekeri) | Scroll | ✓ | -| Fill/Fit geçişi | Sol çift tık | ✓ | -| Contrast | Sağ tık + dikey sürükleme | ✓ (matrix row-major) | -| Saturation | Ctrl + sağ tık + dikey sürükleme | ✓ (desaturate + oversaturate) | -| Reset (tüm efektler) | Sağ çift tık | ✓ | - -### Tamamlanmamış / İptal -| Özellik | Durum | -|---|---| -|| Rotate | Shift + Sağ tık + dikey sürükleme | ✓ (viewport merkezi, ~100px=90°) | -| Brightness | **Kaldırıldı** — hiç çalışmadı | -| Single-instance | **İptal** — kullanıcı istemedi. Bunun yerine StartupWMClass ile taskbar gruplama | - ---- - -## Performans Durumu - -- **Çıplak mod (pan + zoom)**: Süper hızlı ✓ -- **ColorFiltered (contrast + saturation)**: Ciddi performans düşüşü ✗ -- **Çözüm**: `hasFilter` mantığı eklendi. `_saturation == 1.0 && _contrast == 1.0` ise `ColorFiltered` widget'ı **hiç render edilmez**. Efekt uygulanınca devreye girer. -|- **Rotate**: Ayrı `Transform` katmanı, sadece `_angle != 0` iken render edilir. Sıfır ek yük. - ---- - -## Teknik Detaylar - -### Color Matrix -- **Format**: Row-major 5x4 (column-major çalışmadı!) -- **Birleşik matrix**: `M_combined = M_contrast × M_saturation` (tek GPU pass) -- **Cache**: `_cachedFilter` ile değerler değişmediğinde yeniden oluşturulmaz -- **Lazy render**: `hasFilter` false ise `ColorFiltered` widget ağacına eklenmez - -### Klavye Modifier Tracking -- `HardwareKeyboard.instance.addHandler(_onHardwareKey)` ile global seviyede -- `HardwareKeyboard.instance.isShiftPressed` / `isControlPressed` **çalışmıyor** (Linux bug'ı) -- Kendi `_ctrlPressed` / `_shiftPressed` state'imizi tuttuk - -### install.sh -- Önce `build/linux/x64/debug/bundle/`, yoksa `release/bundle/` yolunu dener - -### Gesture Sistemi -- `Listener` (sağ tık event'leri) + `GestureDetector` (sol tık pan/çift tık) hibrit yapısı -- `Listener.onPointerDown/Move/Up` ile manuel gesture handling - ---- - -## Bilinen Sorunlar - -1. **Zoom throttle kaldırıldı** — her scroll event'inde `setState`, bu da performansı etkiliyor olabilir -2. **Rotate düzeltildi** — viewport merkezi, lazy Transform. Pan/zoom ile uyumlu. -3. **BlendMode saturation oversaturate yapamıyor** — matrix'e geçildi (çalışıyor) - ---- - -## Dosya Yapısı - -| Dosya | Açıklama | -|---|---| -| `lib/widgets/image_canvas.dart` | Tüm görüntü işleme, filtreler, gesture'lar | -| `install.sh` | Kurulum scripti (debug/release otomatik seçim) | -| `docs/image-canvas-interactions.md` | Detaylı analiz raporu | -| `docs/index.md` | Dokümantasyon indeksi | - ---- - -## Sonraki Session İçin - -1. Single-instance implementasyonu (lock file / Unix socket) -2. Rotate'i düzeltip tekrar ekleme (performans ve pan yönü sorunları) -3. Zoom performans optimizasyonu (throttle veya GestureDetector.onScaleUpdate) -4. `.deb` ve Windows installer güncellemesi diff --git a/docs/session-2026-08-03.md b/docs/session-2026-08-03.md deleted file mode 100644 index fb9c300..0000000 --- a/docs/session-2026-08-03.md +++ /dev/null @@ -1,33 +0,0 @@ -# Session Özeti — 2026-08-03 - -## Yapılanlar - -### 1. Ek Görüntü Formatları (jfif / jpe / wbmp) -- TIFF değerlendirildi, **vazgeçildi** (kullanıcı kararı — image paketi/decode katmanı istenmedi) -- AVIF değerlendirildi, **eklenmedi** (Flutter'da yerleşik decoder yok — native libavif gerekir, "ultra hafif" felsefeye aykırı) -- Eklendi: `.jfif`, `.jpe` (JPEG içerikli, Skia zaten açıyor), `.wbmp` (Skia WBMP codec) -- Değişen yerler: file_handler (seçici), image_manager (filtre), 2× .desktop + install.sh (MimeType) - -### 2. Pencere Cascade Yerleşimi -- Yeni pencere artık öncekinin **x + width** kadar sağında açılıyor -- Ekrandan taşarsa → alt satıra, satır başından -- O da taşarsa → ekran sol-üst köşesine -- Ekran bilgisi: `screen_retriever` paketi (window_manager'ın zaten transitive bağımlısıydı, doğrudan bağımlılık yapıldı) -- Yeni: `WindowPersistence.cascadePosition()` — getPrimaryDisplay → visibleSize/visiblePosition - -### 3. Pencere Konum Kaydı Düzeltmesi (kök neden) -- **Sorun:** Konum kaydedilemiyordu → çözüm olarak 5 yere kayıt koyulmuştu (initState + 2sn polling + move/resize debounce + close + main). Birden fazla pencere açıkken polling konum yarışı yaratıyordu → yeni pencere eski konuma göre hesaplanıp üstüne düşüyordu. -- **Çözüm:** Polling kaldırıldı. Tek akış: - 1. `main.dart` — pencere açılışında konum anında saveSync (sıradaki pencere güncel referans okur) - 2. `viewer_screen` move/resize — son bilinen bounds hemen `_lastBounds`'a, 300ms debounce ile saveSync - 3. `viewer_screen` close — `_lastBounds` **senkron** saveSync (async çağrı kapanışta tamamlanmıyordu) - -## Doğrulama -- `flutter analyze` — 18 info uyarı (mevcut kod stil/deprecated), hata yok -- `flutter build linux --release` ✓ -- Kurulum: release bundle `~/.local/lib/imajviewer/` (install.sh debug bundle'ı önceliklendirdiği için elle kopyalandı) -- Kullanıcı manuel test: WebP açılıyor ✓, cascade düzgün çalışıyor ✓ - -## Not -- `docs/session-windows-ci.md` bozuk encoding (mojibake) — içerik git log'dan biliniyor, düzeltilmedi -- Windows setup.exe **yeniden derlenmedi** — sadece Linux build + .deb güncellendi (Windows CI zaten Gitea Actions'ta) diff --git a/docs/session-windows-ci.md b/docs/session-windows-ci.md deleted file mode 100644 index 02b4de2..0000000 --- a/docs/session-windows-ci.md +++ /dev/null @@ -1,30 +0,0 @@ -# Session — Windows CI & Installer - -**Tarih:** 2026-07-27 - -## Yapılanlar - -1. **Gitea'dan son değişiklikler çekildi** - - `feat: rotate (Shift+sag tik), StartupWMClass taskbar gruplama` - - `feat: right-click contrast, Ctrl+saturation, Shift+rotate filters` - - `feat: use imajviewericon.png as app icon` - - `chore: reorganize dist/, remove release/, update install paths` - -2. **Windows release yeniden derlendi** - - `flutter pub get` ile bağımlılıklar güncellendi - - `flutter build windows --release` ile yeni özellikleri (contrast, saturation, rotate) içeren binary oluşturuldu - - Yeni ikon (imajviewericon.png) build'e dahil edildi - -3. **Inno Setup installer yeniden oluşturuldu** - - `dist/imajviewer_setup_1.0.0.exe` — 24 MB - - İçerir: imajviewer.exe + tüm DLL'ler + data dosyaları - - Dil desteği: Türkçe + İngilizce - - Dosya ilişkilendirmeleri: .png, .jpg, .jpeg, .webp, .bmp, .gif - - İsteğe bağlı masaüstü kısayolu - -4. **Gitea CI workflow güncellendi** (`.gitea/workflows/build-windows.yml`) - - Flutter build adımı korundu - - Inno Setup kurulumu (`choco install innosetup`) eklendi - - `ISCC.exe` ile installer build adımı eklendi - - Release artifact'e hem build çıktısı hem de `dist/*.exe` eklendi - - Tag bazlı release job'ı eklendi (opsiyonel, `GITEA_TOKEN` secret gerektirir) diff --git a/docs/update_plan_01.md b/docs/update_plan_01.md deleted file mode 100644 index fea06e6..0000000 --- a/docs/update_plan_01.md +++ /dev/null @@ -1,45 +0,0 @@ -# ImajViewer — Pan Sınırlama & Zoom Merkezi Sorunu ✅ ÇÖZÜLDÜ - -> **Tarih:** 2026-07-23 -> **Dosya:** `lib/widgets/image_canvas.dart` -> **Durum:** ✅ Çözüldü - ---- - -## Nihai Çözüm - -### 1. Zoom — `Matrix4.copy(m)..translate..scale..translate` -Cursor merkezli zoomAroundPoint. Hem fill hem fit modunda çalışır çünkü `localPosition` her zaman Listener'a göre doğru koordinat verir. `_currentScale` listener'da güncellenir. - -### 2. `_clampAfterZoom` — Image offset + sadece boundary clamp -**Kritik:** Fit modunda (BoxFit.contain) Image, Center widget içinde **ortalanır**. Offset hesaplanarak image'ın viewport'taki gerçek sınırları bulunur: - -```dart -imgOffsetX = (viewportW - imageW) / 2 // fill modunda 0, fit modunda >0 -imgLeft = tx + imgOffsetX * scale -imgRight = tx + (imgOffsetX + imageW) * scale -``` - -**SADECE** image viewport'tan büyükse clamp yapılır. Küçükse **dokunulmaz** — zoom-around-point merkezi korunur. Kullanıcı double-tap ile istediği zaman ortalamayı sıfırlayabilir (`Matrix4.identity()`). - -### 3. `translate` — asla `setTranslationRaw` kullanma -`setTranslationRaw` matrisin translation sütununu ezer → zoom merkezini bozar. `translate(dx,dy)` ekleme yapar → zoom merkezi korunur. - -### 4. Listener'da clamp YOK -Sadece `_currentScale` takip edilir. Pan → `boundaryMargin: EdgeInsets.zero`, Zoom → `_handleScroll` içinde `_clampAfterZoom`. - ---- - -## Akış - -``` -Scroll → _handleScroll → zoomAroundPoint → _controller.value - ↓ - _clampAfterZoom() - ├─ image > viewport → translate ile clamp - └─ image ≤ viewport → dokunma - -Pan → InteractiveViewer → _controller.value (boundaryMargin sınırlar) - ↓ - _onTransformChanged → sadece _currentScale -``` diff --git a/docs/windows-setup.md b/docs/windows-setup.md deleted file mode 100644 index e5a5098..0000000 --- a/docs/windows-setup.md +++ /dev/null @@ -1,217 +0,0 @@ -# Windows Development Ortam Kurulum Rehberi - -Bu doküman, ImajViewer Flutter projesini Windows üzerinde geliştirmek için gerekli ortamın sıfırdan kurulmasını adım adım anlatır. - -## Gereksinimler - -| Araç | Minimum Versiyon | Açıklama | -|------|-----------------|----------| -| Flutter SDK | 3.27+ (stable) | UI framework | -| Visual Studio 2022 | Build Tools veya Community | C++ derleyici (MSVC) | -| CMake | 3.14+ | Build sistemi | -| Inno Setup | 6.x | Windows installer (.exe) oluşturma | - -## Adım 1: Flutter SDK Kurulumu - -### Yöntem A: Resmi kurulum (Önerilen) - -1. [Flutter SDK'yı indirin](https://docs.flutter.dev/get-started/install/windows) -2. ZIP'i `C:\src\flutter` dizinine çıkarın -3. PATH'e ekleyin: - -```powershell -# PowerShell (Admin) -[Environment]::SetEnvironmentVariable( - "PATH", - "$([Environment]::GetEnvironmentVariable("PATH", "Machine"))\;C:\src\flutter\bin", - "Machine" -) -``` - -4. Yeni terminalde doğrulayın: - -```powershell -flutter doctor -``` - -### Yöntem B: Winget ile kurulum - -```powershell -winget install --id OpenFlutter.FlutterSDK -``` - -### Flutter doctor kontrolleri - -```powershell -flutter doctor -``` - -Çıktıda `[✗]` işaretli bileşenler için önerilen adımları uygulayın. En azından şunlar `✓` olmalı: - -- Flutter -- Android toolchain (opsiyonel, sadece mobile geliştirme için) -- Visual Studio (eğer kuruluysa) -- Connected device (Windows) - -## Adım 2: Visual Studio 2022 Build Tools Kurulumu - -Flutter Windows build'i için MSVC C++ derleyicisi gerekir. - -### Seçenek A: Visual Studio Build Tools (Sadece build araçları) - -1. [Visual Studio Build Tools 2022](https://visualstudio.microsoft.com/visual-cpp-build-tools/) indirin -2. Kurulum sihirbazında **"Desktop development with C++"** workload'unu işaretleyin -3. Aşağıdaki bileşenler otomatik seçilecektir: - - MSVC v143 - VS 2022 C++ x64/x86 build tools - - Windows 10/11 SDK - - CMake tools (opsiyonel ama önerilir) - -### Seçenek B: Visual Studio Community (Tam IDE) - -1. [Visual Studio Community 2022](https://visualstudio.microsoft.com/vs/community/) indirin -2. Kurulumda **"Desktop development with C++"** workload'unu seçin - -### Doğrulama - -```powershell -cl -``` - -Çıktıda `Microsoft (R) C/C++ Optimizing Compiler` görünmelidir. - -## Adım 3: CMake Kurulumu - -Visual Studio Build Tools ile birlikte gelir ancak bağımsız kurulum da yapılabilir. - -### Winget ile: - -```powershell -winget install --id Kitware.CMake -``` - -### Doğrulama: - -```powershell -cmake --version -``` - -## Adım 4: Inno Setup 6 Kurulumu - -Windows installer (.exe) oluşturmak için gereklidir. - -1. [Inno Setup 6](http://www.jrsoftware.org/isdl.php#stable) indirin (`is.exe`) -2. Kurulumu tamamlayın (varsayılan: `C:\Program Files (x86)\Inno Setup 6`) - -### Doğrulama: - -```powershell -Test-Path "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" -``` - -## Adım 5: Projeyi Hazırlama - -```powershell -cd C:\projects\imajviewer - -# Bağımlılıkları indir -flutter pub get - -# Analiz et -flutter analyze - -# Test çalıştır -flutter test -``` - -## Adım 6: Windows Build - -### Debug build: - -```powershell -flutter build windows --debug -``` - -Çıktı: `build\windows\x64\runner\Debug\` - -### Release build: - -```powershell -flutter build windows --release -``` - -Çıktı: `build\windows\x64\runner\Release\` - -## Adım 7: Installer Oluşturma (Inno Setup) - -```powershell -# Release build yapıldıktan sonra -& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" windows\installer.iss -``` - -Çıktı: `dist\imajviewer_setup_1.1.0.exe` - -## Tek Komutla Her Şey - -Tüm build + installer pipeline'ını tek komutla çalıştırın: - -```powershell -.\build-windows.ps1 -``` - -Bu script: -1. `flutter pub get` -2. `flutter build windows --release` -3. Inno Setup ile `.exe` installer oluşturur - -## Sorun Giderme - -### "Unable to find Visual Studio installation" hatası - -```powershell -flutter config --enable-windows-desktop -``` - -Visual Studio 2022'nin doğru kurulduğunu `flutter doctor` ile kontrol edin. - -### CMake hataları - -CMake'in PATH'te olduğundan emin olun: - -```powershell -$env:PATH += ";C:\Program Files\CMake\bin" -cmake --version -``` - -### Inno Setup bulunamadı - -`build-windows.ps1` script'i otomatik arar. Manuel çalıştırmak için: - -```powershell -& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" windows\installer.iss -``` - -### Flutter doctor'da Android SDK uyarısı - -Sadece desktop geliştirme yapıyorsanız bu uyarıları görmezden gelebilirsiniz. - -## Kurulum Sonrası - -Ortam hazır olduğunda: - -```powershell -# Build + installer -.\build-windows.ps1 - -# Manuel kurulum (build çıktılarından) -.\install.ps1 - -# Uninstall -.\uninstall.ps1 -``` - -## Sistem Gereksinimleri (Minimum) - -- Windows 10 (1809) veya üzeri -- 4 GB RAM (8 GB önerilir) -- ~4 GB disk alanı (Flutter SDK + VS Build Tools + proje) -- x64 mimari diff --git a/docs/yonerge-downsampling-swap.md b/docs/yonerge-downsampling-swap.md deleted file mode 100644 index 3949082..0000000 --- a/docs/yonerge-downsampling-swap.md +++ /dev/null @@ -1,101 +0,0 @@ -# Yönerge: Downsampling + Full-Res Swap + RAM Leak Fix - -## Amaç -Yüksek çözünürlüklü görsellerde RAM tüketimini azaltmak ve resim geçişlerinde bellek sızıntısını gidermek. - -## Dosya -`lib/widgets/image_canvas.dart` - -## 1. State değişkenleri ekle - -`_ImageCanvasState` sınıfına aşağıdaki değişkenleri ekle: - -```dart -ui.Image? _uiImage; -ui.Image? _fullResImage; -bool _fullResLoading = false; -bool _usingFullRes = false; -static const int _downsampleTarget = 2048; -``` - -## 2. `_loadUiImage()` — Downsample decode - -Mevcut `_loadUiImage()` metodunu aşağıdaki gibi değiştir: - -- `ui.instantiateImageCodec()` çağrısına `targetWidth: _downsampleTarget, targetHeight: _downsampleTarget` parametrelerini ekle -- Decode edilen resmin boyutunu `_uiImage` ve `_tiledImageSize` olarak kaydet -- Eğer decode boyutu target'tan büyükse → `_loadFullResImage(bytes)` başlat (arka plan) -- Eğer decode boyutu target'tan küçükse → resim zaten küçük, `_usingFullRes = true` yap - -## 3. `_loadFullResImage()` — Arka plan full-res yükleme - -Yeni metot: - -```dart -Future _loadFullResImage(Uint8List bytes) async { - if (_fullResLoading) return; - _fullResLoading = true; - try { - final codec = await ui.instantiateImageCodec(bytes); - final frame = await codec.getNextFrame(); - if (mounted && !_usingFullRes) { - setState(() => _fullResImage = frame.image); - } else { - frame.image.dispose(); - } - } catch (e) { /* log */ } - finally { _fullResLoading = false; } -} -``` - -## 4. `_checkFullResSwap()` — Zoom threshold'unda swap - -Yeni metot. `_handleScroll()` içinde `_sc` güncellendikten sonra çağrılır. - -- Downsample resmin ne kadar stretch edildiğini hesapla (`stretchX`, `stretchY`) -- `maxStretch >= 1.5` ise: - - `_uiImage?.dispose()` - - `_uiImage = _fullResImage` - - `_fullResImage = null` - - `_tiledImageSize` güncelle - - `_usingFullRes = true` - -## 5. `_handleScroll()` — Swap kontrolü ekle - -`_sc = newSc;` satırından sonra `_checkFullResSwap()` çağrısı ekle. - -## 6. RAM Leak Fix — `didUpdateWidget` - -Resim değiştiğinde eski tüm referansları temizle: - -```dart -_uiImage?.dispose(); -_fullResImage?.dispose(); -_cachedFileImage?.evict(); -_cachedProvider?.evict(); -_cachedFileImage = null; -_cachedProvider = null; -_cachedPath = null; -_cachedCacheWidth = null; -PaintingBinding.instance.imageCache.evict(FileImage(File(old.filePath))); -``` - -## 7. `_resetView()` — Tüm state sıfırla - -`_fullResImage`, `_fullResLoading`, `_usingFullRes` sıfırlansın. - -## 8. `dispose()` — Her iki resmi de dispose et - -```dart -_uiImage?.dispose(); -_fullResImage?.dispose(); -``` - -## 9. Import - -`dart:typed_data` import'u ekle (`Uint8List` için). - -## Sonuç -- İlk yükleme: max 2048px downsample (RAM ~1.6MB) -- Zoom ile yakınlaşınca: full-res swap (~512MB+ büyük resimlerde) -- Resim geçişinde: eski tüm cache dispose + evict diff --git a/docs/zoom-clamp-mekanizmasi.md b/docs/zoom-clamp-mekanizmasi.md deleted file mode 100644 index 8ea39f7..0000000 --- a/docs/zoom-clamp-mekanizmasi.md +++ /dev/null @@ -1,233 +0,0 @@ -# Zoom / Pan / Clamp Mekanizması — Referans Doküman - -**Dosya:** `lib/widgets/image_canvas.dart` -**Son güncelleme:** 23 Temmuz 2026 - ---- - -## 1. Mimari Karar: Neden InteractiveViewer Yok? - -Başlangıçta `InteractiveViewer` kullanılıyordu. Ancak InteractiveViewer'ın built-in pan sınırlaması (`boundaryMargin`) çocuk widget'ın **tam boyutuna** göre çalışır. Fit modda `Center` kullanılınca, Center widget'ı viewport boyutunda olduğu halde içindeki Image daha küçüktür. IV, Image'ın değil Center'ın kenarlarına göre sınırlama yapar → Image'ın etrafında boşluk kalır. - -**Çözüm:** InteractiveViewer tamamen kaldırıldı. Yerine: - -``` -ClipRect → GestureDetector → Listener → Transform → Center/SizedBox → Image -``` - -- `Transform`: zoom/pan uygular (`_tx`, `_ty`, `_sc`) -- `ClipRect`: taşan kısımları kırpar -- `GestureDetector`: pan (drag) algılar -- `Listener`: scroll (tekerlek) algılar - ---- - -## 2. Durum Değişkenleri - -```dart -double _tx = 0; // viewport-space X kaydırma (piksel) -double _ty = 0; // viewport-space Y kaydırma (piksel) -double _sc = 1.0; // ölçek (1.0 = orijinal boyut) -bool _isFilled; // true=fill mod, false=fit mod -Size _vpSize; // LayoutBuilder'dan alınan viewport boyutu -``` - -**Dönüşüm matrisi** (Transform widget'ına verilen): - -``` -M = T(_tx, _ty) · S(_sc) -``` - -Yani: child-space'deki (cx, cy) noktası → viewport-space: `vx = cx * _sc + _tx` - ---- - -## 3. Widget Ağacı (build metodu, satır 111–154) - -``` -LayoutBuilder - ClipRect - GestureDetector - onDoubleTap → _toggleFillFit() - onPanUpdate → _onPanUpdate() - Listener - onPointerSignal → _handleScroll() - Transform(translate(_tx,_ty) · scale(_sc)) - _isFilled ? - SizedBox.expand(Image.file(fit: BoxFit.cover)) // FILL - : Center(Image.file(fit: BoxFit.contain)) // FIT -``` - -- Viewport boyutu (`_vpSize`), `LayoutBuilder`'ın `constraints.biggest`'inden okunur (satır 115). Her build'te güncellenir. `context.findRenderObject()` kullanılmaz çünkü LayoutBuilder'ın verdiği constraints daha güvenilirdir. -- `_imageKey`, `Image.file` widget'ına bağlıdır. Sadece errorBuilder için kullanılır (clamping'te kullanılmaz). - ---- - -## 4. Scroll Zoom Mekanizması (satır 50–71) - -``` -_handleScroll(PointerScrollEvent e) -``` - -```dart -// 1. İmleci viewport-space'den child-space'e çevir -cx = (vx - _tx) / _sc -cy = (vy - _ty) / _sc - -// 2. Yeni ölçek (her tick'te %10) -factor = 1.1 -newSc = (_sc * (tekerlek_yönüne_göre factor veya 1/factor)).clamp(0.1, 10.0) - -// 3. Zoom'u imleç merkezli uygula -_tx = _tx + cx * _sc - cx * newSc -_ty = _ty + cy * _sc - cy * newSc -_sc = newSc -``` - -**Formülün türetilmesi:** Zoom öncesi imleç altındaki child-space noktası `(cx, cy)`, viewport'ta `(vx, vy)` konumundadır. Zoom sonrası aynı noktanın yine `(vx, vy)`'de kalması için: - -``` -vx = _tx_old + cx * _sc_old (zoom öncesi) -vx = _tx_new + cx * _sc_new (zoom sonrası, aynı nokta) -→ _tx_new = vx - cx * _sc_new -→ _tx_new = (_tx_old + cx * _sc_old) - cx * _sc_new -→ _tx_new = _tx_old + cx * _sc_old - cx * _sc_new -``` - ---- - -## 5. Pan Mekanizması (satır 75–80) - -``` -_onPanUpdate(DragUpdateDetails d) -``` - -```dart -_tx += d.delta.dx; // her drag update'te delta biriktir -_ty += d.delta.dy; -if (_isFilled) _clampFill(_viewport()); -setState(() {}); -``` - -`d.delta`, son update'ten beri olan değişimi verir (toplam değil). Her update'te delta eklenir, clamp yapılır, setState ile yeniden çizilir. - ---- - -## 6. Clamp Sistemi — SADECE Fill Mod (satır 84–107) - -Fit modda hiçbir sınırlama yoktur. Kullanıcı özgürce pan/zoom yapabilir. - -### `_clampFill(Size vp)` - -``` -imgL = _tx → Image'ın viewport-space sol kenarı -imgR = _tx + vpw * _sc → Image'ın viewport-space sağ kenarı -vw = imgR - imgL → Image'ın viewport-space genişliği -``` - -**Mantık:** Fill modda `SizedBox.expand(BoxFit.cover)` kullanıldığı için Image her zaman viewport'u tam doldurur. Yani Image'ın child-space genişliği = vpw. Ölçeklenmiş genişliği = `vpw * _sc`. - -**Yatay sınırlama:** -``` -if (vw > vpw + 0.5) // Image viewport'tan geniş mi? - if (imgL > 0) // sol kenar viewport içinde → sola çek - _tx -= imgL - else if (imgR < vpw) // sağ kenar viewport içinde → sağa çek - _tx += vpw - imgR -``` - -**Dikey sınırlama:** Aynı mantık `imgT`/`imgB` ve `vph` ile. - -**Epsilon (`0.5`):** Floating-point hatalarını önlemek için. Tam eşitlik durumunda (vw = vpw) hiçbir işlem yapılmaz. - ---- - -## 7. Fill / Fit Geçişi (satır 39–45) - -``` -_toggleFillFit() -``` - -```dart -_isFilled = !_isFilled; -_tx = 0; _ty = 0; _sc = 1.0; // sıfırla -``` - -Çift tıklandığında: -- Mod değiştirilir -- Transform sıfırlanır (identity) -- Viewport cache'i sıfırlanır (sonraki build'te yeniden okunur) - -Fill → Fit: Image Center içinde BoxFit.contain ile ortalanır, sınırlama yok. -Fit → Fill: Image SizedBox.expand içinde BoxFit.cover ile doldurur, sınırlama aktif. - ---- - -## 8. Viewport Boyutu (satır 46, 115) - -```dart -Size _viewport() => _vpSize; -``` - -`_vpSize` değeri `LayoutBuilder`'ın `builder` callback'inde `constraints.biggest` olarak alınır (satır 115). Her build'te güncellenir. Neden `context.findRenderObject()` değil? - -- `LayoutBuilder`'ın constraints'i, widget'ın parent'tan aldığı en güncel boyut bilgisidir -- `context.findRenderObject()?.size` bazen null veya eski değer döndürebilir (henüz layout olmamışsa) -- `constraints.biggest` her zaman doğru ve günceldir - ---- - -## 9. Görüntü Boyutu Nereden Alınıyor? - -**Fill mod:** `SizedBox.expand(BoxFit.cover)` → Image viewport'u tam doldurur. Görüntü boyutu = viewport boyutu. Clamp'ta `vp.width` ve `vp.height` kullanılır. - -**Fit mod:** `Center(BoxFit.contain)` → Image fitted boyutta çizilir. Clamp OLMADIĞI için görüntü boyutuna ihtiyaç yoktur. - -Eski versiyonlarda `_imageKey.currentContext?.findRenderObject()` ile render box okunuyordu. Bunun iki sorunu vardı: -1. `frameBuilder` içinde çağrılınca render object henüz oluşmamış olabiliyor → null veya 0 boyut dönüyor -2. `SizedBox.expand` içinde render box viewport boyutunu döndürüyor (fitted boyutu değil) - ---- - -## 10. Özet: Hangi Fonksiyon Ne Yapıyor? - -| Fonksiyon | Satır | Görevi | -|-----------|-------|--------| -| `_handleScroll()` | 50 | Mouse tekerleği ile zoom (imleç merkezli) | -| `_onPanUpdate()` | 75 | Sürükleme ile pan | -| `_clampFill()` | 84 | Fill modda kenar sınırlaması | -| `_toggleFillFit()` | 39 | Fill/Fit geçişi ve sıfırlama | -| `_resetView()` | 33 | Yeni resim açıldığında sıfırlama | -| `_viewport()` | 46 | Viewport boyutunu döndürür | -| `build()` | 112 | Widget ağacını kurar, `_vpSize`'ı günceller | - ---- - -## 11. Widget Tree (görsel) - -``` -ImageCanvas - └─ LayoutBuilder ─(constraints.biggest→_vpSize)─ - └─ ClipRect - └─ GestureDetector (doubleTap, panUpdate) - └─ Listener (onPointerSignal→scroll) - └─ Transform [T(_tx,_ty)·S(_sc)] - └─ [_isFilled ? SizedBox.expand : Center] - └─ Image.file(key:_imageKey, fit:cover/contain) -``` - ---- - -## 12. Geçmiş / Alınan Dersler - -1. **InteractiveViewer çıkarıldı** — boundaryMargin çocuk widget'ın tam boyutuna göre çalışıyor, Center içindeki Image'ın asıl boyutunu bilmiyor. - -2. **Center yerine SizedBox.expand denenmesi** — `_baseContentRect` ile fitted boyut hesaplandı ama `_intrinsicSize` asenkron yükleniyor, ilk frame'lerde yanlış boyut kullanılıyordu. - -3. **Render box'tan boyut okuma** — `frameBuilder` içinde render object henüz oluşmamış olabiliyor (build → layout sırası). Retry mekanizması eklendi ama güvenilir olmadı. - -4. **Fit modda sınırlamanın tamamen kaldırılması** — En temiz çözüm. Center widget'ı zaten layout seviyesinde ortalar. Zoom sonrası kullanıcı istediği gibi pan yapabilir. - -5. **translateByDouble / scaleByDouble hatası** — `w` parametresi `1` olmalıydı, `0` kullanılınca transform bozuldu. Eski `translate`/`scale` API'sine geri dönüldü. - -6. **_vpSize = Size.zero sıfırlaması (2026-08-03 düzeltildi)** — `_resetView()` ve `_toggleFillFit()` içinde `_vpSize = Size.zero` vardı. Çift tıklama (fill/fit toggle) `setState` yerine yalnızca `_markRenderDirty()` çağırdığı için LayoutBuilder yeniden çalışmıyor, `_vpSize` sıfır kalıyordu → `_handleScroll` ve `_clamp` erken return edip zoom ölüyor, pan sınırsız oluyordu. Çözüm: metot içi sıfırlama kaldırıldı; `_vpSize` yalnızca LayoutBuilder'ın `constraints.biggest`'inden beslenir (alan tanımındaki ilk değer korunur). Linux'ta da mevcuttu, Windows testinde fark edildi.