import '../models/tradition_model.dart'; import '../services/traditions_repository.dart'; import '../services/lenormand_pairs_repository.dart'; import '../services/tarot_majors_repository.dart'; import '../services/ogham_feda_repository.dart'; import 'lenormand_station_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../models/culture_branch.dart'; import '../models/word_model.dart'; import '../services/words_repository.dart'; import '../widgets/gradient_star_badge.dart'; import 'oracle_chat_screen.dart'; import 'edit_symbol_screen.dart'; class SymbolDetailScreen extends StatefulWidget { final WordItem word; const SymbolDetailScreen({super.key, required this.word}); @override State createState() => _SymbolDetailScreenState(); } class _SymbolDetailScreenState extends State { late WordItem _currentWord; final _repo = WordsRepository(); @override void initState() { super.initState(); _currentWord = widget.word; } static const Map _systemToCanonId = { 'руны': 'runes', 'runes': 'runes', 'огам': 'ogham', 'ogham': 'ogham', 'каббала': 'kabbalah', 'kabbalah': 'kabbalah', 'алхимия': 'alchemy', 'alchemy': 'alchemy', 'таро': 'tarot', 'tarot': 'tarot', 'ленорман': 'lenormand', 'lenormand': 'lenormand', 'и цзин': 'iching', 'iching': 'iching', 'геомантия': 'geomancy', 'geomancy': 'geomancy', 'септенер': 'days_of_week', 'дни недели': 'days_of_week', 'days_of_week': 'days_of_week', 'додекада': 'months_of_year', 'месяцы': 'months_of_year', 'месяцы года': 'months_of_year', 'months_of_year': 'months_of_year', 'западный зодиак': 'western_zodiac', 'зодиак': 'western_zodiac', 'гороскоп': 'western_zodiac', 'western_zodiac': 'western_zodiac', 'китайский зодиак': 'zodiac', 'китайский гороскоп': 'zodiac', 'zodiac': 'zodiac', 'космология': 'cosmology', 'cosmology': 'cosmology', 'додекада созвездий': 'western_zodiac', 'планетарный септенер': 'days_of_week', 'нумерология': 'numerology', 'матрица': 'numerology', }; bool _isStockText(String text) { final t = text.toLowerCase(); return t.contains('сакральный символ') || t.contains('архетипический образ в поле судьбы') || t.contains('ключевую сферу кристаллизации') || t.contains('вектор трансформации') || t.contains('модус судьбы') || t.contains('активный процесс духовной динамики') || t.contains('искажение первообраза') || t.contains('суета, поспешность'); } TraditionStation? _matchStation(String canonId, String detail) { final canon = TraditionsRepository().byId(canonId); if (canon == null) return null; final stations = canon.stations; final detailLower = detail.toLowerCase(); // 1. Substring in name for (final s in stations) { final sName = s.name.toLowerCase(); if (sName.contains(detailLower) || detailLower.contains(sName)) { return s; } } // 2. Number match final numMatch = RegExp(r'\b(\d+)\b').firstMatch(detail); if (numMatch != null) { final n = int.tryParse(numMatch.group(1)!); if (n != null) { for (final s in stations) { if (s.id == '$n' || s.id == '${canonId}_$n' || s.name.startsWith('$n ') || s.name.startsWith('Карта $n ') || s.name.startsWith('Карта $n«')) { return s; } } } } // 3. Token match final tokens = RegExp(r'[a-zA-Zа-яА-ЯёЁ]+') .allMatches(detailLower) .map((m) => m.group(0)!) .where((w) => w.length >= 3 && w != 'карта' && w != 'аркан' && w != 'триграмма') .toList(); TraditionStation? best; int bestScore = 0; for (final s in stations) { final sText = '${s.name} ${s.group} ${s.lore}'.toLowerCase(); int score = 0; for (final t in tokens) { if (sText.contains(t)) score++; } if (score > bestScore) { bestScore = score; best = s; } } if (bestScore > 0) return best; return null; } void _openStation(TraditionCanon tradition, TraditionStation station) { HapticFeedback.selectionClick(); if (tradition.id == 'tarot') { final card = TarotMajorsRepository().byCanonId(station.id); if (card != null) { Navigator.of(context).push( MaterialPageRoute( builder: (_) => LenormandStationScreen( number: card.n, lookup: TarotMajorsRepository().byNumber, title: 'Таро', ), ), ); return; } } if (tradition.id == 'ogham') { final card = OghamFedaRepository().byCanonId(station.id); if (card != null) { Navigator.of(context).push( MaterialPageRoute( builder: (_) => LenormandStationScreen( number: card.n, lookup: OghamFedaRepository().byNumber, title: 'Огам', ), ), ); return; } } if (tradition.id == 'lenormand') { final card = LenormandPairsRepository().byStationId(station.id); if (card != null) { Navigator.of(context).push( MaterialPageRoute( builder: (_) => LenormandStationScreen(number: card.n), ), ); return; } } // Modal sheet with full station lore, glyph, parallels, primary & cluster words _showStationDetailsSheet(tradition, station); } void _openStationBySystemAndDetail(String system, String detail) { final canonId = _systemToCanonId[system.trim().toLowerCase()]; if (canonId == null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('$system: $detail')), ); return; } final canon = TraditionsRepository().byId(canonId); if (canon == null) return; final station = _matchStation(canonId, detail); if (station != null) { _openStation(canon, station); } else { // If no single station matched, open full canon sheet _showCanonOverviewSheet(canon, detail); } } void _showCanonOverviewSheet(TraditionCanon canon, String query) { showModalBottomSheet( context: context, backgroundColor: const Color(0xFF151829), isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), builder: (ctx) { return DraggableScrollableSheet( initialChildSize: 0.7, minChildSize: 0.4, maxChildSize: 0.95, expand: false, builder: (ctx, scroll) { return Padding( padding: const EdgeInsets.all(20), child: ListView( controller: scroll, children: [ Row( children: [ Text(canon.icon, style: const TextStyle(fontSize: 24)), const SizedBox(width: 10), Expanded( child: Text( canon.name, style: const TextStyle( color: Color(0xFFFFF9C4), fontSize: 18, fontWeight: FontWeight.bold, ), ), ), ], ), const SizedBox(height: 8), Text( canon.description, style: const TextStyle(color: Color(0xFFD1D5DB), fontSize: 13, height: 1.4), ), const SizedBox(height: 16), const Divider(color: Color(0xFF2A2D4A)), const SizedBox(height: 12), const Text( 'СТАНЦИИ ТРАДИЦИИ:', style: TextStyle( color: Color(0xFFFFD54F), fontSize: 12, fontWeight: FontWeight.w900, letterSpacing: 1.0, ), ), const SizedBox(height: 10), for (final st in canon.stations) ListTile( contentPadding: EdgeInsets.zero, leading: Text(st.symbol, style: const TextStyle(fontSize: 22, color: Color(0xFFFFD54F))), title: Text(st.name, style: const TextStyle(color: Color(0xFFFFF9C4), fontWeight: FontWeight.bold)), subtitle: Text(st.group, style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 12)), trailing: const Icon(Icons.arrow_forward_ios_rounded, size: 14, color: Color(0xFFFFD54F)), onTap: () { Navigator.pop(ctx); _openStation(canon, st); }, ), ], ), ); }, ); }, ); } void _showStationDetailsSheet(TraditionCanon tradition, TraditionStation station) { showModalBottomSheet( context: context, backgroundColor: const Color(0xFF151829), isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), builder: (ctx) { return DraggableScrollableSheet( initialChildSize: 0.75, minChildSize: 0.4, maxChildSize: 0.95, expand: false, builder: (ctx, scroll) { return Padding( padding: const EdgeInsets.all(20), child: ListView( controller: scroll, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFF232742), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.4)), ), child: Text( station.symbol.isNotEmpty ? station.symbol : tradition.icon, style: const TextStyle(fontSize: 28, color: Color(0xFFFFD54F)), ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( station.name, style: const TextStyle( color: Color(0xFFFFF9C4), fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( '${tradition.name} • ${station.group}', style: const TextStyle( color: Color(0xFF80DEEA), fontSize: 12, fontWeight: FontWeight.w600, ), ), ], ), ), ], ), const SizedBox(height: 16), const Divider(color: Color(0xFF2A2D4A)), const SizedBox(height: 12), const Text( 'КАНОНИЧЕСКИЙ МИФ И ЗНАЧЕНИЕ', style: TextStyle( color: Color(0xFFFFD54F), fontSize: 12, fontWeight: FontWeight.w900, letterSpacing: 1.0, ), ), const SizedBox(height: 8), Text( station.lore, style: const TextStyle( color: Color(0xFFE0E2EC), fontSize: 14.5, height: 1.55, ), ), if (station.primary != null) ...[ const SizedBox(height: 16), const Text( 'ПЕРВИЧНЫЙ ЗНАК В ЛЕКСИКОНЕ', style: TextStyle( color: Color(0xFF81C784), fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 0.8, ), ), const SizedBox(height: 8), ActionChip( avatar: const Icon(Icons.star_rounded, color: Color(0xFFFFD54F), size: 16), label: Text('#${station.primary!.id} ${station.primary!.word.toUpperCase()}'), labelStyle: const TextStyle( color: Color(0xFFFFF9C4), fontWeight: FontWeight.bold, fontSize: 13, ), backgroundColor: const Color(0xFF232742), side: const BorderSide(color: Color(0xFFFFD54F)), onPressed: () { Navigator.pop(ctx); final w = _repo.getWordById(station.primary!.id) ?? _repo.getWordByName(station.primary!.word); if (w != null) _navigateToWord(w); }, ), ], if (station.cluster.isNotEmpty) ...[ const SizedBox(height: 16), const Text( 'ЗНАКИ СОЗВУЧИЯ И КЛАСТЕРА', style: TextStyle( color: Color(0xFF80DEEA), fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 0.8, ), ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: [ for (final cw in station.cluster) ActionChip( label: Text(cw.word), labelStyle: const TextStyle(color: Color(0xFFE0E2EC), fontSize: 12), backgroundColor: const Color(0xFF1E2238), side: const BorderSide(color: Color(0xFF2A2D4A)), onPressed: () { Navigator.pop(ctx); final w = _repo.getWordById(cw.id) ?? _repo.getWordByName(cw.word); if (w != null) _navigateToWord(w); }, ), ], ), ], if (station.parallels.isNotEmpty) ...[ const SizedBox(height: 16), const Text( 'ПАРАЛЛЕЛИ В ДРУГИХ СИСТЕМАХ', style: TextStyle( color: Color(0xFFFFB74D), fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 0.8, ), ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: [ for (final p in station.parallels) Chip( label: Text('${WordItemLocalization.localizeSystem(p['system_name'] ?? p['system'] ?? '')}: ${p['name']}'), labelStyle: const TextStyle(color: Color(0xFFFFE082), fontSize: 12), backgroundColor: const Color(0xFF2A2145), side: const BorderSide(color: Color(0xFF7E57C2)), ), ], ), ], ], ), ); }, ); }, ); } Future _openEditScreen() async { final res = await Navigator.push( context, MaterialPageRoute( builder: (context) => EditSymbolScreen(word: _currentWord), ), ); if (res == true && mounted) { final updated = _repo.getWordByName(_currentWord.word) ?? _repo.getWordBySessionNumber(_currentWord.sessionNumber) ?? _currentWord; setState(() { _currentWord = updated; }); } } void _navigateToWord(WordItem word) { setState(() { _currentWord = word; }); HapticFeedback.selectionClick(); } Color _getValenceColor(String valence) { switch (valence.toLowerCase()) { case 'active': case 'light': return const Color(0xFFFFB74D); // Amber case 'receptive': return const Color(0xFF4DD0E1); // Cyan case 'dark': return const Color(0xFFE57373); // Red case 'liminal': return const Color(0xFFBA68C8); // Purple default: return const Color(0xFF90A4AE); // Neutral } } IconData _getSystemIcon(String system) { switch (system.toLowerCase()) { case 'tarot': return Icons.style_rounded; case 'lenormand': return Icons.auto_stories_rounded; case 'runes': return Icons.gesture_rounded; case 'iching': return Icons.view_headline_rounded; case 'astrology': return Icons.brightness_3_rounded; case 'bestiary': return Icons.cruelty_free_rounded; case 'slavic': return Icons.shield_moon_rounded; case 'dream': return Icons.bedtime_rounded; case 'alchemy': return Icons.local_fire_department_rounded; case 'ogham': return Icons.nature_rounded; case 'kabbalah': return Icons.star_rounded; case 'cosmology': return Icons.public_rounded; default: return Icons.auto_awesome_rounded; } } void _copyDossier() { final w = _currentWord; final buffer = StringBuffer(); buffer.writeln('🔮 ОРАКУЛ — ДОСЬЕ ЗНАКА'); buffer.writeln('№${w.formattedSessionNumber} «${w.word.toUpperCase()}»'); if (w.category.isNotEmpty) buffer.writeln('Категория: ${w.category}'); if (w.partOfSpeech.isNotEmpty) buffer.writeln('Часть речи: ${w.partOfSpeech}'); if (w.operator.isNotEmpty) buffer.writeln('Оператор: ${w.localizedOperator}'); if (w.valence.isNotEmpty) buffer.writeln('Полярность: ${w.localizedValence}'); if (w.core.isNotEmpty) { buffer.writeln('\n✨ ЯДРО:'); buffer.writeln(w.core); } if (w.oracle.isNotEmpty) { buffer.writeln('\n📖 ОРАКУЛ:'); buffer.writeln(w.oracle); } else if (w.description.isNotEmpty) { buffer.writeln('\n📖 ЗНАЧЕНИЕ:'); buffer.writeln(w.cleanDescription); } if (w.layers.isNotEmpty) { buffer.writeln('\n🏛 СЛОИ ТРАДИЦИЙ:'); for (final l in w.layers) { buffer.writeln('— ${l.title}: ${l.law}'); } } if (w.shadow.isNotEmpty) { buffer.writeln('\n🌑 ТЕНЬ: ${w.shadow}'); } if (w.asks.isNotEmpty) { buffer.writeln('\n❓ ВОПРОС: ${w.asks}'); } if (w.step.isNotEmpty) { buffer.writeln('\n⚡ ЖЕСТ: ${w.step}'); } if (w.binds.isNotEmpty) { buffer.writeln('\n🔗 СОЗВУЧИЯ: ${w.binds.join(", ")}'); } Clipboard.setData(ClipboardData(text: buffer.toString())); HapticFeedback.mediumImpact(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ const GradientStar(size: 18), const SizedBox(width: 10), Expanded(child: Text('Полное досье «${w.word}» скопировано')), ], ), behavior: SnackBarBehavior.floating, ), ); } void _shareSymbol() { final w = _currentWord; final buffer = StringBuffer(); buffer.writeln('🔮 ОРАКУЛ • САХРАЛЬНЫЙ ЗНАК #${w.formattedSessionNumber}'); buffer.writeln('«${w.word.toUpperCase()}» ${w.emoji}'); if (w.category.isNotEmpty) { buffer.writeln('Категория: ${w.category}'); } buffer.writeln(''); if (w.core.isNotEmpty) { buffer.writeln('📖 Суть:'); buffer.writeln(w.core); buffer.writeln(''); } buffer.writeln('🔮 Толкование:'); buffer.writeln(w.description); buffer.writeln(''); buffer.writeln('— Система сакрального жребия «Оракул»'); Clipboard.setData(ClipboardData(text: buffer.toString())); HapticFeedback.lightImpact(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ const Icon(Icons.share_rounded, color: Color(0xFFFFD54F), size: 18), const SizedBox(width: 10), Expanded(child: Text('Символ «${w.word}» скопирован для отправки!')), ], ), duration: const Duration(seconds: 2), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final w = _currentWord; return Scaffold( backgroundColor: const Color(0xFF0C0E1A), appBar: AppBar( title: Row( children: [ const GradientStar(size: 20), const SizedBox(width: 8), Text('#${w.formattedSessionNumber}'), ], ), actions: [ IconButton( tooltip: 'Предыдущий знак', icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20), onPressed: () { final prev = _repo.getPreviousWord(w.sessionNumber); _navigateToWord(prev); }, ), IconButton( tooltip: 'Случайный знак', icon: const Icon(Icons.shuffle_rounded, size: 22), onPressed: () { final rnd = _repo.getRandomWord(); _navigateToWord(rnd); }, ), IconButton( tooltip: 'Следующий знак', icon: const Icon(Icons.arrow_forward_ios_rounded, size: 20), onPressed: () { final next = _repo.getNextWord(w.sessionNumber); _navigateToWord(next); }, ), IconButton( tooltip: 'Редактировать толкование', icon: const Icon(Icons.edit_note_rounded, size: 24, color: Color(0xFF81C784)), onPressed: _openEditScreen, ), IconButton( tooltip: 'Скопировать всё досье', icon: const Icon(Icons.copy_rounded, size: 20), onPressed: _copyDossier, ), IconButton( tooltip: 'Поделиться символом', icon: const Icon(Icons.share_rounded, size: 21, color: Color(0xFFFFD54F)), onPressed: _shareSymbol, ), ], ), body: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 8, 16, 32), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Hero Title Card _buildHeroCard(theme, w), const SizedBox(height: 16), // Core Law Card if (w.core.isNotEmpty) ...[ _buildCoreCard(theme, w), const SizedBox(height: 16), ], // Symbolic Meaning & Canon Card (with parsed clickable stations) _buildDescriptionCard(theme, w), const SizedBox(height: 16), // Oracle Multi-paragraph Interpretation Card if (w.oracle.isNotEmpty) ...[ _buildOracleCard(theme, w), const SizedBox(height: 16), ], // Atlas Traditions Station Matches (Primary & Cluster) _buildTraditionsMatchesSection(theme, w), const SizedBox(height: 16), // Traditions / Layers Section if (w.layers.isNotEmpty) ...[ _buildLayersSection(theme, w), const SizedBox(height: 16), ], // Cultural branches and language notes written by hand or by a later merge _buildCulturesSection(w), _buildLanguagesSection(w), // Shadow, Asks, Step Grid / Cards _buildPracticeSection(theme, w), const SizedBox(height: 16), // Binds / Harmonious Connections if (w.binds.isNotEmpty) ...[ _buildBindsSection(theme, w), const SizedBox(height: 16), ], // Sources Anchors if (w.sources.isNotEmpty) ...[ _buildSourcesSection(theme, w), const SizedBox(height: 20), ], // Action Button: Ask Oracle about this word ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF2A2D4A), foregroundColor: const Color(0xFFFFF9C4), padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: const BorderSide(color: Color(0xFFFFD54F), width: 1), ), ), onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => const OracleChatScreen(), ), ); }, icon: const Icon(Icons.auto_awesome_rounded, color: Color(0xFFFFD54F)), label: Text( 'Спросить Оракул про «${w.word}»', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15), ), ), const SizedBox(height: 12), OutlinedButton.icon( style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFFA5D6A7), side: const BorderSide(color: Color(0xFF81C784)), padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), ), onPressed: _openEditScreen, icon: const Icon(Icons.edit_rounded, size: 18), label: Text( w.isUserEdited ? 'Изменить свою редакцию знака' : 'Редактировать канон знака', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15), ), ), ], ), ), ); } Widget _buildHeroCard(ThemeData theme, WordItem w) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFF1E2238), Color(0xFF151829)], ), borderRadius: BorderRadius.circular(24), border: Border.all(color: const Color(0xFF3949AB).withOpacity(0.5)), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.3), blurRadius: 16, offset: const Offset(0, 6), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( '#${w.formattedSessionNumber}', style: const TextStyle( color: Color(0xFFFFD54F), fontWeight: FontWeight.w900, fontSize: 22, letterSpacing: 1.2, ), ), const Spacer(), if (w.lookup.isNotEmpty) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: const Color(0xFF2A2D4A), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFF9FA8DA).withOpacity(0.3)), ), child: Text( 'Ранг ${w.lookup}', style: const TextStyle( color: Color(0xFFE8EAF6), fontSize: 11, fontWeight: FontWeight.bold, ), ), ), ], ), const SizedBox(height: 6), Text( w.word.toUpperCase(), style: const TextStyle( color: Color(0xFFFFF9C4), fontSize: 34, fontWeight: FontWeight.w900, letterSpacing: 1.5, ), ), const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: [ if (w.category.isNotEmpty) _buildPill(w.category, const Color(0xFFCE93D8)), if (w.partOfSpeech.isNotEmpty) _buildPill(w.partOfSpeech, const Color(0xFF90CAF9)), if (w.operator.isNotEmpty) _buildPill(w.localizedOperator, const Color(0xFF81C784)), if (w.valence.isNotEmpty) _buildPill(w.localizedValence, _getValenceColor(w.valence)), ], ), if (w.isUserEdited) ...[ const SizedBox(height: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: const Color(0xFF2E7D32).withOpacity(0.25), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFF81C784).withOpacity(0.7)), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.edit_note_rounded, size: 16, color: Color(0xFFA5D6A7)), SizedBox(width: 6), Text( 'Кастомная редакция (ваша правка)', style: TextStyle( color: Color(0xFFA5D6A7), fontSize: 12, fontWeight: FontWeight.w600, ), ), ], ), ), ], ], ), ); } Widget _buildCoreCard(ThemeData theme, WordItem w) { return Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: const Color(0xFF1E2238), borderRadius: BorderRadius.circular(20), border: Border.all(color: const Color(0xFFFFD54F).withOpacity(0.6), width: 1.5), boxShadow: [ BoxShadow( color: const Color(0xFFFFD54F).withOpacity(0.08), blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.stars_rounded, color: Color(0xFFFFD54F), size: 18), const SizedBox(width: 8), Text( _isStockText(w.core) ? 'ЯДРО ЗНАКА (ЗАКОН)' : 'ДЕНЬ · ЯДРО ЗНАКА', style: const TextStyle( color: Color(0xFFFFD54F), fontSize: 12, fontWeight: FontWeight.w900, letterSpacing: 1.0, ), ), ], ), const SizedBox(height: 10), Text( w.core, style: const TextStyle( color: Color(0xFFFFFFFF), fontSize: 16, height: 1.45, fontWeight: FontWeight.w600, ), ), if (w.grammar?.modifies.isNotEmpty == true) ...[ const SizedBox(height: 10), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: const Color(0xFF121424), borderRadius: BorderRadius.circular(10), ), child: Text( 'В связке: ${w.grammar!.modifies}', style: const TextStyle( color: Color(0xFFB0BEC5), fontSize: 13, fontStyle: FontStyle.italic, ), ), ), ], ], ), ); } Widget _buildDescriptionCard(ThemeData theme, WordItem w) { if (w.description.isEmpty) return const SizedBox.shrink(); // Parse [Система: Детали] brackets final bracketRe = RegExp(r'\[([^:\]]+):\s*([^\]]+)\]'); final matches = bracketRe.allMatches(w.description); // w.cleanDescription is already cleaned of bracket tags return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: const Color(0xFF151829), borderRadius: BorderRadius.circular(20), border: Border.all(color: const Color(0xFF2A2D4A)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ GradientStar(size: 18), SizedBox(width: 8), Text( 'СИМВОЛИЧЕСКИЙ СМЫСЛ И КАНОН', style: TextStyle( color: Color(0xFFFFD54F), fontSize: 12, fontWeight: FontWeight.w900, letterSpacing: 1.0, ), ), ], ), const SizedBox(height: 12), Text( w.cleanDescription, style: const TextStyle( color: Color(0xFFE0E2EC), fontSize: 15, height: 1.6, ), ), if (matches.isNotEmpty) ...[ const SizedBox(height: 14), const Text( 'СИСТЕМНЫЕ СВЯЗИ И СТАНЦИИ:', style: TextStyle( color: Color(0xFF80DEEA), fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 0.8, ), ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: [ for (final m in matches) ...[ Builder( builder: (context) { final sys = m.group(1)!.trim(); final det = m.group(2)!.trim(); return ActionChip( avatar: Icon(_getSystemIcon(sys), size: 15, color: const Color(0xFFFFD54F)), label: Text('[${WordItemLocalization.localizeSystem(sys)}: ${WordItemLocalization.cleanTagDetail(det)}]'), labelStyle: const TextStyle( color: Color(0xFFFFF9C4), fontSize: 12, fontWeight: FontWeight.bold, ), backgroundColor: const Color(0xFF232742), side: BorderSide(color: const Color(0xFFFFD54F).withValues(alpha: 0.5)), onPressed: () => _openStationBySystemAndDetail(sys, det), ); }, ), ], ], ), ], ], ), ); } Widget _buildOracleCard(ThemeData theme, WordItem w) { if (w.oracle.isEmpty) return const SizedBox.shrink(); return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: const Color(0xFF151829), borderRadius: BorderRadius.circular(20), border: Border.all(color: const Color(0xFF2A2D4A)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ Icon(Icons.auto_stories_rounded, color: Color(0xFF80DEEA), size: 18), SizedBox(width: 8), Text( 'ТОЛКОВАНИЕ И СТАТЬЯ ОРАКУЛА', style: TextStyle( color: Color(0xFF80DEEA), fontSize: 12, fontWeight: FontWeight.w900, letterSpacing: 1.0, ), ), ], ), const SizedBox(height: 14), _buildRichArticleContent(w.oracle), ], ), ); } Widget _buildRichArticleContent(String rawText) { final lines = rawText.split('\n'); final widgets = []; for (var i = 0; i < lines.length; i++) { final line = lines[i].trim(); if (line.isEmpty) { widgets.add(const SizedBox(height: 6)); continue; } if (line.startsWith('### ') || line.startsWith('## ')) { final title = line.replaceFirst(RegExp(r'^#{2,3}\s*'), ''); widgets.add(Padding( padding: const EdgeInsets.only(top: 10, bottom: 6), child: Row( children: [ Container( width: 3, height: 14, decoration: BoxDecoration( color: const Color(0xFF80DEEA), borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 8), Expanded( child: Text( title, style: const TextStyle( color: Color(0xFF80DEEA), fontSize: 14, fontWeight: FontWeight.bold, letterSpacing: 0.5, ), ), ), ], ), )); } else if (line.startsWith('- ') || line.startsWith('• ') || line.startsWith('* ')) { final content = line.substring(2).trim(); widgets.add(Padding( padding: const EdgeInsets.only(bottom: 6), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: EdgeInsets.only(top: 6, right: 8), child: Icon(Icons.circle, size: 5, color: Color(0xFFFFD54F)), ), Expanded( child: _buildInlineFormattedText(content, fontSize: 14, height: 1.45), ), ], ), )); } else { widgets.add(Padding( padding: const EdgeInsets.only(bottom: 8), child: _buildInlineFormattedText(line, fontSize: 14.5, height: 1.55), )); } } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: widgets, ); } Widget _buildInlineFormattedText(String text, {double fontSize = 14, double height = 1.5}) { final spans = []; final parts = text.split('**'); for (var i = 0; i < parts.length; i++) { if (parts[i].isEmpty) continue; final isBold = i % 2 == 1; spans.add(TextSpan( text: parts[i], style: TextStyle( color: isBold ? const Color(0xFFFFF9C4) : const Color(0xFFE0E2EC), fontWeight: isBold ? FontWeight.bold : FontWeight.normal, ), )); } return Text.rich( TextSpan(children: spans), style: TextStyle(fontSize: fontSize, height: height), ); } Widget _buildLayersSection(ThemeData theme, WordItem w) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.layers_rounded, color: Color(0xFFFFD54F), size: 20), const SizedBox(width: 8), Text( 'СЛОИ ТРАДИЦИЙ (${w.layers.length})', style: const TextStyle( color: Color(0xFFFFF9C4), fontSize: 14, fontWeight: FontWeight.w900, letterSpacing: 1.0, ), ), ], ), const SizedBox(height: 12), for (final layer in w.layers) Builder( builder: (context) { final canonId = _systemToCanonId[layer.system.toLowerCase()] ?? _systemToCanonId[layer.title.toLowerCase()]; final canon = canonId != null ? TraditionsRepository().byId(canonId) : null; final station = canon != null ? _matchStation(canon.id, '${layer.title} ${layer.law}') : null; return Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF181C30), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFF2A2D4A)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(_getSystemIcon(layer.system), color: const Color(0xFFFFD54F), size: 18), const SizedBox(width: 8), Expanded( child: Text( layer.title, style: const TextStyle( color: Color(0xFFFFD54F), fontSize: 14, fontWeight: FontWeight.bold, ), ), ), if (canon != null) TextButton.icon( style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), icon: const Icon(Icons.open_in_new_rounded, size: 14, color: Color(0xFF80DEEA)), label: Text( station != null ? 'Станция' : 'В атлас', style: const TextStyle(fontSize: 12, color: Color(0xFF80DEEA), fontWeight: FontWeight.bold), ), onPressed: () { if (station != null) { _openStation(canon, station); } else { _showCanonOverviewSheet(canon, layer.title); } }, ), ], ), const SizedBox(height: 8), Text( layer.law, style: const TextStyle( color: Color(0xFFD1D5DB), fontSize: 14, height: 1.5, ), ), ], ), ); }, ), ], ); } Widget _buildTraditionsMatchesSection(ThemeData theme, WordItem w) { final matches = TraditionsRepository().findMatchesForWord(w.word, wordId: w.id); if (matches.isEmpty) return const SizedBox.shrink(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.auto_awesome_motion_rounded, color: Color(0xFF80DEEA), size: 20), const SizedBox(width: 8), Text( 'СТАНЦИИ В КАНОНЕ АТЛАСА (${matches.length})', style: const TextStyle( color: Color(0xFF80DEEA), fontSize: 14, fontWeight: FontWeight.w900, letterSpacing: 1.0, ), ), ], ), const SizedBox(height: 12), for (final m in matches) Container( margin: const EdgeInsets.only(bottom: 10), decoration: BoxDecoration( color: const Color(0xFF181C30), borderRadius: BorderRadius.circular(16), border: Border.all( color: m.isPrimary ? const Color(0xFFFFD54F).withValues(alpha: 0.5) : const Color(0xFF2A2D4A), ), ), child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), leading: Container( width: 38, height: 38, alignment: Alignment.center, decoration: BoxDecoration( color: const Color(0xFF232742), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.3)), ), child: Text( m.station.symbol.isNotEmpty ? m.station.symbol : m.tradition.icon, style: const TextStyle(fontSize: 18, color: Color(0xFFFFD54F)), ), ), title: Row( children: [ Expanded( child: Text( m.station.name, style: const TextStyle( color: Color(0xFFFFF9C4), fontSize: 14, fontWeight: FontWeight.bold, ), ), ), if (m.isPrimary) Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: const Color(0xFF4A3E1E), borderRadius: BorderRadius.circular(6), border: Border.all(color: const Color(0xFFFFD54F)), ), child: const Text( 'Первичная', style: TextStyle( color: Color(0xFFFFD54F), fontSize: 10, fontWeight: FontWeight.w900, ), ), ), ], ), subtitle: Text( '${m.tradition.name} • ${m.station.group}', style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 12), ), trailing: const Icon(Icons.arrow_forward_ios_rounded, size: 14, color: Color(0xFFFFD54F)), onTap: () => _openStation(m.tradition, m.station), ), ), ], ); } Widget _buildCulturesSection(WordItem w) { final rows = orderedCultures(w.cultures); if (rows.isEmpty) return const SizedBox.shrink(); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Padding( padding: EdgeInsets.only(bottom: 8), child: Text( 'КУЛЬТУРНЫЕ ВЕТВИ', style: TextStyle( color: Color(0xFFFFD54F), fontSize: 12, fontWeight: FontWeight.w900, letterSpacing: 0.8, ), ), ), for (final row in rows) ...[ Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF151829), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0x33FFD54F)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${cultureBranchByName(row.key)?.emoji ?? "🌍"} ${row.key}', style: const TextStyle( color: Color(0xFFFFF9C4), fontSize: 14, fontWeight: FontWeight.w800, ), ), const SizedBox(height: 8), SelectableText( row.value, style: const TextStyle(color: Color(0xFFE0E2EC), fontSize: 14, height: 1.5), ), ], ), ), ], const SizedBox(height: 6), ], ); } Widget _buildLanguagesSection(WordItem w) { final rows = w.languages.entries.where((e) => e.value.trim().isNotEmpty).toList() ..sort((a, b) => a.key.compareTo(b.key)); if (rows.isEmpty) return const SizedBox.shrink(); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Padding( padding: EdgeInsets.only(bottom: 8), child: Text( 'ЯЗЫКИ', style: TextStyle( color: Color(0xFFFFD54F), fontSize: 12, fontWeight: FontWeight.w900, letterSpacing: 0.8, ), ), ), for (final row in rows) Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF151829), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0x33FFD54F)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( row.key, style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 14, fontWeight: FontWeight.w800), ), const SizedBox(height: 8), SelectableText( row.value, style: const TextStyle(color: Color(0xFFE0E2EC), fontSize: 14, height: 1.5), ), ], ), ), const SizedBox(height: 6), ], ); } Widget _buildPracticeSection(ThemeData theme, WordItem w) { final hasShadow = w.shadow.isNotEmpty; final hasAsks = w.asks.isNotEmpty; final hasStep = w.step.isNotEmpty; if (!hasShadow && !hasAsks && !hasStep) return const SizedBox.shrink(); return Column( children: [ if (hasShadow) _buildPracticeTile( icon: Icons.dark_mode_rounded, title: _isStockText(w.shadow) ? 'ТЕНЬ ЗНАКА' : 'НОЧЬ · ТЕНЬ ЗНАКА', accentColor: const Color(0xFFEF5350), content: w.shadow, ), if (hasAsks) _buildPracticeTile( icon: Icons.help_outline_rounded, title: 'ВОПРОС СПРАШИВАЮЩЕМУ', accentColor: const Color(0xFF80DEEA), content: w.asks, ), if (hasStep) _buildPracticeTile( icon: Icons.bolt_rounded, title: 'НЕОБРАТИМЫЙ ЖЕСТ', accentColor: const Color(0xFFFFD54F), content: w.step, ), ], ); } Widget _buildPracticeTile({ required IconData icon, required String title, required Color accentColor, required String content, }) { return Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF151829), borderRadius: BorderRadius.circular(16), border: Border.all(color: accentColor.withOpacity(0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(icon, color: accentColor, size: 18), const SizedBox(width: 8), Text( title, style: TextStyle( color: accentColor, fontSize: 12, fontWeight: FontWeight.w900, letterSpacing: 0.8, ), ), ], ), const SizedBox(height: 8), Text( content, style: const TextStyle( color: Color(0xFFE0E2EC), fontSize: 14, height: 1.5, ), ), ], ), ); } Widget _buildBindsSection(ThemeData theme, WordItem w) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF151829), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFF2A2D4A)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ Icon(Icons.link_rounded, color: Color(0xFF80DEEA), size: 18), SizedBox(width: 8), Text( 'СОЗВУЧИЯ И СВЯЗКИ (BINDS)', style: TextStyle( color: Color(0xFF80DEEA), fontSize: 12, fontWeight: FontWeight.w900, letterSpacing: 0.8, ), ), ], ), const SizedBox(height: 10), Wrap( spacing: 8, runSpacing: 8, children: [ for (final boundWord in w.binds) ActionChip( label: Text(boundWord), labelStyle: const TextStyle( color: Color(0xFFFFF9C4), fontSize: 13, fontWeight: FontWeight.bold, ), backgroundColor: const Color(0xFF2A2D4A), side: const BorderSide(color: Color(0xFF3949AB)), onPressed: () { final target = _repo.getWordByName(boundWord); if (target != null) { _navigateToWord(target); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Знак «$boundWord» не найден в текущей выборке'), duration: const Duration(seconds: 1), ), ); } }, ), ], ), ], ), ); } Widget _buildSourcesSection(ThemeData theme, WordItem w) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF151829), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFF2A2D4A)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ Icon(Icons.anchor_rounded, color: Color(0xFF9FA8DA), size: 18), SizedBox(width: 8), Text( 'ПЕРВИЧНЫЕ ЯКОРЯ В СИСТЕМАХ', style: TextStyle( color: Color(0xFF9FA8DA), fontSize: 12, fontWeight: FontWeight.w900, letterSpacing: 0.8, ), ), ], ), const SizedBox(height: 10), Wrap( spacing: 8, runSpacing: 8, children: [ for (final src in w.sources) InkWell( onTap: () => _openStationBySystemAndDetail(src.system, src.ref), borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: const Color(0xFF1E2238), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFF3949AB).withOpacity(0.5)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Builder( builder: (context) { final sysRu = WordItemLocalization.localizeSystem(src.system); final refRu = WordItemLocalization.localizeSourceRef(src.system, src.ref); return Text( '$sysRu: $refRu', style: const TextStyle( color: Color(0xFFFFF9C4), fontSize: 12, fontWeight: FontWeight.bold, ), ); }, ), const SizedBox(width: 4), const Icon(Icons.arrow_forward_ios_rounded, size: 10, color: Color(0xFFFFD54F)), ], ), ), ), ], ), ], ), ); } Widget _buildPill(String label, Color color) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: color.withOpacity(0.12), borderRadius: BorderRadius.circular(8), border: Border.all(color: color.withOpacity(0.4)), ), child: Text( label, style: TextStyle( color: color, fontSize: 12, fontWeight: FontWeight.bold, ), ), ); } }