import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../models/divination_layout.dart'; import '../models/history_entry_model.dart'; import '../models/lenormand_pairs.dart'; import '../models/syncretic_session_model.dart'; import '../models/tradition_model.dart'; import '../models/word_model.dart'; import '../services/ai_oracle_service.dart'; import '../services/ogham_feda_repository.dart'; import '../services/traditions_repository.dart'; import '../services/words_repository.dart'; import '../widgets/divination_instruction_modal.dart'; import '../widgets/gradient_star_badge.dart'; import '../services/oracle_master_prompt_generator.dart'; import 'symbol_detail_screen.dart'; class OghamDivinationScreen extends StatefulWidget { final String? initialQuestion; const OghamDivinationScreen({super.key, this.initialQuestion}); @override State createState() => _OghamDivinationScreenState(); } class _OghamDivinationScreenState extends State with SingleTickerProviderStateMixin { late TabController _tabController; final _questionController = TextEditingController(); final _repo = WordsRepository(); final _oghamRepo = OghamFedaRepository(); final _aiService = AiOracleService(); List _stations = []; DivinationLayout _selectedLayout = DivinationLayouts.oghamLayouts[0]; bool _isManualReversed = false; bool _isCasting = false; bool _savedToSession = false; bool _isLoadingAi = false; String? _aiInterpretation; // Drawn results: Station, Feda (meanings/shadow), isReversed, position title final List<({TraditionStation station, LenormandCardEntry feda, bool isReversed, String positionTitle})> _drawnResults = []; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); if (widget.initialQuestion != null) { _questionController.text = widget.initialQuestion!; } _loadOgham(); } @override void dispose() { _tabController.dispose(); _questionController.dispose(); super.dispose(); } Future _loadOgham() async { await TraditionsRepository().init(); await _oghamRepo.init(); final canon = TraditionsRepository().byId('ogham'); if (canon != null && mounted) { setState(() { _stations = canon.stations.take(20).toList(); }); } } void _resetSession() { HapticFeedback.mediumImpact(); setState(() { _drawnResults.clear(); _savedToSession = false; _aiInterpretation = null; _isLoadingAi = false; }); } void _addFedaToSpread(TraditionStation station, bool isReversed) { if (_drawnResults.length >= _selectedLayout.count) return; final feda = _oghamRepo.byCanonId(station.id) ?? _oghamRepo.allFedas.firstWhere( (f) => f.id == station.id, orElse: () => LenormandCardEntry( n: 0, id: station.id, name: station.name, symbol: station.symbol, lore: station.lore, group: station.group, meaning: station.lore, shadow: 'Теневой аспект феды ${station.name}', meanings: const [], pairs: const [], ), ); final posTitle = _selectedLayout.positionTitles[_drawnResults.length]; setState(() { _drawnResults.add(( station: station, feda: feda, isReversed: isReversed, positionTitle: posTitle, )); }); if (_drawnResults.length == _selectedLayout.count) { _recordToSyncreticSession(); } } Future _performDruidicStickCast() async { if (_isCasting || _stations.isEmpty) return; if (_drawnResults.length >= _selectedLayout.count) return; setState(() => _isCasting = true); HapticFeedback.heavyImpact(); await Future.delayed(const Duration(milliseconds: 350)); final drawnIds = _drawnResults.map((r) => r.station.id).toSet(); final available = _stations.where((s) => !drawnIds.contains(s.id)).toList(); if (available.isEmpty) { setState(() => _isCasting = false); return; } final rng = Random(); final chosen = available[rng.nextInt(available.length)]; // ~25% chance of reversed position in druidic stick throwing final isReversed = rng.nextInt(100) < 25; if (mounted) { setState(() => _isCasting = false); _addFedaToSpread(chosen, isReversed); } } WordItem? _resolveLinkedWord(TraditionStation station) { if (station.primary != null) { return _repo.getWordByName(station.primary!.word) ?? _repo.getWordBySessionNumber(station.primary!.id); } return null; } Future _recordToSyncreticSession() async { if (_drawnResults.isEmpty) return; final items = _drawnResults.map((r) { final word = _resolveLinkedWord(r.station); final loreText = r.isReversed && r.feda.shadow.isNotEmpty ? '[ТЕНЬ]: ${r.feda.shadow}' : r.feda.displayMeaning.isNotEmpty ? r.feda.displayMeaning : r.station.lore; return ReadingSignItem( title: '${r.positionTitle}: ${r.station.name}', subtitle: '${r.station.group} ${r.isReversed ? "• ТЕНЬ" : "• ПРЯМАЯ"}', symbol: r.station.symbol, lore: loreText, isReversed: r.isReversed, linkedWord: word, ); }).toList(); final reading = SyncreticReading( id: 'reading_${DateTime.now().millisecondsSinceEpoch}', systemId: 'ogham', systemName: 'Кельтский Древесный Огам (20 Фед)', systemIcon: '᚛', timestamp: DateTime.now(), layoutTitle: _selectedLayout.title, inputMethod: _tabController.index == 0 ? 'druid_sticks' : 'manual_grove', items: items, question: _questionController.text.trim(), ); await _repo.addReadingToSyncreticSession(reading, question: _questionController.text.trim()); if (mounted) { setState(() => _savedToSession = true); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Расклад Огама (${items.length} фед) записан в Синхроническую сессию дня!'), backgroundColor: const Color(0xFF1E382A), behavior: SnackBarBehavior.floating, ), ); } } Future _requestAiInterpretation() async { if (_drawnResults.isEmpty) return; setState(() => _isLoadingAi = true); HapticFeedback.lightImpact(); final words = []; for (final r in _drawnResults) { final w = _resolveLinkedWord(r.station); if (w != null) { words.add(SessionWord.fromWordItem( w, positionTitle: '${r.positionTitle}: ${r.station.name}${r.isReversed ? " [ТЕНЬ]" : ""}', )); } } try { final promptText = StringBuffer(); promptText.writeln('РАСКЛАД КЕЛЬТСКОГО ОГАМА («${_selectedLayout.title}»):'); for (final r in _drawnResults) { promptText.writeln( '• Позиция «${r.positionTitle}»: ${r.station.name} (${r.station.symbol}) [${r.isReversed ? "ТЕНЕВОЕ ПОЛОЖЕНИЕ" : "ПРЯМОЕ ПОЛОЖЕНИЕ"}]', ); promptText.writeln(' Древесная сила: ${r.isReversed ? r.feda.shadow : r.feda.displayMeaning}'); } final q = _questionController.text.trim().isNotEmpty ? _questionController.text.trim() : 'Мудрость Священной Рощи Огама'; final res = await _aiService.interpretSession( words: words, question: '$q\n\n$promptText', ); if (mounted) { setState(() { _aiInterpretation = res; _isLoadingAi = false; }); } } catch (e) { if (mounted) { setState(() { _aiInterpretation = 'Связь с духами рощи прервалась: $e'; _isLoadingAi = false; }); } } } void _copySpreadSummary() { final buffer = StringBuffer(); buffer.writeln('᚛ ОРАКУЛ — КЕЛЬТСКИЙ ДРЕВЕСНЫЙ ОГАМ ᚜'); buffer.writeln('Расклад: ${_selectedLayout.title}'); if (_questionController.text.trim().isNotEmpty) { buffer.writeln('Вопрос: «${_questionController.text.trim()}»\n'); } for (final r in _drawnResults) { buffer.writeln( '• [${r.positionTitle}]: ${r.station.symbol} ${r.station.name} (${r.isReversed ? "ТЕНЬ" : "ПРЯМАЯ"})', ); final lore = r.isReversed && r.feda.shadow.isNotEmpty ? r.feda.shadow : r.feda.displayMeaning; if (lore.isNotEmpty) { buffer.writeln(' $lore'); } } if (_aiInterpretation != null && _aiInterpretation!.isNotEmpty) { buffer.writeln('\n🤖 Толкование Оракула:\n$_aiInterpretation'); } Clipboard.setData(ClipboardData(text: buffer.toString())); HapticFeedback.selectionClick(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Расклад Огама скопирован в буфер обмена'), behavior: SnackBarBehavior.floating, ), ); } String _buildMasterPrompt() { final fedas = _drawnResults.map((r) { return OghamPromptFeda( position: r.positionTitle, name: r.station.name, symbol: r.station.symbol, aicme: r.station.group, isReversed: r.isReversed, lore: r.isReversed && r.feda.shadow.isNotEmpty ? r.feda.shadow : r.feda.displayMeaning, linkedWord: _resolveLinkedWord(r.station), ); }).toList(); return OracleMasterPromptGenerator.generateOghamMasterPrompt( question: _questionController.text.trim(), layoutTitle: _selectedLayout.title, fedas: fedas, ); } void _showMasterPromptModal() { final prompt = _buildMasterPrompt(); showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: const Color(0xFF0C1710), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), builder: (ctx) => DraggableScrollableSheet( initialChildSize: 0.8, minChildSize: 0.4, maxChildSize: 0.95, expand: false, builder: (_, scrollController) => Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 20), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Center( child: Container( width: 40, height: 4, margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: const Color(0xFF81C784).withValues(alpha: 0.4), borderRadius: BorderRadius.circular(2), ), ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Row( children: [ Icon(Icons.psychology_alt_rounded, color: Color(0xFF81C784), size: 24), SizedBox(width: 10), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Мастер-Промпт Огама Рощи', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold), ), Text( 'Для Grok, Claude 3.5 Sonnet, Gemini 1.5 Pro, GPT-4o', style: TextStyle(color: Color(0xFFA5D6A7), fontSize: 11), ), ], ), ], ), IconButton( icon: const Icon(Icons.close_rounded, color: Color(0xFF8C93B8)), onPressed: () => Navigator.pop(ctx), ), ], ), const SizedBox(height: 12), Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: const Color(0xFF162D1F), borderRadius: BorderRadius.circular(8), ), child: Text( '${prompt.length} символов • ${prompt.split(RegExp(r"\s+")).length} слов', style: const TextStyle(color: Color(0xFF81C784), fontSize: 11, fontWeight: FontWeight.bold), ), ), const Spacer(), ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF81C784), foregroundColor: Colors.black, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), ), onPressed: () { Clipboard.setData(ClipboardData(text: prompt)); HapticFeedback.heavyImpact(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Мастер-промпт Огама скопирован! Вставьте в Grok или Claude.'), backgroundColor: Color(0xFF2E7D32), behavior: SnackBarBehavior.floating, ), ); }, icon: const Icon(Icons.copy_rounded, size: 16), label: const Text('Скопировать всё', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12.5)), ), ], ), const SizedBox(height: 12), Expanded( child: Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFF060D09), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFF1E382A), width: 1.0), ), child: SingleChildScrollView( controller: scrollController, child: SelectableText( prompt, style: const TextStyle( color: Color(0xFFECEFF1), fontFamily: 'monospace', fontSize: 12.5, height: 1.45, ), ), ), ), ), ], ), ), ), ); } void _openWordDossier(WordItem word) { HapticFeedback.selectionClick(); Navigator.push( context, MaterialPageRoute(builder: (_) => SymbolDetailScreen(word: word)), ); } @override Widget build(BuildContext context) { final isComplete = _drawnResults.length >= _selectedLayout.count; return Scaffold( backgroundColor: const Color(0xFF09120D), appBar: AppBar( title: const Row( children: [ GradientStar(size: 20), SizedBox(width: 8), Text( 'Кельтский Огам', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), ), ], ), actions: [ IconButton( tooltip: 'Инструкция традиции Огама', icon: const Icon(Icons.help_outline_rounded, color: Color(0xFF81C784)), onPressed: () => DivinationInstructionModal.show(context, 'ogham'), ), IconButton( tooltip: 'Очистить расклад', icon: const Icon(Icons.refresh_rounded), onPressed: _resetSession, ), if (_drawnResults.isNotEmpty) ...[ IconButton( tooltip: 'Мастер-Промпт для Нейросетей', icon: const Icon(Icons.psychology_alt_rounded, color: Color(0xFFFFD54F)), onPressed: _showMasterPromptModal, ), IconButton( tooltip: 'Скопировать расклад', icon: const Icon(Icons.copy_rounded), onPressed: _copySpreadSummary, ), ], ], ), body: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 12, 16, 40), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Question Banner Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF132B1D), Color(0xFF0D1B13)], ), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFF2E7D32).withValues(alpha: 0.4)), ), child: TextField( controller: _questionController, style: const TextStyle(color: Colors.white, fontSize: 14.5), decoration: const InputDecoration( icon: Icon(Icons.psychology_alt_outlined, color: Color(0xFF81C784)), hintText: 'Задайте вопрос священной роще Огама...', hintStyle: TextStyle(color: Color(0xFF6B8B77)), border: InputBorder.none, ), ), ), const SizedBox(height: 14), // Layout Picker Row( children: [ const Icon(Icons.auto_awesome_mosaic_outlined, color: Color(0xFF81C784), size: 18), const SizedBox(width: 8), const Text( 'РАСКЛАД ОГАМА:', style: TextStyle( color: Color(0xFF81C784), fontSize: 12, fontWeight: FontWeight.w900, letterSpacing: 0.8, ), ), ], ), const SizedBox(height: 8), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: DivinationLayouts.oghamLayouts.map((layout) { final isSelected = layout.id == _selectedLayout.id; return Padding( padding: const EdgeInsets.only(right: 8), child: ChoiceChip( selected: isSelected, label: Text(layout.title), selectedColor: const Color(0xFF2E7D32), backgroundColor: const Color(0xFF122218), labelStyle: TextStyle( color: isSelected ? Colors.white : const Color(0xFFA5D6A7), fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, fontSize: 13, ), side: BorderSide( color: isSelected ? const Color(0xFF81C784) : const Color(0xFF1E382A), ), onSelected: (val) { if (val && _selectedLayout.id != layout.id) { setState(() { _selectedLayout = layout; _drawnResults.clear(); _savedToSession = false; _aiInterpretation = null; }); } }, ), ); }).toList(), ), ), const SizedBox(height: 14), // Tab bar: Druid Sticks vs Sacred Grove Container( decoration: BoxDecoration( color: const Color(0xFF102016), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFF1E382A)), ), child: TabBar( controller: _tabController, indicatorSize: TabBarIndicatorSize.tab, indicator: BoxDecoration( color: const Color(0xFF2E7D32), borderRadius: BorderRadius.circular(12), ), labelColor: Colors.white, unselectedLabelColor: const Color(0xFF81C784), labelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13), tabs: const [ Tab(icon: Icon(Icons.casino_outlined, size: 18), text: 'Жребий Палочек'), Tab(icon: Icon(Icons.forest_outlined, size: 18), text: 'Священная Роща (20)'), ], ), ), const SizedBox(height: 14), // Tab content if (!isComplete) ...[ SizedBox( height: 260, child: TabBarView( controller: _tabController, children: [ _buildDruidStickTab(), _buildSacredGroveTab(), ], ), ), ] else ...[ // Completion banner Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFF12281B), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFF81C784)), ), child: Row( children: [ const Icon(Icons.check_circle_rounded, color: Color(0xFF81C784), size: 24), const SizedBox(width: 10), Expanded( child: Text( 'Расклад завершён: получено ${_drawnResults.length} из ${_selectedLayout.count} фед Огама.', style: const TextStyle(color: Color(0xFFE8F5E9), fontWeight: FontWeight.bold), ), ), ], ), ), ], const SizedBox(height: 20), // Results Section if (_drawnResults.isNotEmpty) ...[ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ const Icon(Icons.park_rounded, color: Color(0xFF81C784), size: 20), const SizedBox(width: 8), Text( 'ВЫПАВШИЕ ФЕДЫ (${_drawnResults.length} из ${_selectedLayout.count})', style: const TextStyle( color: Color(0xFF81C784), fontSize: 13, fontWeight: FontWeight.w900, letterSpacing: 0.8, ), ), ], ), if (_savedToSession) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: const Color(0xFF1B3826), borderRadius: BorderRadius.circular(8), ), child: const Row( children: [ Icon(Icons.bookmark_added_rounded, color: Color(0xFF81C784), size: 14), SizedBox(width: 4), Text('В сессии дня', style: TextStyle(color: Color(0xFF81C784), fontSize: 11)), ], ), ), ], ), const SizedBox(height: 12), for (int i = 0; i < _drawnResults.length; i++) ...[ _buildFedaResultCard(_drawnResults[i], i + 1), const SizedBox(height: 12), ], const SizedBox(height: 16), // Master Prompt and AI Oracle Buttons if (isComplete) ...[ OutlinedButton.icon( style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFFFFD54F), side: const BorderSide(color: Color(0xFFFFD54F)), padding: const EdgeInsets.symmetric(vertical: 13), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), ), onPressed: _showMasterPromptModal, icon: const Icon(Icons.psychology_alt_rounded, size: 18), label: const Text( 'Мастер-Промпт для Нейросетей (Grok / Claude / GPT)', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13), ), ), const SizedBox(height: 10), FilledButton.icon( onPressed: _isLoadingAi ? null : _requestAiInterpretation, icon: _isLoadingAi ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.black), ) : const Icon(Icons.auto_awesome_rounded, color: Colors.black), label: Text( _isLoadingAi ? 'Слушаю шёпот рощи...' : 'Толкование Друидического Оракула', style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.black), ), style: FilledButton.styleFrom( backgroundColor: const Color(0xFF81C784), padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), ), ), if (_aiInterpretation != null) ...[ const SizedBox(height: 14), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF122218), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFF2E7D32)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ Icon(Icons.psychology_alt_rounded, color: Color(0xFF81C784), size: 20), SizedBox(width: 8), Text( 'ВЕРДИКТ СВЯЩЕННОЙ РОЩИ', style: TextStyle( color: Color(0xFF81C784), fontWeight: FontWeight.bold, fontSize: 14, ), ), ], ), const SizedBox(height: 10), Text( _aiInterpretation!, style: const TextStyle(color: Color(0xFFE8F5E9), fontSize: 13.5, height: 1.5), ), ], ), ), ], ], ], ], ), ), ); } Widget _buildDruidStickTab() { final nextPosTitle = _drawnResults.length < _selectedLayout.count ? _selectedLayout.positionTitles[_drawnResults.length] : 'Расклад заполнен'; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF102016), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFF1E382A)), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Следующая позиция: $nextPosTitle', textAlign: TextAlign.center, style: const TextStyle(color: Color(0xFFA5D6A7), fontSize: 13, fontWeight: FontWeight.w600), ), const SizedBox(height: 16), InkWell( onTap: _isCasting ? null : _performDruidicStickCast, borderRadius: BorderRadius.circular(20), child: AnimatedContainer( duration: const Duration(milliseconds: 200), width: 140, height: 120, decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF2A5539), Color(0xFF183323)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(20), border: Border.all(color: const Color(0xFF81C784), width: 1.5), boxShadow: [ BoxShadow( color: const Color(0xFF81C784).withValues(alpha: 0.2), blurRadius: 16, ), ], ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('᚛ ᚌ ᚜', style: TextStyle(fontSize: 34, color: Color(0xFFFFD54F))), const SizedBox(height: 6), Text( _isCasting ? 'Бросок...' : 'Бросить палочку', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12), ), ], ), ), ), const SizedBox(height: 14), const Text( 'Жребий выбирает одну из 20 фед и определяет прямую или теневую ветвь.', textAlign: TextAlign.center, style: TextStyle(color: Color(0xFF6B8B77), fontSize: 11.5), ), ], ), ); } Widget _buildSacredGroveTab() { return Column( children: [ // Reversal Toggle Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Положение для выбора:', style: TextStyle(color: Color(0xFFA5D6A7), fontSize: 12.5, fontWeight: FontWeight.bold), ), Row( children: [ ChoiceChip( label: const Text('Прямая 🌿'), selected: !_isManualReversed, selectedColor: const Color(0xFF2E7D32), backgroundColor: const Color(0xFF102016), labelStyle: TextStyle( color: !_isManualReversed ? Colors.white : const Color(0xFF81C784), fontSize: 11.5, ), onSelected: (val) { if (val) setState(() => _isManualReversed = false); }, ), const SizedBox(width: 6), ChoiceChip( label: const Text('Тень / Перев. 🍂'), selected: _isManualReversed, selectedColor: const Color(0xFF8D4B28), backgroundColor: const Color(0xFF102016), labelStyle: TextStyle( color: _isManualReversed ? Colors.white : const Color(0xFFFFB74D), fontSize: 11.5, ), onSelected: (val) { if (val) setState(() => _isManualReversed = true); }, ), ], ), ], ), const SizedBox(height: 8), // Grid of 20 Fedas Expanded( child: _stations.isEmpty ? const Center(child: CircularProgressIndicator()) : GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 5, mainAxisSpacing: 6, crossAxisSpacing: 6, childAspectRatio: 0.85, ), itemCount: _stations.length, itemBuilder: (context, idx) { final station = _stations[idx]; final isDrawn = _drawnResults.any((r) => r.station.id == station.id); return InkWell( onTap: isDrawn ? null : () { HapticFeedback.selectionClick(); _addFedaToSpread(station, _isManualReversed); }, borderRadius: BorderRadius.circular(10), child: Container( decoration: BoxDecoration( color: isDrawn ? const Color(0xFF0A140F) : const Color(0xFF162B1F), borderRadius: BorderRadius.circular(10), border: Border.all( color: isDrawn ? const Color(0xFF1A3324) : const Color(0xFF81C784).withValues(alpha: 0.4), ), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( station.symbol, style: TextStyle( fontSize: 22, color: isDrawn ? const Color(0xFF3B5745) : const Color(0xFFFFD54F), ), ), const SizedBox(height: 2), Text( station.name.split(' ').first, style: TextStyle( fontSize: 9.5, fontWeight: FontWeight.bold, color: isDrawn ? const Color(0xFF3B5745) : Colors.white, ), overflow: TextOverflow.ellipsis, ), ], ), ), ); }, ), ), ], ); } Widget _buildFedaResultCard( ({TraditionStation station, LenormandCardEntry feda, bool isReversed, String positionTitle}) result, int index, ) { final word = _resolveLinkedWord(result.station); final isRev = result.isReversed; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF122218), borderRadius: BorderRadius.circular(16), border: Border.all( color: isRev ? const Color(0xFFFFB74D) : const Color(0xFF81C784), width: 1.2, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Row Row( children: [ Container( width: 44, height: 44, alignment: Alignment.center, decoration: BoxDecoration( gradient: LinearGradient( colors: isRev ? [const Color(0xFF5D2E17), const Color(0xFF3B1E0F)] : [const Color(0xFF1E462B), const Color(0xFF102B19)], ), borderRadius: BorderRadius.circular(12), border: Border.all( color: isRev ? const Color(0xFFFFB74D) : const Color(0xFF81C784), ), ), child: Text( result.station.symbol, style: TextStyle( fontSize: 26, color: isRev ? const Color(0xFFFFCC80) : const Color(0xFFFFD54F), ), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '#$index. ${result.positionTitle}', style: const TextStyle(color: Color(0xFF81C784), fontSize: 11.5, fontWeight: FontWeight.bold), ), Text( result.station.name, style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.bold), ), Text( result.station.group, style: const TextStyle(color: Color(0xFFA5D6A7), fontSize: 11), ), ], ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: isRev ? const Color(0xFF421F0D) : const Color(0xFF1B3D26), borderRadius: BorderRadius.circular(8), ), child: Text( isRev ? '🍂 ТЕНЬ' : '🌿 ПРЯМАЯ', style: TextStyle( color: isRev ? const Color(0xFFFFB74D) : const Color(0xFF81C784), fontSize: 10, fontWeight: FontWeight.bold, ), ), ), ], ), const SizedBox(height: 12), const Divider(color: Color(0xFF1E382A), height: 1), const SizedBox(height: 10), // Lore / Meaning Text( isRev ? result.feda.shadow : (result.feda.displayMeaning.isNotEmpty ? result.feda.displayMeaning : result.station.lore), style: const TextStyle(color: Color(0xFFE8F5E9), fontSize: 13, height: 1.45), ), const SizedBox(height: 10), // Linked 999 Word if (word != null) InkWell( onTap: () => _openWordDossier(word), borderRadius: BorderRadius.circular(10), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( color: const Color(0xFF0B1710), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFF2E7D32).withValues(alpha: 0.5)), ), child: Row( children: [ Text(word.emoji.isNotEmpty ? word.emoji : '🌳', style: const TextStyle(fontSize: 16)), const SizedBox(width: 8), Expanded( child: Text( 'Знак 999: #${word.formattedSessionNumber} «${word.word.toUpperCase()}» (${word.category})', style: const TextStyle( color: Color(0xFFFFD54F), fontSize: 12, fontWeight: FontWeight.bold, ), ), ), const Icon(Icons.arrow_forward_ios_rounded, color: Color(0xFF81C784), size: 12), ], ), ), ), ], ), ); } }