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

+48
View File
@@ -8,6 +8,7 @@ import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart';
import '../services/image_manager.dart';
import '../services/file_handler.dart';
import '../services/crop_save_service.dart';
import '../widgets/custom_title_bar.dart';
import '../widgets/image_canvas.dart';
import '../widgets/window_resize_zones.dart';
@@ -28,6 +29,7 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
bool _controlsVisible = false;
Timer? _saveDebounce;
Rect _lastBounds = Rect.zero;
final GlobalKey<ImageCanvasState> _canvasKey = GlobalKey<ImageCanvasState>();
@override
void initState() {
@@ -188,9 +190,53 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
);
}
Future<void> _cropSave() async {
final path = ImageManager.instance.currentImagePath;
final state = _canvasKey.currentState;
if (path.isEmpty || state == null) return;
final l10n = AppLocalizations.of(context)!;
String? message;
try {
final source = await CropSaveService.load(path);
final rect = state.cropSourceRect(source.width, source.height);
if (rect == null || rect.isEmpty || !rect.isFinite) {
message = l10n.cropFailed;
} else {
final outPath = await CropSaveService.saveCrop(
srcPath: path,
source: source,
rect: rect,
matrix: state.activeColorMatrix,
);
message = l10n.cropSaved(outPath);
}
} on CropSaveError catch (e) {
switch (e.code) {
case 'file_exists':
message = l10n.cropFileExists;
case 'decode':
message = l10n.cropUnsupported;
default:
message = l10n.cropFailed;
}
} catch (_) {
message = l10n.cropFailed;
}
if (!mounted) return;
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(SnackBar(
content: Text(message, style: const TextStyle(fontSize: 13)),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
width: 520,
));
}
@override
Widget build(BuildContext context) {
return AppShortcuts(
onCropSave: _hasImages ? _cropSave : null,
child: Scaffold(
backgroundColor: Colors.transparent,
body: WindowResizeZones(
@@ -223,6 +269,7 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
color: Colors.transparent,
child: _hasImages
? ImageCanvas(
key: _canvasKey,
filePath: ImageManager.instance.currentImagePath,
)
: Center(
@@ -242,6 +289,7 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
),
CustomTitleBar(
onSettings: _showSettings,
onSaveCrop: _hasImages ? _cropSave : null,
visible: _controlsVisible,
),
if (_hasImages && _controlsVisible) ...[