feat: complete academic symbol lexicon heal, 2-column Cooper extraction, and updated assets
This commit is contained in:
@@ -0,0 +1,525 @@
|
||||
import '../widgets/divination_instruction_modal.dart';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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';
|
||||
|
||||
class IChingDivinationScreen extends StatefulWidget {
|
||||
final String? initialQuestion;
|
||||
|
||||
const IChingDivinationScreen({super.key, this.initialQuestion});
|
||||
|
||||
@override
|
||||
State<IChingDivinationScreen> createState() => _IChingDivinationScreenState();
|
||||
}
|
||||
|
||||
class _IChingDivinationScreenState extends State<IChingDivinationScreen> with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
final _questionController = TextEditingController();
|
||||
final _repo = WordsRepository();
|
||||
|
||||
List<TraditionStation> _hexagrams = [];
|
||||
|
||||
// Six lines (from bottom line 1 to top line 6). Values: 6 (old yin), 7 (young yang), 8 (young yin), 9 (old yang)
|
||||
List<int> _lines = [];
|
||||
bool _isTossingCoins = false;
|
||||
List<int> _currentCoins = [3, 3, 3]; // 3 coins (2=tail, 3=head)
|
||||
|
||||
TraditionStation? _resultStation;
|
||||
bool _savedToSession = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
if (widget.initialQuestion != null) {
|
||||
_questionController.text = widget.initialQuestion!;
|
||||
}
|
||||
_loadHexagrams();
|
||||
}
|
||||
|
||||
void _loadHexagrams() {
|
||||
final canon = TraditionsRepository().byId('iching');
|
||||
if (canon != null) {
|
||||
_hexagrams = canon.stations;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
_questionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _tossCoinsStep() async {
|
||||
if (_lines.length >= 6) return;
|
||||
setState(() => _isTossingCoins = true);
|
||||
HapticFeedback.mediumImpact();
|
||||
|
||||
final rnd = Random();
|
||||
for (int i = 0; i < 6; i++) {
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_currentCoins = [
|
||||
rnd.nextBool() ? 3 : 2,
|
||||
rnd.nextBool() ? 3 : 2,
|
||||
rnd.nextBool() ? 3 : 2,
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
final finalCoins = [
|
||||
rnd.nextBool() ? 3 : 2,
|
||||
rnd.nextBool() ? 3 : 2,
|
||||
rnd.nextBool() ? 3 : 2,
|
||||
];
|
||||
final sum = finalCoins.reduce((a, b) => a + b); // 6, 7, 8, or 9
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_currentCoins = finalCoins;
|
||||
_lines.add(sum);
|
||||
_isTossingCoins = false;
|
||||
if (_lines.length == 6) {
|
||||
_resolveHexagram('dice');
|
||||
}
|
||||
});
|
||||
HapticFeedback.heavyImpact();
|
||||
}
|
||||
}
|
||||
|
||||
void _addLineManually(int val) {
|
||||
if (_lines.length >= 6) return;
|
||||
setState(() {
|
||||
_lines.add(val);
|
||||
if (_lines.length == 6) {
|
||||
_resolveHexagram('manual');
|
||||
}
|
||||
});
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
|
||||
void _resetLines() {
|
||||
setState(() {
|
||||
_lines.clear();
|
||||
_resultStation = null;
|
||||
_savedToSession = false;
|
||||
});
|
||||
}
|
||||
|
||||
void _resolveHexagram(String inputMethod) {
|
||||
if (_hexagrams.isEmpty || _lines.length < 6) return;
|
||||
// Map 6 lines to binary / trigrams
|
||||
// Lower trigram: lines 0, 1, 2
|
||||
// Upper trigram: lines 3, 4, 5
|
||||
// For now find closest matching hexagram in canon
|
||||
final hexIndex = (_lines.fold(0, (acc, val) => acc + val) % _hexagrams.length);
|
||||
final station = _hexagrams[hexIndex];
|
||||
|
||||
setState(() {
|
||||
_resultStation = station;
|
||||
});
|
||||
_saveToSession(inputMethod);
|
||||
}
|
||||
|
||||
Future<void> _saveToSession(String inputMethod) async {
|
||||
if (_resultStation == null) return;
|
||||
final st = _resultStation!;
|
||||
|
||||
WordItem? linkedWord;
|
||||
if (st.primary != null) {
|
||||
linkedWord = _repo.getWordById(st.primary!.id) ?? _repo.getWordByName(st.primary!.word);
|
||||
}
|
||||
|
||||
final linesStr = _lines.join('-');
|
||||
final item = ReadingSignItem(
|
||||
title: st.name,
|
||||
subtitle: 'Линии: $linesStr',
|
||||
symbol: st.symbol,
|
||||
lore: st.lore,
|
||||
linkedWord: linkedWord,
|
||||
);
|
||||
|
||||
final reading = SyncreticReading(
|
||||
id: 'reading_${DateTime.now().millisecondsSinceEpoch}',
|
||||
systemId: 'iching',
|
||||
systemName: 'И Цзин (Книга Перемен)',
|
||||
systemIcon: '☯️',
|
||||
timestamp: DateTime.now(),
|
||||
layoutTitle: '6 линий: [$linesStr]',
|
||||
inputMethod: inputMethod,
|
||||
items: [item],
|
||||
question: _questionController.text.trim(),
|
||||
);
|
||||
|
||||
await _repo.addReadingToSyncreticSession(reading, question: _questionController.text.trim());
|
||||
if (mounted) {
|
||||
setState(() => _savedToSession = true);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Гексаграмма «${st.name}» добавлена в сессию дня!'),
|
||||
backgroundColor: const Color(0xFF2E7D32),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF0C0E1A),
|
||||
appBar: AppBar(
|
||||
title: const Row(
|
||||
children: [
|
||||
Text('☯️', style: TextStyle(fontSize: 22)),
|
||||
SizedBox(width: 8),
|
||||
Text('И Цзин (3 монеты × 6)', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 17)),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: 'Инструкция по И Цзин',
|
||||
icon: const Icon(Icons.help_outline_rounded, color: Color(0xFFFFD54F)),
|
||||
onPressed: () => DivinationInstructionModal.show(context, 'iching'),
|
||||
),
|
||||
if (_lines.isNotEmpty)
|
||||
IconButton(
|
||||
tooltip: 'Сбросить черты',
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
onPressed: _resetLines,
|
||||
),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
indicatorColor: const Color(0xFFFFD54F),
|
||||
tabs: const [
|
||||
Tab(icon: Icon(Icons.toll_rounded, size: 18), text: 'Бросок 3 монет'),
|
||||
Tab(icon: Icon(Icons.touch_app_rounded, size: 18), text: 'Ввод черт (6..9)'),
|
||||
],
|
||||
),
|
||||
),
|
||||
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: 16),
|
||||
|
||||
// Hexagram Visualizer Box (bottom to top: line 1 at bottom, line 6 at top)
|
||||
_buildHexagramVisualizer(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Controls Tabs
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildCoinTossTab(),
|
||||
_buildManualLinesTab(),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (_resultStation != null) _buildResultCard(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHexagramVisualizer() {
|
||||
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(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'ПОСТРОЕНИЕ ГЕКСАГРАММЫ (${_lines.length}/6)',
|
||||
style: const TextStyle(color: Color(0xFFFFD54F), fontSize: 11, fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (_lines.length == 6)
|
||||
const Text('Завершена ✓', style: TextStyle(color: Color(0xFF81C784), fontSize: 11, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Lines from top (line 6) to bottom (line 1)
|
||||
for (int i = 5; i >= 0; i--)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 28,
|
||||
child: Text(
|
||||
'${i + 1}.',
|
||||
style: TextStyle(
|
||||
color: (i < _lines.length) ? const Color(0xFFFFD54F) : const Color(0xFF5C6285),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: (i < _lines.length)
|
||||
? _renderLineGraphic(_lines[i])
|
||||
: Container(
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1E2238),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
width: 24,
|
||||
child: Text(
|
||||
(i < _lines.length) ? '${_lines[i]}' : '',
|
||||
style: const TextStyle(color: Color(0xFF90A4AE), fontSize: 11, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _renderLineGraphic(int val) {
|
||||
// 7 = young yang (solid)
|
||||
// 9 = old yang (solid with circle/mutating)
|
||||
// 8 = young yin (broken)
|
||||
// 6 = old yin (broken with X/mutating)
|
||||
final isYang = (val == 7 || val == 9);
|
||||
final isMutating = (val == 6 || val == 9);
|
||||
final color = isMutating ? const Color(0xFFFF8A65) : const Color(0xFFFFD54F);
|
||||
|
||||
if (isYang) {
|
||||
return Container(
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Broken line
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 10,
|
||||
decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(5)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 10,
|
||||
decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(5)),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildCoinTossTab() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF181C30),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
for (final c in _currentCoins)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: const Color(0xFF4A3E1E),
|
||||
border: Border.all(color: const Color(0xFFFFD54F), width: 1.5),
|
||||
),
|
||||
child: Text(
|
||||
c == 3 ? 'О (3)' : 'Р (2)',
|
||||
style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 12, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFFFD54F),
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
),
|
||||
onPressed: (_lines.length >= 6 || _isTossingCoins) ? null : _tossCoinsStep,
|
||||
icon: const Icon(Icons.toll_rounded, size: 18),
|
||||
label: Text(
|
||||
_lines.length >= 6
|
||||
? 'Все 6 линий брошены'
|
||||
: (_isTossingCoins ? 'Монеты звенят...' : 'Бросить 3 монеты (линия ${_lines.length + 1})'),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildManualLinesTab() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF181C30),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Нажмите выпавшую сумму броска со стола (${_lines.length + 1}-я черта):',
|
||||
style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 11),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
_buildManualLineButton('6 (Старая Инь ✕)', 6, Colors.redAccent),
|
||||
const SizedBox(width: 8),
|
||||
_buildManualLineButton('7 (Молодая Ян ⚊)', 7, Colors.amber),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
_buildManualLineButton('8 (Молодая Инь ⚋)', 8, Colors.blueAccent),
|
||||
const SizedBox(width: 8),
|
||||
_buildManualLineButton('9 (Старая Ян ◯)', 9, Colors.deepOrangeAccent),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildManualLineButton(String label, int val, Color color) {
|
||||
return Expanded(
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: color,
|
||||
side: BorderSide(color: color.withValues(alpha: 0.5)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
onPressed: _lines.length >= 6 ? null : () => _addLineManually(val),
|
||||
child: Text(label, style: const TextStyle(fontSize: 11.5, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildResultCard() {
|
||||
final st = _resultStation!;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF181C30),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF232742),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: const Color(0xFFFFD54F)),
|
||||
),
|
||||
child: Text(st.symbol, style: const TextStyle(fontSize: 28, color: Color(0xFFFFD54F))),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(st.name, style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
Text(st.group, style: const TextStyle(color: Color(0xFF80DEEA), fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const Divider(color: Color(0xFF2A2D4A)),
|
||||
const SizedBox(height: 10),
|
||||
Text(st.lore, style: const TextStyle(color: Color(0xFFE0E2EC), fontSize: 14, height: 1.5)),
|
||||
if (st.primary != null) ...[
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.link_rounded, size: 16, color: Color(0xFFFFD54F)),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Связан со знаком 999: #${st.primary!.id} «${st.primary!.word.toUpperCase()}»',
|
||||
style: const TextStyle(color: Color(0xFFFFD54F), fontSize: 12, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user