feat: jfif/jpe/wbmp format destegi, pencere cascade yerlesimi, konum kaydi duzeltmesi
- file_handler/image_manager: jfif, jpe, wbmp uzantilari - .desktop + install.sh MimeType: image/vnd.wap.wbmp eklendi - main.dart: yeni pencere oncekinin saginda acilir (screen_retriever ile ekran tespiti) - viewer_screen: 2sn polling kaldirildi, konum kaydi tek akisa oturtuldu (acilis + move/resize debounce + close senkron) - dist/rebuild-deb.sh: .desktop kaynaktan kopyalanir (MimeType/StartupWMClass guncel kalir) - .deb guncellendi, docs: session-2026-08-03 + plan dokumanlari
This commit is contained in:
@@ -4,7 +4,7 @@ Ultra hafif görüntü izleyici. Flutter ile yazıldı — Linux & Windows deste
|
|||||||
|
|
||||||
## Özellikler
|
## Özellikler
|
||||||
|
|
||||||
- **Dosya formatları:** PNG, JPEG, WebP, BMP, GIF
|
- **Dosya formatları:** PNG, JPEG, JFIF, WebP, BMP, GIF, WBMP
|
||||||
- **Pan:** Sol tık + sürükle
|
- **Pan:** Sol tık + sürükle
|
||||||
- **Zoom:** Fare tekerleği (imleç merkezli, rotate modunda viewport merkezli)
|
- **Zoom:** Fare tekerleği (imleç merkezli, rotate modunda viewport merkezli)
|
||||||
- **Fill/Fit geçişi:** Sol çift tık
|
- **Fill/Fit geçişi:** Sol çift tık
|
||||||
@@ -15,6 +15,7 @@ Ultra hafif görüntü izleyici. Flutter ile yazıldı — Linux & Windows deste
|
|||||||
- Sürükle-bırak ile resim açma
|
- Sürükle-bırak ile resim açma
|
||||||
- Pencere boyutu/konumu hatırlama (JSON persistence)
|
- Pencere boyutu/konumu hatırlama (JSON persistence)
|
||||||
- Varsayılan görüntü izleyici olarak ayarlanabilir
|
- Varsayılan görüntü izleyici olarak ayarlanabilir
|
||||||
|
- Yeni pencere öncekinin yanında açılır (cascade — taşarsa alt satır)
|
||||||
- Taskbar gruplama (Linux: `StartupWMClass`, otomatik)
|
- Taskbar gruplama (Linux: `StartupWMClass`, otomatik)
|
||||||
|
|
||||||
## Linux Kurulum
|
## Linux Kurulum
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ Exec=/usr/bin/imajviewer %F
|
|||||||
Icon=imajviewer
|
Icon=imajviewer
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Categories=Graphics;Viewer;RasterGraphics;
|
Categories=Graphics;Viewer;RasterGraphics;
|
||||||
MimeType=image/png;image/jpeg;image/webp;image/bmp;image/gif;image/jpg;
|
MimeType=image/png;image/jpeg;image/webp;image/bmp;image/gif;image/jpg;image/vnd.wap.wbmp;
|
||||||
StartupNotify=false
|
StartupNotify=false
|
||||||
|
StartupWMClass=com.example.imajviewer
|
||||||
|
|||||||
BIN
dist/imajviewer_1.0.0_amd64.deb
vendored
BIN
dist/imajviewer_1.0.0_amd64.deb
vendored
Binary file not shown.
6
dist/rebuild-deb.sh
vendored
6
dist/rebuild-deb.sh
vendored
@@ -23,11 +23,9 @@ cp -r "$RELEASE_DIR"/* "$TMPDIR/pkg/usr/lib/${APP}/"
|
|||||||
echo "• Symlink güncelleniyor…"
|
echo "• Symlink güncelleniyor…"
|
||||||
ln -sf "/usr/lib/${APP}/${APP}" "$TMPDIR/pkg/usr/bin/${APP}"
|
ln -sf "/usr/lib/${APP}/${APP}" "$TMPDIR/pkg/usr/bin/${APP}"
|
||||||
|
|
||||||
echo "• .desktop dosyasına StartupWMClass ekleniyor…"
|
echo "• .desktop dosyası kaynaktan kopyalanıyor…"
|
||||||
DESKTOP_FILE="$TMPDIR/pkg/usr/share/applications/${APP}.desktop"
|
DESKTOP_FILE="$TMPDIR/pkg/usr/share/applications/${APP}.desktop"
|
||||||
if ! grep -q 'StartupWMClass' "$DESKTOP_FILE" 2>/dev/null; then
|
cp "${PROJECT_DIR}/deb/usr/share/applications/${APP}.desktop" "$DESKTOP_FILE"
|
||||||
sed -i '/^StartupNotify=false$/a StartupWMClass=com.example.imajviewer' "$DESKTOP_FILE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "• Yeni .deb paketi oluşturuluyor…"
|
echo "• Yeni .deb paketi oluşturuluyor…"
|
||||||
fakeroot dpkg-deb --build "$TMPDIR/pkg" "${DEB}.tmp" >/dev/null
|
fakeroot dpkg-deb --build "$TMPDIR/pkg" "${DEB}.tmp" >/dev/null
|
||||||
|
|||||||
59
docs/format-destegi-ek-formatlar.md
Normal file
59
docs/format-destegi-ek-formatlar.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# 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
|
||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
| Tarih | Dosya | Konu |
|
| Tarih | Dosya | Konu |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| 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-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 | [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-07-27 | [image-canvas-interactions.md](image-canvas-interactions.md) | Detaylı analiz: contrast, saturation, rotate implementasyon planı |
|
||||||
|
|||||||
111
docs/pencere-cascade-yerlesim.md
Normal file
111
docs/pencere-cascade-yerlesim.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# 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)
|
||||||
33
docs/session-2026-08-03.md
Normal file
33
docs/session-2026-08-03.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# 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)
|
||||||
@@ -6,5 +6,5 @@ Exec=imajviewer %F
|
|||||||
Icon=imajviewer
|
Icon=imajviewer
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Categories=Graphics;Viewer;RasterGraphics;
|
Categories=Graphics;Viewer;RasterGraphics;
|
||||||
MimeType=image/png;image/jpeg;image/webp;image/bmp;image/gif;image/jpg;
|
MimeType=image/png;image/jpeg;image/webp;image/bmp;image/gif;image/jpg;image/vnd.wap.wbmp;
|
||||||
StartupNotify=false
|
StartupNotify=false
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ Exec=${BIN_LINK} %F
|
|||||||
Icon=${APP}
|
Icon=${APP}
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Categories=Graphics;Viewer;RasterGraphics;
|
Categories=Graphics;Viewer;RasterGraphics;
|
||||||
MimeType=image/png;image/jpeg;image/webp;image/bmp;image/gif;image/jpg;
|
MimeType=image/png;image/jpeg;image/webp;image/bmp;image/gif;image/jpg;image/vnd.wap.wbmp;
|
||||||
StartupNotify=false
|
StartupNotify=false
|
||||||
StartupWMClass=com.example.imajviewer
|
StartupWMClass=com.example.imajviewer
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ void main(List<String> args) async {
|
|||||||
|
|
||||||
await windowManager.waitUntilReadyToShow(windowOptions, () async {
|
await windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||||
if (saved != null) {
|
if (saved != null) {
|
||||||
await windowManager.setPosition(saved.position);
|
final position = await WindowPersistence.cascadePosition(
|
||||||
|
previousPosition: saved.position,
|
||||||
|
previousSize: saved.size,
|
||||||
|
);
|
||||||
|
await windowManager.setPosition(position);
|
||||||
await windowManager.setSize(saved.size);
|
await windowManager.setSize(saved.size);
|
||||||
} else {
|
} else {
|
||||||
await windowManager.center();
|
await windowManager.center();
|
||||||
@@ -33,6 +37,11 @@ void main(List<String> args) async {
|
|||||||
await windowManager.setResizable(true);
|
await windowManager.setResizable(true);
|
||||||
await windowManager.show();
|
await windowManager.show();
|
||||||
await windowManager.focus();
|
await windowManager.focus();
|
||||||
|
|
||||||
|
// Açılış konumunu hemen kaydet — sıradaki pencere güncel referans okusun
|
||||||
|
final pos = await windowManager.getPosition();
|
||||||
|
final size = await windowManager.getSize();
|
||||||
|
WindowPersistence.saveSync(position: pos, size: size);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Extract file paths from command-line arguments
|
// Extract file paths from command-line arguments
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
|
|||||||
bool _hasImages = false;
|
bool _hasImages = false;
|
||||||
bool _isDragging = false;
|
bool _isDragging = false;
|
||||||
Timer? _saveDebounce;
|
Timer? _saveDebounce;
|
||||||
Timer? _pollTimer;
|
|
||||||
Rect _lastBounds = Rect.zero;
|
Rect _lastBounds = Rect.zero;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -35,29 +34,6 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
|
|||||||
FileHandler.loadImagesFromPaths(widget.initialFiles);
|
FileHandler.loadImagesFromPaths(widget.initialFiles);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// İlk kayıt + periyodik polling başlat
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
|
||||||
await Future.delayed(const Duration(milliseconds: 200));
|
|
||||||
await _doSave();
|
|
||||||
_startPolling();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startPolling() {
|
|
||||||
_pollTimer?.cancel();
|
|
||||||
_pollTimer = Timer.periodic(const Duration(seconds: 2), (_) async {
|
|
||||||
try {
|
|
||||||
final bounds = await windowManager.getBounds();
|
|
||||||
if (bounds != _lastBounds) {
|
|
||||||
_lastBounds = bounds;
|
|
||||||
WindowPersistence.saveSync(
|
|
||||||
position: bounds.topLeft,
|
|
||||||
size: bounds.size,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -65,7 +41,6 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
|
|||||||
windowManager.removeListener(this);
|
windowManager.removeListener(this);
|
||||||
ImageManager.instance.removeListener(_onImageManagerChanged);
|
ImageManager.instance.removeListener(_onImageManagerChanged);
|
||||||
_saveDebounce?.cancel();
|
_saveDebounce?.cancel();
|
||||||
_pollTimer?.cancel();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,10 +49,18 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
|
|||||||
void onWindowEvent(String eventName) {
|
void onWindowEvent(String eventName) {
|
||||||
switch (eventName) {
|
switch (eventName) {
|
||||||
case 'close':
|
case 'close':
|
||||||
_doSave();
|
// Senkron kayıt — async çağrı kapanışta tamamlanmayabilir
|
||||||
|
if (_lastBounds != Rect.zero) {
|
||||||
|
WindowPersistence.saveSync(
|
||||||
|
position: _lastBounds.topLeft,
|
||||||
|
size: _lastBounds.size,
|
||||||
|
);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'resize':
|
case 'resize':
|
||||||
case 'move':
|
case 'move':
|
||||||
|
// Son bilinen konumu hemen güncelle (kapanış senkron kaydı için)
|
||||||
|
windowManager.getBounds().then((b) => _lastBounds = b).catchError((_) {});
|
||||||
_saveDebounce?.cancel();
|
_saveDebounce?.cancel();
|
||||||
_saveDebounce = Timer(const Duration(milliseconds: 300), () {
|
_saveDebounce = Timer(const Duration(milliseconds: 300), () {
|
||||||
_doSave();
|
_doSave();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class FileHandler {
|
|||||||
static Future<void> openFileDialog() async {
|
static Future<void> openFileDialog() async {
|
||||||
final result = await FilePicker.platform.pickFiles(
|
final result = await FilePicker.platform.pickFiles(
|
||||||
type: FileType.custom,
|
type: FileType.custom,
|
||||||
allowedExtensions: ['png', 'jpg', 'jpeg', 'webp', 'bmp', 'gif'],
|
allowedExtensions: ['png', 'jpg', 'jpeg', 'jfif', 'jpe', 'webp', 'bmp', 'gif', 'wbmp'],
|
||||||
allowMultiple: true,
|
allowMultiple: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class ImageManager extends ChangeNotifier {
|
|||||||
void addImages(List<String> paths) {
|
void addImages(List<String> paths) {
|
||||||
final validPaths = paths.where((p) {
|
final validPaths = paths.where((p) {
|
||||||
final ext = p.toLowerCase().split('.').last;
|
final ext = p.toLowerCase().split('.').last;
|
||||||
const supported = ['png', 'jpg', 'jpeg', 'webp', 'bmp', 'gif'];
|
const supported = ['png', 'jpg', 'jpeg', 'jfif', 'jpe', 'webp', 'bmp', 'gif', 'wbmp'];
|
||||||
return supported.contains(ext) && File(p).existsSync();
|
return supported.contains(ext) && File(p).existsSync();
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:screen_retriever/screen_retriever.dart';
|
||||||
|
|
||||||
/// Persists and restores window position and size.
|
/// Persists and restores window position and size.
|
||||||
class WindowPersistence {
|
class WindowPersistence {
|
||||||
@@ -51,4 +52,38 @@ class WindowPersistence {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cascade: place new window to the right of the previous one.
|
||||||
|
/// Falls to the next row (row start) when it would overflow the screen,
|
||||||
|
/// and wraps to the top-left if the next row also overflows.
|
||||||
|
static Future<Offset> cascadePosition({
|
||||||
|
required Offset previousPosition,
|
||||||
|
required Size previousSize,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final display = await screenRetriever.getPrimaryDisplay();
|
||||||
|
final workLeft = display.visiblePosition?.dx ?? 0;
|
||||||
|
final workTop = display.visiblePosition?.dy ?? 0;
|
||||||
|
final workWidth = display.visibleSize?.width ?? display.size.width;
|
||||||
|
final workHeight = display.visibleSize?.height ?? display.size.height;
|
||||||
|
|
||||||
|
var x = previousPosition.dx + previousSize.width;
|
||||||
|
var y = previousPosition.dy;
|
||||||
|
|
||||||
|
if (x + previousSize.width > workLeft + workWidth) {
|
||||||
|
x = workLeft;
|
||||||
|
y = previousPosition.dy + previousSize.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y + previousSize.height > workTop + workHeight) {
|
||||||
|
x = workLeft;
|
||||||
|
y = workTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Offset(x, y);
|
||||||
|
} catch (_) {
|
||||||
|
// Fallback: keep previous position if screen info is unavailable.
|
||||||
|
return previousPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
screen_retriever:
|
screen_retriever:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: screen_retriever
|
name: screen_retriever
|
||||||
sha256: ace919117a7520c13a50a6259e60c4a0d4cbe98809468792a91b5c5adada2aa6
|
sha256: ace919117a7520c13a50a6259e60c4a0d4cbe98809468792a91b5c5adada2aa6
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ dependencies:
|
|||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
window_manager: ^0.4.3
|
window_manager: ^0.4.3
|
||||||
|
screen_retriever: ^0.2.2
|
||||||
file_picker: ^8.0.0
|
file_picker: ^8.0.0
|
||||||
path: ^1.9.0
|
path: ^1.9.0
|
||||||
desktop_drop: ^0.7.1
|
desktop_drop: ^0.7.1
|
||||||
|
|||||||
Reference in New Issue
Block a user