feat: file properties in settings (resolution, size, format, modified, path)
This commit is contained in:
1 parent
805c7eedf2
commit
e6808aaf8e
6 files changed
+209
No files matched your search
@@ -8,6 +8,12 @@
|
||||
"shortcuts": "Shortcuts",
|
||||
"shortcutsList": "• Left/Right arrow — previous/next image\n• Ctrl+O — open file\n• Ctrl+S — save visible area (JPEG 95)\n• Ctrl+R — flip horizontally (clears effects, press again to undo)\n• Ctrl+Q — close window\n• Ctrl+Shift+Q — close all windows\n• F11 — toggle fullscreen\n• Esc — exit fullscreen\n• Scroll — zoom (10% per step)\n• Ctrl+scroll — fine zoom (1% per step)\n• Left-drag — pan\n• Double click — toggle fill/fit\n• Right double click — reset effects\n• Right-drag — contrast\n• Shift+right-drag — rotate\n• Ctrl+right-drag — saturation\n• Ctrl+Shift+right-drag — brightness",
|
||||
"print": "Print",
|
||||
"fileProperties": "File properties",
|
||||
"propResolution": "Resolution",
|
||||
"propFileSize": "File size",
|
||||
"propFormat": "Format",
|
||||
"propModified": "Modified",
|
||||
"propPath": "Path",
|
||||
"close": "Close",
|
||||
"saveCrop": "Save visible area (JPEG 95)",
|
||||
"cropSaved": "Saved: {path}",
|
||||
|
||||
@@ -146,6 +146,42 @@ abstract class AppLocalizations {
|
||||
/// **'Print'**
|
||||
String get print;
|
||||
|
||||
/// No description provided for @fileProperties.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'File properties'**
|
||||
String get fileProperties;
|
||||
|
||||
/// No description provided for @propResolution.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Resolution'**
|
||||
String get propResolution;
|
||||
|
||||
/// No description provided for @propFileSize.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'File size'**
|
||||
String get propFileSize;
|
||||
|
||||
/// No description provided for @propFormat.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Format'**
|
||||
String get propFormat;
|
||||
|
||||
/// No description provided for @propModified.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Modified'**
|
||||
String get propModified;
|
||||
|
||||
/// No description provided for @propPath.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Path'**
|
||||
String get propPath;
|
||||
|
||||
/// No description provided for @close.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -34,6 +34,24 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get print => 'Print';
|
||||
|
||||
@override
|
||||
String get fileProperties => 'File properties';
|
||||
|
||||
@override
|
||||
String get propResolution => 'Resolution';
|
||||
|
||||
@override
|
||||
String get propFileSize => 'File size';
|
||||
|
||||
@override
|
||||
String get propFormat => 'Format';
|
||||
|
||||
@override
|
||||
String get propModified => 'Modified';
|
||||
|
||||
@override
|
||||
String get propPath => 'Path';
|
||||
|
||||
@override
|
||||
String get close => 'Close';
|
||||
|
||||
|
||||
@@ -34,6 +34,24 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
@override
|
||||
String get print => 'Yazdır';
|
||||
|
||||
@override
|
||||
String get fileProperties => 'Dosya özellikleri';
|
||||
|
||||
@override
|
||||
String get propResolution => 'Çözünürlük';
|
||||
|
||||
@override
|
||||
String get propFileSize => 'Dosya boyutu';
|
||||
|
||||
@override
|
||||
String get propFormat => 'Biçim';
|
||||
|
||||
@override
|
||||
String get propModified => 'Değiştirilme';
|
||||
|
||||
@override
|
||||
String get propPath => 'Yol';
|
||||
|
||||
@override
|
||||
String get close => 'Kapat';
|
||||
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
"shortcuts": "Kısayollar",
|
||||
"shortcutsList": "• Sol/Sağ ok — önceki/sonraki resim\n• Ctrl+O — dosya aç\n• Ctrl+S — görünen alanı kaydet (JPEG 95)\n• Ctrl+R — yatay aynala (efektleri sıfırlar, tekrar basınca geri alır)\n• Ctrl+Q — pencereyi kapat\n• Ctrl+Shift+Q — tüm pencereleri kapat\n• F11 — tam ekran / normal geçişi\n• Esc — tam ekrandan çık\n• Kaydırma — yakınlaştır (tık başına %10)\n• Ctrl+kaydırma — ince yakınlaştırma (tık başına %1)\n• Sol tık sürükle — kaydır\n• Çift tık — doldur/sığdır geçişi\n• Sağ çift tık — efektleri sıfırla\n• Sağ tık sürükle — kontrast\n• Shift+sağ tık — döndür\n• Ctrl+sağ tık — doygunluk\n• Ctrl+Shift+sağ tık — parlaklık",
|
||||
"print": "Yazdır",
|
||||
"fileProperties": "Dosya özellikleri",
|
||||
"propResolution": "Çözünürlük",
|
||||
"propFileSize": "Dosya boyutu",
|
||||
"propFormat": "Biçim",
|
||||
"propModified": "Değiştirilme",
|
||||
"propPath": "Yol",
|
||||
"close": "Kapat",
|
||||
"saveCrop": "Görünen alanı kaydet (JPEG 95)",
|
||||
"cropSaved": "Kaydedildi: {path}",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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';
|
||||
@@ -134,6 +136,29 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
|
||||
},
|
||||
),
|
||||
const Divider(color: Colors.white24, height: 24),
|
||||
if (_hasImages) ...[
|
||||
Text(AppLocalizations.of(context)!.fileProperties,
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 13)),
|
||||
const SizedBox(height: 4),
|
||||
FutureBuilder<List<(String, String)>>(
|
||||
future: _fileProperties(ImageManager.instance.currentImagePath),
|
||||
builder: (context, snap) {
|
||||
if (!snap.hasData) return const SizedBox(height: 20);
|
||||
return Column(
|
||||
children: snap.data!.map((r) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: Text.rich(
|
||||
TextSpan(children: [
|
||||
TextSpan(text: '${r.$1}: ', style: const TextStyle(color: Colors.white70, fontSize: 13)),
|
||||
TextSpan(text: r.$2, style: const TextStyle(color: Colors.white, fontSize: 13)),
|
||||
]),
|
||||
),
|
||||
)).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
Text('imajViewer $appVersion',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 15)),
|
||||
const SizedBox(height: 4),
|
||||
@@ -193,6 +218,106 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<(String, String)>> _fileProperties(String path) async {
|
||||
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()}'));
|
||||
try {
|
||||
rows.add((l10n.propFileSize, _fmtSize(file.statSync().size)));
|
||||
} catch (_) {
|
||||
rows.add((l10n.propFileSize, '—'));
|
||||
}
|
||||
rows.add((l10n.propFormat, path.toLowerCase().split('.').last.toUpperCase()));
|
||||
try {
|
||||
rows.add((l10n.propModified, _fmtDate(file.statSync().modified)));
|
||||
} catch (_) {
|
||||
rows.add((l10n.propModified, '—'));
|
||||
}
|
||||
rows.add((l10n.propPath, path));
|
||||
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'];
|
||||
double v = bytes / 1024;
|
||||
var i = 0;
|
||||
while (v >= 1024 && i < units.length - 1) {
|
||||
v /= 1024;
|
||||
i++;
|
||||
}
|
||||
return '${v.toStringAsFixed(v >= 100 ? 0 : 1)} ${units[i]}';
|
||||
}
|
||||
|
||||
String _fmtDate(DateTime t) {
|
||||
String two(int n) => n.toString().padLeft(2, '0');
|
||||
return '${t.year}-${two(t.month)}-${two(t.day)} ${two(t.hour)}:${two(t.minute)}';
|
||||
}
|
||||
|
||||
void _flipHorizontal() => _canvasKey.currentState?.toggleFlipH();
|
||||
|
||||
Future<void> _cropSave() async {
|
||||
|
||||
Reference in new issue
Block a user