chore: remove internal docs and old release binaries from tracking
- 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:
1 parent
168e087c8b
commit
268dc879c8
29 files changed
+7
-2567
No files matched your search
+7
-1
@@ -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/
|
||||
docs/.bak/
|
||||
|
||||
# Internal dev docs stay local, not in the public repo
|
||||
/docs/
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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 |
|
||||
@@ -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/`)
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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?
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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ı).
|
||||
@@ -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?)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user