1704 lines
59 KiB
Dart
1704 lines
59 KiB
Dart
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,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|