diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a3048f4..50d1ffc 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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}", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index b3d444e..0acde6c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -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: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 5f63f4b..cf8499d 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -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'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 4a47a62..09578ec 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -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'; diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 138aae6..89c2135 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -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}", diff --git a/lib/screens/viewer_screen.dart b/lib/screens/viewer_screen.dart index ec0dc86..3d353bb 100644 --- a/lib/screens/viewer_screen.dart +++ b/lib/screens/viewer_screen.dart @@ -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 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>( + 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 with WindowListener { ); } + Future> _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 _cropSave() async {