Files
999/lib/screens/symbol_detail_screen.dart

1704 lines
59 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<SymbolDetailScreen> createState() => _SymbolDetailScreenState();
}
class _SymbolDetailScreenState extends State<SymbolDetailScreen> {
late WordItem _currentWord;
final _repo = WordsRepository();
@override
void initState() {
super.initState();
_currentWord = widget.word;
}
static const Map<String, String> _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<void> _openEditScreen() async {
final res = await Navigator.push<bool>(
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 = <Widget>[];
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 = <TextSpan>[];
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,
),
),
);
}
}