Files
999/lib/screens/tarot_divination_screen.dart
T

512 lines
19 KiB
Dart

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/divination_layout.dart';
import '../models/syncretic_session_model.dart';
import '../models/tradition_model.dart';
import '../models/word_model.dart';
import '../services/traditions_repository.dart';
import '../services/words_repository.dart';
import '../widgets/divination_instruction_modal.dart';
import 'lenormand_station_screen.dart';
import '../services/tarot_majors_repository.dart';
class TarotDivinationScreen extends StatefulWidget {
final String? initialQuestion;
const TarotDivinationScreen({super.key, this.initialQuestion});
@override
State<TarotDivinationScreen> createState() => _TarotDivinationScreenState();
}
class _TarotDivinationScreenState extends State<TarotDivinationScreen> with SingleTickerProviderStateMixin {
late TabController _tabController;
final _questionController = TextEditingController();
final _repo = WordsRepository();
List<TraditionStation> _arcana = [];
bool _isShuffling = false;
// Layouts: 1 arcanum, 3 arcana, 5 arcana
DivinationLayout _selectedLayout = DivinationLayouts.tarotLayouts[0];
int _currentStepIndex = 0;
bool _isManualReversed = false;
// Drawn results
List<({TraditionStation arcanum, bool isReversed, String positionTitle})> _drawnResults = [];
bool _savedToSession = false;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
if (widget.initialQuestion != null) {
_questionController.text = widget.initialQuestion!;
}
_loadArcana();
}
void _loadArcana() {
final canon = TraditionsRepository().byId('tarot');
if (canon != null) {
_arcana = canon.stations;
}
}
@override
void dispose() {
_tabController.dispose();
_questionController.dispose();
super.dispose();
}
void _onLayoutChanged(DivinationLayout layout) {
setState(() {
_selectedLayout = layout;
_currentStepIndex = 0;
_drawnResults.clear();
_savedToSession = false;
});
}
Future<void> _drawRandomCardForStep() async {
if (_arcana.isEmpty || _currentStepIndex >= _selectedLayout.count) return;
setState(() => _isShuffling = true);
HapticFeedback.mediumImpact();
final rnd = Random();
await Future.delayed(const Duration(milliseconds: 350));
final finalIdx = rnd.nextInt(_arcana.length);
final isRev = rnd.nextBool() && rnd.nextBool(); // 25% chance
final posTitle = _selectedLayout.positionTitles[_currentStepIndex];
if (mounted) {
setState(() {
_isShuffling = false;
_drawnResults.add((arcanum: _arcana[finalIdx], isReversed: isRev, positionTitle: posTitle));
_currentStepIndex++;
if (_currentStepIndex >= _selectedLayout.count) {
_saveToSession('dice');
}
});
HapticFeedback.heavyImpact();
}
}
void _selectManualArcanum(int idx) {
if (idx < 0 || idx >= _arcana.length || _currentStepIndex >= _selectedLayout.count) return;
final posTitle = _selectedLayout.positionTitles[_currentStepIndex];
setState(() {
_drawnResults.add((arcanum: _arcana[idx], isReversed: _isManualReversed, positionTitle: posTitle));
_isManualReversed = false;
_currentStepIndex++;
if (_currentStepIndex >= _selectedLayout.count) {
_saveToSession('manual');
}
});
HapticFeedback.selectionClick();
}
void _resetSpread() {
setState(() {
_currentStepIndex = 0;
_drawnResults.clear();
_savedToSession = false;
});
}
Future<void> _saveToSession(String inputMethod) async {
if (_drawnResults.isEmpty) return;
final items = _drawnResults.map((r) {
WordItem? linkedWord;
if (r.arcanum.primary != null) {
linkedWord = _repo.getWordById(r.arcanum.primary!.id) ?? _repo.getWordByName(r.arcanum.primary!.word);
}
return ReadingSignItem(
title: '${r.positionTitle}: ${r.arcanum.name}',
subtitle: r.arcanum.group,
symbol: r.arcanum.symbol.isNotEmpty ? r.arcanum.symbol : '🎴',
lore: r.arcanum.lore,
isReversed: r.isReversed,
linkedWord: linkedWord,
);
}).toList();
final reading = SyncreticReading(
id: 'reading_${DateTime.now().millisecondsSinceEpoch}',
systemId: 'tarot',
systemName: 'Таро (Старшие Арканы)',
systemIcon: '🎴',
timestamp: DateTime.now(),
layoutTitle: '${_selectedLayout.title} (${inputMethod == "dice" ? "колода" : "со стола"})',
inputMethod: inputMethod,
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('Расклад Таро «${_selectedLayout.title}» сохранён в сессию дня!'),
backgroundColor: const Color(0xFF2E7D32),
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 2),
),
);
}
}
@override
Widget build(BuildContext context) {
final isSpreadComplete = _currentStepIndex >= _selectedLayout.count;
return Scaffold(
backgroundColor: const Color(0xFF0C0E1A),
appBar: AppBar(
actions: [
IconButton(
tooltip: 'Инструкция по Таро',
icon: const Icon(Icons.help_outline_rounded, color: Color(0xFFFFD54F)),
onPressed: () => DivinationInstructionModal.show(context, 'tarot'),
),
if (_drawnResults.isNotEmpty)
IconButton(
tooltip: 'Сбросить расклад',
icon: const Icon(Icons.refresh_rounded),
onPressed: _resetSpread,
),
],
title: const Row(
children: [
Text('🎴', style: TextStyle(fontSize: 22)),
SizedBox(width: 8),
Text('Таро (Старшие Арканы)', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 17)),
],
),
bottom: TabBar(
controller: _tabController,
indicatorColor: const Color(0xFFFFD54F),
tabs: const [
Tab(icon: Icon(Icons.style_rounded, size: 18), text: 'Жребий колоды'),
Tab(icon: Icon(Icons.touch_app_rounded, size: 18), text: 'Выбор из 22 Арканов'),
],
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: _questionController,
style: const TextStyle(color: Colors.white, fontSize: 14),
decoration: InputDecoration(
hintText: 'Вопрос к Старшим Арканам...',
hintStyle: const TextStyle(color: Color(0xFF8C93B8), fontSize: 13),
prefixIcon: const Icon(Icons.help_outline_rounded, color: Color(0xFFFFD54F), size: 18),
filled: true,
fillColor: const Color(0xFF151829),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(14)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Color(0xFF2A2D4A)),
),
),
),
const SizedBox(height: 14),
// Layout Picker Row
Row(
children: [
const Icon(Icons.dashboard_customize_rounded, size: 16, color: Color(0xFFFFD54F)),
const SizedBox(width: 6),
const Text('РАСКЛАД:', style: TextStyle(color: Color(0xFFFFD54F), fontSize: 11, fontWeight: FontWeight.bold)),
const SizedBox(width: 8),
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: DivinationLayouts.tarotLayouts.map((l) {
final isSel = _selectedLayout.id == l.id;
return Padding(
padding: const EdgeInsets.only(right: 6),
child: ChoiceChip(
label: Text(l.title, style: TextStyle(fontSize: 11.5, fontWeight: isSel ? FontWeight.bold : FontWeight.normal)),
selected: isSel,
selectedColor: const Color(0xFF4A3E1E),
side: BorderSide(color: isSel ? const Color(0xFFFFD54F) : const Color(0xFF2A2D4A)),
onSelected: (_) => _onLayoutChanged(l),
),
);
}).toList(),
),
),
),
],
),
const SizedBox(height: 14),
// Progress Banner
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFF181C30),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF2A2D4A)),
),
child: Row(
children: [
Text(
'Позиция: ${_currentStepIndex + 1} из ${_selectedLayout.count}',
style: const TextStyle(color: Color(0xFF80DEEA), fontSize: 12, fontWeight: FontWeight.bold),
),
const SizedBox(width: 8),
if (!isSpreadComplete)
Expanded(
child: Text(
'«${_selectedLayout.positionTitles[_currentStepIndex]}»',
style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 12, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
)
else
const Text('✓ Расклад собран', style: TextStyle(color: Color(0xFF81C784), fontSize: 12, fontWeight: FontWeight.bold)),
],
),
),
const SizedBox(height: 14),
if (!isSpreadComplete)
SizedBox(
height: 280,
child: TabBarView(
controller: _tabController,
children: [
_buildDeckTab(),
_buildManualTab(),
],
),
),
if (isSpreadComplete) ...[
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF1E2238),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFF81C784)),
),
child: Row(
children: [
const Icon(Icons.check_circle_rounded, color: Color(0xFF81C784), size: 20),
const SizedBox(width: 10),
const Expanded(
child: Text(
'Все арканы расклада извлечены и записаны в сессию дня!',
style: TextStyle(color: Color(0xFFECEFF1), fontSize: 12.5, fontWeight: FontWeight.bold),
),
),
TextButton(
onPressed: _resetSpread,
child: const Text('Заново', style: TextStyle(color: Color(0xFFFFD54F), fontWeight: FontWeight.bold)),
),
],
),
),
],
const SizedBox(height: 16),
if (_drawnResults.isNotEmpty) ...[
const Text(
'ВЫПАВШИЕ АРКАНЫ В РАСКЛАДЕ:',
style: TextStyle(color: Color(0xFFFFD54F), fontSize: 11.5, fontWeight: FontWeight.w900, letterSpacing: 0.8),
),
const SizedBox(height: 10),
for (int i = 0; i < _drawnResults.length; i++) ...[
_buildResultArcanumCard(_drawnResults[i], i + 1),
const SizedBox(height: 10),
],
],
],
),
),
);
}
Widget _buildDeckTab() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF151829),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFF2A2D4A)),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 115,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF39295C), Color(0xFF1E1738)],
),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFFFFD54F), width: 1.5),
),
child: const Center(
child: Icon(Icons.auto_awesome_rounded, color: Color(0xFFFFD54F), size: 30),
),
),
const SizedBox(height: 12),
Text(
'Извлечение позиции: «' + _selectedLayout.positionTitles[_currentStepIndex] + '»',
textAlign: TextAlign.center,
style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 11.5),
),
const SizedBox(height: 14),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFFD54F),
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
),
onPressed: _isShuffling ? null : _drawRandomCardForStep,
icon: const Icon(Icons.style_rounded, size: 20),
label: Text(
_isShuffling ? 'Тасовка колоды...' : 'Вытянуть для шага ${_currentStepIndex + 1}',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13.5),
),
),
],
),
);
}
Widget _buildManualTab() {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF151829),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFF2A2D4A)),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('АРКАН ДЛЯ ШАГА ${_currentStepIndex + 1}:', style: const TextStyle(color: Color(0xFFFFD54F), fontSize: 11, fontWeight: FontWeight.bold)),
FilterChip(
label: const Text('Перевёрнутый', style: TextStyle(fontSize: 10.5)),
selected: _isManualReversed,
selectedColor: const Color(0xFFEF5350).withValues(alpha: 0.3),
side: BorderSide(color: _isManualReversed ? const Color(0xFFEF5350) : const Color(0xFF2A2D4A)),
onSelected: (v) => setState(() => _isManualReversed = v),
),
],
),
const SizedBox(height: 8),
Expanded(
child: ListView.builder(
itemCount: _arcana.length,
itemBuilder: (ctx, i) {
final a = _arcana[i];
return ListTile(
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
leading: Text(
'${i == 0 ? "0" : i}',
style: const TextStyle(color: Color(0xFF8C93B8), fontWeight: FontWeight.bold),
),
title: Text(
a.name,
style: const TextStyle(color: Color(0xFFE0E2EC), fontSize: 13),
),
trailing: const Icon(Icons.touch_app_rounded, size: 16, color: Color(0xFFFFD54F)),
onTap: () => _selectManualArcanum(i),
);
},
),
),
],
),
);
}
Widget _buildResultArcanumCard(({TraditionStation arcanum, bool isReversed, String positionTitle}) res, int index) {
final a = res.arcanum;
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF181C30),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.4)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 44,
height: 44,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xFF232742),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFFFD54F)),
),
child: const Icon(Icons.style_rounded, color: Color(0xFFFFD54F), size: 24),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
'#$index. ${res.positionTitle}: ${a.name}',
style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 14, fontWeight: FontWeight.bold),
),
),
if (res.isReversed)
Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFF5C1D1D),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFFEF5350)),
),
child: const Text('Перевёрнутый', style: TextStyle(color: Color(0xFFFF8A80), fontSize: 9.5, fontWeight: FontWeight.bold)),
),
],
),
const SizedBox(height: 4),
Text(a.group, style: const TextStyle(color: Color(0xFF80DEEA), fontSize: 11)),
const SizedBox(height: 6),
Text(
a.lore,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Color(0xFFD1D5DB), fontSize: 12, height: 1.4),
),
],
),
),
],
),
);
}
}