feat: rotate (Shift+sag tik), StartupWMClass taskbar gruplama, dist guncelle
Build Windows / build-windows (push) Has been cancelled
Build Windows / build-windows (push) Has been cancelled
This commit is contained in:
1 parent
35d3bc3749
commit
171476d30b
6 files changed
+179
-34
No files matched your search
Vendored
BIN
Binary file not shown.
Vendored
+36
@@ -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
-1
@@ -2,5 +2,6 @@
|
|||||||
|
|
||||||
| Tarih | Dosya | Konu |
|
| 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) |
|
| önceki | [zoom-clamp-mekanizmasi.md](zoom-clamp-mekanizmasi.md) | Zoom clamp mekanizması (letterbox-aware) |
|
||||||
@@ -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;
|
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;
|
||||||
StartupNotify=false
|
StartupNotify=false
|
||||||
|
StartupWMClass=com.example.imajviewer
|
||||||
EOF
|
EOF
|
||||||
ok "${DESKTOP_FILE}"
|
ok "${DESKTOP_FILE}"
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
|||||||
|
|
||||||
void _clamp() {
|
void _clamp() {
|
||||||
if (_vpSize.width <= 0 || _vpSize.height <= 0) return;
|
if (_vpSize.width <= 0 || _vpSize.height <= 0) return;
|
||||||
|
if (_angle != 0) return; // rotate varsa serbest pan
|
||||||
if (_isFilled) {
|
if (_isFilled) {
|
||||||
final double w = _vpSize.width * _sc;
|
final double w = _vpSize.width * _sc;
|
||||||
final double h = _vpSize.height * _sc;
|
final double h = _vpSize.height * _sc;
|
||||||
@@ -153,15 +154,23 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
|||||||
|
|
||||||
void _handleScroll(PointerScrollEvent e) {
|
void _handleScroll(PointerScrollEvent e) {
|
||||||
if (_vpSize.width <= 0 || _vpSize.height <= 0) return;
|
if (_vpSize.width <= 0 || _vpSize.height <= 0) return;
|
||||||
final double vx = e.localPosition.dx;
|
final double vx = _angle == 0 ? e.localPosition.dx : _vpSize.width / 2;
|
||||||
final double vy = e.localPosition.dy;
|
final double vy = _angle == 0 ? e.localPosition.dy : _vpSize.height / 2;
|
||||||
final double cx = (vx - _tx) / _sc;
|
final double cx = (vx - _tx) / _sc;
|
||||||
final double cy = (vy - _ty) / _sc;
|
final double cy = (vy - _ty) / _sc;
|
||||||
const double factor = 1.1;
|
const double factor = 1.1;
|
||||||
final double rawSc = _sc * (e.scrollDelta.dy < 0 ? factor : 1.0 / factor);
|
final double rawSc = _sc * (e.scrollDelta.dy < 0 ? factor : 1.0 / factor);
|
||||||
final double newSc = rawSc.clamp(1.0, 10.0);
|
final double newSc = rawSc.clamp(1.0, 10.0);
|
||||||
_tx = _tx + cx * _sc - cx * newSc;
|
if (_angle == 0) {
|
||||||
_ty = _ty + cy * _sc - cy * newSc;
|
_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;
|
_sc = newSc;
|
||||||
_clamp();
|
_clamp();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
@@ -235,32 +244,39 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
|||||||
_interactionMode = 'none';
|
_interactionMode = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Color filters ──
|
// ── Combined color filter (saturation × contrast) ──
|
||||||
|
|
||||||
static const double _rLum = 0.2126;
|
static const double _rLum = 0.2126;
|
||||||
static const double _gLum = 0.7152;
|
static const double _gLum = 0.7152;
|
||||||
static const double _bLum = 0.0722;
|
static const double _bLum = 0.0722;
|
||||||
|
|
||||||
ColorFilter _buildSaturationFilter() {
|
ColorFilter? _cachedFilter;
|
||||||
final s = _saturation;
|
double _cachedSat = -1;
|
||||||
final invS = 1.0 - s;
|
double _cachedCon = -1;
|
||||||
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 _buildContrastFilter() {
|
ColorFilter _buildCombinedFilter() {
|
||||||
|
if (_cachedFilter != null && _cachedSat == _saturation && _cachedCon == _contrast) {
|
||||||
|
return _cachedFilter!;
|
||||||
|
}
|
||||||
|
_cachedSat = _saturation;
|
||||||
|
_cachedCon = _contrast;
|
||||||
|
|
||||||
|
final s = _saturation;
|
||||||
final c = _contrast;
|
final c = _contrast;
|
||||||
final t = (1.0 - c) / 2.0;
|
final t = (1.0 - c) / 2.0;
|
||||||
return ColorFilter.matrix(<double>[
|
final invS = 1.0 - s;
|
||||||
c, 0, 0, 0, t,
|
|
||||||
0, c, 0, 0, t,
|
final sR_R = invS * _rLum + s, sR_G = invS * _gLum, sR_B = invS * _bLum;
|
||||||
0, 0, c, 0, t,
|
final sG_R = invS * _rLum, sG_G = invS * _gLum + s, sG_B = invS * _bLum;
|
||||||
0, 0, 0, 1, 0,
|
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() {
|
Widget _buildImage() {
|
||||||
@@ -284,6 +300,29 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
|||||||
final vpCenterY = _vpSize.height / 2;
|
final vpCenterY = _vpSize.height / 2;
|
||||||
final angleRad = _angle * math.pi / 180;
|
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(
|
return SizedBox.expand(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
@@ -301,20 +340,9 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
|||||||
child: Transform(
|
child: Transform(
|
||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
transform: Matrix4.identity()
|
transform: Matrix4.identity()
|
||||||
..translate(vpCenterX, vpCenterY)
|
|
||||||
..rotateZ(angleRad)
|
|
||||||
..translate(-vpCenterX, -vpCenterY)
|
|
||||||
..translate(_tx, _ty)
|
..translate(_tx, _ty)
|
||||||
..scale(_sc),
|
..scale(_sc),
|
||||||
child: ColorFiltered(
|
child: imageContent,
|
||||||
colorFilter: _buildSaturationFilter(),
|
|
||||||
child: ColorFiltered(
|
|
||||||
colorFilter: _buildContrastFilter(),
|
|
||||||
child: _isFilled
|
|
||||||
? SizedBox.expand(child: _buildImage())
|
|
||||||
: Center(child: _buildImage()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in new issue
Block a user