chore: remove internal docs and old release binaries from tracking
Build & Release / build-linux (push) Has been cancelled
Build & Release / build-windows (push) Has been cancelled
Build & Release / create-release (push) Has been cancelled

- docs/ untracked: internal dev docs (reports, sessions, plans) stay local
- dist/: keep only latest .deb (1.3.1) and .exe (1.1.0); remove 4 old builds
- .gitignore: ignore /docs/ and all dist binaries except the two latest
This commit is contained in:
Alhan committed 2026-10-11 21:47:33 +03:00
1 parent 168e087c8b
commit 268dc879c8
29 files changed
+6 -2566

No files matched your search

+6
View File
@@ -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/
# Internal dev docs stay local, not in the public repo
/docs/
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
-209
View File
@@ -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<String> 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<List<File>>` 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** |
-100
View File
@@ -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
-59
View File
@@ -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
-65
View File
@@ -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)
-187
View File
@@ -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 |
-40
View File
@@ -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/`)
-111
View File
@@ -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>`
- `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<Offset?> 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)
-71
View File
@@ -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<int> _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.
-98
View File
@@ -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<double>? 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<double>? activeColorMatrix` getter |
| `lib/screens/viewer_screen.dart` | `GlobalKey<ImageCanvasState>`, `_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)
-135
View File
@@ -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<Object> 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)
-62
View File
@@ -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?
-58
View File
@@ -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.
-80
View File
@@ -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).
-88
View File
@@ -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 <url>` →
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)
-139
View File
@@ -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
-199
View File
@@ -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ı).
-127
View File
@@ -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?)
-79
View File
@@ -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
-33
View File
@@ -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)
-30
View File
@@ -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)
-45
View File
@@ -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
```
-217
View File
@@ -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
-101
View File
@@ -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<void> _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
-233
View File
@@ -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.