// "Everything" (voidtools) dosya arama penceresi (alt engine tarafı). // // Ayrı bir engine/isolate'ta çalışır (desktop_multi_window). Ana pencereyle // konuşması yalnızca pencere yaşam döngüsü içindir (ready/ping) — arama // TAMAMEN burada olur: Everything'in yerel HTTP sunucusuna // (`http://localhost:/?search=...&json=1`) doğrudan istek atılır. // Port, `session_settings.json`dan okunur/yazılır (SessionService — salt // dosya I/O, ana pencereye ihtiyaç duymaz). // // Gereksinim: Everything (voidtools) açık olmalı ve Tools ▸ Options ▸ // HTTP Server ▸ Enable HTTP server işaretli olmalı. import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show Clipboard, ClipboardData; import 'package:http/http.dart' as http; import '../services/everything_search_service.dart'; import '../services/everything_settings_service.dart'; import '../services/session_service.dart'; import '../theme/app_theme.dart'; import '../widgets/bouncy_mouse_scroll.dart'; import '../widgets/menu_bar_entry.dart'; import '../widgets/mini_pill.dart'; import '../widgets/mini_toolbar.dart'; // --------------------------------------------------------------------------- // Alt pencere ↔ ana pencere bağlantısı (yalnız yaşam döngüsü) // --------------------------------------------------------------------------- class EverythingChildLink { static const _mainChannel = WindowMethodChannel( EverythingSearchWindowService.mainChannelName, mode: ChannelMode.unidirectional, ); /// Ana pencerenin (CatiaProvider) itilen açık-döküman anlık görüntüsü /// ('catiaOpenPaths') geldiğinde çağrılır — bkz. _EverythingSearchScreenState. /// Normalize edilmiş (küçük harf) tam yol → o dosyanın açık olduğu CATIA /// kartının başlığı. static void Function(Map)? onOpenPathsUpdate; /// Son alınan anlık görüntü — widget henüz mount OLMADAN ('initState' /// [onOpenPathsUpdate]'i kaydetmeden ÖNCE) bir push gelirse kaybolmasın /// diye tamponlanır; `initState` bunu okuyup başlangıç değeri yapar. static Map? lastOpenPaths; static Future init(WindowController controller) async { try { await controller .setWindowMethodHandler((call) async { if (call.method == 'ping') return true; if (call.method == 'catiaOpenPaths') { try { final raw = call.arguments as Map; final parsed = raw.map((k, v) => MapEntry(k.toString(), v.toString())); lastOpenPaths = parsed; onOpenPathsUpdate?.call(parsed); } catch (e) { debugPrint('EVERYTHING-CHILD: catiaOpenPaths parse HATA: $e'); } return true; } return null; }) .timeout(const Duration(seconds: 4)); } catch (e) { debugPrint('EVERYTHING-CHILD: setWindowMethodHandler HATA: $e'); } try { await _mainChannel .invokeMethod('ready') .timeout(const Duration(seconds: 4)); } catch (e) { debugPrint('EVERYTHING-CHILD: ready BAŞARISIZ: $e'); } } } // --------------------------------------------------------------------------- // Arayüz // --------------------------------------------------------------------------- class EverythingSearchApp extends StatelessWidget { const EverythingSearchApp({super.key}); @override Widget build(BuildContext context) { return AppThemeBuilder( builder: (context, theme) => MaterialApp( title: EverythingSearchWindowService.windowTitle, debugShowCheckedModeBanner: false, theme: theme.copyWith(scaffoldBackgroundColor: AppColors.opaqueBg), home: const _EverythingSearchScreen(), ), ); } } class _EverythingResult { final String name; final String path; final bool isFolder; final int? size; final DateTime? dateModified; const _EverythingResult({ required this.name, required this.path, required this.isFolder, this.size, this.dateModified, }); String get fullPath => path.isEmpty ? name : '$path${Platform.pathSeparator}$name'; /// "1.2 KB" / "3.4 MB" / "5.1 GB" — klasörler ve boyutu bilinmeyenler null. String? get sizeLabel { if (isFolder || size == null) return null; const units = ['B', 'KB', 'MB', 'GB', 'TB']; double v = size!.toDouble(); var u = 0; while (v >= 1024 && u < units.length - 1) { v /= 1024; u++; } return u == 0 ? '${size!} B' : '${v.toStringAsFixed(1)} ${units[u]}'; } /// "2026-09-22 14:05" — yerel saate çevrilmiş, bilinmiyorsa null. String? get dateModifiedLabel { final d = dateModified; if (d == null) return null; String two(int v) => v.toString().padLeft(2, '0'); return '${d.year}-${two(d.month)}-${two(d.day)} ' '${two(d.hour)}:${two(d.minute)}'; } } /// Everything, FILETIME'ı (1601-01-01 UTC'den beri geçen 100ns aralık) JSON'a /// sayı ya da metin olarak koyabiliyor — burada ikisi de kabul edilir. /// Ayrıştırılamazsa (0/biçim dışı) null döner; UI o zaman etiketi gizler. DateTime? _parseFiletime(dynamic raw) { if (raw == null) return null; final ticks = raw is num ? raw.toInt() : int.tryParse(raw.toString()); if (ticks == null || ticks <= 0) return null; // FILETIME epoch (1601-01-01) ile Unix epoch (1970-01-01) arası: 11644473600s. const epochDiffMicros = 11644473600000000; final unixMicros = (ticks ~/ 10) - epochDiffMicros; if (unixMicros <= 0) return null; try { return DateTime.fromMicrosecondsSinceEpoch(unixMicros, isUtc: true) .toLocal(); } catch (_) { return null; } } class _EverythingSearchScreen extends StatefulWidget { const _EverythingSearchScreen(); @override State<_EverythingSearchScreen> createState() => _EverythingSearchScreenState(); } class _EverythingSearchScreenState extends State<_EverythingSearchScreen> { final _queryCtrl = TextEditingController(text: "*"); Timer? _debounce; int _epoch = 0; int _port = SessionSettings.defaultEverythingPort; EverythingSort _sort = EverythingSort.name; bool _ascending = true; List<_EverythingResult> _results = const []; bool _loading = false; String? _error; int _totalResults = 0; int _page = 0; /// Sayfa başına sonuç — Everything Ayarları'ndan gelir (bkz. /// _loadSettings), diske yazılı değer yoksa `defaultEverythingPageSize`. int _pageSize = SessionSettings.defaultEverythingPageSize; /// Normalize edilmiş (küçük harf) tam yol → o dosyanın açık olduğu CATIA /// kartının başlığı. Ana pencereden `EverythingChildLink.onOpenPathsUpdate` /// ile itilir (bkz. everything_search_service.dart pushOpenPaths). Map _openPaths = const {}; /// "🔗 Yalnızca bağlı olanlar" filtresi — mevcut (o anki sayfadaki) /// sonuçları CATIA'da açık olanlarla sınırlar, sunucuya yeni istek atmaz. bool _onlyOpen = false; @override void initState() { super.initState(); EverythingChildLink.onOpenPathsUpdate = _onOpenPathsUpdate; _loadSettings(); } void _onOpenPathsUpdate(Map paths) { if (!mounted) return; setState(() => _openPaths = paths); } static String _normPath(String p) => p.trim().toLowerCase(); void _toggleOnlyOpen() { setState(() => _onlyOpen = !_onlyOpen); } List<_EverythingResult> get _visibleResults => _onlyOpen ? _results.where((r) => _openPaths.containsKey(_normPath(r.fullPath))).toList() : _results; @override void dispose() { if (identical(EverythingChildLink.onOpenPathsUpdate, _onOpenPathsUpdate)) { EverythingChildLink.onOpenPathsUpdate = null; } _debounce?.cancel(); _queryCtrl.dispose(); super.dispose(); } Future _loadSettings() async { try { final s = await SessionService.loadSettings(); if (!mounted) return; setState(() { _port = s.everythingPort; _sort = s.everythingSort; _ascending = s.everythingSortAscending; _pageSize = s.everythingPageSize; }); if (_queryCtrl.text.trim().isNotEmpty) _search(_queryCtrl.text); } catch (_) { // Diskten okunamadıysa mevcut değerlerle devam. } } /// ⚙️ Everything ayarları — ayrı torun pencere (bkz. /// everything_settings_service.dart). Kaydedilince [_loadSettings] /// tekrar çağrılır, böylece yeni port hemen etkin olur. Future _openSettings() async { try { await EverythingSettingsService.open(onChanged: _loadSettings); } catch (e) { debugPrint('EVERYTHING: ayarlar penceresi açılamadı: $e'); } } Future _saveSort() async { try { final s = await SessionService.loadSettings(); await SessionService.saveSettings(s.copyWith( everythingSort: _sort, everythingSortAscending: _ascending, )); } catch (e) { debugPrint('EVERYTHING: sıralama kaydedilemedi: $e'); } } void _selectSort(EverythingSort s) { if (s == _sort) return; setState(() => _sort = s); _saveSort(); if (_queryCtrl.text.trim().isNotEmpty) _search(_queryCtrl.text); } void _toggleAscending() { setState(() => _ascending = !_ascending); _saveSort(); if (_queryCtrl.text.trim().isNotEmpty) _search(_queryCtrl.text); } void _onQueryChanged(String q) { _debounce?.cancel(); if (q.trim().isEmpty) { setState(() { _results = const []; _loading = false; _error = null; _totalResults = 0; _page = 0; }); return; } _debounce = Timer(const Duration(milliseconds: 300), () => _search(q)); } Future _search(String q, {int page = 0}) async { final myEpoch = ++_epoch; setState(() { _loading = true; _error = null; _page = page; }); final uri = Uri.parse( 'http://localhost:$_port/?search=${Uri.encodeQueryComponent(q)}' '&json=1&path_column=1&size_column=1&date_modified_column=1' '&count=$_pageSize&offset=${page * _pageSize}' '&sort=${_sort.id}&ascending=${_ascending ? 1 : 0}', ); try { final resp = await http.get(uri).timeout(const Duration(seconds: 6)); if (myEpoch != _epoch || !mounted) return; if (resp.statusCode != 200) { setState(() { _loading = false; _error = 'HTTP ${resp.statusCode}'; }); return; } final decoded = jsonDecode(resp.body) as Map; final rawResults = (decoded['results'] as List? ?? const []); final results = [ for (final r in rawResults) _EverythingResult( name: (r as Map)['name']?.toString() ?? '', path: r['path']?.toString() ?? '', isFolder: r['type']?.toString() == 'folder', size: int.tryParse(r['size']?.toString() ?? ''), dateModified: _parseFiletime(r['date_modified']), ), ]; setState(() { _results = results; _totalResults = (decoded['totalResults'] as num?)?.toInt() ?? results.length; _loading = false; }); } catch (e) { if (myEpoch != _epoch || !mounted) return; setState(() { _loading = false; _error = 'Bağlanılamadı — Everything açık mı? ' 'Tools ▸ Options ▸ HTTP Server etkin mi? (port $_port)'; }); } } Future _reveal(_EverythingResult r) async { try { if (r.isFolder) { await Process.run('explorer.exe', [r.fullPath]); } else { await Process.run('explorer.exe', ['/select,', r.fullPath]); } } catch (e) { debugPrint('EVERYTHING: gezginde açılamadı: $e'); } } void _copyPath(_EverythingResult r) { Clipboard.setData(ClipboardData(text: r.fullPath)); } int get _maxPage => _totalResults == 0 ? 0 : (_totalResults - 1) ~/ _pageSize; void _goToPage(int page) { if (page < 0 || page > _maxPage || page == _page) return; if (_queryCtrl.text.trim().isEmpty) return; _search(_queryCtrl.text, page: page); } @override Widget build(BuildContext context) { final visible = _visibleResults; return Scaffold( backgroundColor: AppColors.opaqueBg, body: SafeArea( child: Column( children: [ _TitleBar(onSettingsTap: _openSettings), _Header( queryCtrl: _queryCtrl, onQueryChanged: _onQueryChanged, onSubmitted: _search, sort: _sort, ascending: _ascending, onSortSelect: _selectSort, onAscendingTap: _toggleAscending, onlyOpen: _onlyOpen, onToggleOnlyOpen: _toggleOnlyOpen, ), if (_loading) const LinearProgressIndicator(minHeight: 2) else const SizedBox(height: 2), Expanded( child: _error != null ? _Message(text: _error!, isError: true) : visible.isEmpty ? _Message( text: _queryCtrl.text.trim().isEmpty ? 'Aramaya başlamak için yazın…' : (_loading ? '' : (_onlyOpen && _results.isNotEmpty ? 'Bu sayfada bağlı dosya yok' : 'Sonuç yok')), ) : _ResultList( results: visible, total: _totalResults, page: _page, pageSize: _pageSize, openPaths: _openPaths, onReveal: _reveal, onCopy: _copyPath, ), ), if (_totalResults > _pageSize) _PaginationBar( page: _page, maxPage: _maxPage, onPrev: () => _goToPage(_page - 1), onNext: () => _goToPage(_page + 1), ), ], ), ), ); } } // --------------------------------------------------------------------------- // Başlık şeridi: ana popup'taki Ayarlar/Detay pencereleriyle AYNI marka // şeridi — emoji + ad, opak zemin, alt kenarlık (bkz. settings_screen.dart // _Header, detail_window_screen.dart). Bu pencere çerçeveli (native OS // title bar) olduğundan sürükleme alanı gerekmez, yalnız görsel tutarlılık. // --------------------------------------------------------------------------- class _TitleBar extends StatelessWidget { final VoidCallback onSettingsTap; const _TitleBar({required this.onSettingsTap}); @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(8, 5, 5, 5), decoration: BoxDecoration( color: AppColors.opaqueSurface, border: Border(bottom: BorderSide(color: AppColors.border)), ), child: Row( children: [ Text('🔍', style: TextStyle( fontSize: 12, height: 1.0, fontFamilyFallback: kEmojiFontFallback)), const SizedBox(width: 5), Text('Everything Ara', style: TextStyle( color: AppColors.textPrimary, fontSize: 10, fontWeight: FontWeight.w700, )), const Spacer(), MiniToolbar( size: 18, actions: [ MiniToolAction( icon: Icons.settings, tooltip: 'Everything ayarları', onTap: onSettingsTap, ), ], ), ], ), ); } } // --------------------------------------------------------------------------- // Başlık: arama kutusu + sıralama menüsü + artan/azalan (tek satır, // kompakt). Port ayarı artık ayrı bir pencerede (bkz. _TitleBar'daki // ⚙️ düğmesi / everything_settings_screen.dart). // --------------------------------------------------------------------------- class _Header extends StatelessWidget { final TextEditingController queryCtrl; final ValueChanged onQueryChanged; final ValueChanged onSubmitted; final EverythingSort sort; final bool ascending; final ValueChanged onSortSelect; final VoidCallback onAscendingTap; final bool onlyOpen; final VoidCallback onToggleOnlyOpen; const _Header({ required this.queryCtrl, required this.onQueryChanged, required this.onSubmitted, required this.sort, required this.ascending, required this.onSortSelect, required this.onAscendingTap, required this.onlyOpen, required this.onToggleOnlyOpen, }); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), decoration: BoxDecoration( color: AppColors.opaqueSurface, border: Border(bottom: BorderSide(color: AppColors.border)), ), child: Row( children: [ Expanded( child: TextField( controller: queryCtrl, autofocus: true, onChanged: onQueryChanged, onSubmitted: onSubmitted, style: TextStyle(color: AppColors.textPrimary, fontSize: 11), decoration: InputDecoration( isDense: true, prefixIcon: Icon(Icons.search, size: 12, color: AppColors.textMuted), prefixIconConstraints: const BoxConstraints(minWidth: 26, minHeight: 12), hintText: 'Everything ile dosya/klasör ara…', hintStyle: TextStyle(color: AppColors.textMuted, fontSize: 11), filled: true, fillColor: AppColors.surfaceAlt, border: OutlineInputBorder( borderRadius: BorderRadius.circular(5), borderSide: BorderSide(color: AppColors.border), ), contentPadding: const EdgeInsets.symmetric(vertical: 3, horizontal: 6), ), ), ), const SizedBox(width: 4), MenuEntry( label: sort.emoji, tooltip: 'Sıralama: ${sort.label} — tıkla: değiştir', items: [ for (final s in EverythingSort.values) MenuItemData( emoji: s.emoji, label: s.label, trailing: s == sort ? '✓' : null, onTap: () => onSortSelect(s), ), ], ), MiniToolbar( size: 18, gap: 2, actions: [ MiniToolAction( icon: Icons.link, tooltip: onlyOpen ? 'Yalnızca bağlı olanlar — tıkla: tümünü göster' : 'Yalnızca CATIA\'da açık olanları göster', tint: onlyOpen ? AppColors.ok : null, onTap: onToggleOnlyOpen, ), MiniToolAction( icon: ascending ? Icons.arrow_upward : Icons.arrow_downward, tooltip: ascending ? 'Artan — tıkla: azalan' : 'Azalan — tıkla: artan', onTap: onAscendingTap, ), ], ), ], ), ); } } class _Message extends StatelessWidget { final String text; final bool isError; const _Message({required this.text, this.isError = false}); @override Widget build(BuildContext context) { if (text.isEmpty) return const SizedBox.shrink(); return Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Text( text, textAlign: TextAlign.center, style: TextStyle( color: isError ? AppColors.danger : AppColors.textMuted, fontSize: 12, height: 1.4, ), ), ), ); } } // --------------------------------------------------------------------------- // Sonuç listesi (mouse-sürükle + lastik kaydırma) // --------------------------------------------------------------------------- class _ResultList extends StatelessWidget { final List<_EverythingResult> results; final int total; final int page; final int pageSize; final Map openPaths; final void Function(_EverythingResult) onReveal; final void Function(_EverythingResult) onCopy; const _ResultList({ required this.results, required this.total, required this.page, required this.pageSize, required this.openPaths, required this.onReveal, required this.onCopy, }); @override Widget build(BuildContext context) { final start = page * pageSize + 1; final end = page * pageSize + results.length; final rangeLabel = total > results.length ? ' ($start-$end gösteriliyor)' : ''; final openCount = openPaths.isEmpty ? 0 : results .where((r) => openPaths .containsKey(_EverythingSearchScreenState._normPath(r.fullPath))) .length; final openLabel = openCount > 0 ? ' · $openCount bağlı' : ''; return Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(8, 0, 8, 3), child: Align( alignment: Alignment.centerLeft, child: Text( '$total sonuç$rangeLabel$openLabel', style: TextStyle(color: AppColors.textMuted, fontSize: 9), ), ), ), Expanded( child: BouncyMouseScroll( child: ListView.builder( padding: const EdgeInsets.fromLTRB(5, 0, 5, 6), itemCount: results.length, itemBuilder: (context, i) => _ResultRow( result: results[i], openLabel: openPaths[ _EverythingSearchScreenState._normPath(results[i].fullPath)], onTap: () => onReveal(results[i]), onCopy: () => onCopy(results[i]), ), ), ), ), ], ); } } class _ResultRow extends StatelessWidget { final _EverythingResult result; /// Non-null ise bu dosya o CATIA kartında (başlık burada) açık — kart /// üzerinde 🔗 rozeti gösterilir (bkz. build()). final String? openLabel; final VoidCallback onTap; final VoidCallback onCopy; const _ResultRow({ required this.result, this.openLabel, required this.onTap, required this.onCopy, }); Future _openFolder() async { try { await Process.run('explorer.exe', [result.path]); } catch (e) { debugPrint('EVERYTHING: klasör açılamadı: $e'); } } void _copyName() { Clipboard.setData(ClipboardData(text: result.name)); } @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.symmetric(vertical: 1.5, horizontal: 5), decoration: BoxDecoration( color: AppColors.surfaceAlt, borderRadius: BorderRadius.circular(5), border: Border.all(color: AppColors.border), ), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(5), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( children: [ Icon( result.isFolder ? Icons.folder : Icons.description_outlined, size: 13, color: result.isFolder ? AppColors.warn : AppColors.textSecondary, ), const SizedBox(width: 5), Expanded( child: Text( result.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: AppColors.textPrimary, fontWeight: FontWeight.w700, fontSize: 11, ), ), ), if (openLabel != null) ...[ const SizedBox(width: 4), MiniPill( emoji: '🔗', label: '$openLabel içinde açık', color: AppColors.ok, ), ], if (result.sizeLabel != null || result.dateModifiedLabel != null) ...[ const SizedBox(width: 5), Column( crossAxisAlignment: CrossAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ if (result.sizeLabel != null) Text( result.sizeLabel!, style: TextStyle( color: AppColors.textMuted, fontSize: 9), ), if (result.dateModifiedLabel != null) Text( result.dateModifiedLabel!, style: TextStyle( color: AppColors.textMuted, fontSize: 8.5), ), ], ), ], ], ), Padding( padding: const EdgeInsets.only(left: 18), child: Text( result.path, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(color: AppColors.textMuted, fontSize: 9), ), ), const SizedBox(height: 3), MiniToolbar( size: 16, actions: [ MiniToolAction( icon: Icons.folder_open, tooltip: 'Konumda göster', onTap: onTap, ), MiniToolAction( icon: Icons.copy, tooltip: 'Yolu kopyala', onTap: onCopy, ), MiniToolAction( icon: Icons.badge_outlined, tooltip: 'Adı kopyala', onTap: _copyName, ), MiniToolAction( icon: Icons.folder_open_outlined, tooltip: 'Klasörü aç', onTap: _openFolder, ), ], ), ], ), ), ), ); } } // --------------------------------------------------------------------------- // Alt bilgi çubuğu: sayfalama (◀ Sayfa X / Y ▶) — yalnız sonuç sayısı // _pageSize'ı aşınca gösterilir (bkz. build()). // --------------------------------------------------------------------------- class _PaginationBar extends StatelessWidget { final int page; final int maxPage; final VoidCallback onPrev; final VoidCallback onNext; const _PaginationBar({ required this.page, required this.maxPage, required this.onPrev, required this.onNext, }); @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: AppColors.opaqueSurface, border: Border(top: BorderSide(color: AppColors.border)), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ MiniToolbar( size: 18, gap: 2, actions: [ MiniToolAction( icon: Icons.chevron_left, tooltip: 'Önceki sayfa', onTap: page > 0 ? onPrev : null, ), ], ), const SizedBox(width: 8), Text( 'Sayfa ${page + 1} / ${maxPage + 1}', style: TextStyle(color: AppColors.textMuted, fontSize: 10), ), const SizedBox(width: 8), MiniToolbar( size: 18, gap: 2, actions: [ MiniToolAction( icon: Icons.chevron_right, tooltip: 'Sonraki sayfa', onTap: page < maxPage ? onNext : null, ), ], ), ], ), ); } }