import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../models/tradition_model.dart'; import '../screens/lenormand_extras_screen.dart'; import '../screens/lenormand_station_screen.dart'; import '../services/lenormand_pairs_repository.dart'; import '../services/ogham_feda_repository.dart'; import '../services/tarot_majors_repository.dart'; typedef OpenLexiconWord = void Function(int id, String word); class TraditionsAtlasView extends StatelessWidget { final List traditions; final TraditionCanon? selected; final String searchQuery; final String? focusStationId; final ValueChanged onSelectTradition; final VoidCallback onBackToOverview; final OpenLexiconWord onOpenWord; const TraditionsAtlasView({ super.key, required this.traditions, required this.selected, required this.searchQuery, this.focusStationId, required this.onSelectTradition, required this.onBackToOverview, required this.onOpenWord, }); @override Widget build(BuildContext context) { if (selected == null) { return _Overview( traditions: traditions, onSelect: onSelectTradition, ); } return _TraditionStations( tradition: selected!, traditions: traditions, searchQuery: searchQuery, focusStationId: focusStationId, onBack: onBackToOverview, onOpenWord: onOpenWord, onSelectTradition: onSelectTradition, ); } } class _Overview extends StatelessWidget { final List traditions; final ValueChanged onSelect; const _Overview({required this.traditions, required this.onSelect}); @override Widget build(BuildContext context) { return ListView( padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), children: [ Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xCC1E2238), Color(0xE6151829)], ), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0x4DFFD54F)), ), child: const Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '🏛️ Сакральные каноны', style: TextStyle( color: Color(0xFFFFF9C4), fontSize: 20, fontWeight: FontWeight.w900, ), ), SizedBox(height: 6), Text( 'Выберите традицию, чтобы открыть её станции, первородные архетипы и созвучия базы 999 слов.', style: TextStyle(color: Color(0xFFCBD5E1), fontSize: 13, height: 1.45), ), ], ), ), const SizedBox(height: 8), Align( alignment: Alignment.centerRight, child: Text( '${traditions.length} великих систем', style: const TextStyle( color: Color(0xFFFFD54F), fontWeight: FontWeight.w800, fontSize: 13, ), ), ), const SizedBox(height: 12), ...traditions.map( (t) => Padding( padding: const EdgeInsets.only(bottom: 12), child: _TraditionCard(tradition: t, onTap: () => onSelect(t)), ), ), ], ); } } class _TraditionCard extends StatelessWidget { final TraditionCanon tradition; final VoidCallback onTap; const _TraditionCard({required this.tradition, required this.onTap}); @override Widget build(BuildContext context) { return Material( color: const Color(0xFF151829), borderRadius: BorderRadius.circular(16), child: InkWell( onTap: () { HapticFeedback.selectionClick(); onTap(); }, borderRadius: BorderRadius.circular(16), child: Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0x1AFFFFFF)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 52, height: 52, alignment: Alignment.center, decoration: BoxDecoration( color: const Color(0x1AFFD54F), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0x4DFFD54F)), ), child: Text(tradition.icon, style: const TextStyle(fontSize: 26)), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( tradition.name, style: const TextStyle( color: Color(0xFFFFF9C4), fontWeight: FontWeight.w900, fontSize: 16, ), ), const SizedBox(height: 2), Text( '${tradition.count} сакральных станций', style: const TextStyle( color: Color(0xFF90CDF4), fontSize: 12, fontWeight: FontWeight.w600, ), ), ], ), ), ], ), const SizedBox(height: 10), Text( tradition.description, style: const TextStyle( color: Color(0xFFCBD5E1), fontSize: 13, height: 1.45, ), ), const SizedBox(height: 12), const Row( children: [ Text( 'Открыть атлас традиции', style: TextStyle( color: Color(0xFFFFD54F), fontWeight: FontWeight.w700, fontSize: 13, ), ), SizedBox(width: 6), Icon(Icons.arrow_forward_rounded, color: Color(0xFFFFD54F), size: 16), ], ), ], ), ), ), ); } } class _TraditionStations extends StatefulWidget { final TraditionCanon tradition; final List traditions; final String searchQuery; final String? focusStationId; final VoidCallback onBack; final OpenLexiconWord onOpenWord; final ValueChanged onSelectTradition; const _TraditionStations({ required this.tradition, required this.traditions, required this.searchQuery, required this.focusStationId, required this.onBack, required this.onOpenWord, required this.onSelectTradition, }); @override State<_TraditionStations> createState() => _TraditionStationsState(); } class _TraditionStationsState extends State<_TraditionStations> { final Map _stationKeys = {}; String? _highlightedId; TraditionCanon get tradition => widget.tradition; String get searchQuery => widget.searchQuery; VoidCallback get onBack => widget.onBack; OpenLexiconWord get onOpenWord => widget.onOpenWord; bool get _isLenormand => tradition.id == 'lenormand'; bool get _isTarot => tradition.id == 'tarot'; bool get _isOgham => tradition.id == 'ogham'; bool get _hasDeck => _isLenormand || _isTarot || _isOgham; GlobalKey _keyFor(String id) => _stationKeys.putIfAbsent(id, GlobalKey.new); @override void initState() { super.initState(); _scheduleFocus(); } @override void didUpdateWidget(covariant _TraditionStations oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.focusStationId != widget.focusStationId || oldWidget.tradition.id != widget.tradition.id) { _scheduleFocus(); } } void _scheduleFocus() { final id = widget.focusStationId; if (id == null || id.isEmpty) return; WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; final ctx = _stationKeys[id]?.currentContext; if (ctx != null) { Scrollable.ensureVisible( ctx, alignment: 0.25, duration: const Duration(milliseconds: 320), curve: Curves.easeOut, ); } setState(() => _highlightedId = id); }); } @override Widget build(BuildContext context) { final stations = tradition.stations .where((s) => s.matchesQuery(searchQuery)) .toList(); final groups = >{}; final order = []; for (final st in stations) { final key = st.groupKey.isEmpty ? tradition.name : st.groupKey; if (!groups.containsKey(key)) { groups[key] = []; order.add(key); } groups[key]!.add(st); } final hasGroups = order.length > 1; return ListView( padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), children: [ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xCC1E2238), Color(0xE6151829)], ), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0x4DFFD54F)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ IconButton( tooltip: 'К списку слов', onPressed: onBack, icon: const Icon(Icons.arrow_back_rounded, color: Color(0xFFFFD54F)), ), Text(tradition.icon, style: const TextStyle(fontSize: 26)), const SizedBox(width: 10), Expanded( child: Text( tradition.name, style: const TextStyle( color: Color(0xFFFFF9C4), fontSize: 18, fontWeight: FontWeight.w900, ), ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: const Color(0x26FFD54F), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0x66FFD54F)), ), child: Text( '${stations.length} из ${tradition.count}', style: const TextStyle( color: Color(0xFFFFD54F), fontWeight: FontWeight.w800, fontSize: 12, ), ), ), ], ), Padding( padding: const EdgeInsets.fromLTRB(12, 4, 8, 4), child: Text( tradition.description, style: const TextStyle(color: Color(0xFFCBD5E1), fontSize: 13, height: 1.45), ), ), if (_isLenormand) Padding( padding: const EdgeInsets.fromLTRB(12, 8, 8, 4), child: OutlinedButton.icon( onPressed: () { HapticFeedback.selectionClick(); Navigator.of(context).push( MaterialPageRoute( builder: (_) => const LenormandExtrasScreen(), ), ); }, icon: const Icon(Icons.layers_outlined, color: Color(0xFFFFD54F), size: 18), label: const Text( 'Триады, любовь, работа, карма', style: TextStyle(color: Color(0xFFFFD54F), fontWeight: FontWeight.w700), ), style: OutlinedButton.styleFrom( side: const BorderSide(color: Color(0x66FFD54F)), ), ), ), ], ), ), const SizedBox(height: 8), if (stations.isEmpty) const Padding( padding: EdgeInsets.only(top: 48), child: Center( child: Text( 'Станций не найдено', style: TextStyle(color: Color(0xFF8C93B8), fontWeight: FontWeight.bold), ), ), ) else for (final gKey in order) ...[ if (hasGroups) _GroupHeader(label: gKey), for (final st in groups[gKey]!) Padding( key: _keyFor(st.id), padding: const EdgeInsets.only(bottom: 12), child: _StationCard( station: st, highlighted: st.id == _highlightedId, onOpenWord: onOpenWord, deckButtonLabel: _isOgham ? 'Значение феды' : (_isTarot ? 'Значение аркана и пары' : 'Описания и сочетания'), onOpenLenormand: _hasDeck ? () { HapticFeedback.selectionClick(); if (_isTarot) { final card = TarotMajorsRepository().byCanonId(st.id); if (card == null) return; Navigator.of(context).push( MaterialPageRoute( builder: (_) => LenormandStationScreen( number: card.n, lookup: TarotMajorsRepository().byNumber, title: 'Таро', ), ), ); return; } if (_isOgham) { final card = OghamFedaRepository().byCanonId(st.id); if (card == null) return; Navigator.of(context).push( MaterialPageRoute( builder: (_) => LenormandStationScreen( number: card.n, lookup: OghamFedaRepository().byNumber, title: 'Огам', ), ), ); return; } final card = LenormandPairsRepository().byStationId(st.id); if (card == null) return; Navigator.of(context).push( MaterialPageRoute( builder: (_) => LenormandStationScreen(number: card.n), ), ); } : null, ), ), ], ], ); } } class _GroupHeader extends StatelessWidget { final String label; const _GroupHeader({required this.label}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(0, 16, 0, 10), child: Row( children: [ const Expanded(child: Divider(color: Color(0x4DFBBF24), height: 1)), Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( color: const Color(0x14FBBF24), borderRadius: BorderRadius.circular(20), border: Border.all(color: const Color(0x40FBBF24)), ), child: Text( label.toUpperCase(), style: const TextStyle( color: Color(0xFFFBBF24), fontSize: 11, fontWeight: FontWeight.w800, letterSpacing: 0.8, ), ), ), ), const Expanded(child: Divider(color: Color(0x4DFBBF24), height: 1)), ], ), ); } } class _StationCard extends StatelessWidget { final TraditionStation station; final bool highlighted; final OpenLexiconWord onOpenWord; final VoidCallback? onOpenLenormand; final String? deckButtonLabel; const _StationCard({ required this.station, required this.onOpenWord, this.highlighted = false, this.onOpenLenormand, this.deckButtonLabel, }); @override Widget build(BuildContext context) { final sub = station.arcanum?.isNotEmpty == true ? station.arcanum! : station.groupSub; final primary = station.primary; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF151829), borderRadius: BorderRadius.circular(16), border: Border.all( color: highlighted ? const Color(0xFFFFD54F) : const Color(0x1AFFFFFF), width: highlighted ? 1.6 : 1, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 52, height: 52, alignment: Alignment.center, decoration: BoxDecoration( color: const Color(0x1AFFD54F), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0x4DFFD54F)), ), child: station.geomancyRows.isNotEmpty ? _GeomancyGlyph(rows: station.geomancyRows) : Text( station.symbol, style: const TextStyle( fontSize: 22, color: Color(0xFFFFD54F), height: 1.1, ), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text.rich( TextSpan( text: station.name, style: const TextStyle( color: Color(0xFFFFF9C4), fontWeight: FontWeight.w900, fontSize: 15, height: 1.25, ), children: [ if (station.hebrew != null && station.hebrew!.isNotEmpty) TextSpan( text: ' ${station.hebrew}', style: const TextStyle( color: Color(0xD9D4A855), fontWeight: FontWeight.w500, fontSize: 18, height: 1.2, ), ), if (station.hanzi != null && station.hanzi!.isNotEmpty) TextSpan( text: ' ${station.hanzi}', style: const TextStyle( color: Color(0xD9D4A855), fontWeight: FontWeight.w600, fontSize: 18, height: 1.2, ), ), if (station.arabic != null && station.arabic!.isNotEmpty) TextSpan( text: ' ${station.arabic}', style: const TextStyle( color: Color(0xD9D4A855), fontWeight: FontWeight.w500, fontSize: 17, height: 1.2, ), ), ], ), ), if (sub.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 2), child: Text( sub, style: const TextStyle( color: Color(0xFF90CDF4), fontSize: 12, fontWeight: FontWeight.w600, ), ), ), ], ), ), ], ), const SizedBox(height: 10), Text( station.lore, style: const TextStyle( color: Color(0xFFD1D5DB), fontSize: 13, height: 1.45, ), ), if (primary != null) ...[ const SizedBox(height: 12), Material( color: const Color(0x14FFD54F), borderRadius: BorderRadius.circular(10), child: InkWell( onTap: () => onOpenWord(primary.id, primary.word), borderRadius: BorderRadius.circular(10), child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0x4DFFD54F)), ), child: Row( children: [ if (primary.emoji.isNotEmpty) Text(primary.emoji, style: const TextStyle(fontSize: 16)), if (primary.emoji.isNotEmpty) const SizedBox(width: 8), Expanded( child: Text( primary.word.toUpperCase(), style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w800, fontSize: 14, letterSpacing: 0.4, ), ), ), const Icon(Icons.chevron_right_rounded, color: Color(0xFFFFD54F), size: 18), ], ), ), ), ), ], if (station.cluster.isNotEmpty) ...[ const SizedBox(height: 12), Wrap( spacing: 6, runSpacing: 6, children: [ for (final cw in station.cluster) InkWell( onTap: () => onOpenWord(cw.id, cw.word), borderRadius: BorderRadius.circular(6), child: Tooltip( message: 'Открыть знак «${cw.word}»', child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: const Color(0x0FFFFFFF), borderRadius: BorderRadius.circular(6), border: Border.all(color: const Color(0x1FFFFFFF)), ), child: Text( '${cw.emoji} ${cw.word}', style: const TextStyle( color: Color(0xFFE2E8F0), fontSize: 12, fontWeight: FontWeight.w600, ), ), ), ), ), ], ), ], if (onOpenLenormand != null) ...[ const SizedBox(height: 12), SizedBox( width: double.infinity, child: FilledButton.tonalIcon( onPressed: onOpenLenormand, icon: const Icon(Icons.hub_outlined, size: 18), label: Text(deckButtonLabel ?? 'Описания и сочетания'), style: FilledButton.styleFrom( backgroundColor: const Color(0x26FFD54F), foregroundColor: const Color(0xFFFFD54F), textStyle: const TextStyle(fontWeight: FontWeight.w800), ), ), ), ], ], ), ); } } /// Four-row geomantic figure: one centered dot or a centered pair per row. class _GeomancyGlyph extends StatelessWidget { final List rows; const _GeomancyGlyph({required this.rows}); @override Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ for (final n in rows) Padding( padding: const EdgeInsets.symmetric(vertical: 1.5), child: Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ for (int i = 0; i < n; i++) ...[ if (i > 0) const SizedBox(width: 5), Container( width: 7, height: 7, decoration: const BoxDecoration( color: Color(0xFFFFD54F), shape: BoxShape.circle, ), ), ], ], ), ), ], ); } }