feat: rotate (Shift+sag tik), StartupWMClass taskbar gruplama, dist guncelle
Some checks failed
Build Windows / build-windows (push) Has been cancelled
Some checks failed
Build Windows / build-windows (push) Has been cancelled
This commit is contained in:
BIN
dist/imajviewer_1.0.0_amd64.deb
vendored
BIN
dist/imajviewer_1.0.0_amd64.deb
vendored
Binary file not shown.
36
dist/rebuild-deb.sh
vendored
Normal file
36
dist/rebuild-deb.sh
vendored
Normal 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))"
|
||||
@@ -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) |
|
||||
|
||||
79
docs/session-2026-07-27.md
Normal file
79
docs/session-2026-07-27.md
Normal 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
|
||||
@@ -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}"
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user