1056 lines
40 KiB
Dart
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),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|