Files
999/lib/screens/ogham_divination_screen.dart
T

1056 lines
40 KiB
Dart

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