import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' show RenderAbstractViewport; import 'package:provider/provider.dart'; import 'package:window_manager/window_manager.dart'; import '../services/catia_provider.dart'; import '../theme/app_theme.dart'; import '../theme/desktop_tints.dart'; import '../services/desktop_groups.dart'; import '../services/session_service.dart'; import '../widgets/bouncy_mouse_scroll.dart'; import '../widgets/catia_list_tile.dart'; import '../widgets/desktop_group_header.dart'; import '../widgets/everything_search_panel.dart'; import '../widgets/global_toolbar.dart'; import '../widgets/history_list_tile.dart'; import '../widgets/search_field.dart'; import '../widgets/session_banner.dart'; import '../widgets/session_drop_zone.dart'; import '../widgets/top_menu_bar.dart'; import '../widgets/window_chrome.dart'; /// 🎯 Odaklanan CATIA kartını listede en üste getirecek şekilde animasyonlu /// kaydırma (Ayarlar ▸ "Odaklanan CATIA'yı üste kaydır"). `ListView.builder` /// TEMBEL inşa ettiği için hedef kart ekran dışındaysa henüz inşa edilmemiş /// olabilir — bu yüzden İKİ AŞAMALI çalışır: (1) tahmini konuma [jumpTo] ile /// sıçranır (bu, hedefin çevresindeki öğelerin inşa edilmesini tetikler), /// (2) birkaç kare beklenip [key]'in artık hazır olan gerçek RenderBox'ı /// üzerinden `RenderAbstractViewport.getOffsetToReveal` ile TAM üst hizalama /// yapılır. Hedef hâlâ inşa edilmediyse (çok uzak/çok hızlı ardışık kaydırma) /// sessizce bırakılır — tahmini konum zaten yeterince yakındır. Future _scrollActiveInstanceToTop({ required ScrollController controller, required int index, required int itemCount, required GlobalKey key, }) async { if (!controller.hasClients || itemCount <= 0 || index < 0) return; final startPos = controller.position; final estimate = itemCount <= 1 ? 0.0 : (index / itemCount) * startPos.maxScrollExtent; controller.jumpTo( estimate.clamp(startPos.minScrollExtent, startPos.maxScrollExtent), ); // Build + layout tamamlansın diye birkaç kare bekle. await WidgetsBinding.instance.endOfFrame; await WidgetsBinding.instance.endOfFrame; if (!controller.hasClients) return; final ctx = key.currentContext; if (ctx == null) return; // hâlâ inşa edilmedi — tahmini konumla yetin final renderObject = ctx.findRenderObject(); if (renderObject == null) return; final viewport = RenderAbstractViewport.of(renderObject); final revealed = viewport.getOffsetToReveal(renderObject, 0.0); final pos = controller.position; final target = revealed.offset.clamp(pos.minScrollExtent, pos.maxScrollExtent); if (!controller.hasClients) return; await controller.animateTo( target, duration: const Duration(milliseconds: 320), curve: Curves.easeOutCubic, ); } class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); @override Widget build(BuildContext context) { // 🖐 Boyutlandırma: pencere çerçevesiz (bkz. WindowTrayService.init) — // kenar/köşeden fare ile sürükleyerek boyutlandırma native çerçeve // olmadan çalışmaz, bu yüzden `DragToResizeArea` ile görünmez sürükleme // şeritleri ekleniyor (window_manager paketi, `startResizing` native // çağrısını sarmalıyor). EN DIŞTA olmalı ki şeritler pencerenin gerçek // kenarına otursun; içerideki `WindowChrome`'un `DragToMoveArea`'sı // (taşıma) ile çakışmaz — o, üst şeridin ALTINDA kalan geniş bölgeyi // kullanır. return DragToResizeArea( resizeEdgeSize: 5, child: Scaffold( appBar: const WindowChrome(), body: SessionDropZone( child: Consumer( builder: (context, provider, _) { return Column( children: [ // 1) Üst menü: Dosya / Görünüm / Araçlar / Yardım const TopMenuBar(), // 2) Menü altı genel toolbar: 🟢 🕘 ➕ ▶️ ⏹ 🔄 🧹 🗗 const GlobalToolbar(), // 2b) Oturum bandı: geri yükleme teklifi / kaydet-geri yükle mesajı const SessionBanner(), // 3) Arama kutusu (ultra kompakt) — Everything paneli (arama // modu) KENDİ arama kutusunu getirir, bu yüzden o moddayken // gizlenir (iki arama kutusu üst üste görünmesin). if (provider.tab != HomeTab.search) Padding( padding: const EdgeInsets.fromLTRB(6, 2, 6, 2), child: SearchField( hint: 'Ara...', onChanged: provider.setQuery, ), ), // 4) Liste (sekme çubuğu gizli; geçiş üst toolbar/menüden). // "search" modunda aktif CATIA listesinin YERİNE ana pencereye // gömülü Everything (dosya arama) paneli geçer — bkz. // widgets/everything_search_panel.dart dosya başı açıklaması. Expanded( child: switch (provider.tab) { HomeTab.active => _ActiveList(provider: provider), HomeTab.history => _HistoryList(provider: provider), HomeTab.search => const EverythingSearchPanel(), }, ), ], ); }, ), ), ), ); } } class _ActiveList extends StatefulWidget { final CatiaProvider provider; const _ActiveList({required this.provider}); @override State<_ActiveList> createState() => _ActiveListState(); } class _ActiveListState extends State<_ActiveList> { // 🎯 Odaklanan CATIA'yı üste kaydırma: kaydırma pozisyonunu ve her kartın // gerçek RenderBox'ını bulmak için GlobalKey'lerini burada tutuyoruz // (bkz. dosya başındaki [_scrollActiveInstanceToTop]). Yalnız bu düz liste // görünümünde etkili — gruplu (masaüstüne göre) görünümde devre dışı. final ScrollController _scrollController = ScrollController(); final Map _itemKeys = {}; String? _lastActiveId; GlobalKey _keyFor(String id) => _itemKeys.putIfAbsent(id, () => GlobalKey()); @override void dispose() { _scrollController.dispose(); super.dispose(); } /// Odaklanan CATIA değiştiyse (ve ayar açıksa) o kartı üste kaydır. /// `build` içinden çağrılır; yalnız bir sonraki kareye kaydırma /// PLANLAR, senkron state değişikliği yapmaz — bu yüzden build sırasında /// çağırmak güvenli. void _maybeScrollToActive(CatiaProvider provider, List items) { final activeId = provider.activeInstanceId; if (!provider.sessionSettings.autoScrollActiveToTop) { _lastActiveId = activeId; return; } if (activeId == null || activeId == _lastActiveId) { _lastActiveId = activeId; return; } _lastActiveId = activeId; final index = items.indexWhere((i) => i.id == activeId); final key = _itemKeys[activeId]; if (index <= 0 || key == null) return; // zaten en üstte / listede yok WidgetsBinding.instance.addPostFrameCallback((_) { _scrollActiveInstanceToTop( controller: _scrollController, index: index, itemCount: items.length, key: key, ); }); } @override Widget build(BuildContext context) { final provider = widget.provider; if (provider.listMode == ListMode.byDesktop) { return _GroupedList(provider: provider); } final items = provider.filteredInstances; _maybeScrollToActive(provider, items); final searching = provider.query.isNotEmpty; if (items.isEmpty) { // Boş listede de yeni CATIA başlatılabilsin (arama yokken). return Column( children: [ const Expanded(child: _EmptyState(text: 'Aktif CATIA bulunamadı')), if (!searching) const Padding( padding: EdgeInsets.fromLTRB(6, 0, 6, 6), child: NewCatiaRow(key: ValueKey('new-catia')), ), ], ); } return BouncyMouseScroll( child: ListView.builder( controller: _scrollController, padding: const EdgeInsets.fromLTRB(6, 0, 6, 6), itemCount: items.length + (searching ? 0 : 1), itemBuilder: (context, i) { if (i == items.length) { return const NewCatiaRow(key: ValueKey('new-catia')); } final instance = items[i]; // 🖌 RepaintBoundary: kart kendi katmanında raster edilir — bir // kart yeniden boyanınca (ör. aktif-pencere vurgusu) komşu kartlar // GPU'da yeniden çizilmez. Build maliyetini azaltmaz (onu tile'ın // kendisi çözmeli, bkz. performans.md #6) ama compositing/scroll // maliyetini düşürür. // 🎯 KeyedSubtree: dıştaki GlobalKey yalnız "üste kaydır" için // gerçek RenderBox konumunu ölçmede kullanılır; CatiaListTile'ın // KENDİ ValueKey'i (state korunması için) ayrı ve değişmeden kalır. return KeyedSubtree( key: _keyFor(instance.id), child: RepaintBoundary( child: CatiaListTile( // Sıralama değişince kartlar yer değiştirir; key olmadan Flutter // state'i (📄 açık/kapalı) YANLIŞ karta geçerdi. key: ValueKey(instance.id), instance: instance, onTap: () => provider.selectInstance(instance.id), ), ), ); }, ), ); } } /// Masaüstüne göre gruplu liste: başlık + (daraltılmadıysa) o masaüstünün /// kartları. Kartlar düz listedekiyle AYNI `CatiaListTile`. "Odaklanan /// CATIA'yı üste kaydır" burada da AYNI mantıkla çalışır (bkz. dosya başı /// [_scrollActiveInstanceToTop] ve _ActiveListState) — tek fark: "üst" burada /// hedef kartın satırı, masaüstü başlıklarının/dilimlerinin ALTINDA kalan /// düz `rows` listesindeki konumudur. Kart DARALTILMIŞ bir masaüstünün /// içindeyse (rows'ta hiç yoktur) sessizce atlanır — grubu otomatik açmıyoruz. class _GroupedList extends StatefulWidget { final CatiaProvider provider; const _GroupedList({required this.provider}); @override State<_GroupedList> createState() => _GroupedListState(); } class _GroupedListState extends State<_GroupedList> { final ScrollController _scrollController = ScrollController(); final Map _itemKeys = {}; String? _lastActiveId; GlobalKey _keyFor(String id) => _itemKeys.putIfAbsent(id, () => GlobalKey()); @override void dispose() { _scrollController.dispose(); super.dispose(); } void _maybeScrollToActive(CatiaProvider provider, List rows, int rowIndex) { final activeId = provider.activeInstanceId; if (!provider.sessionSettings.autoScrollActiveToTop) { _lastActiveId = activeId; return; } if (activeId == null || activeId == _lastActiveId) { _lastActiveId = activeId; return; } _lastActiveId = activeId; final key = _itemKeys[activeId]; if (rowIndex <= 0 || key == null) return; // zaten en üstte / daraltılmış grupta WidgetsBinding.instance.addPostFrameCallback((_) { _scrollActiveInstanceToTop( controller: _scrollController, index: rowIndex, itemCount: rows.length, key: key, ); }); } @override Widget build(BuildContext context) { final provider = widget.provider; final groups = provider.desktopGroups; final searching = provider.query.isNotEmpty; if (groups.isEmpty) { return _EmptyState( text: searching ? 'Aramayla eşleşen CATIA yok' : 'Aktif CATIA bulunamadı'); } // Düz satır listesi: başlık | kart | boş ipucu | (sonda) yeni masaüstü. final rows = []; final activeId = provider.activeInstanceId; var activeRowIndex = -1; for (final DesktopGroup g in groups) { final collapsed = provider.isDesktopCollapsed(g.key); rows.add(DesktopGroupHeader( key: ValueKey('desk:${g.key}'), group: g, collapsed: collapsed, )); if (collapsed) continue; // Her masaüstü kendi tonunda tek blok: başlık + dilimler. final tint = DesktopTint.forNumber(g.number); if (g.instances.isEmpty) { rows.add(DesktopPanelSlice( key: ValueKey('empty:${g.key}'), tint: tint, last: true, child: const DesktopEmptyHint(), )); continue; } for (var n = 0; n < g.instances.length; n++) { final instance = g.instances[n]; if (instance.id == activeId) activeRowIndex = rows.length; // Slice'ı kart yerine SARMALAYICI olarak ekliyoruz; kartın kendi // key'i (state korunsun) sliceın içinde kalır. Dıştaki KeyedSubtree // yalnız "üste kaydır" için gerçek RenderBox konumunu ölçmede // kullanılır (bkz. _ActiveListState aynı deseni). rows.add(KeyedSubtree( key: _keyFor(instance.id), child: DesktopPanelSlice( key: ValueKey('slice:${instance.id}'), tint: tint, last: n == g.instances.length - 1, child: RepaintBoundary( child: CatiaListTile( key: ValueKey(instance.id), instance: instance, onTap: () => provider.selectInstance(instance.id), ), ), ), )); } } if (!searching) { rows.add(const NewCatiaRow(key: ValueKey('new-catia'))); rows.add(const NewDesktopRow(key: ValueKey('new-desktop'))); } _maybeScrollToActive(provider, rows, activeRowIndex); return BouncyMouseScroll( child: ListView.builder( controller: _scrollController, padding: const EdgeInsets.fromLTRB(6, 0, 6, 6), itemCount: rows.length, itemBuilder: (context, i) => rows[i], ), ); } } class _HistoryList extends StatelessWidget { final CatiaProvider provider; const _HistoryList({required this.provider}); @override Widget build(BuildContext context) { final items = provider.filteredHistory; if (items.isEmpty) { return const _EmptyState(text: 'Geçmiş kaydı yok'); } return BouncyMouseScroll( child: ListView.builder( padding: const EdgeInsets.fromLTRB(6, 0, 6, 6), itemCount: items.length, itemBuilder: (context, i) => HistoryListTile(entry: items[i]), ), ); } } class _EmptyState extends StatelessWidget { final String text; const _EmptyState({required this.text}); @override Widget build(BuildContext context) { return Center( child: Text(text, style: TextStyle(color: AppColors.textMuted, fontSize: 13)), ); } }