import 'dart:async'; import 'dart:io'; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:path/path.dart' as p; 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'; import '../services/hover_color.dart'; import '../widgets/custom_title_bar.dart'; import '../widgets/image_canvas.dart'; import '../widgets/window_resize_zones.dart'; import '../widgets/window_size_selector.dart'; import '../shortcuts/app_shortcuts.dart'; import '../services/window_persistence.dart'; import '../l10n/app_localizations.dart'; const appVersion = '1.9.2'; class ViewerScreen extends StatefulWidget { final List initialFiles; const ViewerScreen({super.key, this.initialFiles = const []}); @override State createState() => _ViewerScreenState(); } class _ViewerScreenState extends State with WindowListener { bool _hasImages = false; bool _isDragging = false; bool _controlsVisible = false; Timer? _saveDebounce; Rect _lastBounds = Rect.zero; final ValueNotifier _zoomPercent = ValueNotifier(100); final GlobalKey _canvasKey = GlobalKey(); @override void initState() { super.initState(); windowManager.addListener(this); ImageManager.instance.addListener(_onImageManagerChanged); if (widget.initialFiles.isNotEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) { FileHandler.loadImagesFromPaths(widget.initialFiles); _updateTitle(); }); } else { WidgetsBinding.instance.addPostFrameCallback((_) => _updateTitle()); } } @override void dispose() { windowManager.removeListener(this); ImageManager.instance.removeListener(_onImageManagerChanged); _saveDebounce?.cancel(); _zoomPercent.dispose(); super.dispose(); } /// Universal event handler @override void onWindowEvent(String eventName) { switch (eventName) { case 'close': // Senkron kayıt — async çağrı kapanışta tamamlanmayabilir if (_lastBounds != Rect.zero) { WindowPersistence.saveSync( position: _lastBounds.topLeft, size: _lastBounds.size, ); } break; case 'resize': case 'move': // Son bilinen konumu hemen güncelle (kapanış senkron kaydı için) windowManager.getBounds().then((b) => _lastBounds = b).catchError((_) => _lastBounds); _saveDebounce?.cancel(); _saveDebounce = Timer(const Duration(milliseconds: 300), () { _doSave(); }); break; } } void _onImageManagerChanged() { setState(() => _hasImages = ImageManager.instance.images.isNotEmpty); _updateTitle(); } /// Taskbar/alt-tab başlığı = açılan dosyanın adı (görsel yoksa uygulama adı). Future _updateTitle() async { final image = ImageManager.instance.currentImagePath; final name = image.isEmpty ? 'imajViewer' : p.basename(image); try { await windowManager.setTitle(name); } catch (_) { // Başlık güncellenemezse sessizce geç. } } Future _doSave() async { try { final pos = await windowManager.getPosition(); final size = await windowManager.getSize(); _lastBounds = Rect.fromLTWH(pos.dx, pos.dy, size.width, size.height); WindowPersistence.saveSync(position: pos, size: size); } catch (_) { // Silently ignore save errors } } static const double _shortcutKeyColumnWidth = 126; int _settingsTab = 0; void _showSettings() { showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setDialogState) { final l10n = AppLocalizations.of(context)!; return Dialog( insetPadding: EdgeInsets.zero, backgroundColor: Colors.transparent, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), child: LayoutBuilder( builder: (context, c) => Container( width: math.min(c.maxWidth, 520), height: math.min(c.maxHeight, 560), padding: const EdgeInsets.fromLTRB(14, 4, 14, 10), decoration: BoxDecoration( color: const Color(0xFF2a2a2a), border: Border.all(color: Colors.white12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text(l10n.settingsTitle, style: const TextStyle(color: Colors.white, fontSize: 18)), const Spacer(), IconButton( tooltip: l10n.print, icon: const Icon(Icons.print, color: Colors.white70, size: 18), onPressed: () { Navigator.pop(context); _printImage(); }, ), IconButton( tooltip: l10n.close, icon: const Icon(Icons.close, color: Colors.white70, size: 18), onPressed: () => Navigator.pop(context), ), ], ), const SizedBox(height: 4), Align( alignment: Alignment.center, child: SegmentedButton( expandedInsets: EdgeInsets.zero, segments: [ ButtonSegment( value: 0, label: Text(l10n.tabGeneral, style: const TextStyle(fontSize: 13)), ), ButtonSegment( value: 1, label: Text(l10n.sizeSection, style: const TextStyle(fontSize: 13)), ), ButtonSegment( value: 2, label: Text(l10n.shortcuts, style: const TextStyle(fontSize: 13)), ), ], selected: {_settingsTab}, showSelectedIcon: false, style: _segmentedStyle, onSelectionChanged: (selection) => setDialogState(() => _settingsTab = selection.first), ), ), const SizedBox(height: 10), Expanded( child: _settingsTabBody(l10n, setDialogState), ), const Divider(color: Colors.white10, height: 12), Row( children: [ Text('imajViewer $appVersion', style: const TextStyle(color: Colors.white54, fontSize: 11)), const SizedBox(width: 16), Expanded( child: Text(l10n.appDescription, maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.right, style: const TextStyle(color: Colors.white54, fontSize: 11)), ), ], ), ], ), ), ), ); }, ), ); } Widget _settingsTabBody(AppLocalizations l10n, StateSetter setDialogState) { switch (_settingsTab) { case 1: return WindowSizeSelector(workArea: _workArea, onPick: _applyWindowSize); case 2: return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ for (final (key, action) in _shortcutRows(l10n)) Padding( padding: const EdgeInsets.only(bottom: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: _shortcutKeyColumnWidth, child: Text(key, style: const TextStyle(fontSize: 12, color: Colors.white)), ), const SizedBox(width: 8), Expanded( child: Text(action, style: const TextStyle(color: Colors.white70, fontSize: 13)), ), ], ), ), ], ), ); default: return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (_hasImages) ...[ _settingsCard( Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _sectionTitle(l10n.fileProperties), const SizedBox(height: 4), FutureBuilder>( future: _fileProperties(ImageManager.instance.currentImagePath), builder: (context, snap) { if (!snap.hasData) return const SizedBox(height: 20); return Column( crossAxisAlignment: CrossAxisAlignment.start, 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), ], _settingsCard( Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _sectionTitle(l10n.sortBy), const SizedBox(height: 4), SegmentedButton( segments: [ ButtonSegment( value: false, label: Text(l10n.sortByTime, style: const TextStyle(fontSize: 13)), ), ButtonSegment( value: true, label: Text(l10n.sortByName, style: const TextStyle(fontSize: 13)), ), ], selected: {ImageManager.instance.sortByName}, showSelectedIcon: false, style: _segmentedStyle, onSelectionChanged: (selection) { ImageManager.instance.setSortByName(selection.first); setDialogState(() {}); }, ), ], ), ), const SizedBox(height: 12), _settingsCard( Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _sectionTitle(l10n.hoverColor), const SizedBox(height: 8), Wrap( spacing: 10, runSpacing: 10, children: [ for (final color in HoverColor.palette) GestureDetector( onTap: () { HoverColor.set(color); setDialogState(() {}); }, child: MouseRegion( cursor: SystemMouseCursors.click, child: Container( width: 24, height: 24, decoration: BoxDecoration( color: color, shape: BoxShape.circle, border: Border.all( color: HoverColor.notifier.value.toARGB32() == color.toARGB32() ? Colors.white : Colors.white24, width: 2, ), ), ), ), ), ], ), ], ), ), const SizedBox(height: 12), _settingsCard( Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _sectionTitle(l10n.language), const SizedBox(height: 6), SegmentedButton( segments: const [ ButtonSegment( value: 'en', label: Text('English', style: TextStyle(fontSize: 13))), ButtonSegment( value: 'tr', label: Text('Türkçe', style: TextStyle(fontSize: 13))), ButtonSegment( value: 'fr', label: Text('Français', style: TextStyle(fontSize: 13))), ], selected: const {'tr'}, showSelectedIcon: false, ), ], ), ), ], ), ); } } Widget _sectionTitle(String text) { return Text(text, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600)); } ButtonStyle get _segmentedStyle => ButtonStyle( backgroundColor: WidgetStateProperty.resolveWith( (states) => states.contains(WidgetState.selected) ? Colors.blueAccent.withValues(alpha: 0.25) : Colors.white10, ), foregroundColor: const WidgetStatePropertyAll(Colors.white), side: const WidgetStatePropertyAll(BorderSide(color: Colors.white24)), ); Widget _settingsCard(Widget child) { return Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(8), ), child: child, ); } List<(String, String)> _shortcutRows(AppLocalizations l10n) { return [ (l10n.scNavKey, l10n.scNavAction), (l10n.scOpenKey, l10n.scOpenAction), (l10n.scSaveKey, l10n.scSaveAction), (l10n.scFlipKey, l10n.scFlipAction), (l10n.scCloseKey, l10n.scCloseAction), (l10n.scCloseAllKey, l10n.scCloseAllAction), (l10n.scFullscreenKey, l10n.scFullscreenAction), (l10n.scExitFullscreenKey, l10n.scExitFullscreenAction), (l10n.scZoomKey, l10n.scZoomAction), (l10n.scFineZoomKey, l10n.scFineZoomAction), (l10n.scPanKey, l10n.scPanAction), (l10n.scFillKey, l10n.scFillAction), (l10n.scResetKey, l10n.scResetAction), (l10n.scContrastKey, l10n.scContrastAction), (l10n.scRotateKey, l10n.scRotateAction), (l10n.scSaturationKey, l10n.scSaturationAction), (l10n.scBrightnessKey, l10n.scBrightnessAction), ]; } Future _printImage() async { final path = ImageManager.instance.currentImagePath; if (path.isEmpty) return; final bytes = await File(path).readAsBytes(); final image = pw.MemoryImage(bytes); await Printing.layoutPdf( onLayout: (format) async { final doc = pw.Document(); doc.addPage( pw.Page( pageFormat: PdfPageFormat.a4, build: (context) => pw.Center( child: pw.Image(image, fit: pw.BoxFit.contain), ), ), ); return doc.save(); }, ); } Future> _fileProperties(String path) async { final l10n = AppLocalizations.of(context)!; final file = File(path); final rows = <(String, String)>[]; 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 (_) { 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; } 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 _workArea() async { final pos = await windowManager.getPosition(); return WindowPersistence.workAreaSize(windowPosition: pos); } Future _applyWindowSize(int rows, int cols, Size work) async { try { await windowManager.setSize(Size(work.width / cols, work.height / rows)); } catch (_) { // Boyutlandırma başarısız olursa sessizce geç (buton saf eylem). } } Future _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 vp = state.cropViewParams(source.width, source.height); if (vp == null) { message = l10n.cropFailed; } else { final outPath = await CropSaveService.saveCrop( srcPath: path, source: source, vp: vp, 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, onFlipH: _hasImages ? _flipHorizontal : null, child: Scaffold( backgroundColor: Colors.transparent, body: WindowResizeZones( child: DropTarget( onDragEntered: (_) => setState(() => _isDragging = true), onDragExited: (_) => setState(() => _isDragging = false), onDragDone: (details) { setState(() => _isDragging = false); FileHandler.loadImagesFromPaths( details.files.map((f) => f.path).toList()); }, child: MouseRegion( onEnter: (_) => setState(() => _controlsVisible = true), onExit: (_) => setState(() => _controlsVisible = false), child: Stack( children: [ Positioned( top: 0, left: 0, right: 0, bottom: 0, child: Container(color: const Color(0xFF1a1a1a)), ), Positioned( top: 0, left: 0, right: 0, bottom: 0, child: Container( color: Colors.transparent, child: _hasImages ? ImageCanvas( key: _canvasKey, filePath: ImageManager.instance.currentImagePath, zoomPercent: _zoomPercent, ) : Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.image, size: 64, color: Colors.white24), const SizedBox(height: 16), Text(AppLocalizations.of(context)!.dragDropHint, key: const Key('dragDropHint'), textAlign: TextAlign.center, style: const TextStyle(color: Colors.white38, fontSize: 16)), ], ), ), ), ), CustomTitleBar( onSettings: _showSettings, onSaveCrop: _hasImages ? _cropSave : null, visible: _controlsVisible, ), if (_hasImages && _controlsVisible) ...[ Positioned( left: 8, bottom: 8, child: _NavButton( icon: Icons.chevron_left, onTap: () => ImageManager.instance.previousImage(), ), ), Positioned( right: 8, bottom: 8, child: _NavButton( icon: Icons.chevron_right, onTap: () => ImageManager.instance.nextImage(), ), ), Positioned( left: 0, right: 0, bottom: 12, child: IgnorePointer( child: Center( child: ValueListenableBuilder( 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( child: Container( color: Colors.blue.withValues(alpha: 0.1), child: Center( child: Container( padding: const EdgeInsets.all(32), decoration: BoxDecoration( color: Colors.black87, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.blueAccent, width: 2), ), child: Text(AppLocalizations.of(context)!.dropHere, style: const TextStyle(color: Colors.white, fontSize: 24)), ), ), ), ), ], ), ), ), ), ), ); } } class _NavButton extends StatefulWidget { final IconData icon; final VoidCallback onTap; const _NavButton({required this.icon, required this.onTap}); @override State<_NavButton> createState() => _NavButtonState(); } class _NavButtonState extends State<_NavButton> { bool _hovered = false; @override Widget build(BuildContext context) { return GestureDetector( onTap: widget.onTap, child: MouseRegion( cursor: SystemMouseCursors.click, onEnter: (_) => setState(() => _hovered = true), onExit: (_) => setState(() => _hovered = false), child: ValueListenableBuilder( valueListenable: HoverColor.notifier, builder: (context, hover, _) => Container( width: 36, height: 36, decoration: BoxDecoration( color: _hovered ? hover.withValues(alpha: 0.8) : Colors.black.withValues(alpha: 0.35), shape: BoxShape.circle, ), child: Icon(widget.icon, color: Colors.white, size: 22), ), ), ), ); } }