feat: crop save (save visible area at original resolution) v1.4.0
Build & Release / build-linux (push) Has been cancelled
Build & Release / build-windows (push) Has been cancelled
Build & Release / create-release (push) Has been cancelled

- Save visible area at original resolution as JPEG q90 (title bar button or Ctrl+S)
- Output name: original name + _HHmmss suffix; no overwrite, warning shown
- Active color effects baked in; rotation saves bounding box
- New: crop_save_service, l10n strings TR/EN, docs/plan-crop-save.md
This commit is contained in:
Alhan committed 2026-10-11 21:36:20 +03:00
1 parent 73c1ff51d6
commit 168e087c8b
16 files changed
+399 -14

No files matched your search

+7 -1
View File
@@ -3,9 +3,10 @@ import 'package:window_manager/window_manager.dart';
class CustomTitleBar extends StatelessWidget {
final VoidCallback? onSettings;
final VoidCallback? onSaveCrop;
final bool visible;
const CustomTitleBar({super.key, this.onSettings, this.visible = true});
const CustomTitleBar({super.key, this.onSettings, this.onSaveCrop, this.visible = true});
@override
Widget build(BuildContext context) {
@@ -43,6 +44,11 @@ class CustomTitleBar extends StatelessWidget {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (onSaveCrop != null)
_BarButton(
icon: Icons.save_outlined,
onTap: onSaveCrop!,
),
_BarButton(
icon: Icons.horizontal_rule,
onTap: () => windowManager.minimize(),
+54 -4
View File
@@ -10,10 +10,10 @@ class ImageCanvas extends StatefulWidget {
final String filePath;
const ImageCanvas({super.key, required this.filePath});
@override
State<ImageCanvas> createState() => _ImageCanvasState();
State<ImageCanvas> createState() => ImageCanvasState();
}
class _ImageCanvasState extends State<ImageCanvas> {
class ImageCanvasState extends State<ImageCanvas> {
bool _isFilled = true;
final GlobalKey _imageKey = GlobalKey();
double _tx = 0, _ty = 0, _sc = 1.0;
@@ -314,6 +314,7 @@ class _ImageCanvasState extends State<ImageCanvas> {
static const double _bLum = 0.0722;
ColorFilter? _cachedFilter;
List<double>? _cachedMatrix;
double _cachedSat = -1;
double _cachedCon = -1;
double _cachedBri = double.nan;
@@ -380,11 +381,60 @@ class _ImageCanvasState extends State<ImageCanvas> {
];
// M_final = M_contrast * M_brightness * M_saturation
_cachedFilter = ColorFilter.matrix(
_multiply4x5(_multiply4x5(contrastMatrix, brightnessMatrix), saturationMatrix));
final matrix =
_multiply4x5(_multiply4x5(contrastMatrix, brightnessMatrix), saturationMatrix);
_cachedMatrix = matrix;
_cachedFilter = ColorFilter.matrix(matrix);
return _cachedFilter!;
}
/// Aktif renk matrisi (4x5 row-major); identity ise null.
/// CropSaveService'e aynen taşınır (aynı alan/semantik: 0-255).
List<double>? get activeColorMatrix {
if (_saturation == 1.0 && _contrast == 1.0 && _brightness == 0.0) return null;
_buildCombinedFilter();
return _cachedMatrix;
}
/// Viewport'un görünen bölgesini kaynak (tam çözünürlük) uzayına eşler.
/// [w]x[h] = gerçek dosya boyutu. Degrade durumda null.
Rect? cropSourceRect(int w, int h) {
if (_vpSize.width <= 0 || _vpSize.height <= 0 || w <= 0 || h <= 0) return null;
final cx = _vpSize.width / 2, cy = _vpSize.height / 2;
final rad = -_angle * math.pi / 180;
final cos = math.cos(rad), sin = math.sin(rad);
Offset toSrc(Offset p) {
final double qx = (p.dx - _tx) / _sc, qy = (p.dy - _ty) / _sc;
final double rx = (_angle == 0) ? qx : (cos * (qx - cx) - sin * (qy - cy) + cx);
final double ry = (_angle == 0) ? qy : (sin * (qx - cx) + cos * (qy - cy) + cy);
final double k = _isFilled
? math.max(_vpSize.width / w, _vpSize.height / h)
: math.min(_vpSize.width / w, _vpSize.height / h);
final double ox = (_vpSize.width - w * k) / 2, oy = (_vpSize.height - h * k) / 2;
return Offset((rx - ox) / k, (ry - oy) / k);
}
final corners = [
toSrc(Offset.zero),
toSrc(Offset(_vpSize.width, 0)),
toSrc(Offset(_vpSize.width, _vpSize.height)),
toSrc(Offset(0, _vpSize.height)),
];
double xmin = double.infinity, xmax = -double.infinity;
double ymin = double.infinity, ymax = -double.infinity;
for (final c in corners) {
xmin = math.min(xmin, c.dx); xmax = math.max(xmax, c.dx);
ymin = math.min(ymin, c.dy); ymax = math.max(ymax, c.dy);
}
return Rect.fromLTRB(
xmin.clamp(0.0, w.toDouble()),
ymin.clamp(0.0, h.toDouble()),
xmax.clamp(0.0, w.toDouble()),
ymax.clamp(0.0, h.toDouble()),
);
}
FileImage? _cachedFileImage;
ImageProvider<Object>? _cachedProvider;
String? _cachedPath;