feat: zoom percentage badge at bottom center (v1.7.1)
Build & Release / build-linux (push) Canceled after 0s
Build & Release / build-windows (push) Canceled after 0s
Build & Release / create-release (push) Canceled after 0s

- Percentage shown between the prev/next buttons, together with the hover
  controls, on a translucent background.
- Value is the on-screen scale relative to the image's real pixel size
  (100% = 1:1); updated on wheel zoom, fill/fit toggle, image change and
  window resize.
- Pixel-size header parsing moved to lib/services/image_header.dart, shared
  with the settings file-properties panel.
- dist: 1.7.1 .deb replaces the tracked 1.7.0 package.
This commit is contained in:
Alhan committed 2026-10-25 18:53:59 +03:00
1 parent abbde04e50
commit 32fb12c210
7 files changed
+185 -71

No files matched your search

+37 -67
View File
@@ -1,13 +1,12 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:image/image.dart' as img;
import 'package:window_manager/window_manager.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart';
import '../services/image_header.dart';
import '../services/image_manager.dart';
import '../services/file_handler.dart';
import '../services/crop_save_service.dart';
@@ -18,7 +17,7 @@ import '../shortcuts/app_shortcuts.dart';
import '../services/window_persistence.dart';
import '../l10n/app_localizations.dart';
const appVersion = '1.7.0';
const appVersion = '1.7.1';
class ViewerScreen extends StatefulWidget {
final List<String> initialFiles;
@@ -33,6 +32,7 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
bool _controlsVisible = false;
Timer? _saveDebounce;
Rect _lastBounds = Rect.zero;
final ValueNotifier<double> _zoomPercent = ValueNotifier<double>(100);
final GlobalKey<ImageCanvasState> _canvasKey = GlobalKey<ImageCanvasState>();
@override
@@ -53,6 +53,7 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
windowManager.removeListener(this);
ImageManager.instance.removeListener(_onImageManagerChanged);
_saveDebounce?.cancel();
_zoomPercent.dispose();
super.dispose();
}
@@ -222,16 +223,8 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
final l10n = AppLocalizations.of(context)!;
final file = File(path);
final rows = <(String, String)>[];
Size? size;
try {
final stat = file.statSync();
const maxHead = 512 * 1024;
final raf = file.openSync();
final header = raf.readSync(stat.size < maxHead ? stat.size : maxHead);
raf.closeSync();
size = _headerPixelSize(header);
} catch (_) {}
rows.add((l10n.propResolution, size == null ? '—' : '${size!.width.toInt()} × ${size!.height.toInt()}'));
final Size? size = pixelSizeOfFile(path);
rows.add((l10n.propResolution, size == null ? '—' : '${size.width.toInt()} × ${size.height.toInt()}'));
try {
rows.add((l10n.propFileSize, _fmtSize(file.statSync().size)));
} catch (_) {
@@ -247,60 +240,6 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
return rows;
}
/// İlk 512 KB'dan çözünürlük (piksel decode yok).
/// PNG/GIF/BMP/WebP: boyut ilk 33 baytta → elle parse.
/// JPEG: SOF büyük APP/EXIF segmentlerinin ardından gelebilir → package:image startDecode.
Size? _headerPixelSize(Uint8List b) {
try {
if (b.length >= 24 && b[0] == 0x89 && b[1] == 0x50) {
// PNG
final w = (b[16] << 24) | (b[17] << 16) | (b[18] << 8) | b[19];
final h = (b[20] << 24) | (b[21] << 16) | (b[22] << 8) | b[23];
return Size(w.toDouble(), h.toDouble());
}
if (b.length >= 10 && b[0] == 0x47 && b[1] == 0x49 && b[2] == 0x46) {
// GIF (WBMP da GIF tabanlı olabilir)
final w = b[6] | (b[7] << 8);
final h = b[8] | (b[9] << 8);
return Size(w.toDouble(), h.toDouble());
}
if (b.length >= 26 && b[0] == 0x42 && b[1] == 0x4D) {
// BMP
final w = b[18] | (b[19] << 8) | (b[20] << 16) | (b[21] << 24);
final h = b[22] | (b[23] << 8) | (b[24] << 16) | (b[25] << 24);
return Size(w.toDouble(), h.abs().toDouble());
}
if (b.length >= 30 && b[0] == 0x52 && b[1] == 0x49 && b[2] == 0x46 && b[3] == 0x46 &&
b[8] == 0x57 && b[9] == 0x45 && b[10] == 0x42 && b[11] == 0x50) {
// WebP
if (b[12] == 0x56 && b[13] == 0x50 && b[15] == 0x58) {
// VP8X
final w = b[24] | (b[25] << 8) | (b[26] << 16);
final h = b[27] | (b[28] << 8) | (b[29] << 16);
return Size((w + 1).toDouble(), (h + 1).toDouble());
}
if (b.length >= 25 && b[12] == 0x56 && b[13] == 0x50 && b[15] == 0x4C && b[20] == 0x2F) {
// VP8L
final w = (b[21] & 0x3F) | ((b[22] & 0x0F) << 6);
final h = (b[23] & 0x3F) | ((b[24] & 0x0F) << 6);
return Size((w + 1).toDouble(), (h + 1).toDouble());
}
if (b[12] == 0x56 && b[13] == 0x50 && b[15] == 0x20) {
// VP8 (lossy)
final w = (b[26] | (b[27] << 8)) & 0x3FFF;
final h = (b[28] | (b[29] << 8)) & 0x3FFF;
return Size(w.toDouble(), h.toDouble());
}
}
} catch (_) {}
try {
// JPEG
final info = img.JpegDecoder().startDecode(b);
if (info != null) return Size(info.width.toDouble(), info.height.toDouble());
} catch (_) {}
return null;
}
String _fmtSize(int bytes) {
if (bytes < 1024) return '$bytes B';
const units = ['KB', 'MB', 'GB', 'TB'];
@@ -402,6 +341,7 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
? ImageCanvas(
key: _canvasKey,
filePath: ImageManager.instance.currentImagePath,
zoomPercent: _zoomPercent,
)
: Center(
child: Column(
@@ -440,6 +380,36 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
onTap: () => ImageManager.instance.nextImage(),
),
),
Positioned(
left: 0,
right: 0,
bottom: 12,
child: IgnorePointer(
child: Center(
child: ValueListenableBuilder<double>(
valueListenable: _zoomPercent,
builder: (context, zoom, _) {
final pct = zoom.round();
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.35),
borderRadius: BorderRadius.circular(13),
),
child: Text(
pct < 1 ? '<1%' : '$pct%',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
);
},
),
),
),
),
],
if (_isDragging)
Positioned.fill(