feat: crop save (save visible area at original resolution) v1.4.0
- 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:
1 parent
73c1ff51d6
commit
168e087c8b
16 files changed
+399
-14
No files matched your search
@@ -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) ...[
|
||||
|
||||
Reference in new issue
Block a user