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:
Alhan
2026-08-03 06:44:57 +03:00
parent d09351feef
commit a65bdc152c
17 changed files with 272 additions and 38 deletions

View File

@@ -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

View File

@@ -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

Binary file not shown.

6
dist/rebuild-deb.sh vendored
View File

@@ -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

View 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ı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

View File

@@ -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ı |

View 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**ı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)

View 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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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();

View File

@@ -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,
); );

View File

@@ -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();

View File

@@ -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;
}
}
} }

View File

@@ -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

View File

@@ -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