Files
imajviewer/lib/widgets/image_canvas.dart
Alhan 35d3bc3749
Some checks failed
Build Windows / build-windows (push) Has been cancelled
feat: right-click contrast, Ctrl+saturation, Shift+rotate filters
- Add ColorFilter.matrix (row-major) for saturation and contrast
- Saturation: 0=grayscale, 1=normal, 2=oversaturated (luminance-weighted)
- Contrast: standard contrast matrix (pivot at 0.5)
- Keyboard modifier tracking via HardwareKeyboard.addHandler
- Right-click + vertical drag: contrast (0-2)
- Ctrl + right-click + vertical drag: saturation (0-2)
- Shift + right-click + vertical drag: rotate
- Right double-click: reset all values
- Update install.sh to check debug/ bundle before release/
- Update .deb package with new release build
2026-07-27 19:39:54 +03:00

342 lines
9.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:io';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
class ImageCanvas extends StatefulWidget {
final String filePath;
const ImageCanvas({super.key, required this.filePath});
@override
State<ImageCanvas> createState() => _ImageCanvasState();
}
class _ImageCanvasState extends State<ImageCanvas> {
bool _isFilled = true;
final GlobalKey _imageKey = GlobalKey();
double _tx = 0, _ty = 0, _sc = 1.0;
Size _vpSize = Size.zero;
// ── Görüntü ayarları ──
double _angle = 0;
double _contrast = 1.0;
double _saturation = 1.0;
// ── Gesture tracking ──
String _interactionMode = 'none'; // 'contrast' | 'rotate' | 'saturation'
Offset _pointerDownPos = Offset.zero;
double _startAngle = 0;
double _startContrast = 1.0;
double _startSaturation = 1.0;
DateTime? _lastRightTapTime;
Offset _lastRightTapPos = Offset.zero;
// ── Klavye modifier tracking ──
bool _ctrlPressed = false;
bool _shiftPressed = false;
void _clamp() {
if (_vpSize.width <= 0 || _vpSize.height <= 0) return;
if (_isFilled) {
final double w = _vpSize.width * _sc;
final double h = _vpSize.height * _sc;
if (w >= _vpSize.width) {
if (_tx > 0) _tx = 0;
else if (_tx + w < _vpSize.width) _tx = _vpSize.width - w;
} else { if (_tx != 0) _tx = 0; }
if (h >= _vpSize.height) {
if (_ty > 0) _ty = 0;
else if (_ty + h < _vpSize.height) _ty = _vpSize.height - h;
} else { if (_ty != 0) _ty = 0; }
return;
}
double m = 0, n = 0;
double w = _vpSize.width * _sc;
double h = _vpSize.height * _sc;
final box = _imageKey.currentContext?.findRenderObject() as RenderBox?;
if (box != null && box.hasSize && box.size.width > 0) {
final Size img = box.size;
m = (_vpSize.width - img.width) / 2;
n = (_vpSize.height - img.height) / 2;
w = img.width * _sc;
h = img.height * _sc;
}
if (w >= _vpSize.width) {
final double lo = -m * _sc;
final double hi = _vpSize.width - m * _sc - w;
if (lo <= hi) {
if (_tx < lo) _tx = lo;
if (_tx > hi) _tx = hi;
} else {
if (_tx < hi) _tx = hi;
if (_tx > lo) _tx = lo;
}
} else {
final double lo = -m * _sc;
final double hi = _vpSize.width - m * _sc - w;
if (_tx < lo) _tx = lo;
if (_tx > hi) _tx = hi;
}
if (h >= _vpSize.height) {
final double lo = -n * _sc;
final double hi = _vpSize.height - n * _sc - h;
if (lo <= hi) {
if (_ty < lo) _ty = lo;
if (_ty > hi) _ty = hi;
} else {
if (_ty < hi) _ty = hi;
if (_ty > lo) _ty = lo;
}
} else {
final double lo = -n * _sc;
final double hi = _vpSize.height - n * _sc - h;
if (_ty < lo) _ty = lo;
if (_ty > hi) _ty = hi;
}
}
@override
void initState() {
super.initState();
HardwareKeyboard.instance.addHandler(_onHardwareKey);
}
@override
void dispose() {
HardwareKeyboard.instance.removeHandler(_onHardwareKey);
super.dispose();
}
bool _onHardwareKey(KeyEvent event) {
final key = event.logicalKey;
if (event is KeyDownEvent) {
if (key == LogicalKeyboardKey.controlLeft ||
key == LogicalKeyboardKey.controlRight) {
_ctrlPressed = true;
} else if (key == LogicalKeyboardKey.shiftLeft ||
key == LogicalKeyboardKey.shiftRight) {
_shiftPressed = true;
}
} else if (event is KeyUpEvent) {
if (key == LogicalKeyboardKey.controlLeft ||
key == LogicalKeyboardKey.controlRight) {
_ctrlPressed = false;
} else if (key == LogicalKeyboardKey.shiftLeft ||
key == LogicalKeyboardKey.shiftRight) {
_shiftPressed = false;
}
}
return false;
}
@override
void didUpdateWidget(ImageCanvas old) {
super.didUpdateWidget(old);
if (old.filePath != widget.filePath) _resetView();
}
void _resetView() {
setState(() {
_tx = 0; _ty = 0; _sc = 1.0; _isFilled = true; _vpSize = Size.zero;
_angle = 0; _contrast = 1.0; _saturation = 1.0;
});
}
void _toggleFillFit() {
setState(() {
_isFilled = !_isFilled; _tx = 0; _ty = 0; _sc = 1.0; _vpSize = Size.zero;
_angle = 0; _contrast = 1.0; _saturation = 1.0;
});
}
void _handleDoubleTap() => _toggleFillFit();
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 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;
_sc = newSc;
_clamp();
setState(() {});
}
void _onPanUpdate(DragUpdateDetails d) {
_tx += d.delta.dx;
_ty += d.delta.dy;
_clamp();
setState(() {});
}
void _onPointerDown(PointerDownEvent e) {
if (e.buttons != kSecondaryMouseButton) return;
_pointerDownPos = e.localPosition;
final now = DateTime.now();
if (_lastRightTapTime != null &&
now.difference(_lastRightTapTime!) < const Duration(milliseconds: 400) &&
(e.localPosition - _lastRightTapPos).distance < 20) {
setState(() {
_angle = 0;
_contrast = 1.0;
_saturation = 1.0;
_interactionMode = 'none';
});
_lastRightTapTime = null;
return;
}
_lastRightTapTime = now;
_lastRightTapPos = e.localPosition;
if (_ctrlPressed) {
_interactionMode = 'saturation';
_startSaturation = _saturation;
} else if (_shiftPressed) {
_interactionMode = 'rotate';
_startAngle = _angle;
} else {
_interactionMode = 'contrast';
_startContrast = _contrast;
}
}
void _onPointerMove(PointerMoveEvent e) {
if (e.buttons != kSecondaryMouseButton) return;
final dy = e.localPosition.dy - _pointerDownPos.dy;
switch (_interactionMode) {
case 'contrast':
setState(() {
_contrast = (_startContrast + dy * 0.005).clamp(0.0, 2.0);
});
break;
case 'rotate':
setState(() {
_angle = _startAngle + dy * 0.9;
});
break;
case 'saturation':
setState(() {
_saturation = (_startSaturation + dy * 0.005).clamp(0.0, 2.0);
});
break;
}
}
void _onPointerUp(PointerUpEvent e) {
_interactionMode = 'none';
}
// ── Color filters ──
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 _buildContrastFilter() {
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,
]);
}
Widget _buildImage() {
return Image.file(
key: _imageKey,
File(widget.filePath),
fit: _isFilled ? BoxFit.cover : BoxFit.contain,
filterQuality: FilterQuality.high,
errorBuilder: (_, _, _) => _errorWidget(),
);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
_vpSize = constraints.biggest;
_clamp();
final vpCenterX = _vpSize.width / 2;
final vpCenterY = _vpSize.height / 2;
final angleRad = _angle * math.pi / 180;
return SizedBox.expand(
child: Stack(
children: [
ClipRect(
child: Listener(
onPointerDown: _onPointerDown,
onPointerMove: _onPointerMove,
onPointerUp: _onPointerUp,
onPointerSignal: (event) {
if (event is PointerScrollEvent) _handleScroll(event);
},
child: GestureDetector(
onDoubleTap: _handleDoubleTap,
onPanUpdate: _onPanUpdate,
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()),
),
),
),
),
),
),
],
),
);
},
);
}
Widget _errorWidget() {
return const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.broken_image, size: 48, color: Colors.white38),
SizedBox(height: 8),
Text('Resim yüklenemedi', style: TextStyle(color: Colors.white38)),
],
),
);
}
}