feat: rotate (Shift+sag tik), StartupWMClass taskbar gruplama, dist guncelle
Some checks failed
Build Windows / build-windows (push) Has been cancelled

This commit is contained in:
Alhan
2026-07-27 20:41:14 +03:00
parent 35d3bc3749
commit 171476d30b
6 changed files with 179 additions and 34 deletions

Binary file not shown.

36
dist/rebuild-deb.sh vendored Normal file
View File

@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -euo pipefail
APP=imajviewer
VERSION=1.0.0
DIST_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$DIST_DIR")"
RELEASE_DIR="${PROJECT_DIR}/build/linux/x64/release/bundle"
DEB="${DIST_DIR}/${APP}_${VERSION}_amd64.deb"
# ── Geçici çalışma dizini ──
TMPDIR=$(mktemp -d)
trap "rm -rf $TMPDIR" EXIT
echo "• Eski .deb çıkartılıyor…"
dpkg-deb -x "$DEB" "$TMPDIR/pkg"
dpkg-deb -e "$DEB" "$TMPDIR/pkg/DEBIAN"
echo "• Yeni release binary ve kütüphaneler kopyalanıyor…"
cp -r "$RELEASE_DIR"/* "$TMPDIR/pkg/usr/lib/${APP}/"
echo "• Symlink güncelleniyor…"
ln -sf "/usr/lib/${APP}/${APP}" "$TMPDIR/pkg/usr/bin/${APP}"
echo "• .desktop dosyasına StartupWMClass ekleniyor…"
DESKTOP_FILE="$TMPDIR/pkg/usr/share/applications/${APP}.desktop"
if ! grep -q 'StartupWMClass' "$DESKTOP_FILE" 2>/dev/null; then
sed -i '/^StartupNotify=false$/a StartupWMClass=com.example.imajviewer' "$DESKTOP_FILE"
fi
echo "• Yeni .deb paketi oluşturuluyor…"
fakeroot dpkg-deb --build "$TMPDIR/pkg" "${DEB}.tmp" >/dev/null
mv "${DEB}.tmp" "$DEB"
echo "${DEB} güncellendi ($(du -h "$DEB" | cut -f1))"

View File

@@ -2,5 +2,6 @@
| Tarih | Dosya | Konu |
|---|---|---|
| 2026-07-27 | [image-canvas-interactions.md](image-canvas-interactions.md) | Sağ tık etkileşimleri: contrast, saturation, rotate |
| 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ı |
| önceki | [zoom-clamp-mekanizmasi.md](zoom-clamp-mekanizmasi.md) | Zoom clamp mekanizması (letterbox-aware) |

View File

@@ -0,0 +1,79 @@
# 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

View File

@@ -76,6 +76,7 @@ Terminal=false
Categories=Graphics;Viewer;RasterGraphics;
MimeType=image/png;image/jpeg;image/webp;image/bmp;image/gif;image/jpg;
StartupNotify=false
StartupWMClass=com.example.imajviewer
EOF
ok "${DESKTOP_FILE}"

View File

@@ -37,6 +37,7 @@ class _ImageCanvasState extends State<ImageCanvas> {
void _clamp() {
if (_vpSize.width <= 0 || _vpSize.height <= 0) return;
if (_angle != 0) return; // rotate varsa serbest pan
if (_isFilled) {
final double w = _vpSize.width * _sc;
final double h = _vpSize.height * _sc;
@@ -153,15 +154,23 @@ class _ImageCanvasState extends State<ImageCanvas> {
void _handleScroll(PointerScrollEvent e) {
if (_vpSize.width <= 0 || _vpSize.height <= 0) return;
final double vx = e.localPosition.dx;
final double vy = e.localPosition.dy;
final double vx = _angle == 0 ? e.localPosition.dx : _vpSize.width / 2;
final double vy = _angle == 0 ? e.localPosition.dy : _vpSize.height / 2;
final double cx = (vx - _tx) / _sc;
final double cy = (vy - _ty) / _sc;
const double factor = 1.1;
final double rawSc = _sc * (e.scrollDelta.dy < 0 ? factor : 1.0 / factor);
final double newSc = rawSc.clamp(1.0, 10.0);
_tx = _tx + cx * _sc - cx * newSc;
_ty = _ty + cy * _sc - cy * newSc;
if (_angle == 0) {
_tx = _tx + cx * _sc - cx * newSc;
_ty = _ty + cy * _sc - cy * newSc;
} else {
// Rotate'li zoom: viewport merkezini koru
final double dw = _vpSize.width * (newSc - _sc);
final double dh = _vpSize.height * (newSc - _sc);
_tx -= dw / 2;
_ty -= dh / 2;
}
_sc = newSc;
_clamp();
setState(() {});
@@ -235,32 +244,39 @@ class _ImageCanvasState extends State<ImageCanvas> {
_interactionMode = 'none';
}
// ── Color filters ──
// ── Combined color filter (saturation × contrast) ──
static const double _rLum = 0.2126;
static const double _gLum = 0.7152;
static const double _bLum = 0.0722;
ColorFilter _buildSaturationFilter() {
final s = _saturation;
final invS = 1.0 - s;
return ColorFilter.matrix(<double>[
invS * _rLum + s, invS * _gLum, invS * _bLum, 0, 0,
invS * _rLum, invS * _gLum + s, invS * _bLum, 0, 0,
invS * _rLum, invS * _gLum, invS * _bLum + s, 0, 0,
0, 0, 0, 1, 0,
]);
}
ColorFilter? _cachedFilter;
double _cachedSat = -1;
double _cachedCon = -1;
ColorFilter _buildContrastFilter() {
ColorFilter _buildCombinedFilter() {
if (_cachedFilter != null && _cachedSat == _saturation && _cachedCon == _contrast) {
return _cachedFilter!;
}
_cachedSat = _saturation;
_cachedCon = _contrast;
final s = _saturation;
final c = _contrast;
final t = (1.0 - c) / 2.0;
return ColorFilter.matrix(<double>[
c, 0, 0, 0, t,
0, c, 0, 0, t,
0, 0, c, 0, t,
0, 0, 0, 1, 0,
final invS = 1.0 - s;
final sR_R = invS * _rLum + s, sR_G = invS * _gLum, sR_B = invS * _bLum;
final sG_R = invS * _rLum, sG_G = invS * _gLum + s, sG_B = invS * _bLum;
final sB_R = invS * _rLum, sB_G = invS * _gLum, sB_B = invS * _bLum + s;
_cachedFilter = ColorFilter.matrix(<double>[
c * sR_R, c * sR_G, c * sR_B, 0, t,
c * sG_R, c * sG_G, c * sG_B, 0, t,
c * sB_R, c * sB_G, c * sB_B, 0, t,
0, 0, 0, 1, 0,
]);
return _cachedFilter!;
}
Widget _buildImage() {
@@ -284,6 +300,29 @@ class _ImageCanvasState extends State<ImageCanvas> {
final vpCenterY = _vpSize.height / 2;
final angleRad = _angle * math.pi / 180;
Widget imageContent = Builder(
builder: (context) {
final hasFilter = _saturation != 1.0 || _contrast != 1.0;
final image = _isFilled
? SizedBox.expand(child: _buildImage())
: Center(child: _buildImage());
return hasFilter
? ColorFiltered(colorFilter: _buildCombinedFilter(), child: image)
: image;
},
);
if (_angle != 0) {
imageContent = Transform(
alignment: Alignment.topLeft,
transform: Matrix4.identity()
..translate(vpCenterX, vpCenterY)
..rotateZ(angleRad)
..translate(-vpCenterX, -vpCenterY),
child: imageContent,
);
}
return SizedBox.expand(
child: Stack(
children: [
@@ -301,20 +340,9 @@ class _ImageCanvasState extends State<ImageCanvas> {
child: Transform(
alignment: Alignment.topLeft,
transform: Matrix4.identity()
..translate(vpCenterX, vpCenterY)
..rotateZ(angleRad)
..translate(-vpCenterX, -vpCenterY)
..translate(_tx, _ty)
..scale(_sc),
child: ColorFiltered(
colorFilter: _buildSaturationFilter(),
child: ColorFiltered(
colorFilter: _buildContrastFilter(),
child: _isFilled
? SizedBox.expand(child: _buildImage())
: Center(child: _buildImage()),
),
),
child: imageContent,
),
),
),