feat: complete academic symbol lexicon heal, 2-column Cooper extraction, and updated assets

This commit is contained in:
2026-10-09 15:26:58 +05:00
parent 7f91366201
commit 0b12e4583b
244 changed files with 611461 additions and 1617 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,390 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/syncretic_session_model.dart';
import '../services/oracle_master_prompt_generator.dart';
import '../services/words_repository.dart';
import '../widgets/gradient_star_badge.dart';
class CrossSystemComparisonScreen extends StatefulWidget {
final SyncreticSession session;
const CrossSystemComparisonScreen({super.key, required this.session});
@override
State<CrossSystemComparisonScreen> createState() => _CrossSystemComparisonScreenState();
}
class _CrossSystemComparisonScreenState extends State<CrossSystemComparisonScreen> {
late SyncreticSession _session;
@override
void initState() {
super.initState();
_session = widget.session;
}
void _showMasterPrompt() {
HapticFeedback.mediumImpact();
final prompt = OracleMasterPromptGenerator.generateCrossSystemComparison(_session);
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: const Color(0xFF0F1222),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (ctx) {
return DraggableScrollableSheet(
initialChildSize: 0.85,
minChildSize: 0.5,
maxChildSize: 0.95,
expand: false,
builder: (context, scrollController) {
return Container(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 44,
height: 4,
decoration: BoxDecoration(
color: Colors.white24,
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 14),
Row(
children: [
const Icon(Icons.psychology_alt_rounded, color: Color(0xFFFFD54F), size: 24),
const SizedBox(width: 10),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Великий Мета-Промпт Сравнения',
style: TextStyle(color: Colors.white, fontSize: 17, fontWeight: FontWeight.bold),
),
Text(
'Для Grok, Claude 3.5 Sonnet, GPT-4o, Gemini 1.5 Pro',
style: TextStyle(color: Color(0xFF90A4AE), fontSize: 11),
),
],
),
),
IconButton(
icon: const Icon(Icons.close_rounded, color: Color(0xFF90A4AE)),
onPressed: () => Navigator.pop(ctx),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF1C223D),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'${prompt.length} симв. • ${_session.readings.length} систем',
style: const TextStyle(color: Color(0xFF90CAF9), fontSize: 11),
),
),
const Spacer(),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFFD54F),
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(0xFF388E3C),
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(0xFF090A14),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF232742), width: 1.0),
),
child: SingleChildScrollView(
controller: scrollController,
child: SelectableText(
prompt,
style: const TextStyle(
color: Color(0xFFECEFF1),
fontFamily: 'monospace',
fontSize: 11.5,
height: 1.45,
),
),
),
),
),
],
),
);
},
);
},
);
}
@override
Widget build(BuildContext context) {
final readings = _session.readings;
return Scaffold(
backgroundColor: const Color(0xFF0C0E1A),
appBar: AppBar(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Синхронистическая Сессия', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
Text(
'${_session.formattedDate} • ${readings.length} ${readings.length == 1 ? "гадание" : (readings.length < 5 ? "гадания" : "гаданий")}',
style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 11),
),
],
),
actions: [
IconButton(
tooltip: 'Великий Мета-Промпт',
icon: const Icon(Icons.psychology_alt_rounded, color: Color(0xFFFFD54F)),
onPressed: _showMasterPrompt,
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Question Banner
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF2A1F45), Color(0xFF17192E)],
),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.4)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
GradientStar(size: 16),
SizedBox(width: 8),
Text(
'ФОКУС ВОПРОШАНИЯ ДНЯ',
style: TextStyle(
color: Color(0xFFFFD54F),
fontSize: 11,
fontWeight: FontWeight.w900,
letterSpacing: 0.8,
),
),
],
),
const SizedBox(height: 8),
Text(
_session.mainQuestion?.isNotEmpty == true
? '«${_session.mainQuestion!}»'
: 'Общий срез судьбы и самосогласованность мантических систем.',
style: const TextStyle(
color: Color(0xFFFFF9C4),
fontSize: 15,
fontWeight: FontWeight.bold,
height: 1.4,
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFFD54F),
foregroundColor: Colors.black,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 11),
),
onPressed: _showMasterPrompt,
icon: const Icon(Icons.psychology_alt_rounded, size: 18),
label: const Text(
'Сформировать Мета-Промпт для нейросетей',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
),
),
),
],
),
),
const SizedBox(height: 20),
Row(
children: [
const Icon(Icons.history_edu_rounded, color: Color(0xFF80DEEA), size: 20),
const SizedBox(width: 8),
Text(
'ЛЕНТА РАСКЛАДОВ СЕССИИ (${readings.length})',
style: const TextStyle(
color: Color(0xFF80DEEA),
fontSize: 13,
fontWeight: FontWeight.w900,
letterSpacing: 0.8,
),
),
],
),
const SizedBox(height: 12),
if (readings.isEmpty)
Container(
padding: const EdgeInsets.all(24),
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xFF151829),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF2A2D4A)),
),
child: const Text(
'В этой сессии пока нет проведённых гаданий.\nСделайте расклад в любой из систем (Оракул, Руны, Ленорман, Таро, И Цзин), и они появятся здесь.',
textAlign: TextAlign.center,
style: TextStyle(color: Color(0xFF8C93B8), fontSize: 13, height: 1.5),
),
)
else
for (int i = 0; i < readings.length; i++) ...[
_buildReadingCard(readings[i], i + 1),
const SizedBox(height: 12),
],
],
),
),
);
}
Widget _buildReadingCard(SyncreticReading reading, int index) {
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: [
Row(
children: [
Container(
width: 36,
height: 36,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xFF232742),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.4)),
),
child: Text(reading.systemIcon, style: const TextStyle(fontSize: 18)),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'#$index. ${reading.systemName}',
style: const TextStyle(
color: Color(0xFFFFF9C4),
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
Text(
'${reading.formattedTime} • ${reading.layoutTitle}',
style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 11),
),
],
),
),
],
),
const SizedBox(height: 12),
const Divider(color: Color(0xFF2A2D4A), height: 1),
const SizedBox(height: 10),
for (final item in reading.items) ...[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (item.symbol != null) ...[
Text(
item.symbol!,
style: const TextStyle(fontSize: 20, color: Color(0xFFFFD54F)),
),
const SizedBox(width: 8),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title + (item.isReversed ? ' (перевёрнутая)' : ''),
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13),
),
if (item.subtitle != null)
Text(item.subtitle!, style: const TextStyle(color: Color(0xFF80DEEA), fontSize: 11)),
if (item.lore != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
item.lore!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Color(0xFFB0BEC5), fontSize: 11.5, height: 1.4),
),
),
if (item.linkedWord != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'🔗 Знак 999: #${item.linkedWord!.formattedSessionNumber} «${item.linkedWord!.word}»',
style: const TextStyle(color: Color(0xFFFFD54F), fontSize: 11, fontWeight: FontWeight.bold),
),
),
],
),
),
],
),
const SizedBox(height: 6),
],
],
),
);
}
}
+659
View File
@@ -0,0 +1,659 @@
import '../widgets/divination_instruction_modal.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../services/words_repository.dart';
import '../widgets/gradient_star_badge.dart';
import 'home_search_screen.dart';
import 'houses_field_screen.dart';
import 'runes_divination_screen.dart';
import 'lenormand_divination_screen.dart';
import 'tarot_divination_screen.dart';
import 'iching_divination_screen.dart';
import 'cross_system_comparison_screen.dart';
import 'numerology_divination_screen.dart';
import 'geomancy_divination_screen.dart';
import 'ogham_divination_screen.dart';
import '../services/word_of_the_day_service.dart';
import 'word_of_the_day_screen.dart';
class DivinationHubScreen extends StatefulWidget {
const DivinationHubScreen({super.key});
@override
State<DivinationHubScreen> createState() => _DivinationHubScreenState();
}
class _DivinationHubScreenState extends State<DivinationHubScreen> {
final _repo = WordsRepository();
final _wordOfDayService = WordOfTheDayService();
void _openComparisonScreen() {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => CrossSystemComparisonScreen(
session: _repo.currentSyncreticSession,
),
),
).then((_) => setState(() {}));
}
void _showNewSessionDialog() {
final controller = TextEditingController();
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF151829),
title: const Row(
children: [
GradientStar(size: 20),
SizedBox(width: 8),
Text('Новая Сессия Дня', style: TextStyle(color: Color(0xFFFFF9C4), fontSize: 17, fontWeight: FontWeight.bold)),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Начните новую синхронистическую сессию по новой теме или вопросу. Предыдущая сессия сохранится в архиве.',
style: TextStyle(color: Color(0xFF8C93B8), fontSize: 13, height: 1.4),
),
const SizedBox(height: 14),
TextField(
controller: controller,
style: const TextStyle(color: Colors.white, fontSize: 14),
decoration: InputDecoration(
hintText: 'Главный вопрос / тема сессии...',
hintStyle: const TextStyle(color: Color(0xFF8C93B8), fontSize: 12),
filled: true,
fillColor: const Color(0xFF1E2238),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Отмена'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFFD54F),
foregroundColor: Colors.black,
),
onPressed: () async {
Navigator.pop(ctx);
await _repo.startNewSyncreticSession(question: controller.text.trim());
setState(() {});
HapticFeedback.mediumImpact();
},
child: const Text('Начать'),
),
],
),
);
}
Widget _buildWordOfDayHeroBanner() {
final now = DateTime.now();
final rec = _wordOfDayService.getRecord(now);
final isTriptych = rec.isTriptychMode;
final singleWord = _wordOfDayService.getWordForDate(now, phase: 'main');
final triptych = _wordOfDayService.getTriptychWordsForDate(now);
final streak = _wordOfDayService.calculateStreak();
final isRevealed = isTriptych ? rec.isAnyRevealed : rec.isRevealed;
final isDone = isTriptych ? rec.isDayStepCompleted : rec.isStepCompleted;
String titleTag = isTriptych ? 'ТРИПТИХ СУТОК (3 СЛОВА)' : 'СЛОВО ДНЯ • КАЛЕНДАРЬ';
String subtitle;
if (isTriptych) {
if (rec.areAllTriptychRevealed) {
subtitle = '🌅 ${triptych.morning.word.toUpperCase()} • ⚡ ${triptych.day.word.toUpperCase()} • 🌙 ${triptych.evening.word.toUpperCase()}';
} else if (rec.isAnyRevealed) {
final m = rec.isMorningRevealed ? triptych.morning.word.toUpperCase() : '•••';
final d = rec.isDayRevealed ? triptych.day.word.toUpperCase() : '•••';
final e = rec.isEveningRevealed ? triptych.evening.word.toUpperCase() : '•••';
subtitle = '🌅 $m • ⚡ $d • 🌙 $e';
} else {
subtitle = 'Печати суток не распечатаны';
}
} else {
subtitle = rec.isRevealed
? '${singleWord.word.toUpperCase()} (#${singleWord.sessionNumber})'
: 'Печать не распечатана';
}
String statusText;
if (isDone) {
statusText = 'Выполнено ✓';
} else if (isTriptych) {
statusText = rec.areAllTriptychRevealed
? 'Открыты 3/3'
: (rec.isAnyRevealed ? 'В процессе' : 'Триптих');
} else {
statusText = rec.isRevealed ? 'В процессе' : 'Открыть';
}
String descText;
if (isTriptych) {
if (rec.areAllTriptychRevealed) {
descText = 'Утро: «${triptych.morning.asks}» | Вызов дня: «${triptych.day.step}»';
} else {
descText = 'Коснитесь, чтобы распечатать три слова дня: Настройка, Действие и Тень.';
}
} else {
descText = rec.isRevealed
? (singleWord.asks.isNotEmpty
? '«${singleWord.asks}»'
: (singleWord.oracle.isNotEmpty ? singleWord.oracle : singleWord.description))
: 'Коснитесь, чтобы узнать архетип, коан и практическое задание на сегодня.';
}
return InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const WordOfTheDayScreen()),
).then((_) => setState(() {}));
},
borderRadius: BorderRadius.circular(20),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF2E2045), Color(0xFF14172B)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isRevealed
? const Color(0xFFFFD54F)
: const Color(0xFFFFD54F).withValues(alpha: 0.5),
width: isRevealed ? 1.5 : 1.0,
),
boxShadow: [
BoxShadow(
color: const Color(0xFFFFD54F).withValues(alpha: 0.12),
blurRadius: 12,
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 38,
height: 38,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const LinearGradient(
colors: [Color(0xFFFFD54F), Color(0xFFFF8F00)],
),
boxShadow: [
BoxShadow(
color: const Color(0xFFFFD54F).withValues(alpha: 0.3),
blurRadius: 8,
),
],
),
child: Text(
isTriptych
? '⏳'
: (rec.isRevealed && singleWord.emoji.isNotEmpty
? singleWord.emoji
: '☀️'),
style: const TextStyle(fontSize: 18),
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
titleTag,
style: const TextStyle(
color: Color(0xFFFFD54F),
fontSize: 11,
fontWeight: FontWeight.w900,
letterSpacing: 1.0,
),
),
if (streak > 0) ...[
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFF3B2314),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFFFFB74D).withValues(alpha: 0.4)),
),
child: Text(
'🔥 $streak дн.',
style: const TextStyle(
color: Color(0xFFFFB74D),
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
],
],
),
const SizedBox(height: 2),
Text(
subtitle,
style: TextStyle(
color: isRevealed ? const Color(0xFFFFF9C4) : const Color(0xFF8C93B8),
fontSize: isTriptych ? 12.5 : 14,
fontWeight: FontWeight.bold,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isDone
? const Color(0xFF1B3D23)
: (isRevealed ? const Color(0xFF2A2D4A) : const Color(0xFF332A1C)),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isDone
? const Color(0xFF81C784)
: const Color(0xFFFFD54F).withValues(alpha: 0.4),
),
),
child: Text(
statusText,
style: TextStyle(
color: isDone
? const Color(0xFF81C784)
: const Color(0xFFFFD54F),
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 10),
Text(
descText,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 12, height: 1.35),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
final session = _repo.currentSyncreticSession;
final readingsCount = session.readings.length;
return Scaffold(
backgroundColor: const Color(0xFF0C0E1A),
appBar: AppBar(
title: const Row(
children: [
GradientStar(size: 22),
SizedBox(width: 8),
Text('Предсказания', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
],
),
actions: [
IconButton(
tooltip: 'Начать новую тему сессии',
icon: const Icon(Icons.add_circle_outline_rounded, color: Color(0xFFFFD54F)),
onPressed: _showNewSessionDialog,
),
IconButton(
tooltip: 'Сравнительный Мета-Промпт',
icon: const Icon(Icons.psychology_alt_rounded, color: Color(0xFFFFD54F)),
onPressed: _openComparisonScreen,
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Word of the Day Hero Banner
_buildWordOfDayHeroBanner(),
const SizedBox(height: 14),
// Active Day Session Banner
InkWell(
onTap: _openComparisonScreen,
borderRadius: BorderRadius.circular(20),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF2A1F45), Color(0xFF151829)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.5)),
boxShadow: [
BoxShadow(color: const Color(0xFFFFD54F).withValues(alpha: 0.1), blurRadius: 10),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const GradientStar(size: 18),
const SizedBox(width: 8),
const Expanded(
child: Text(
'АКТИВНАЯ СЕССИЯ ДНЯ',
style: TextStyle(
color: Color(0xFFFFD54F),
fontSize: 11,
fontWeight: FontWeight.w900,
letterSpacing: 1.0,
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: const Color(0xFF3B2B5E),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.4)),
),
child: Text(
'$readingsCount ${readingsCount == 1 ? "гадание" : (readingsCount < 5 ? "гадания" : "гаданий")}',
style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 11, fontWeight: FontWeight.bold),
),
),
],
),
const SizedBox(height: 8),
Text(
session.mainQuestion?.isNotEmpty == true
? '«${session.mainQuestion!}»'
: 'Синхронистический сосуд дня (все гадания аккумулируются здесь)',
style: const TextStyle(color: Color(0xFFECEFF1), fontSize: 13.5, height: 1.4),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
readingsCount > 0 ? 'Нажмите, чтобы сравнить системы' : 'Сделайте расклад в любой системе ниже',
style: const TextStyle(color: Color(0xFF80DEEA), fontSize: 11, fontWeight: FontWeight.bold),
),
const Icon(Icons.arrow_forward_ios_rounded, size: 12, color: Color(0xFFFFD54F)),
],
),
],
),
),
),
const SizedBox(height: 20),
// Section 1: Core 999 Systems
_buildSectionHeader('СИСТЕМЫ ОРАКУЛА 999', Icons.auto_awesome_rounded),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: _buildSystemCard(
title: 'Слово Дня',
instructionId: 'word_of_day',
subtitle: 'Календарь & Квест',
icon: '☀️',
desc: 'Ежедневный архетип, коан для ума, микродействие и дневник',
accentColor: const Color(0xFFFFD54F),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const WordOfTheDayScreen())).then((_) => setState(() {})),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildSystemCard(
title: 'Оракул',
instructionId: 'cleromancy',
subtitle: 'Клеромантия d10',
icon: '🎲',
desc: '1..10 знаков по жребию костей или прямому вводу',
accentColor: const Color(0xFFFFD54F),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const HomeSearchScreen())).then((_) => setState(() {})),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildSystemCard(
title: 'Матрица 33×30',
instructionId: 'matrix',
subtitle: '990 Домов Судьбы',
icon: '🏛️',
desc: 'Полотно Домов, Архонты и цепочки диспозиций',
accentColor: const Color(0xFF80DEEA),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const HousesFieldScreen())).then((_) => setState(() {})),
),
),
],
),
const SizedBox(height: 20),
// Section 2: Classical Divination Systems
_buildSectionHeader('КЛАССИЧЕСКИЕ ТРАДИЦИИ', Icons.history_edu_rounded),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: _buildSystemCard(
title: 'Руны Футарка',
instructionId: 'runes',
subtitle: '24 Руны (2×d12)',
icon: 'ᚱ',
desc: 'Жребий двумя d12 либо сетка рун (прямые/перев.)',
accentColor: const Color(0xFF81C784),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const RunesDivinationScreen())).then((_) => setState(() {})),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildSystemCard(
title: 'Кельтский Огам',
instructionId: 'ogham',
subtitle: '20 Фед Рощи (d20)',
icon: '᚛',
desc: 'Священные деревья друидов: жребий палочек и 4 Аикмы',
accentColor: const Color(0xFF66BB6A),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const OghamDivinationScreen())).then((_) => setState(() {})),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildSystemCard(
title: 'Таро',
instructionId: 'tarot',
subtitle: '22 Старших Аркана',
icon: '🎴',
desc: 'Жребий колоды или выбор из 22 Арканов',
accentColor: const Color(0xFFBA68C8),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const TarotDivinationScreen())).then((_) => setState(() {})),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildSystemCard(
title: 'Ленорман',
instructionId: 'lenormand',
subtitle: '36 Домов (d6+d12)',
icon: '🪶',
desc: 'Бросок d6+d12 со смещением или ввод 1..36',
accentColor: const Color(0xFFFFB74D),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const LenormandDivinationScreen())).then((_) => setState(() {})),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildSystemCard(
title: 'И Цзин',
instructionId: 'iching',
subtitle: '3 монеты × 6',
icon: '☯️',
desc: 'Книга Перемен: 6 бросков монет или ввод черт',
accentColor: const Color(0xFF4DD0E1),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const IChingDivinationScreen())).then((_) => setState(() {})),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildSystemCard(
title: 'Геомантия',
instructionId: 'geomancy',
subtitle: "16 Фигур ('Илм ар-рамл)",
icon: '⌛',
desc: 'Земной жребий песка, Свидетели и Щит Судьбы',
accentColor: const Color(0xFFFFB74D),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const GeomancyDivinationScreen())).then((_) => setState(() {})),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildSystemCard(
title: 'Нумерология',
instructionId: 'numerology',
subtitle: 'Психоматрица 3×3',
icon: '🔢',
desc: '4 рабочих числа, психоматрица Пифагора и Число Судьбы',
accentColor: const Color(0xFFFFD54F),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const NumerologyDivinationScreen())).then((_) => setState(() {})),
),
),
],
),
],
),
),
);
}
Widget _buildSectionHeader(String title, IconData icon) {
return Row(
children: [
Icon(icon, size: 16, color: const Color(0xFFFFD54F)),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(
color: Color(0xFFFFD54F),
fontSize: 11.5,
fontWeight: FontWeight.w900,
letterSpacing: 1.0,
),
),
],
);
}
Widget _buildSystemCard({
required String title,
required String subtitle,
required String icon,
required String desc,
required Color accentColor,
String? instructionId,
required VoidCallback onTap,
}) {
return InkWell(
onTap: () {
HapticFeedback.selectionClick();
onTap();
},
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF151829),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF2A2D4A)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 36,
height: 36,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xFF232742),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: accentColor.withValues(alpha: 0.4)),
),
child: Text(icon, style: const TextStyle(fontSize: 18)),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)),
Text(subtitle, style: TextStyle(color: accentColor, fontSize: 10, fontWeight: FontWeight.bold)),
],
),
),
if (instructionId != null)
IconButton(
icon: const Icon(Icons.info_outline_rounded, size: 18, color: Color(0xFF8C93B8)),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
tooltip: 'Инструкция',
onPressed: () => DivinationInstructionModal.show(context, instructionId),
),
],
),
const SizedBox(height: 8),
Text(
desc,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 11, height: 1.3),
),
],
),
),
);
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+538 -191
View File
@@ -1,9 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/history_entry_model.dart';
import '../models/word_of_day_model.dart';
import '../services/word_of_the_day_service.dart';
import '../services/words_repository.dart';
import '../widgets/gradient_star_badge.dart';
import 'session_result_screen.dart';
import 'word_of_the_day_screen.dart';
class HistoryScreen extends StatefulWidget {
const HistoryScreen({super.key});
@@ -14,20 +17,23 @@ class HistoryScreen extends StatefulWidget {
class _HistoryScreenState extends State<HistoryScreen> {
final _repo = WordsRepository();
final _wordOfDayService = WordOfTheDayService();
void _confirmClearHistory() {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF151829),
title: const Row(
children: [
GradientStar(size: 20),
SizedBox(width: 8),
Text('Очистить историю?'),
Text('Очистить историю сессий?', style: TextStyle(color: Color(0xFFFFF9C4))),
],
),
content: const Text(
'Все сохранённые сессии гаданий и AI-трактовки будут удалены.',
'Все сохранённые сессии гаданий и AI-трактовки будут удалены. (Хроника Слов Дня в календаре сохранится).',
style: TextStyle(color: Color(0xFF8C93B8)),
),
actions: [
TextButton(
@@ -51,7 +57,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
void _copySession(OracleSession session) {
final buf = StringBuffer();
buf.writeln('✨ Оракул 999 — Запись Сессии ✨');
buf.writeln('✨ Оракул — Запись Сессии ✨');
buf.writeln('Дата: ${session.formattedDate}');
if (session.question != null && session.question!.isNotEmpty) {
buf.writeln('Вопрос: «${session.question}»');
@@ -60,12 +66,12 @@ class _HistoryScreenState extends State<HistoryScreen> {
for (int i = 0; i < session.words.length; i++) {
final w = session.words[i];
buf.writeln('🔮 Знак #${w.formattedSessionNumber}: ${w.word.toUpperCase()} (${w.partOfSpeech})');
buf.writeln('🔮 Знак #${w.formattedSessionNumber}: ${w.word.toUpperCase()}');
buf.writeln(' ${w.symbolicMeaning}\n');
}
if (session.aiInterpretation != null && session.aiInterpretation!.isNotEmpty) {
buf.writeln('🤖 AI Трактовка (G4F):');
buf.writeln('🤖 AI Трактовка:');
buf.writeln(session.aiInterpretation);
}
@@ -87,246 +93,560 @@ class _HistoryScreenState extends State<HistoryScreen> {
);
}
String _formatDateString(String dateKey) {
final d = WordOfTheDayService.parseDateKey(dateKey);
const months = [
'января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'
];
const weekdays = [
'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'
];
return '${d.day} ${months[d.month - 1]} ${d.year} • ${weekdays[d.weekday - 1]}';
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final sessionsList = _repo.sessions;
final dayRecords = _wordOfDayService.getAllRecords();
return Scaffold(
appBar: AppBar(
title: const Row(
return DefaultTabController(
length: 2,
child: Scaffold(
backgroundColor: const Color(0xFF0C0E1A),
appBar: AppBar(
title: const Row(
children: [
GradientStar(size: 24),
SizedBox(width: 10),
Text(
'История & Календарь',
style: TextStyle(fontWeight: FontWeight.w900),
),
],
),
actions: [
if (sessionsList.isNotEmpty)
IconButton(
tooltip: 'Очистить историю сессий',
icon: const Icon(Icons.delete_outline_rounded),
onPressed: _confirmClearHistory,
),
IconButton(
tooltip: 'Открыть календарь Слов Дня',
icon: const Icon(Icons.today_rounded, color: Color(0xFFFFD54F)),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const WordOfTheDayScreen()),
).then((_) => setState(() {}));
},
),
],
bottom: const TabBar(
indicatorColor: Color(0xFFFFD54F),
indicatorSize: TabBarIndicatorSize.tab,
dividerColor: Color(0xFF1E2238),
labelColor: Color(0xFFFFD54F),
unselectedLabelColor: Color(0xFF8C93B8),
labelStyle: TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
tabs: [
Tab(
icon: Icon(Icons.history_edu_rounded, size: 18),
text: 'Сессии Раскладов',
),
Tab(
icon: Icon(Icons.calendar_month_rounded, size: 18),
text: 'Хроника Слов Дня',
),
],
),
),
body: TabBarView(
children: [
GradientStar(size: 24),
SizedBox(width: 10),
Text(
'История сессий',
style: TextStyle(fontWeight: FontWeight.w900),
// Tab 1: Oracle Divination Sessions
_buildSessionsTab(sessionsList),
// Tab 2: Word of the Day Chronicle / Calendar
_buildWordOfDayChronicleTab(dayRecords),
],
),
),
);
}
Widget _buildSessionsTab(List<OracleSession> sessionsList) {
if (sessionsList.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const GradientStar(size: 48),
const SizedBox(height: 16),
const Text(
'История сессий пока пуста',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Colors.white),
),
const SizedBox(height: 8),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 32),
child: Text(
'Задайте вопрос, совершите броски и откройте знаки, чтобы сохранить сессию здесь.',
textAlign: TextAlign.center,
style: TextStyle(color: Color(0xFF8C93B8)),
),
),
],
),
actions: [
if (sessionsList.isNotEmpty)
IconButton(
tooltip: 'Очистить историю',
icon: const Icon(Icons.delete_outline_rounded),
onPressed: _confirmClearHistory,
);
}
return ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
itemCount: sessionsList.length,
separatorBuilder: (_, _) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final session = sessionsList[index];
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: const Color(0xFFFFD54F).withValues(alpha: 0.25),
),
],
),
body: sessionsList.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const GradientStar(size: 48),
const SizedBox(height: 16),
Text(
'История сессий пока пуста',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
color: const Color(0xFF1B1D30),
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => SessionResultScreen(
session: session,
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
'Задайте вопрос, совершите броски и откройте знаки, чтобы сохранить сессию здесь.',
textAlign: TextAlign.center,
style: TextStyle(color: theme.colorScheme.outline),
),
).then((_) => setState(() {}));
},
borderRadius: BorderRadius.circular(20),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const Icon(
Icons.calendar_today_rounded,
size: 14,
color: Color(0xFFFFD54F),
),
const SizedBox(width: 6),
Text(
session.formattedDate,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFFFFD54F),
),
),
],
),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFF2A2D4A),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0xFFFFD54F).withValues(alpha: 0.3),
),
),
child: Text(
'${session.words.length} ${session.words.length == 1 ? "знак" : (session.words.length < 5 ? "знака" : "знаков")}',
style: const TextStyle(
fontSize: 11,
color: Color(0xFFFFF9C4),
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 4),
IconButton(
icon: const Icon(Icons.copy_rounded, size: 16),
tooltip: 'Скопировать',
onPressed: () => _copySession(session),
),
IconButton(
icon: const Icon(Icons.delete_outline, size: 16),
tooltip: 'Удалить',
onPressed: () async {
await _repo.deleteSession(session.id);
setState(() {});
},
),
],
),
],
),
if (session.question != null && session.question!.isNotEmpty) ...[
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF22243C),
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
const Icon(Icons.help_outline_rounded, size: 14, color: Color(0xFFFFD54F)),
const SizedBox(width: 6),
Expanded(
child: Text(
'Вопрос: «${session.question}»',
style: const TextStyle(
fontSize: 12.5,
fontStyle: FontStyle.italic,
color: Color(0xFFFFF9C4),
),
),
),
],
),
),
],
const SizedBox(height: 10),
Wrap(
spacing: 6,
runSpacing: 6,
children: session.words.map((w) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF131524),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: const Color(0xFFFFD54F).withValues(alpha: 0.3),
),
),
child: Text(
'#${w.formattedSessionNumber} ${w.word.toUpperCase()}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
color: Color(0xFFFFD54F),
),
),
);
}).toList(),
),
if (session.aiInterpretation != null && session.aiInterpretation!.isNotEmpty) ...[
const SizedBox(height: 10),
Container(
width: double.infinity,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: const Color(0xFF261D3A),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFFBA68C8).withValues(alpha: 0.4),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.psychology_rounded, size: 14, color: Color(0xFFE1BEE7)),
SizedBox(width: 6),
Text(
'AI Трактовка',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Color(0xFFE1BEE7),
),
),
],
),
const SizedBox(height: 4),
Text(
session.aiInterpretation!,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 12,
height: 1.4,
color: Color(0xFFF3E5F5),
),
),
],
),
),
],
],
),
),
),
);
},
);
}
Widget _buildWordOfDayChronicleTab(List<WordOfDayRecord> dayRecords) {
final streak = _wordOfDayService.calculateStreak();
final completedCount = _wordOfDayService.totalCompletedPractices;
final revealedCount = _wordOfDayService.totalRevealedDays;
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(14, 12, 14, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Stats Ribbon
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF2E2045), Color(0xFF14172B)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.5)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatPill('🔥 СТРИК', '$streak дн.', const Color(0xFFFFB74D)),
_buildStatPill('☀️ ОТКРЫТО', '$revealedCount дн.', const Color(0xFFFFD54F)),
_buildStatPill('✓ ПРАКТИКИ', '$completedCount', const Color(0xFF81C784)),
],
),
),
const SizedBox(height: 16),
// Action Button: Open Today
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFFD54F),
foregroundColor: const Color(0xFF0C0E1A),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
elevation: 4,
),
icon: const Icon(Icons.wb_sunny_rounded, size: 20),
label: const Text(
'ОТКРЫТЬ СЛОВО СЕГОДНЯШНЕГО ДНЯ',
style: TextStyle(fontWeight: FontWeight.w900, letterSpacing: 0.8, fontSize: 13),
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const WordOfTheDayScreen()),
).then((_) => setState(() {}));
},
),
const SizedBox(height: 16),
const Row(
children: [
Icon(Icons.history_toggle_off_rounded, size: 16, color: Color(0xFFFFD54F)),
SizedBox(width: 8),
Text(
'ХРОНИКА СЛОВ И ПРАКТИК',
style: TextStyle(
color: Color(0xFFFFD54F),
fontSize: 11.5,
fontWeight: FontWeight.w900,
letterSpacing: 1.0,
),
),
],
),
const SizedBox(height: 10),
if (dayRecords.isEmpty)
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: const Color(0xFF151829),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF2A2D4A)),
),
child: const Column(
children: [
GradientStar(size: 32),
SizedBox(height: 12),
Text(
'Пока нет открытых слов',
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white, fontSize: 14),
),
SizedBox(height: 6),
Text(
'Откройте сегодняшнее Слово Дня, выполните практику и зафиксируйте синхронии.',
textAlign: TextAlign.center,
style: TextStyle(color: Color(0xFF8C93B8), fontSize: 12, height: 1.35),
),
],
),
)
: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
itemCount: sessionsList.length,
separatorBuilder: (_, _) => const SizedBox(height: 12),
else
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: dayRecords.length,
separatorBuilder: (_, _) => const SizedBox(height: 10),
itemBuilder: (context, index) {
final session = sessionsList[index];
final rec = dayRecords[index];
final date = WordOfTheDayService.parseDateKey(rec.dateKey);
final isTriptych = rec.isTriptychMode;
final singleWord = _wordOfDayService.getWordForDate(date, phase: 'main');
final triptych = _wordOfDayService.getTriptychWordsForDate(date);
final isDone = isTriptych ? rec.isDayStepCompleted : rec.isStepCompleted;
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: const Color(0xFFFFD54F).withValues(alpha: 0.25),
return Container(
decoration: BoxDecoration(
color: const Color(0xFF151829),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isDone
? const Color(0xFF81C784).withValues(alpha: 0.5)
: const Color(0xFF2A2D4A),
),
),
color: isDark ? const Color(0xFF1B1D30) : theme.colorScheme.surface,
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => SessionResultScreen(session: session),
builder: (_) => WordOfTheDayScreen(initialDate: date),
),
).then((_) => setState(() {}));
},
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top Header: Date, Words Count, Actions
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const Icon(
Icons.access_time_rounded,
size: 14,
color: Color(0xFFFFD54F),
),
const SizedBox(width: 5),
Text(
session.formattedDate,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFFFFD54F),
),
),
],
Text(
isTriptych
? '⏳'
: (singleWord.emoji.isNotEmpty ? singleWord.emoji : '☀️'),
style: const TextStyle(fontSize: 22),
),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFF2A2D4A),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0xFFFFD54F).withValues(alpha: 0.3),
),
),
child: Text(
'${session.words.length} ${session.words.length == 1 ? "знак" : (session.words.length < 5 ? "знака" : "знаков")}',
style: const TextStyle(
fontSize: 11,
color: Color(0xFFFFF9C4),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isTriptych
? '🌅 ${triptych.morning.word.toUpperCase()} • ⚡ ${triptych.day.word.toUpperCase()} • 🌙 ${triptych.evening.word.toUpperCase()}'
: singleWord.word.toUpperCase(),
style: TextStyle(
color: const Color(0xFFFFF9C4),
fontWeight: FontWeight.bold,
fontSize: isTriptych ? 13 : 15,
letterSpacing: 0.6,
),
),
),
const SizedBox(width: 4),
IconButton(
icon: const Icon(Icons.copy_rounded, size: 16),
tooltip: 'Скопировать',
onPressed: () => _copySession(session),
),
IconButton(
icon: const Icon(Icons.delete_outline, size: 16),
tooltip: 'Удалить',
onPressed: () async {
await _repo.deleteSession(session.id);
setState(() {});
},
),
],
),
],
),
// Question Box if present
if (session.question != null && session.question!.isNotEmpty) ...[
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF22243C),
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
const Icon(Icons.help_outline_rounded, size: 14, color: Color(0xFFFFD54F)),
const SizedBox(width: 6),
Expanded(
child: Text(
'Вопрос: «${session.question}»',
Text(
'${_formatDateString(rec.dateKey)}${isTriptych ? " • Триптих" : ""}',
style: const TextStyle(
fontSize: 12.5,
fontStyle: FontStyle.italic,
color: Color(0xFFFFF9C4),
color: Color(0xFF8C93B8),
fontSize: 11,
),
),
),
],
],
),
),
),
],
const SizedBox(height: 10),
// Words Chips Row
Wrap(
spacing: 6,
runSpacing: 6,
children: session.words.map((w) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF131524) : theme.colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(10),
color: isDone
? const Color(0xFF1B3D23)
: const Color(0xFF262A44),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0xFFFFD54F).withValues(alpha: 0.3),
color: isDone
? const Color(0xFF81C784)
: const Color(0xFF8C93B8).withValues(alpha: 0.3),
),
),
child: Text(
'#${w.formattedSessionNumber} ${w.word.toUpperCase()}',
style: const TextStyle(
isDone
? '✓ Выполнено'
: (isTriptych
? (rec.areAllTriptychRevealed ? 'Открыто 3/3' : 'Триптих')
: 'Открыто'),
style: TextStyle(
color: isDone
? const Color(0xFF81C784)
: const Color(0xFFFFD54F),
fontSize: 11,
fontWeight: FontWeight.bold,
fontSize: 12,
color: Color(0xFFFFD54F),
),
),
);
}).toList(),
),
// AI Interpretation Snippet if generated
if (session.aiInterpretation != null && session.aiInterpretation!.isNotEmpty) ...[
const SizedBox(height: 10),
Container(
width: double.infinity,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: const Color(0xFF261D3A),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFFBA68C8).withValues(alpha: 0.4),
),
),
child: Column(
],
),
if (!isTriptych && singleWord.asks.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
'Коан: «${singleWord.asks}»',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Color(0xFFB39DDB),
fontSize: 12,
fontStyle: FontStyle.italic,
),
),
] else if (isTriptych && triptych.day.step.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
'Вызов дня: «${triptych.day.step}»',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Color(0xFFA5D6A7),
fontSize: 12,
),
),
],
if (rec.diaryNote.isNotEmpty) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFF1A2238),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF80DEEA).withValues(alpha: 0.3)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.psychology_rounded, size: 14, color: Color(0xFFE1BEE7)),
SizedBox(width: 6),
Text(
'AI Трактовка (G4F)',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Color(0xFFE1BEE7),
),
const Text('🪶', style: TextStyle(fontSize: 12)),
const SizedBox(width: 6),
Expanded(
child: Text(
rec.diaryNote,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Color(0xFF80DEEA),
fontSize: 11.5,
height: 1.3,
),
],
),
const SizedBox(height: 4),
Text(
session.aiInterpretation!,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 12,
height: 1.4,
color: Color(0xFFF3E5F5),
),
),
],
@@ -340,6 +660,33 @@ class _HistoryScreenState extends State<HistoryScreen> {
);
},
),
],
),
);
}
Widget _buildStatPill(String title, String val, Color color) {
return Column(
children: [
Text(
title,
style: TextStyle(
color: color,
fontSize: 10,
fontWeight: FontWeight.w900,
letterSpacing: 0.8,
),
),
const SizedBox(height: 4),
Text(
val,
style: const TextStyle(
color: Colors.white,
fontSize: 17,
fontWeight: FontWeight.w900,
),
),
],
);
}
}
+78 -42
View File
@@ -1,3 +1,4 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/history_entry_model.dart';
@@ -7,6 +8,7 @@ import '../services/words_repository.dart';
import '../widgets/cleromancy_dialog.dart';
import '../widgets/d10_dice_roller_widget.dart';
import '../widgets/gradient_star_badge.dart';
import '../widgets/interpreter_persona_picker.dart';
import 'session_result_screen.dart';
class HomeSearchScreen extends StatefulWidget {
@@ -183,7 +185,7 @@ class _HomeSearchScreenState extends State<HomeSearchScreen> {
});
}
Future<void> _reshuffleMatrix() async {
Future<void> _rollRandomCleromancy() async {
HapticFeedback.heavyImpact();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
@@ -193,41 +195,67 @@ class _HomeSearchScreenState extends State<HomeSearchScreen> {
SizedBox(width: 10),
Expanded(
child: Text(
'Запрашиваю суточную матрицу у RANDOM.ORG…',
'Бросаю священный жребий (3 × d10)…',
style: TextStyle(fontWeight: FontWeight.w600),
),
),
],
),
duration: Duration(seconds: 2),
duration: Duration(milliseconds: 1000),
behavior: SnackBarBehavior.floating,
),
);
final source = await _repo.reshuffleSessionNumbers();
if (!mounted) return;
_resetSession();
final viaOrg = source == RandomSource.randomOrg;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const GradientStar(size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(
viaOrg
? 'Матрица #000–#999 на сегодня задана RANDOM.ORG'
: 'RANDOM.ORG недоступен — матрица собрана локально',
style: const TextStyle(fontWeight: FontWeight.w600),
try {
final randomService = TrueRandomService();
final roll = await randomService.take(_wordsCount * 3);
if (!mounted) return;
final newDigits = <String>[];
for (int i = 0; i < _wordsCount; i++) {
final chunk = roll.digits.sublist(i * 3, (i + 1) * 3);
newDigits.add(chunk.join());
}
setState(() {
_wordsDigits = newDigits;
_activeWordIndex = _wordsCount - 1;
});
final viaOrg = roll.source == RandomSource.randomOrg;
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const GradientStar(size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(
viaOrg
? 'Выпал истинный жребий RANDOM.ORG (3 × d10)'
: 'Выпал криптографический жребий (3 × d10)',
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
),
],
],
),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
duration: const Duration(seconds: 3),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
);
);
} catch (_) {
final rng = math.Random();
if (!mounted) return;
setState(() {
_wordsDigits = List.generate(
_wordsCount,
(_) => List.generate(3, (_) => rng.nextInt(10)).join(),
);
_activeWordIndex = _wordsCount - 1;
});
}
}
@override
@@ -244,7 +272,7 @@ class _HomeSearchScreenState extends State<HomeSearchScreen> {
GradientStar(size: 24),
SizedBox(width: 10),
Text(
'Оракул 999',
'Оракул',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w900,
@@ -265,9 +293,9 @@ class _HomeSearchScreenState extends State<HomeSearchScreen> {
onPressed: _resetSession,
),
IconButton(
tooltip: 'Новая суточная матрица через RANDOM.ORG',
icon: const Icon(Icons.shuffle_rounded),
onPressed: _reshuffleMatrix,
tooltip: 'Бросить священный жребий (3 × d10)',
icon: const Icon(Icons.casino_rounded, color: Color(0xFF64B5F6)),
onPressed: _rollRandomCleromancy,
),
IconButton(
tooltip: _showKeypad ? 'Скрыть панель' : 'Показать панель',
@@ -284,6 +312,8 @@ class _HomeSearchScreenState extends State<HomeSearchScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const InterpreterPersonaPicker(),
const SizedBox(height: 12),
// Row: Question & Words count selector (1..9)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -499,20 +529,26 @@ class _HomeSearchScreenState extends State<HomeSearchScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const Icon(Icons.casino_outlined, size: 15, color: Color(0xFFFFD54F)),
const SizedBox(width: 6),
Text(
'Ввод граней (Слово ${_activeWordIndex + 1} из $_wordsCount):',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: Color(0xFFFFD54F),
Expanded(
child: Row(
children: [
const Icon(Icons.casino_outlined, size: 15, color: Color(0xFFFFD54F)),
const SizedBox(width: 6),
Flexible(
child: Text(
'Ввод граней (Слово ${_activeWordIndex + 1} из $_wordsCount):',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: Color(0xFFFFD54F),
),
overflow: TextOverflow.ellipsis,
),
),
),
],
],
),
),
const SizedBox(width: 8),
Text(
currentActiveDigits.length == 3
? 'Код: #$currentActiveDigits'
+599
View File
@@ -0,0 +1,599 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/history_entry_model.dart';
import '../models/houses_session_model.dart';
import '../models/word_model.dart';
import '../services/ai_oracle_service.dart';
import '../widgets/gradient_star_badge.dart';
import 'symbol_detail_screen.dart';
import 'matrix_geometry_screen.dart';
import '../models/syncretic_session_model.dart';
import '../services/words_repository.dart';
class HouseChainsScreen extends StatefulWidget {
final HousesSession session;
const HouseChainsScreen({super.key, required this.session});
@override
State<HouseChainsScreen> createState() => _HouseChainsScreenState();
}
class _HouseChainsScreenState extends State<HouseChainsScreen> {
final _aiService = AiOracleService();
final Map<int, String> _chainAiInterpretations = {};
final Set<int> _loadingChains = {};
@override
void initState() {
super.initState();
_recordChainsToSyncreticSession(showToast: false);
}
Future<void> _recordChainsToSyncreticSession({bool showToast = false}) async {
final session = widget.session;
if (session.chains.isEmpty) return;
final chainItems = session.chains.map((chain) {
final pathStr = chain.steps.map((s) => '«${s.guest.word.toUpperCase()}»').join(' ➔ ');
final startWord = chain.startingHouse.houseWord;
final aiText = _chainAiInterpretations[chain.chainIndex];
var loreText = 'Траектория: ' + pathStr + '\n' + chain.description;
if (aiText != null && aiText.isNotEmpty) {
loreText += '\n\nТолкование Оракула:\n' + aiText;
}
return ReadingSignItem(
title: 'Цепочка #${chain.chainIndex}: ${chain.endTypeTitle}',
subtitle: 'Старт: Дом #${chain.startingHouse.houseIndex + 1} «${startWord.word}» • Шагов: ${chain.steps.length}',
symbol: chain.hasLoop ? '🔄' : (chain.endType == ChainEndType.archonThrone ? '👑' : '🔗'),
lore: loreText,
linkedWord: chain.steps.isNotEmpty ? chain.steps.last.guest : startWord,
meta: {
'chainIndex': chain.chainIndex,
'endType': chain.endType.name,
'stepsCount': chain.steps.length,
'path': pathStr,
},
);
}).toList();
await WordsRepository().addReadingToSyncreticSession(
SyncreticReading(
id: 'chains_${session.id}',
systemId: 'matrix_chains',
systemName: 'Матрица 33×30: Цепочки Домов',
systemIcon: '🔗',
timestamp: session.createdAt,
layoutTitle: '${session.chains.length} ${session.chains.length == 1 ? "цепочка" : (session.chains.length < 5 ? "цепочки" : "цепочек")}',
inputMethod: 'dice',
items: chainItems,
question: session.question,
),
question: session.question,
);
if (showToast && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Цепочки домов (${session.chains.length}) сохранены в Синхроническую сессию дня',
style: const TextStyle(fontWeight: FontWeight.w600),
),
backgroundColor: const Color(0xFF1E2238),
behavior: SnackBarBehavior.floating,
),
);
}
}
Future<void> _interpretChain(HouseChain chain) async {
setState(() => _loadingChains.add(chain.chainIndex));
HapticFeedback.lightImpact();
final buffer = StringBuffer();
buffer.writeln('Ты — верховный оракул, читающий цепочки домов Ленорман в системе Оракула 999.');
buffer.writeln('Проанализируй путь перетекания силы в цепочке домов:');
buffer.writeln('ЦЕПОЧКА #${chain.chainIndex} (${chain.steps.length} шагов).');
buffer.writeln('ИТОГ ЦЕПОЧКИ: ${chain.description}.\n');
for (int i = 0; i < chain.steps.length; i++) {
final step = chain.steps[i];
final sector = widget.session.sectors[step.house.sectorIndex];
buffer.writeln(
'Шаг ${i + 1}: В Доме #${step.house.houseIndex + 1} «${step.house.houseWord.word.toUpperCase()}» (Архонт «${sector.archonWord.word.toUpperCase()}») выпал Гость «${step.guest.word.toUpperCase()}».',
);
if (step.isLoopClosing) {
buffer.writeln(' ➔ [ПЕТЛЯ ЗАМКНУЛАСЬ: возврат энергии в исходный дом]');
} else if (step.leadsToArchon) {
buffer.writeln(' ➔ [ВОСХОЖДЕНИЕ К АРХОНТУ: увенчание Престолом «${step.destinationArchon?.archonWord.word.toUpperCase()}»]');
} else if (step.computed) {
buffer.writeln(' ➔ [ВЫЧИСЛЕНО ИЗ ПОЛЯ, НЕ ЖРЕБИЙ: дом не был открыт]');
}
}
buffer.writeln('\nДай емкое и глубокое толкование: что запускает этот поток, где ключевой перелом и к какому исходу приводит замыкание цепи.');
try {
final res = await _aiService.interpretSession(
words: chain.steps
.map((s) => SessionWord.fromWordItem(
s.guest,
positionTitle: 'Дом «${s.house.houseWord.word}»',
))
.toList(),
question: widget.session.question ?? 'Цепочка домов судьбы',
);
if (mounted) {
setState(() {
_chainAiInterpretations[chain.chainIndex] = res;
_loadingChains.remove(chain.chainIndex);
});
_recordChainsToSyncreticSession(showToast: false);
}
} catch (e) {
if (mounted) {
setState(() {
_chainAiInterpretations[chain.chainIndex] = 'Связь с оракулом прервалась: $e';
_loadingChains.remove(chain.chainIndex);
});
}
}
}
void _openWordDossier(WordItem word) {
HapticFeedback.selectionClick();
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SymbolDetailScreen(word: word)),
);
}
void _copyChain(HouseChain chain) {
final buffer = StringBuffer();
buffer.writeln('🔗 ОРАКУЛ — ЦЕПОЧКА ДОМОВ #${chain.chainIndex}');
buffer.writeln('Итог: ${chain.description}\n');
for (int i = 0; i < chain.steps.length; i++) {
final s = chain.steps[i];
final archon = widget.session.sectors[s.house.sectorIndex].archonWord.word.toUpperCase();
buffer.writeln(
'${i + 1}. [Архонт: $archon] ➔ Дом «${s.house.houseWord.word.toUpperCase()}» ➔ Гость «${s.guest.word.toUpperCase()}»',
);
if (s.isLoopClosing) {
buffer.writeln(' 🔄 Цикл замкнулся!');
} else if (s.leadsToArchon) {
buffer.writeln(' 👑 Восхождение к Архонту «${s.destinationArchon?.archonWord.word.toUpperCase()}»');
} else if (s.computed) {
buffer.writeln(' ◎ Знак вычислен из поля, это не жребий.');
}
}
if (_chainAiInterpretations[chain.chainIndex] != null) {
buffer.writeln('\n🤖 Толкование Оракула:\n${_chainAiInterpretations[chain.chainIndex]}');
}
Clipboard.setData(ClipboardData(text: buffer.toString()));
HapticFeedback.selectionClick();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Цепочка домов скопирована в буфер'),
behavior: SnackBarBehavior.floating,
),
);
}
@override
Widget build(BuildContext context) {
final chains = widget.session.chains;
return Scaffold(
backgroundColor: const Color(0xFF0A0C16),
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.bookmark_add_rounded, color: Color(0xFFFFD54F)),
onPressed: () => _recordChainsToSyncreticSession(showToast: true),
),
],
),
body: chains.isEmpty
? const Center(
child: Text(
'В данном раскладе нет открытых цепочек',
style: TextStyle(color: Color(0xFFB0BEC5)),
),
)
: ListView.builder(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 40),
itemCount: chains.length + 1,
itemBuilder: (context, index) {
if (index == 0) {
return _buildHeaderInfo();
}
final chain = chains[index - 1];
return _buildChainCard(chain);
},
),
);
}
Widget _buildHeaderInfo() {
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF151829),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.3)),
),
child: const Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.hub_outlined, color: Color(0xFFFFD54F), size: 24),
SizedBox(width: 12),
Expanded(
child: Text(
'Цепочки домов показывают траекторию судьбы: гость из первого дома ведет в собственную обитель на поле 33×30, гость следующего дома ведет дальше — пока энергия не замкнется в петлю судьбы или не взойдет к Престолу Архонта.',
style: TextStyle(color: Color(0xFFCFD8DC), fontSize: 13, height: 1.45),
),
),
],
),
);
}
Widget _buildChainCard(HouseChain chain) {
final isInterpreting = _loadingChains.contains(chain.chainIndex);
final aiText = _chainAiInterpretations[chain.chainIndex];
Color statusColor;
IconData statusIcon;
String statusTitle;
switch (chain.endType) {
case ChainEndType.loop:
statusColor = const Color(0xFFE57373);
statusIcon = Icons.sync_rounded;
statusTitle = 'Петля Судьбы (Замкнутый узел)';
break;
case ChainEndType.archonThrone:
statusColor = const Color(0xFFFFD54F);
statusIcon = Icons.shield_rounded;
statusTitle = 'Восхождение к Престолу Архонта';
break;
case ChainEndType.fieldVector:
statusColor = const Color(0xFF64B5F6);
statusIcon = Icons.trending_flat_rounded;
statusTitle = 'Вектор в поле (Открытый луч)';
break;
}
return Container(
margin: const EdgeInsets.only(bottom: 20),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF131627),
borderRadius: BorderRadius.circular(22),
border: Border.all(
color: chain.hasLoop
? const Color(0xFFFFD54F).withValues(alpha: 0.5)
: const Color(0xFF2A2D4A),
width: chain.hasLoop ? 1.5 : 1.0,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header: Chain index + Status Badge
Row(
children: [
Text(
'ЦЕПОЧКА #${chain.chainIndex}',
style: const TextStyle(
color: Color(0xFFFFD54F),
fontWeight: FontWeight.w900,
fontSize: 16,
letterSpacing: 0.8,
),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: statusColor.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: statusColor, width: 1.0),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(statusIcon, size: 14, color: statusColor),
const SizedBox(width: 4),
Text(
statusTitle,
style: TextStyle(
color: statusColor,
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
const SizedBox(height: 6),
Text(
chain.description,
style: const TextStyle(color: Color(0xFFB0BEC5), fontSize: 12),
),
const SizedBox(height: 16),
// Steps list
for (int i = 0; i < chain.steps.length; i++) ...[
_buildStepRow(chain.steps[i], i + 1),
if (i < chain.steps.length - 1)
Padding(
padding: const EdgeInsets.only(left: 20, top: 4, bottom: 4),
child: Row(
children: [
const Icon(Icons.arrow_downward_rounded, size: 16, color: Color(0xFFFFD54F)),
const SizedBox(width: 8),
Text(
'переход в обитель знака «${chain.steps[i].guest.word.toUpperCase()}» на поле',
style: const TextStyle(color: Color(0xFF78909C), fontSize: 10, fontStyle: FontStyle.italic),
),
],
),
),
],
const SizedBox(height: 16),
// AI Interpretation if exists
if (aiText != null) ...[
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF1E2238),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.4)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.auto_awesome_rounded, color: Color(0xFFFFD54F), size: 16),
SizedBox(width: 6),
Text(
'Толкование цепочки Оракулом:',
style: TextStyle(
color: Color(0xFFFFD54F),
fontWeight: FontWeight.bold,
fontSize: 13,
),
),
],
),
const SizedBox(height: 8),
Text(
aiText,
style: const TextStyle(color: Color(0xFFECEFF1), fontSize: 13, height: 1.45),
),
],
),
),
const SizedBox(height: 12),
],
// Action Buttons
Row(
children: [
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2A2D4A),
foregroundColor: const Color(0xFFFFD54F),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(color: Color(0xFFFFD54F), width: 0.8),
),
),
onPressed: isInterpreting ? null : () => _interpretChain(chain),
icon: isInterpreting
? const SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFFFFD54F)),
)
: const Icon(Icons.auto_awesome, size: 16),
label: Text(
aiText == null ? 'Трактовать (AI)' : 'Обновить (AI)',
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
),
),
const SizedBox(width: 8),
OutlinedButton.icon(
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF00E5FF),
side: const BorderSide(color: Color(0xFF00E5FF), width: 0.8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MatrixGeometryScreen(
session: widget.session,
initialChainIndex: chain.chainIndex,
),
),
);
},
icon: const Icon(Icons.polyline_rounded, size: 15),
label: const Text('Узор на поле', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold)),
),
const Spacer(),
IconButton(
tooltip: 'Скопировать цепочку',
icon: const Icon(Icons.copy_rounded, size: 18, color: Color(0xFF9FA8DA)),
onPressed: () => _copyChain(chain),
),
],
),
],
),
);
}
Widget _buildStepRow(HouseChainStep step, int order) {
final sector = widget.session.sectors[step.house.sectorIndex];
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFF181C33),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: step.isLoopClosing
? const Color(0xFFE57373)
: (step.leadsToArchon ? const Color(0xFFFFD54F) : sector.sectorColor.withValues(alpha: 0.3)),
width: (step.isLoopClosing || step.leadsToArchon) ? 1.5 : 1.0,
),
),
child: Row(
children: [
// Order circle
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: sector.sectorColor.withValues(alpha: 0.2),
shape: BoxShape.circle,
border: Border.all(color: sector.sectorColor),
),
child: Center(
child: Text(
'$order',
style: TextStyle(
color: sector.sectorColor,
fontWeight: FontWeight.w900,
fontSize: 11,
),
),
),
),
const SizedBox(width: 12),
// Content
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// House title
InkWell(
onTap: () => _openWordDossier(step.house.houseWord),
child: Row(
children: [
Text(
'Дом #${(step.house.houseIndex + 1).toString().padLeft(3, '0')}: «${step.house.houseWord.word.toUpperCase()}»',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
fontSize: 13,
),
),
const SizedBox(width: 6),
Text(
'(Архонт: ${sector.archonWord.word})',
style: TextStyle(
color: sector.sectorColor,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
],
),
),
const SizedBox(height: 3),
// Guest title
InkWell(
onTap: () => _openWordDossier(step.guest),
child: Row(
children: [
const Icon(Icons.auto_awesome_rounded, size: 13, color: Color(0xFFFFD54F)),
const SizedBox(width: 4),
Text(
step.computed
? 'Вычислен из поля, не жребий: «${step.guest.word.toUpperCase()}»'
: 'В него пришел Гость: «${step.guest.word.toUpperCase()}»',
style: const TextStyle(
color: Color(0xFFFFD54F),
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
// Special termination notices
if (step.isLoopClosing) ...[
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: const Color(0xFFE57373).withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(6),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.sync_rounded, size: 12, color: Color(0xFFE57373)),
SizedBox(width: 4),
Text(
'🔄 Узел замкнулся: энергия возвращается в исходный дом!',
style: TextStyle(color: Color(0xFFEF9A9A), fontSize: 11, fontWeight: FontWeight.bold),
),
],
),
),
] else if (step.leadsToArchon) ...[
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: const Color(0xFFFFD54F).withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.shield_rounded, size: 12, color: Color(0xFFFFD54F)),
const SizedBox(width: 4),
Text(
'👑 Восхождение к Престолу Архонта «${step.destinationArchon?.archonWord.word.toUpperCase()}»!',
style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 11, fontWeight: FontWeight.bold),
),
],
),
),
],
],
),
),
],
),
);
}
}
File diff suppressed because it is too large Load Diff
+964
View File
@@ -0,0 +1,964 @@
import '../models/syncretic_session_model.dart';
import '../services/words_repository.dart';
import '../models/history_entry_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/houses_session_model.dart';
import '../models/word_model.dart';
import '../services/ai_oracle_service.dart';
import '../widgets/gradient_star_badge.dart';
import 'house_chains_screen.dart';
import 'matrix_geometry_screen.dart';
import '../services/oracle_master_prompt_generator.dart';
import 'symbol_detail_screen.dart';
class HousesResultScreen extends StatefulWidget {
final HousesSession session;
const HousesResultScreen({super.key, required this.session});
@override
State<HousesResultScreen> createState() => _HousesResultScreenState();
}
class _HousesResultScreenState extends State<HousesResultScreen> {
final _aiService = AiOracleService();
String? _aiInterpretation;
bool _isLoadingAi = false;
@override
void initState() {
super.initState();
// Auto-record Houses Session into active Syncretic Day Session
final session = widget.session;
final items = session.selectedHouses.map((h) {
final g = h.guestWord ?? h.houseWord;
return ReadingSignItem(
title: 'Дом #${(h.houseIndex + 1).toString().padLeft(3, "0")} «${g.word.toUpperCase()}»',
subtitle: 'Архонт: ${session.sectors[h.sectorIndex].archonWord.word}',
symbol: '🏛️',
lore: g.core.isNotEmpty ? g.core : g.description,
linkedWord: g,
);
}).toList();
WordsRepository().addReadingToSyncreticSession(
SyncreticReading(
id: 'reading_${session.id}',
systemId: 'matrix',
systemName: 'Матрица 33×30 (Дома)',
systemIcon: '🏛️',
timestamp: session.createdAt,
layoutTitle: '${items.length} ${items.length == 1 ? "дом" : "домов"}',
inputMethod: 'dice',
items: items,
question: session.question,
),
question: session.question,
);
// Auto-record Calculated House Chains into active Syncretic Day Session
if (session.chains.isNotEmpty) {
final chainItems = session.chains.map((chain) {
final pathStr = chain.steps.map((s) => '«${s.guest.word.toUpperCase()}»').join(' ➔ ');
final startWord = chain.startingHouse.houseWord;
return ReadingSignItem(
title: 'Цепочка #${chain.chainIndex}: ${chain.endTypeTitle}',
subtitle: 'Старт: Дом #${chain.startingHouse.houseIndex + 1} «${startWord.word}» • Шагов: ${chain.steps.length}',
symbol: chain.hasLoop ? '🔄' : (chain.endType == ChainEndType.archonThrone ? '👑' : '🔗'),
lore: 'Траектория: ' + pathStr + '\n' + chain.description,
linkedWord: chain.steps.isNotEmpty ? chain.steps.last.guest : startWord,
meta: {
'chainIndex': chain.chainIndex,
'endType': chain.endType.name,
'stepsCount': chain.steps.length,
'path': pathStr,
},
);
}).toList();
WordsRepository().addReadingToSyncreticSession(
SyncreticReading(
id: 'chains_${session.id}',
systemId: 'matrix_chains',
systemName: 'Матрица 33×30: Цепочки Домов',
systemIcon: '🔗',
timestamp: session.createdAt,
layoutTitle: '${session.chains.length} ${session.chains.length == 1 ? "цепочка" : (session.chains.length < 5 ? "цепочки" : "цепочек")}',
inputMethod: 'dice',
items: chainItems,
question: session.question,
),
question: session.question,
);
}
_aiInterpretation = widget.session.aiInterpretation;
}
Future<void> _requestAiInterpretation() async {
setState(() => _isLoadingAi = true);
HapticFeedback.lightImpact();
final buffer = StringBuffer();
buffer.writeln('Ты — верховный оракул, читающий расклад Домов 33×30 в традиции Оракула 999.');
buffer.writeln('В раскладе участвуют 3 уровня реальности:');
buffer.writeln('1. Небесный Архонт Сектора — высший закон и климат триады столбцов.');
buffer.writeln('2. Земной Дом — конкретная сфера и почва действия.');
buffer.writeln('3. Знак-Гость — сила, прибывшая в дом по жребию.\n');
buffer.writeln('ПОСЛЕДОВАТЕЛЬНОСТЬ ОТКРЫТЫХ ДОМОВ:');
for (int i = 0; i < widget.session.selectedHouses.length; i++) {
final h = widget.session.selectedHouses[i];
final sector = widget.session.sectors[h.sectorIndex];
final guest = h.guestWord!;
buffer.writeln(
'Позиция ${i + 1}: Архонт «${sector.archonWord.word.toUpperCase()}» ➔ Дом «${h.houseWord.word.toUpperCase()}» ➔ Гость «${guest.word.toUpperCase()}»',
);
if (guest.core.isNotEmpty) {
buffer.writeln(' Суть гостя: ${guest.core}');
}
}
if (widget.session.chains.isNotEmpty) {
buffer.writeln('\nЦЕПОЧКИ ДОМОВ:');
for (final c in widget.session.chains) {
buffer.writeln('• Цепочка #${c.chainIndex} (${c.hasLoop ? "петля" : "вектор"}): ${c.description}');
}
}
buffer.writeln('\nДай развернутое, цельное толкование расклада: как разворачивается путь от первого шага к исходу, как гости чувствуют себя в домах под властью своих Архонтов, и какой главный совет даёт оракул.');
try {
final res = await _aiService.interpretSession(
words: widget.session.selectedHouses
.map((h) => SessionWord.fromWordItem(
h.guestWord!,
positionTitle: 'Дом #${h.houseIndex + 1} «${h.houseWord.word}»',
))
.toList(),
question: widget.session.question ?? 'Расклад Домов 33×30',
);
if (mounted) {
setState(() {
_aiInterpretation = res;
_isLoadingAi = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_aiInterpretation = 'Ошибка связи с оракулом: $e';
_isLoadingAi = false;
});
}
}
}
void _openWordDossier(WordItem word) {
HapticFeedback.selectionClick();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SymbolDetailScreen(word: word),
),
);
}
void _copyAll() {
final buffer = StringBuffer();
buffer.writeln('🔮 ОРАКУЛ — РАСКЛАД ДОМОВ 33×30');
if (widget.session.question != null && widget.session.question!.isNotEmpty) {
buffer.writeln('Вопрос: «${widget.session.question}»');
}
buffer.writeln('Открыто домов: ${widget.session.selectedHouses.length}\n');
for (int i = 0; i < widget.session.selectedHouses.length; i++) {
final h = widget.session.selectedHouses[i];
final sector = widget.session.sectors[h.sectorIndex];
final guest = h.guestWord!;
buffer.writeln(
'#${i + 1} [Сектор: Архонт ${sector.archonWord.word.toUpperCase()}]',
);
buffer.writeln(' 🏛️ Дом #${(h.houseIndex + 1).toString().padLeft(3, '0')} «${h.houseWord.word.toUpperCase()}»');
buffer.writeln(' ✨ Гость: «${guest.word.toUpperCase()}»');
if (guest.core.isNotEmpty) {
buffer.writeln(' Закон: ${guest.core}');
}
buffer.writeln();
}
if (_aiInterpretation != null) {
buffer.writeln('🤖 Толкование Оракула:\n$_aiInterpretation\n');
}
Clipboard.setData(ClipboardData(text: buffer.toString()));
HapticFeedback.mediumImpact();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Полный расклад домов скопирован в буфер'),
behavior: SnackBarBehavior.floating,
),
);
}
@override
Widget build(BuildContext context) {
final session = widget.session;
final activeSectorIndices = session.selectedHouses.map((h) => h.sectorIndex).toSet();
return Scaffold(
backgroundColor: const Color(0xFF0A0C16),
appBar: AppBar(
title: const Row(
children: [
GradientStar(size: 20),
SizedBox(width: 8),
Text('Расклад Домов 33×30', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 17)),
],
),
actions: [
IconButton(
tooltip: 'Цепочки Домов',
icon: Badge(
label: Text('${session.chains.length}'),
backgroundColor: const Color(0xFFFFD54F),
textColor: Colors.black,
child: const Icon(Icons.hub_outlined, color: Color(0xFFFFD54F)),
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HouseChainsScreen(session: session),
),
);
},
),
IconButton(
tooltip: 'Узоры Матрицы 33×30',
icon: const Icon(Icons.auto_awesome_mosaic_rounded, color: Color(0xFFFFD54F)),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MatrixGeometryScreen(session: session),
),
);
},
),
IconButton(
tooltip: 'Скопировать расклад',
icon: const Icon(Icons.copy_rounded),
onPressed: _copyAll,
),
],
),
body: ListView(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 40),
children: [
// Querent (Significator) Card
_buildQuerentSection(session),
const SizedBox(height: 16),
// Revealed Archons Banner
_buildArchonsRevealSection(activeSectorIndices),
const SizedBox(height: 16),
// Houses sequence header
Row(
children: [
const Icon(Icons.format_list_numbered_rounded, color: Color(0xFFFFD54F), size: 18),
const SizedBox(width: 8),
Text(
'ПОСЛЕДОВАТЕЛЬНОСТЬ ДОМОВ (${session.selectedHouses.length}):',
style: const TextStyle(
color: Color(0xFFFFD54F),
fontWeight: FontWeight.w900,
fontSize: 13,
letterSpacing: 0.8,
),
),
],
),
const SizedBox(height: 12),
// Houses Cards
for (int i = 0; i < session.selectedHouses.length; i++) ...[
_buildHouseStepCard(session.selectedHouses[i], i + 1),
const SizedBox(height: 12),
],
const SizedBox(height: 12),
// 1. Master Prompt for Advanced LLMs (Grok / Claude / Gemini / GPT-4)
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF261D48),
foregroundColor: const Color(0xFFFFD54F),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: Color(0xFFFFD54F), width: 1.4),
),
elevation: 6,
shadowColor: const Color(0x66FFD54F),
),
onPressed: () => _showMasterPromptDialog(context, session),
icon: const Icon(Icons.psychology_alt_rounded, color: Color(0xFFFFD54F), size: 22),
label: const Text(
'🧠 Мастер-Промпт для Нейросети (Grok / Claude / Gemini)',
style: TextStyle(fontWeight: FontWeight.w900, fontSize: 13.5, letterSpacing: 0.3),
),
),
const SizedBox(height: 12),
// 2. Matrix Geometry & Constellation Pattern
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF19203C),
foregroundColor: const Color(0xFF00E5FF),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: Color(0xFF00E5FF), width: 1.2),
),
elevation: 4,
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MatrixGeometryScreen(session: session),
),
);
},
icon: const Icon(Icons.auto_awesome_mosaic_rounded, color: Color(0xFF00E5FF)),
label: Text(
'Узор на Матрице 33×30 (${session.selectedHouses.length} узлов)',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14.5),
),
),
const SizedBox(height: 12),
// Action Button: House Chains Explorer
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1E2238),
foregroundColor: const Color(0xFFFFD54F),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: Color(0xFFFFD54F), width: 1.2),
),
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HouseChainsScreen(session: session),
),
);
},
icon: const Icon(Icons.hub_outlined, color: Color(0xFFFFD54F)),
label: Text(
'Исследовать Цепочки Домов (${session.chains.length})',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
),
),
const SizedBox(height: 16),
// AI Interpretation Section
_buildAiSection(),
],
),
);
}
Widget _buildArchonsRevealSection(Set<int> activeSectors) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF131627),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFF3949AB).withValues(alpha: 0.4)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.shield_moon_rounded, color: Color(0xFFFFD54F), size: 20),
SizedBox(width: 8),
Text(
'АРХОНТЫ АКТИВНЫХ СЕКТОРОВ',
style: TextStyle(
color: Color(0xFFFFF9C4),
fontWeight: FontWeight.w900,
fontSize: 13,
letterSpacing: 0.8,
),
),
],
),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: activeSectors.map((sectorIdx) {
final sector = widget.session.sectors[sectorIdx];
return InkWell(
onTap: () => _openWordDossier(sector.archonWord),
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: sector.sectorColor.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: sector.sectorColor, width: 1.2),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: sector.sectorColor,
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
Text(
'Триада ${sectorIdx + 1}: «${sector.archonWord.word.toUpperCase()}»',
style: TextStyle(
color: sector.sectorColor,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
),
),
);
}).toList(),
),
],
),
);
}
Widget _buildHouseStepCard(HouseCell cell, int order) {
final sector = widget.session.sectors[cell.sectorIndex];
final guest = cell.guestWord!;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF141729),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: sector.sectorColor.withValues(alpha: 0.4), width: 1.2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.25),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header: Order, Archon triad
Row(
children: [
Container(
width: 26,
height: 26,
decoration: BoxDecoration(
color: const Color(0xFFFFD54F),
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
'$order',
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.w900,
fontSize: 14,
),
),
),
),
const SizedBox(width: 10),
Expanded(
child: Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: sector.sectorColor,
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
Text(
'Под властью Архонта «${sector.archonWord.word.toUpperCase()}» (Триада ${cell.sectorIndex + 1})',
style: TextStyle(
color: sector.sectorColor,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
const SizedBox(height: 12),
// Host & Guest Banner
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF1C2038),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFF2A2D4A)),
),
child: Column(
children: [
// Host row
InkWell(
onTap: () => _openWordDossier(cell.houseWord),
child: Row(
children: [
const Icon(Icons.home_work_rounded, color: Color(0xFF90A4AE), size: 18),
const SizedBox(width: 8),
Text(
'Дом #${(cell.houseIndex + 1).toString().padLeft(3, '0')}:',
style: const TextStyle(color: Color(0xFF90A4AE), fontSize: 13),
),
const SizedBox(width: 6),
Text(
cell.houseWord.word.toUpperCase(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
fontSize: 15,
),
),
const Spacer(),
const Icon(Icons.arrow_forward_ios_rounded, size: 12, color: Color(0xFF5C6280)),
],
),
),
const Divider(color: Color(0xFF2A2D4A), height: 16),
// Guest row
InkWell(
onTap: () => _openWordDossier(guest),
child: Row(
children: [
const Icon(Icons.auto_awesome_rounded, color: Color(0xFFFFD54F), size: 18),
const SizedBox(width: 8),
const Text(
'Пришедший Гость:',
style: TextStyle(color: Color(0xFFFFD54F), fontSize: 13, fontWeight: FontWeight.w600),
),
const SizedBox(width: 6),
Text(
guest.word.toUpperCase(),
style: const TextStyle(
color: Color(0xFFFFF9C4),
fontWeight: FontWeight.w900,
fontSize: 16,
letterSpacing: 0.5,
),
),
const Spacer(),
const Icon(Icons.arrow_forward_ios_rounded, size: 12, color: Color(0xFFFFD54F)),
],
),
),
],
),
),
const SizedBox(height: 12),
// Guest Core / Meaning
if (guest.core.isNotEmpty) ...[
Text(
'✨ Закон знака: ${guest.core}',
style: const TextStyle(
color: Color(0xFFE0E0E0),
fontSize: 13,
height: 1.4,
),
),
const SizedBox(height: 8),
],
if (guest.oracle.isNotEmpty) ...[
Text(
guest.oracle,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Color(0xFFB0BEC5),
fontSize: 12,
height: 1.35,
),
),
],
],
),
);
}
Widget _buildAiSection() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF151829),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.4)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const GradientStar(size: 20),
const SizedBox(width: 8),
const Text(
'Толкование Оракула (AI)',
style: TextStyle(
color: Color(0xFFFFF9C4),
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
const Spacer(),
if (!_isLoadingAi)
IconButton(
tooltip: 'Запросить толкование',
icon: const Icon(Icons.auto_awesome, color: Color(0xFFFFD54F)),
onPressed: _requestAiInterpretation,
),
],
),
const SizedBox(height: 10),
if (_isLoadingAi) ...[
const Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 24),
child: Column(
children: [
CircularProgressIndicator(color: Color(0xFFFFD54F)),
SizedBox(height: 12),
Text('Оракул синтезирует Архонтов, Дома и Цепочки...', style: TextStyle(color: Color(0xFF90A4AE), fontSize: 13)),
],
),
),
),
] else if (_aiInterpretation != null) ...[
Text(
_aiInterpretation!,
style: const TextStyle(color: Color(0xFFECEFF1), fontSize: 14, height: 1.5),
),
] else ...[
const Text(
'Нажмите кнопку ниже, чтобы нейросеть Оракула соединила энергии 10 Архонтов, открытых Домов и Цепочек судьбы в единое предсказание.',
style: TextStyle(color: Color(0xFF90A4AE), fontSize: 13, height: 1.4),
),
const SizedBox(height: 14),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2A2D4A),
foregroundColor: const Color(0xFFFFD54F),
minimumSize: const Size.fromHeight(44),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
side: const BorderSide(color: Color(0xFFFFD54F), width: 0.8),
),
),
onPressed: _requestAiInterpretation,
icon: const Icon(Icons.auto_awesome_rounded),
label: const Text('Получить толкование Оракула'),
),
],
],
),
);
}
Widget _buildQuerentSection(HousesSession session) {
final q = session.querentWord;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF2A164D), Color(0xFF161933)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFFFFD54F), width: 1.5),
boxShadow: const [
BoxShadow(
color: Color(0x55FFD54F),
blurRadius: 16,
offset: Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFFFFD54F).withValues(alpha: 0.18),
border: Border.all(color: const Color(0xFFFFD54F), width: 1.2),
),
child: const Icon(Icons.workspace_premium_rounded, color: Color(0xFFFFD54F), size: 20),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'КВЕРЕНТ / СИГНИФИКАТОР РАСКЛАДА',
style: TextStyle(
color: Color(0xFFFFD54F),
fontSize: 10.5,
fontWeight: FontWeight.w900,
letterSpacing: 1.0,
),
),
const SizedBox(height: 2),
Text(
q != null ? q.word.toUpperCase() : 'ОТКРЫТЫЙ ФОКУС',
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
],
),
),
if (session.querentHouseIndex != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF3B246B),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFFFD54F), width: 0.8),
),
child: Text(
'Дом #${(session.querentHouseIndex! + 1).toString().padLeft(3, "0")}',
style: const TextStyle(color: Color(0xFFFFD54F), fontSize: 11, fontWeight: FontWeight.bold),
),
),
],
),
if (q != null) ...[
const SizedBox(height: 10),
if (q.core.isNotEmpty) ...[
Text(
'Суть объекта: ${q.core}',
style: const TextStyle(color: Color(0xFFCFD8DC), fontSize: 13, height: 1.4),
),
const SizedBox(height: 4),
],
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(6),
),
child: Text(
q.category,
style: const TextStyle(color: Color(0xFF9FA8DA), fontSize: 10.5),
),
),
const SizedBox(width: 8),
Text(
q.partOfSpeech,
style: const TextStyle(color: Color(0xFF78909C), fontSize: 11, fontStyle: FontStyle.italic),
),
],
),
],
if (session.question != null && session.question!.trim().isNotEmpty) ...[
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: const Color(0xFF101323),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF33385C), width: 0.8),
),
child: Row(
children: [
const Icon(Icons.help_outline_rounded, size: 16, color: Color(0xFFFFD54F)),
const SizedBox(width: 8),
Expanded(
child: Text(
'Вопрос: «${session.question!.trim()}»',
style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 12.5, fontWeight: FontWeight.w500),
),
),
],
),
),
],
],
),
);
}
void _showMasterPromptDialog(BuildContext context, HousesSession session) {
HapticFeedback.mediumImpact();
final prompt = OracleMasterPromptGenerator.generate(session);
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: const Color(0xFF0F1222),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (ctx) {
return DraggableScrollableSheet(
initialChildSize: 0.85,
minChildSize: 0.5,
maxChildSize: 0.95,
expand: false,
builder: (context, scrollController) {
return Container(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 44,
height: 4,
decoration: BoxDecoration(
color: Colors.white24,
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 14),
Row(
children: [
const Icon(Icons.psychology_alt_rounded, color: Color(0xFFFFD54F), size: 24),
const SizedBox(width: 10),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Мастер-Промпт для Нейросети',
style: TextStyle(color: Colors.white, fontSize: 17, fontWeight: FontWeight.bold),
),
Text(
'Для Grok, Claude 3.5 Sonnet, Gemini 1.5 Pro, GPT-4',
style: TextStyle(color: Color(0xFF90A4AE), fontSize: 11),
),
],
),
),
IconButton(
icon: const Icon(Icons.close_rounded, color: Color(0xFF90A4AE)),
onPressed: () => Navigator.pop(ctx),
),
],
),
const SizedBox(height: 12),
// Quick Actions Bar
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF1C223D),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'${prompt.length} символов • ${prompt.split(RegExp(r"\s+")).length} слов',
style: const TextStyle(color: Color(0xFF90CAF9), fontSize: 11),
),
),
const Spacer(),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFFD54F),
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(0xFF388E3C),
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),
// Scrollable Prompt Text Area
Expanded(
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF090A14),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF232742), width: 1.0),
),
child: SingleChildScrollView(
controller: scrollController,
child: SelectableText(
prompt,
style: const TextStyle(
color: Color(0xFFECEFF1),
fontFamily: 'monospace',
fontSize: 11.5,
height: 1.45,
),
),
),
),
),
],
),
);
},
);
},
);
}
}
+525
View File
@@ -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),
),
],
),
],
],
),
);
}
}
@@ -0,0 +1,663 @@
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';
class LenormandDivinationScreen extends StatefulWidget {
final String? initialQuestion;
const LenormandDivinationScreen({super.key, this.initialQuestion});
@override
State<LenormandDivinationScreen> createState() => _LenormandDivinationScreenState();
}
class _LenormandDivinationScreenState extends State<LenormandDivinationScreen> with SingleTickerProviderStateMixin {
late TabController _tabController;
final _questionController = TextEditingController();
final _directCardNumberController = TextEditingController();
final _repo = WordsRepository();
List<TraditionStation> _cards = [];
// Layouts: 2 cards (pair), 3 cards (line), 5 cards (cross)
DivinationLayout _selectedLayout = DivinationLayouts.lenormandLayouts[0];
int _currentStepIndex = 0;
// Dice simulation (d6 + d12)
int? _d6Value;
int? _d12Value;
bool _isRollingDice = false;
// Drawn results
List<({TraditionStation card, int cardNumber, 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!;
}
_loadCards();
}
void _loadCards() {
final canon = TraditionsRepository().byId('lenormand');
if (canon != null) {
_cards = canon.stations;
}
}
@override
void dispose() {
_tabController.dispose();
_questionController.dispose();
_directCardNumberController.dispose();
super.dispose();
}
int _calculateCardNumber(int d6, int d12) {
int base = 0;
if (d6 == 3 || d6 == 4) base = 12;
if (d6 == 5 || d6 == 6) base = 24;
return (base + d12).clamp(1, 36);
}
void _onLayoutChanged(DivinationLayout layout) {
setState(() {
_selectedLayout = layout;
_currentStepIndex = 0;
_drawnResults.clear();
_d6Value = null;
_d12Value = null;
_savedToSession = false;
});
}
Future<void> _rollDiceForStep() async {
if (_cards.isEmpty || _currentStepIndex >= _selectedLayout.count) return;
setState(() => _isRollingDice = true);
HapticFeedback.mediumImpact();
final rnd = Random();
for (int i = 0; i < 8; i++) {
await Future.delayed(const Duration(milliseconds: 50));
if (mounted) {
setState(() {
_d6Value = rnd.nextInt(6) + 1;
_d12Value = rnd.nextInt(12) + 1;
});
}
}
int finalD6 = rnd.nextInt(6) + 1;
int finalD12 = rnd.nextInt(12) + 1;
int cardNum = _calculateCardNumber(finalD6, finalD12);
// If card already drawn in this spread, pick next available card (no duplicates in a single layout)
final alreadyDrawnNums = _drawnResults.map((r) => r.cardNumber).toSet();
if (alreadyDrawnNums.contains(cardNum)) {
for (int i = 1; i <= 36; i++) {
final candidate = (cardNum + i - 1) % 36 + 1;
if (!alreadyDrawnNums.contains(candidate)) {
cardNum = candidate;
break;
}
}
}
final card = (cardNum - 1 < _cards.length) ? _cards[cardNum - 1] : null;
if (card != null && mounted) {
final posTitle = _selectedLayout.positionTitles[_currentStepIndex];
setState(() {
_d6Value = finalD6;
_d12Value = finalD12;
_isRollingDice = false;
_drawnResults.add((card: card, cardNumber: cardNum, positionTitle: posTitle));
_currentStepIndex++;
if (_currentStepIndex >= _selectedLayout.count) {
_saveToSession('dice');
}
});
HapticFeedback.heavyImpact();
}
}
void _applyManualCardNumber(int cardNum) {
if (cardNum < 1 || cardNum > 36 || _cards.isEmpty || _currentStepIndex >= _selectedLayout.count) return;
final card = (cardNum - 1 < _cards.length) ? _cards[cardNum - 1] : null;
if (card == null) return;
final posTitle = _selectedLayout.positionTitles[_currentStepIndex];
setState(() {
_drawnResults.add((card: card, cardNumber: cardNum, positionTitle: posTitle));
_currentStepIndex++;
_directCardNumberController.clear();
if (_currentStepIndex >= _selectedLayout.count) {
_saveToSession('manual');
}
});
HapticFeedback.selectionClick();
}
void _resetSpread() {
setState(() {
_currentStepIndex = 0;
_drawnResults.clear();
_d6Value = null;
_d12Value = null;
_savedToSession = false;
});
}
Future<void> _saveToSession(String inputMethod) async {
if (_drawnResults.isEmpty) return;
final items = _drawnResults.map((r) {
WordItem? linkedWord;
if (r.card.primary != null) {
linkedWord = _repo.getWordById(r.card.primary!.id) ?? _repo.getWordByName(r.card.primary!.word);
}
return ReadingSignItem(
title: '${r.positionTitle}: #${r.cardNumber} «${r.card.name}»',
subtitle: r.card.group,
symbol: '#${r.cardNumber}',
lore: r.card.lore,
linkedWord: linkedWord,
);
}).toList();
final reading = SyncreticReading(
id: 'reading_${DateTime.now().millisecondsSinceEpoch}',
systemId: 'lenormand',
systemName: 'Малый Оракул Ленорман',
systemIcon: '🪶',
timestamp: DateTime.now(),
layoutTitle: '${_selectedLayout.title} (${inputMethod == "dice" ? "d6+d12" : "со стола"})',
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),
),
);
}
}
Future<void> _autoDealFullTableau() async {
if (_cards.length < 36) return;
setState(() => _isRollingDice = true);
HapticFeedback.mediumImpact();
final rnd = Random();
final shuffledIndices = List<int>.generate(36, (i) => i)..shuffle(rnd);
_drawnResults.clear();
for (int i = 0; i < 36; i++) {
final cardIdx = shuffledIndices[i];
final card = _cards[cardIdx];
final cardNum = cardIdx + 1;
final posTitle = _selectedLayout.positionTitles[i];
_drawnResults.add((card: card, cardNumber: cardNum, positionTitle: posTitle));
}
if (mounted) {
setState(() {
_isRollingDice = false;
_currentStepIndex = 36;
});
HapticFeedback.heavyImpact();
_saveToSession('dice');
}
}
@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, 'lenormand'),
),
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('Ленорман (d6 + d12)', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 17)),
],
),
bottom: TabBar(
controller: _tabController,
indicatorColor: const Color(0xFFFFD54F),
tabs: const [
Tab(icon: Icon(Icons.casino_rounded, size: 18), text: 'Жребий d6+d12'),
Tab(icon: Icon(Icons.touch_app_rounded, size: 18), text: 'Ввод 1..36 со стола'),
],
),
),
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.lenormandLayouts.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: 290,
child: TabBarView(
controller: _tabController,
children: [
_buildDiceTab(),
_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++) ...[
_buildResultCardItem(_drawnResults[i], i + 1),
const SizedBox(height: 10),
],
],
],
),
),
);
}
Widget _buildDiceTab() {
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: [
const Text(
'БРОСОК d6 + d12 (1..36 РАВНОМЕРНО)',
style: TextStyle(color: Color(0xFFFFD54F), fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 0.8),
),
const SizedBox(height: 6),
Text(
'Извлечение позиции: «' + _selectedLayout.positionTitles[_currentStepIndex] + '»',
textAlign: TextAlign.center,
style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 11.5),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildDieBox('d6 (блок)', _d6Value, isD6: true),
const SizedBox(width: 20),
_buildDieBox('d12 (номер)', _d12Value),
],
),
const SizedBox(height: 18),
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: _isRollingDice ? null : _rollDiceForStep,
icon: const Icon(Icons.casino_rounded, size: 20),
label: Text(
_isRollingDice ? 'Кости летят...' : 'Бросить d6+d12 (шаг ${_currentStepIndex + 1})',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13.5),
),
),
if (_selectedLayout.count >= 9) ...[
const SizedBox(height: 10),
OutlinedButton.icon(
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF80DEEA),
side: const BorderSide(color: Color(0xFF80DEEA)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: _isRollingDice ? null : _autoDealFullTableau,
icon: const Icon(Icons.auto_awesome_rounded, size: 16),
label: Text(
_selectedLayout.count == 36 ? 'Случайный расклад всех 36 Домов' : 'Разложить весь квадрат 3×3',
style: const TextStyle(fontSize: 11.5, fontWeight: FontWeight.bold),
),
),
],
],
),
);
}
Widget _buildDieBox(String label, int? val, {bool isD6 = false}) {
String sub = '';
if (val != null && isD6) {
if (val <= 2) sub = 'База +0 (1..12)';
else if (val <= 4) sub = 'База +12 (13..24)';
else sub = 'База +24 (25..36)';
}
return Column(
children: [
Container(
width: 68,
height: 68,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xFF232742),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.6), width: 1.5),
),
child: Text(
val != null ? '$val' : '?',
style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 26, fontWeight: FontWeight.w900),
),
),
const SizedBox(height: 6),
Text(label, style: const TextStyle(color: Color(0xFF90A4AE), fontSize: 10, fontWeight: FontWeight.bold)),
if (sub.isNotEmpty)
Text(sub, style: const TextStyle(color: Color(0xFFFFD54F), fontSize: 9, fontWeight: FontWeight.bold)),
],
);
}
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(
children: [
Expanded(
child: TextField(
controller: _directCardNumberController,
keyboardType: TextInputType.number,
style: const TextStyle(color: Colors.white, fontSize: 14),
decoration: InputDecoration(
hintText: 'Номер карты для шага ${_currentStepIndex + 1} (1..36)...',
hintStyle: const TextStyle(color: Color(0xFF8C93B8), fontSize: 11),
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
filled: true,
fillColor: const Color(0xFF1E2238),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
),
onSubmitted: (val) {
final n = int.tryParse(val.trim());
if (n != null) _applyManualCardNumber(n);
},
),
),
const SizedBox(width: 8),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFFD54F),
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
onPressed: () {
final n = int.tryParse(_directCardNumberController.text.trim());
if (n != null) _applyManualCardNumber(n);
},
child: const Text('Ввод', style: TextStyle(fontWeight: FontWeight.bold)),
),
],
),
const SizedBox(height: 8),
Expanded(
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 6,
crossAxisSpacing: 6,
mainAxisSpacing: 6,
childAspectRatio: 1.0,
),
itemCount: _cards.length,
itemBuilder: (ctx, i) {
return InkWell(
onTap: () => _applyManualCardNumber(i + 1),
borderRadius: BorderRadius.circular(8),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xFF1E2238),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF2A2D4A)),
),
child: Text(
'${i + 1}',
style: const TextStyle(color: Color(0xFFE0E2EC), fontSize: 14, fontWeight: FontWeight.bold),
),
),
);
},
),
),
],
),
);
}
Widget _buildResultCardItem(({TraditionStation card, int cardNumber, String positionTitle}) res, int index) {
final c = res.card;
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: Text(
'#${res.cardNumber}',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFFFFD54F)),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
'#$index. ${res.positionTitle}: ${c.name}',
style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 14, fontWeight: FontWeight.bold),
),
),
IconButton(
icon: const Icon(Icons.auto_stories_rounded, size: 18, color: Color(0xFFFFD54F)),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
tooltip: 'Сочетания карты',
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => LenormandStationScreen(number: res.cardNumber),
),
);
},
),
],
),
const SizedBox(height: 4),
Text(c.group, style: const TextStyle(color: Color(0xFF80DEEA), fontSize: 11)),
const SizedBox(height: 6),
Text(
c.lore,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Color(0xFFD1D5DB), fontSize: 12, height: 1.4),
),
],
),
),
],
),
);
}
}
+110
View File
@@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../services/lenormand_pairs_repository.dart';
import 'lenormand_station_screen.dart';
class LenormandExtrasScreen extends StatelessWidget {
const LenormandExtrasScreen({super.key});
@override
Widget build(BuildContext context) {
final repo = LenormandPairsRepository();
return Scaffold(
backgroundColor: const Color(0xFF0C0E1A),
appBar: AppBar(title: const Text('Ленорман · триады и слои')),
body: ListView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
children: [
const Text(
'Триады',
style: TextStyle(color: Color(0xFFFFD54F), fontWeight: FontWeight.w900, fontSize: 16),
),
const SizedBox(height: 6),
const Text(
'Тройные сочетания с Карусели (порядок карт в раскладе подряд).',
style: TextStyle(color: Color(0x8AFFFFFF), fontSize: 12, height: 1.4),
),
const SizedBox(height: 10),
for (final t in repo.triads)
Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF151829),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0x1AFFFFFF)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 6,
children: [
for (var i = 0; i < t.cards.length; i++)
ActionChip(
label: Text('${t.cards[i]}. ${t.names[i]}'),
onPressed: () {
HapticFeedback.selectionClick();
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => LenormandStationScreen(number: t.cards[i]),
),
);
},
),
],
),
const SizedBox(height: 8),
Text(
t.text,
style: const TextStyle(color: Color(0xFFE8EAF6), fontSize: 13, height: 1.45),
),
],
),
),
const SizedBox(height: 16),
const Text(
'Дополнительные разделы',
style: TextStyle(color: Color(0xFFFFD54F), fontWeight: FontWeight.w900, fontSize: 16),
),
const SizedBox(height: 10),
for (final e in repo.extras)
Theme(
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
collapsedBackgroundColor: const Color(0xFF151829),
backgroundColor: const Color(0xFF151829),
collapsedShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(color: Color(0x1AFFFFFF)),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(color: Color(0x4DFFD54F)),
),
title: Text(
e.title.isEmpty ? e.source : e.title,
style: const TextStyle(
color: Color(0xFFFFF9C4),
fontWeight: FontWeight.w800,
fontSize: 14,
),
),
children: [
Padding(
padding: const EdgeInsets.fromLTRB(14, 0, 14, 14),
child: Text(
e.text,
style: const TextStyle(color: Color(0xFFD1D5DB), fontSize: 13, height: 1.5),
),
),
],
),
),
],
),
);
}
}
+158
View File
@@ -0,0 +1,158 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/lenormand_pairs.dart';
import '../services/lenormand_pairs_repository.dart';
import 'lenormand_station_screen.dart';
class LenormandPairScreen extends StatelessWidget {
final int left;
final int right;
final LenormandCardEntry? Function(int n)? lookup;
final String title;
const LenormandPairScreen({
super.key,
required this.left,
required this.right,
this.lookup,
this.title = 'Сочетание',
});
@override
Widget build(BuildContext context) {
final lookup = this.lookup ?? (n) => LenormandPairsRepository().byNumber(n);
final a = lookup(left);
final b = lookup(right);
LenormandPairLink? link;
if (a != null) {
for (final p in a.pairs) {
if (p.n == right) {
link = p;
break;
}
}
}
if (a == null || b == null || link == null) {
return Scaffold(
backgroundColor: const Color(0xFF0C0E1A),
appBar: AppBar(title: Text(title)),
body: const Center(
child: Text('Пара не найдена', style: TextStyle(color: Color(0xFF8C93B8))),
),
);
}
return Scaffold(
backgroundColor: const Color(0xFF0C0E1A),
appBar: AppBar(
title: Text('${a.name} + ${b.name}'),
),
body: ListView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
children: [
Row(
children: [
Expanded(child: _CardChip(n: a.n, name: a.name, symbol: a.symbol)),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Text('+', style: TextStyle(color: Color(0xFFFFD54F), fontSize: 22, fontWeight: FontWeight.w900)),
),
Expanded(child: _CardChip(n: b.n, name: b.name, symbol: b.symbol)),
],
),
const SizedBox(height: 8),
Row(
children: [
TextButton(
onPressed: () {
HapticFeedback.selectionClick();
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => LenormandStationScreen(
number: a.n,
lookup: lookup,
title: title,
),
),
);
},
child: Text('← ${a.name}'),
),
const Spacer(),
TextButton(
onPressed: () {
HapticFeedback.selectionClick();
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => LenormandStationScreen(
number: b.n,
lookup: lookup,
title: title,
),
),
);
},
child: Text('${b.name} →'),
),
],
),
const SizedBox(height: 14),
Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(14, 12, 14, 14),
decoration: BoxDecoration(
color: const Color(0xFF151829),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0x4DFFD54F)),
),
child: Text(
link.displayText,
style: const TextStyle(
color: Color(0xFFE8EAF6),
fontSize: 14,
height: 1.5,
),
),
),
],
),
);
}
}
class _CardChip extends StatelessWidget {
final int n;
final String name;
final String symbol;
const _CardChip({required this.n, required this.name, required this.symbol});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFF151829),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0x4DFFD54F)),
),
child: Column(
children: [
Text(symbol, style: const TextStyle(fontSize: 22)),
const SizedBox(height: 4),
Text(
'$n. $name',
textAlign: TextAlign.center,
style: const TextStyle(
color: Color(0xFFFFF9C4),
fontWeight: FontWeight.w800,
fontSize: 13,
),
),
],
),
);
}
}
+263
View File
@@ -0,0 +1,263 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/lenormand_pairs.dart';
import '../services/lenormand_pairs_repository.dart';
import 'lenormand_pair_screen.dart';
class LenormandStationScreen extends StatefulWidget {
final int number;
final LenormandCardEntry? Function(int n)? lookup;
final String title;
const LenormandStationScreen({
super.key,
required this.number,
this.lookup,
this.title = 'Ленорман',
});
@override
State<LenormandStationScreen> createState() => _LenormandStationScreenState();
}
class _LenormandStationScreenState extends State<LenormandStationScreen> {
late int _n;
String _query = '';
@override
void initState() {
super.initState();
_n = widget.number;
}
@override
Widget build(BuildContext context) {
final lookup = widget.lookup ?? (n) => LenormandPairsRepository().byNumber(n);
final card = lookup(_n);
if (card == null) {
return Scaffold(
backgroundColor: const Color(0xFF0C0E1A),
appBar: AppBar(title: Text(widget.title)),
body: const Center(
child: Text('Карта не найдена', style: TextStyle(color: Color(0xFF8C93B8))),
),
);
}
final pairs = card.pairs.where((p) {
if (_query.isEmpty) return true;
final q = _query.toLowerCase();
return p.name.toLowerCase().contains(q) ||
p.preview.toLowerCase().contains(q) ||
'${p.n}'.contains(q);
}).toList();
return Scaffold(
backgroundColor: const Color(0xFF0C0E1A),
appBar: AppBar(
title: Text('${card.n}. ${card.name}'),
),
body: ListView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
children: [
Row(
children: [
Container(
width: 56,
height: 56,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0x1AFFD54F),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0x4DFFD54F)),
),
child: Text(card.symbol, style: const TextStyle(fontSize: 24)),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
card.name,
style: const TextStyle(
color: Color(0xFFFFF9C4),
fontWeight: FontWeight.w900,
fontSize: 20,
),
),
if (card.lore.isNotEmpty)
Text(
card.lore,
style: const TextStyle(
color: Color(0xFF90CDF4),
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
const SizedBox(height: 18),
const _SectionTitle('Значение'),
const SizedBox(height: 8),
Text(
card.displayMeaning,
style: const TextStyle(color: Color(0xFFD1D5DB), fontSize: 14, height: 1.5),
),
if (card.shadow.isNotEmpty) ...[
const SizedBox(height: 16),
const _SectionTitle('Тень'),
const SizedBox(height: 8),
Text(
card.shadow,
style: const TextStyle(color: Color(0xFFC5CAE9), fontSize: 13, height: 1.5),
),
],
if (card.pairs.isNotEmpty) ...[
const SizedBox(height: 22),
_SectionTitle('Сочетания (${card.pairs.length})'),
const SizedBox(height: 6),
Text(
'Пара с каждой из остальных ${card.pairs.length} карт. Нажмите, чтобы открыть толкование.',
style: TextStyle(color: Color(0x8AFFFFFF), fontSize: 12, height: 1.4),
),
const SizedBox(height: 10),
TextField(
onChanged: (v) => setState(() => _query = v.trim()),
style: const TextStyle(color: Color(0xFFE8EAF6), fontSize: 14),
decoration: InputDecoration(
hintText: 'Поиск по второй карте…',
hintStyle: const TextStyle(color: Color(0xFF8C93B8)),
filled: true,
fillColor: const Color(0xFF151829),
prefixIcon: const Icon(Icons.search, color: Color(0xFFFFD54F)),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0x1AFFFFFF)),
),
),
),
const SizedBox(height: 12),
for (final p in pairs)
_PairTile(
card: card,
link: p,
onOpen: () {
HapticFeedback.selectionClick();
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => LenormandPairScreen(
left: card.n,
right: p.n,
lookup: lookup,
title: widget.title,
),
),
);
},
),
],
],
),
);
}
}
class _SectionTitle extends StatelessWidget {
final String text;
const _SectionTitle(this.text);
@override
Widget build(BuildContext context) {
return Text(
text,
style: const TextStyle(
color: Color(0xFFFFD54F),
fontWeight: FontWeight.w900,
fontSize: 16,
letterSpacing: 0.3,
),
);
}
}
class _PairTile extends StatelessWidget {
final LenormandCardEntry card;
final LenormandPairLink link;
final VoidCallback onOpen;
const _PairTile({
required this.card,
required this.link,
required this.onOpen,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Material(
color: const Color(0xFF151829),
borderRadius: BorderRadius.circular(12),
child: InkWell(
onTap: onOpen,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.fromLTRB(12, 10, 10, 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0x1AFFFFFF)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 36,
child: Text(
'${link.n}',
style: const TextStyle(
color: Color(0xFFFFD54F),
fontWeight: FontWeight.w900,
fontSize: 14,
),
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${card.name} + ${link.name}',
style: const TextStyle(
color: Color(0xFFFFF9C4),
fontWeight: FontWeight.w800,
fontSize: 14,
),
),
const SizedBox(height: 4),
Text(
link.displayText,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Color(0xFFC5CAE9),
fontSize: 12,
height: 1.4,
),
),
],
),
),
const Icon(Icons.chevron_right_rounded, color: Color(0xFF8C93B8)),
],
),
),
),
),
);
}
}
+963
View File
@@ -0,0 +1,963 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/word_model.dart';
import '../models/word_passport_model.dart';
import '../services/passport_schema_dart.dart';
import '../services/words_repository.dart';
import '../widgets/gradient_star_badge.dart';
/// Ручной обогатитель единого паспорта знака «Оракула 999»
/// Полноценная система окошек и предпромптов по всем 6 разделам канона (78 полей).
class ManualDiveScreen extends StatefulWidget {
final WordItem? initialWord;
const ManualDiveScreen({super.key, this.initialWord});
@override
State<ManualDiveScreen> createState() => _ManualDiveScreenState();
}
class _ManualDiveScreenState extends State<ManualDiveScreen> with SingleTickerProviderStateMixin {
final _repo = WordsRepository();
final _searchController = TextEditingController();
late final TabController _tabController;
// 78 контроллеров по ключам полей
final Map<String, TextEditingController> _controllers = {};
// Контроллер для умной вставки сырого текста
final _pasteRawController = TextEditingController();
WordItem? _selectedWord;
bool _isSaving = false;
final List<String> _tabTitles = const [
'I. Метаданные (4)',
'II. Мантика (17)',
'III. Языки (17)',
'IV. Матрица (3)',
'V. Культуры (33)',
'VI. Синтез (4)',
'📄 Паспорт',
];
@override
void initState() {
super.initState();
_tabController = TabController(length: _tabTitles.length, vsync: this);
// Инициализируем контроллеры для всех 78 полей
for (final field in kPassportFields78) {
_controllers[field.key] = TextEditingController();
_controllers[field.key]!.addListener(_onFieldChanged);
}
// Если передан начальный знак, выбираем его
if (widget.initialWord != null) {
_selectWord(widget.initialWord!);
} else if (_repo.allWords.isNotEmpty) {
_selectWord(_repo.allWords.first);
}
}
void _onFieldChanged() {
if (mounted) setState(() {});
}
@override
void dispose() {
_tabController.dispose();
_searchController.dispose();
_pasteRawController.dispose();
for (final c in _controllers.values) {
c.removeListener(_onFieldChanged);
c.dispose();
}
super.dispose();
}
void _selectWord(WordItem word) {
final fresh = _repo.getWordByName(word.word) ?? word;
final passport = _repo.getPassport(fresh.id) ??
WordPassport(
id: fresh.id,
word: fresh.word,
pos: fresh.partOfSpeech,
category: fresh.category,
emoji: fresh.emoji,
);
setState(() {
_selectedWord = fresh;
_searchController.text = fresh.word;
// Заполняем контроллеры из паспорта
for (final field in kPassportFields78) {
final val = passport.getField(field.key);
_controllers[field.key]!.text = val;
}
});
}
int get _filledCount {
var count = 0;
for (final c in _controllers.values) {
if (c.text.trim().isNotEmpty) count++;
}
return count;
}
int _countFilledInSection(String secNum) {
var count = 0;
for (final field in kPassportFields78) {
if (field.sectionNum == secNum) {
if (_controllers[field.key]!.text.trim().isNotEmpty) {
count++;
}
}
}
return count;
}
int _totalInSection(String secNum) {
return kPassportFields78.where((f) => f.sectionNum == secNum).length;
}
Future<void> _savePassport() async {
final word = _selectedWord;
if (word == null) return;
setState(() => _isSaving = true);
final fieldsMap = <String, String>{};
for (final entry in _controllers.entries) {
fieldsMap[entry.key] = entry.value.text.trim();
}
final existingPassport = _repo.getPassport(word.id);
final updated = WordPassport(
id: word.id,
word: word.word,
pos: word.partOfSpeech,
category: word.category,
emoji: word.emoji,
fields: fieldsMap,
filledCount: _filledCount,
isComplete: _filledCount >= 78,
updatedAt: DateTime.now().toUtc().toIso8601String(),
illustrations: existingPassport?.illustrations ?? const [],
);
await _repo.savePassport(updated);
if (!mounted) return;
setState(() {
_isSaving = false;
});
HapticFeedback.mediumImpact();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: const Color(0xFF2E7D32),
content: Text('Паспорт знака «${word.word.toUpperCase()}» сохранен! (Заполнено $_filledCount/78)'),
),
);
}
void _copyToClipboard(String text, String label) {
Clipboard.setData(ClipboardData(text: text));
HapticFeedback.selectionClick();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
duration: const Duration(seconds: 2),
content: Text('✓ $label скопирован в буфер обмена'),
),
);
}
Future<void> _pasteFromClipboard(TextEditingController controller) async {
final data = await Clipboard.getData(Clipboard.kTextPlain);
if (data?.text != null && data!.text!.isNotEmpty) {
controller.text = data.text!.trim();
HapticFeedback.selectionClick();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
duration: Duration(milliseconds: 1500),
content: Text('✓ Вставлено из буфера'),
),
);
}
}
void _applyParsedFields(Map<String, String> parsed) {
if (parsed.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Не удалось распознать поля шаблона в тексте.')),
);
return;
}
var applied = 0;
setState(() {
for (final entry in parsed.entries) {
if (_controllers.containsKey(entry.key) && entry.value.isNotEmpty) {
_controllers[entry.key]!.text = entry.value;
applied++;
}
}
});
HapticFeedback.heavyImpact();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: const Color(0xFF1565C0),
content: Text('Успешно распознано и заполнено $applied полей! Проверьте и сохраните.'),
),
);
}
void _showSmartParserDialog() {
_pasteRawController.clear();
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF1E2138),
title: const Row(
children: [
Icon(Icons.auto_awesome, color: Color(0xFFFFD54F)),
SizedBox(width: 8),
Text('Умный разбор Markdown'),
],
),
content: SizedBox(
width: 500,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Вставьте сюда любой скопированный ответ нейросети (карточку целиком или отдельный раздел). Система автоматически распознает строки и заполнит соответствующие окошки.',
style: TextStyle(color: Color(0xFFB7B9C8), fontSize: 13),
),
const SizedBox(height: 12),
TextField(
controller: _pasteRawController,
minLines: 6,
maxLines: 12,
decoration: InputDecoration(
hintText: 'Вставьте ответ модели сюда...',
filled: true,
fillColor: const Color(0xFF14172B),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Отмена'),
),
FilledButton.icon(
style: FilledButton.styleFrom(backgroundColor: const Color(0xFFFFD54F), foregroundColor: Colors.black),
onPressed: () {
final raw = _pasteRawController.text.trim();
Navigator.pop(ctx);
if (raw.isNotEmpty) {
final parsed = parsePassportMarkdownToFields(raw);
_applyParsedFields(parsed);
}
},
icon: const Icon(Icons.flash_on_rounded),
label: const Text('Разобрать и заполнить'),
),
],
),
);
}
void _sharePassport(WordItem word) {
final buffer = StringBuffer();
buffer.writeln('🔮 ОРАКУЛ • ПАСПОРТ СИМВОЛА #${word.formattedSessionNumber}');
buffer.writeln('«${word.word.toUpperCase()}» ${word.emoji}');
if (word.category.isNotEmpty) {
buffer.writeln('Категория: ${word.category}');
}
buffer.writeln('');
if (word.core.isNotEmpty) {
buffer.writeln('📖 Суть:');
buffer.writeln(word.core);
buffer.writeln('');
}
buffer.writeln('🔮 Базовое толкование:');
buffer.writeln(word.description);
buffer.writeln('');
final passport = _repo.getPassport(word.id);
if (passport != null) {
final f = passport.fields;
final sm = f['sacred_meaning']?.toString() ?? '';
if (sm.isNotEmpty) {
buffer.writeln('✨ Сакральный смысл:');
buffer.writeln(sm);
buffer.writeln('');
}
final arch = f['archetype']?.toString() ?? '';
if (arch.isNotEmpty) {
buffer.writeln('🏛 Архетип:');
buffer.writeln(arch);
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('Паспорт «${word.word}» скопирован для отправки!')),
],
),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
);
}
@override
Widget build(BuildContext context) {
final word = _selectedWord;
final query = _searchController.text.trim().toLowerCase();
final matches = query.isEmpty
? const <WordItem>[]
: _repo.allWords
.where((w) => w.word.toLowerCase().contains(query) || w.formattedSessionNumber.contains(query))
.take(8)
.toList();
final filled = _filledCount;
final progress = filled / 78.0;
return Scaffold(
backgroundColor: const Color(0xFF0F111E),
appBar: AppBar(
backgroundColor: const Color(0xFF161828),
title: const Row(
children: [
GradientStar(size: 22),
SizedBox(width: 10),
Text('Паспорт знака', style: TextStyle(fontWeight: FontWeight.w900)),
],
),
actions: [
IconButton(
tooltip: 'Поделиться паспортом символа',
onPressed: word == null ? null : () => _sharePassport(word),
icon: const Icon(Icons.share_rounded, color: Color(0xFFFFD54F)),
),
IconButton(
tooltip: 'Умная вставка Markdown',
onPressed: word == null ? null : _showSmartParserDialog,
icon: const Icon(Icons.auto_awesome, color: Color(0xFFFFD54F)),
),
PopupMenuButton<String>(
tooltip: 'Пакеты промптов',
icon: const Icon(Icons.more_vert),
onSelected: (action) {
if (word == null) return;
if (action == 'all_prompts') {
_copyToClipboard(buildAllPassportPrompts(word), 'Пакет из 78 промптов');
} else if (action == 'smart_paste') {
_showSmartParserDialog();
} else if (action == 'export_md') {
_copyToClipboard(_generateCurrentMarkdown(), 'Канонический Markdown карточки');
}
},
itemBuilder: (_) => const [
PopupMenuItem(
value: 'all_prompts',
child: Row(
children: [
Icon(Icons.copy_all_rounded, size: 18),
SizedBox(width: 8),
Text('Скопировать все 78 промптов'),
],
),
),
PopupMenuItem(
value: 'smart_paste',
child: Row(
children: [
Icon(Icons.auto_awesome, size: 18, color: Color(0xFFFFD54F)),
SizedBox(width: 8),
Text('Умная вставка ответа'),
],
),
),
PopupMenuItem(
value: 'export_md',
child: Row(
children: [
Icon(Icons.description_outlined, size: 18),
SizedBox(width: 8),
Text('Скопировать готовую карточку'),
],
),
),
],
),
Padding(
padding: const EdgeInsets.only(right: 8),
child: FilledButton.icon(
style: FilledButton.styleFrom(
backgroundColor: const Color(0xFFFFD54F),
foregroundColor: Colors.black,
),
onPressed: word == null || _isSaving ? null : _savePassport,
icon: Icon(_isSaving ? Icons.hourglass_top : Icons.save_rounded, size: 18),
label: Text(_isSaving ? 'Сохранение...' : 'Записать знак'),
),
),
],
bottom: word == null
? null
: TabBar(
controller: _tabController,
isScrollable: true,
labelColor: const Color(0xFFFFD54F),
unselectedLabelColor: const Color(0xFF8E90A4),
indicatorColor: const Color(0xFFFFD54F),
indicatorWeight: 3,
tabs: [
for (int i = 0; i < _tabTitles.length; i++)
Tab(
child: Row(
children: [
Text(_tabTitles[i]),
if (i < 6) ...[
const SizedBox(width: 6),
_badge(
'${_countFilledInSection(_sectionNumForTab(i))}/${_totalInSection(_sectionNumForTab(i))}',
isComplete: _countFilledInSection(_sectionNumForTab(i)) ==
_totalInSection(_sectionNumForTab(i)),
),
],
],
),
),
],
),
),
body: Column(
children: [
// Поле поиска и быстрый выбор
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 6),
child: TextField(
controller: _searchController,
onChanged: (_) => setState(() {}),
decoration: InputDecoration(
labelText: 'Выбор знака из 1000 (номер или слово)',
hintText: '000 или огонь...',
filled: true,
fillColor: const Color(0xFF161828),
prefixIcon: const Icon(Icons.search, color: Color(0xFFFFD54F)),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear, size: 18),
onPressed: () {
_searchController.clear();
setState(() {});
},
)
: null,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(14)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: const Color(0xFFFFD54F).withValues(alpha: 0.2)),
),
),
),
),
// Выпадающий список совпадений
if (matches.isNotEmpty && (word == null || word.word.toLowerCase() != query))
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
height: 120,
decoration: BoxDecoration(
color: const Color(0xFF161828),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.3)),
),
child: ListView.separated(
itemCount: matches.length,
separatorBuilder: (_, _) => const Divider(height: 1, color: Color(0xFF24273E)),
itemBuilder: (context, idx) {
final m = matches[idx];
return ListTile(
dense: true,
title: Text('#${m.formattedSessionNumber} ${m.word.toUpperCase()} ${m.emoji}',
style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text('${m.partOfSpeech} • ${m.category}', maxLines: 1),
onTap: () {
_selectWord(m);
FocusScope.of(context).unfocus();
},
);
},
),
),
// Шапка выбранного знака и шкала заполнения
if (word != null)
Container(
margin: const EdgeInsets.fromLTRB(16, 4, 16, 8),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFF161828),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.15)),
),
child: Column(
children: [
Row(
children: [
Text(
'#${word.formattedSessionNumber} ${word.word.toUpperCase()}',
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w900, color: Color(0xFFFFD54F)),
),
const SizedBox(width: 8),
Text(word.emoji, style: const TextStyle(fontSize: 18)),
const Spacer(),
Text(
'$filled / 78 заполнено (${(progress * 100).toInt()}%)',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: progress >= 1.0 ? const Color(0xFF66BB6A) : const Color(0xFFE0E0E0),
),
),
],
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: progress,
minHeight: 6,
backgroundColor: const Color(0xFF24273E),
valueColor: AlwaysStoppedAnimation<Color>(
progress >= 1.0
? const Color(0xFF66BB6A)
: (progress > 0.5 ? const Color(0xFFFFD54F) : const Color(0xFFFFA726)),
),
),
),
if ((_repo.getPassport(word.id)?.illustrations ?? const []).isNotEmpty) ...[
const SizedBox(height: 8),
_buildIllustrationsBar(_repo.getPassport(word.id)!.illustrations),
],
],
),
),
// Контент: Вкладки с окошками
Expanded(
child: word == null
? const Center(child: Text('Выберите знак для обогащения'))
: TabBarView(
controller: _tabController,
children: [
_buildSectionTab('I', 'Заголовочный паспорт (Метаданные)'),
_buildSectionTab('II', 'Кросс-мантическая сеть и станции'),
_buildSectionTab('III', 'Мультилингвальный и графический паспорт'),
_buildSectionTab('IV', 'Трехполярная матрица состояний'),
_buildSectionTab('V', 'Культурологическая матрица и Мета-синтез'),
_buildSectionTab('VI', 'Мантический & Обучающий синтез'),
_buildPassportPreviewTab(),
],
),
),
],
),
);
}
String _sectionNumForTab(int tabIndex) {
switch (tabIndex) {
case 0:
return 'I';
case 1:
return 'II';
case 2:
return 'III';
case 3:
return 'IV';
case 4:
return 'V';
case 5:
return 'VI';
default:
return 'I';
}
}
Widget _badge(String text, {required bool isComplete}) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: isComplete ? const Color(0xFF2E7D32) : const Color(0xFF24273E),
borderRadius: BorderRadius.circular(10),
),
child: Text(
text,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: isComplete ? Colors.white : const Color(0xFFB7B9C8),
),
),
);
}
Widget _buildSectionTab(String sectionNum, String sectionTitle) {
final fields = kPassportFields78.where((f) => f.sectionNum == sectionNum).toList();
final word = _selectedWord!;
return ListView(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 28),
children: [
// Панель управления разделом
Container(
margin: const EdgeInsets.only(bottom: 14),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF161828),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.15)),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Раздел $sectionNum: $sectionTitle',
style: const TextStyle(fontWeight: FontWeight.w800, fontSize: 13, color: Color(0xFFFFD54F)),
),
const SizedBox(height: 2),
Text(
'${_countFilledInSection(sectionNum)} из ${fields.length} полей заполнено',
style: const TextStyle(fontSize: 12, color: Color(0xFFB7B9C8)),
),
],
),
),
OutlinedButton.icon(
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFFFFD54F),
side: const BorderSide(color: Color(0xFFFFD54F)),
),
onPressed: () => _copyToClipboard(
buildSectionPrePrompt(sectionNum, word),
'Промпты раздела $sectionNum',
),
icon: const Icon(Icons.copy_rounded, size: 15),
label: const Text('Скопировать раздел', style: TextStyle(fontSize: 12)),
),
],
),
),
// Список окошек для каждого поля
for (final field in fields) _buildFieldCard(field, word),
],
);
}
Widget _buildFieldCard(PassportFieldDef field, WordItem word) {
final controller = _controllers[field.key]!;
final isFilled = controller.text.trim().isNotEmpty;
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF161828),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: isFilled
? const Color(0xFF66BB6A).withValues(alpha: 0.4)
: const Color(0xFFFFD54F).withValues(alpha: 0.15),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Шапка поля
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
decoration: BoxDecoration(
color: isFilled ? const Color(0xFF1B5E20) : const Color(0xFF24273E),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'#${field.index.toString().padLeft(2, '0')}',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w800,
color: isFilled ? const Color(0xFF81C784) : const Color(0xFFB7B9C8),
),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
field.title,
style: const TextStyle(fontWeight: FontWeight.w800, fontSize: 14, color: Color(0xFFFFF9C4)),
),
),
if (isFilled)
const Icon(Icons.check_circle_rounded, color: Color(0xFF66BB6A), size: 18)
else
const Icon(Icons.radio_button_unchecked_rounded, color: Color(0xFF757575), size: 18),
],
),
const SizedBox(height: 8),
// Кнопки предпромпта и вставки
Wrap(
spacing: 8,
runSpacing: 6,
children: [
OutlinedButton.icon(
style: OutlinedButton.styleFrom(
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
side: BorderSide(color: const Color(0xFFFFD54F).withValues(alpha: 0.5)),
foregroundColor: const Color(0xFFFFD54F),
),
onPressed: () => _copyToClipboard(
field.buildPrompt(word),
'Предпромпт «${field.title}»',
),
icon: const Icon(Icons.copy_rounded, size: 14),
label: const Text('Скопировать предпромпт', style: TextStyle(fontSize: 12)),
),
OutlinedButton.icon(
style: OutlinedButton.styleFrom(
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
foregroundColor: const Color(0xFFB7B9C8),
),
onPressed: () => _pasteFromClipboard(controller),
icon: const Icon(Icons.paste_rounded, size: 14),
label: const Text('Вставить', style: TextStyle(fontSize: 12)),
),
if (isFilled)
IconButton(
visualDensity: VisualDensity.compact,
tooltip: 'Очистить поле',
onPressed: () {
controller.clear();
HapticFeedback.selectionClick();
},
icon: const Icon(Icons.delete_outline, size: 16, color: Color(0xFFEF5350)),
),
],
),
const SizedBox(height: 8),
// Текстовое окошко
TextField(
controller: controller,
minLines: field.sectionNum == 'IV' || field.sectionNum == 'VI' ? 3 : 2,
maxLines: 8,
style: const TextStyle(fontSize: 13, height: 1.35),
decoration: InputDecoration(
hintText: 'Ожидаемый формат: ${field.formatTemplate}',
hintStyle: const TextStyle(color: Color(0xFF555870), fontSize: 12),
filled: true,
fillColor: const Color(0xFF0F111E),
contentPadding: const EdgeInsets.all(12),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: const Color(0xFF24273E)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Color(0xFFFFD54F)),
),
),
),
],
),
);
}
String _generateCurrentMarkdown() {
final word = _selectedWord;
if (word == null) return '';
final fieldsMap = <String, String>{};
for (final entry in _controllers.entries) {
fieldsMap[entry.key] = entry.value.text.trim();
}
final tempPassport = WordPassport(
id: word.id,
word: word.word,
pos: word.partOfSpeech,
category: word.category,
emoji: word.emoji,
fields: fieldsMap,
);
return tempPassport.toMarkdown();
}
Widget _buildPassportPreviewTab() {
final md = _generateCurrentMarkdown();
return ListView(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 32),
children: [
Row(
children: [
Expanded(
child: Text(
'Каноническая карточка знака ($_filledCount/78 заполнено)',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: Color(0xFFFFD54F)),
),
),
FilledButton.icon(
style: FilledButton.styleFrom(
backgroundColor: const Color(0xFFFFD54F),
foregroundColor: Colors.black,
),
onPressed: () => _copyToClipboard(md, 'Каноническая карточка знака'),
icon: const Icon(Icons.copy_rounded, size: 16),
label: const Text('Скопировать карточку'),
),
],
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF161828),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.2)),
),
child: SelectableText(
md,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12.5,
height: 1.45,
color: Color(0xFFE2E4F0),
),
),
),
],
);
}
Widget _buildIllustrationsBar(List<Map<String, dynamic>> illustrations) {
return InkWell(
onTap: () => _showIllustrationsDialog(illustrations),
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF24273E).withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.3)),
),
child: Row(
children: [
const Icon(Icons.photo_library_outlined, size: 16, color: Color(0xFFFFD54F)),
const SizedBox(width: 8),
Expanded(
child: Text(
'Академические гравюры: ${illustrations.length} илл. («${illustrations.first['caption']}»)',
style: const TextStyle(fontSize: 12, color: Color(0xFFFFD54F), fontWeight: FontWeight.w600),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const Icon(Icons.arrow_forward_ios_rounded, size: 12, color: Color(0xFFFFD54F)),
],
),
),
);
}
void _showIllustrationsDialog(List<Map<String, dynamic>> illustrations) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF161828),
title: Row(
children: [
const Icon(Icons.image_search_rounded, color: Color(0xFFFFD54F)),
const SizedBox(width: 8),
Text('Гравюры и схемы (${illustrations.length})', style: const TextStyle(fontSize: 18, color: Colors.white)),
],
),
content: SizedBox(
width: 500,
child: ListView.separated(
shrinkWrap: true,
itemCount: illustrations.length,
separatorBuilder: (_, __) => const Divider(color: Color(0xFF24273E)),
itemBuilder: (context, i) {
final ill = illustrations[i];
final filePath = ill['file'] as String? ?? '';
final caption = ill['caption'] as String? ?? '';
final source = ill['source'] as String? ?? '';
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset(
filePath,
fit: BoxFit.contain,
errorBuilder: (_, __, ___) => Container(
height: 120,
color: const Color(0xFF0F111E),
child: Center(child: Text('Изображение: $filePath', style: const TextStyle(fontSize: 12))),
),
),
),
const SizedBox(height: 8),
Text(caption, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Color(0xFFFFD54F))),
const SizedBox(height: 2),
Text(source, style: const TextStyle(fontSize: 11, color: Colors.grey)),
],
);
},
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('Закрыть', style: TextStyle(color: Color(0xFFFFD54F))),
),
],
),
);
}
}
+967
View File
@@ -0,0 +1,967 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/houses_session_model.dart';
import 'symbol_detail_screen.dart';
enum GeometryViewMode {
constellation, // Sacred polygon / constellation of chosen houses
allChains, // All chains overlaid
singleChain, // Focus on one specific chain
}
class MatrixGeometryScreen extends StatefulWidget {
final HousesSession session;
final int? initialChainIndex; // Optional focus on chain 1..N
const MatrixGeometryScreen({
super.key,
required this.session,
this.initialChainIndex,
});
@override
State<MatrixGeometryScreen> createState() => _MatrixGeometryScreenState();
}
class _MatrixGeometryScreenState extends State<MatrixGeometryScreen>
with SingleTickerProviderStateMixin {
final TransformationController _transformController = TransformationController();
late GeometryViewMode _viewMode;
int _selectedChainIndex = 1; // 1-based index
late AnimationController _animController;
static const double kCellSize = 64.0;
static const double kCellMargin = 3.0;
static const double kHeaderHeight = 72.0;
static const List<Color> kChainColors = [
Color(0xFF00E5FF), // Cyan
Color(0xFFFF4081), // Pink / Magenta
Color(0xFFFFD54F), // Gold
Color(0xFFB388FF), // Purple
Color(0xFF00E676), // Emerald
Color(0xFFFF6E40), // Coral
Color(0xFF448AFF), // Electric Blue
Color(0xFFEEFF41), // Lime
Color(0xFFFF80AB), // Rose Gold
];
@override
void initState() {
super.initState();
_animController = AnimationController(
vsync: this,
duration: const Duration(seconds: 4),
)..repeat();
if (widget.initialChainIndex != null &&
widget.initialChainIndex! > 0 &&
widget.initialChainIndex! <= widget.session.chains.length) {
_viewMode = GeometryViewMode.singleChain;
_selectedChainIndex = widget.initialChainIndex!;
} else {
_viewMode = GeometryViewMode.constellation;
}
_transformController.value = Matrix4.diagonal3Values(0.52, 0.52, 1.0);
}
@override
void dispose() {
_animController.dispose();
_transformController.dispose();
super.dispose();
}
void _resetZoom() {
HapticFeedback.lightImpact();
setState(() {
_transformController.value = Matrix4.diagonal3Values(0.52, 0.52, 1.0);
});
}
String _getFigureName(int pointsCount) {
switch (pointsCount) {
case 1:
return 'Монада (Точка Первоистока)';
case 2:
return 'Диада (Вектор Полярности)';
case 3:
return 'Триада (Дельта Творения)';
case 4:
return 'Тетрада (Земной Четверик)';
case 5:
return 'Пентаграмма (Узел Пяти Стихий)';
case 6:
return 'Гексаграмма (Печать Равновесия)';
case 7:
return 'Гептада (Семизвездный Венец)';
case 8:
return 'Октада (Роза Пространства)';
case 9:
return 'Эннеаграмма (Девятикратное Полотно Судьбы)';
default:
return 'Сакральная Геометрия ($pointsCount узлов)';
}
}
@override
Widget build(BuildContext context) {
const totalWidth = 30 * (kCellSize + kCellMargin * 2);
const totalHeight = 33 * (kCellSize + kCellMargin * 2) + kHeaderHeight;
final selectedChain = widget.session.chains.isNotEmpty &&
_selectedChainIndex <= widget.session.chains.length
? widget.session.chains[_selectedChainIndex - 1]
: null;
return Scaffold(
backgroundColor: const Color(0xFF0B0D18),
appBar: AppBar(
backgroundColor: const Color(0xFF101323),
elevation: 0,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Геометрия Матрицы 33×30',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
Text(
_viewMode == GeometryViewMode.constellation
? _getFigureName(widget.session.selectedHouses.length)
: (_viewMode == GeometryViewMode.allChains
? 'Все Цепочки Домов (${widget.session.chains.length})'
: 'Цепочка #$_selectedChainIndex: ${selectedChain?.endTypeTitle ?? ""}'),
style: const TextStyle(
fontSize: 11,
color: Color(0xFFFFD54F),
letterSpacing: 0.3,
),
),
],
),
actions: [
IconButton(
tooltip: 'Центрировать масштаб',
icon: const Icon(Icons.center_focus_strong_rounded, color: Color(0xFF90CAF9)),
onPressed: _resetZoom,
),
IconButton(
tooltip: 'Справка по геометрии',
icon: const Icon(Icons.info_outline_rounded, color: Color(0xFFCFD8DC)),
onPressed: () => _showHelpDialog(context),
),
],
),
body: Stack(
children: [
// 1. Zoomable & Pannable Matrix Canvas
InteractiveViewer(
transformationController: _transformController,
constrained: false,
minScale: 0.15,
maxScale: 2.5,
boundaryMargin: const EdgeInsets.all(500),
child: SizedBox(
width: totalWidth,
height: totalHeight,
child: Stack(
children: [
_buildBaseGrid(totalWidth, totalHeight),
AnimatedBuilder(
animation: _animController,
builder: (context, child) {
return CustomPaint(
size: Size(totalWidth, totalHeight),
painter: _MatrixGeometryPainter(
session: widget.session,
viewMode: _viewMode,
selectedChainIndex: _selectedChainIndex,
pulseValue: _animController.value,
chainColors: kChainColors,
),
);
},
),
],
),
),
),
// 2. Top Control Filter Bar (Modes & Chains selector)
Positioned(
top: 12,
left: 16,
right: 16,
child: _buildModeSelectorBar(),
),
// 3. Bottom Information Card
Positioned(
bottom: 16,
left: 16,
right: 16,
child: _buildBottomInfoCard(selectedChain),
),
],
),
);
}
Widget _buildModeSelectorBar() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xE613162A),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF2C3258), width: 0.8),
boxShadow: const [
BoxShadow(
color: Color(0x66000000),
blurRadius: 16,
offset: Offset(0, 6),
),
],
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_buildModeChip(
icon: Icons.auto_awesome_rounded,
label: 'Фигура (${widget.session.selectedHouses.length})',
isSelected: _viewMode == GeometryViewMode.constellation,
activeColor: const Color(0xFFFFD54F),
onTap: () {
HapticFeedback.selectionClick();
setState(() => _viewMode = GeometryViewMode.constellation);
},
),
const SizedBox(width: 6),
_buildModeChip(
icon: Icons.hub_rounded,
label: 'Все Цепочки',
isSelected: _viewMode == GeometryViewMode.allChains,
activeColor: const Color(0xFF00E5FF),
onTap: () {
HapticFeedback.selectionClick();
setState(() => _viewMode = GeometryViewMode.allChains);
},
),
const SizedBox(width: 8),
Container(width: 1, height: 24, color: const Color(0xFF2C3258)),
const SizedBox(width: 8),
for (int i = 1; i <= widget.session.chains.length; i++) ...[
_buildChainChip(i),
const SizedBox(width: 6),
],
],
),
),
);
}
Widget _buildModeChip({
required IconData icon,
required String label,
required bool isSelected,
required Color activeColor,
required VoidCallback onTap,
}) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(10),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: isSelected ? activeColor.withValues(alpha: 0.22) : Colors.transparent,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isSelected ? activeColor : const Color(0xFF2C3258),
width: isSelected ? 1.2 : 0.6,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: isSelected ? activeColor : const Color(0xFF90A4AE)),
const SizedBox(width: 5),
Text(
label,
style: TextStyle(
color: isSelected ? Colors.white : const Color(0xFF90A4AE),
fontWeight: isSelected ? FontWeight.bold : FontWeight.w500,
fontSize: 12,
),
),
],
),
),
),
);
}
Widget _buildChainChip(int index) {
final isSelected = _viewMode == GeometryViewMode.singleChain && _selectedChainIndex == index;
final color = kChainColors[(index - 1) % kChainColors.length];
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
HapticFeedback.selectionClick();
setState(() {
_viewMode = GeometryViewMode.singleChain;
_selectedChainIndex = index;
});
},
borderRadius: BorderRadius.circular(10),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 6),
decoration: BoxDecoration(
color: isSelected ? color.withValues(alpha: 0.24) : Colors.transparent,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isSelected ? color : const Color(0xFF2C3258),
width: isSelected ? 1.2 : 0.6,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 7,
height: 7,
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
),
const SizedBox(width: 5),
Text(
'#$index',
style: TextStyle(
color: isSelected ? Colors.white : const Color(0xFFB0BEC5),
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
fontSize: 12,
),
),
],
),
),
),
);
}
Widget _buildBottomInfoCard(HouseChain? selectedChain) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xF2121528),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: const Color(0xFF2E355F), width: 0.8),
boxShadow: const [
BoxShadow(
color: Color(0x77000000),
blurRadius: 20,
offset: Offset(0, 8),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.session.querentWord != null) ...[
Row(
children: [
const Icon(Icons.stars_rounded, color: Color(0xFFFFD54F), size: 15),
const SizedBox(width: 5),
Text(
'Кверент: «${widget.session.querentWord!.word.toUpperCase()}» (Дом #${(widget.session.querentHouseIndex! + 1).toString().padLeft(3, "0")})',
style: const TextStyle(
color: Color(0xFFFFD54F),
fontSize: 11.5,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 6),
],
Row(
children: [
if (_viewMode == GeometryViewMode.constellation) ...[
const Icon(Icons.star_rounded, color: Color(0xFFFFD54F), size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
_getFigureName(widget.session.selectedHouses.length),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: const Color(0x33FFD54F),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFFFD54F), width: 0.6),
),
child: Text(
'${widget.session.selectedHouses.length} узлов',
style: const TextStyle(
color: Color(0xFFFFD54F),
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
] else if (_viewMode == GeometryViewMode.allChains) ...[
const Icon(Icons.hub_rounded, color: Color(0xFF00E5FF), size: 20),
const SizedBox(width: 8),
const Expanded(
child: Text(
'Сводная Паутина Судеб (Все Цепочки)',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
),
Text(
'${widget.session.chains.length} лучей',
style: const TextStyle(
color: Color(0xFF00E5FF),
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
] else if (selectedChain != null) ...[
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: kChainColors[(_selectedChainIndex - 1) % kChainColors.length],
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
'Цепочка #$_selectedChainIndex: ${selectedChain.endTypeTitle}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: selectedChain.statusColor.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: selectedChain.statusColor, width: 0.6),
),
child: Text(
'${selectedChain.steps.length} звеньев',
style: TextStyle(
color: selectedChain.statusColor,
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
],
],
),
const SizedBox(height: 8),
Text(
_viewMode == GeometryViewMode.constellation
? 'Выбранные дома образуют сакральный сигил на полотне 33×30. Линии соединяют дома в порядке выпадения жребия, создавая геометрию пространства расклада.'
: (_viewMode == GeometryViewMode.allChains
? 'Трассировка движения импульсов от каждого открытого дома (до 9 звеньев). Узоры переплетаются сквозь матрицу, выявляя пересечения и фокусы судьбы.'
: (selectedChain?.description ?? '')),
style: const TextStyle(
color: Color(0xFFCFD8DC),
fontSize: 12.5,
height: 1.4,
),
),
],
),
);
}
Widget _buildBaseGrid(double totalWidth, double totalHeight) {
final chosenIndices = {
for (final h in widget.session.selectedHouses) h.houseIndex: h
};
return Column(
children: [
SizedBox(
height: kHeaderHeight,
child: Row(
children: List.generate(10, (sectorIdx) {
final sector = widget.session.sectors[sectorIdx];
final triadWidth = 3 * (kCellSize + kCellMargin * 2);
return SizedBox(
width: triadWidth,
height: kHeaderHeight,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 4),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
const Color(0xFF1B1F38),
sector.color.withValues(alpha: 0.18),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: sector.color.withValues(alpha: 0.5), width: 0.8),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'АРХОНТ ${sectorIdx + 1}',
style: TextStyle(
color: sector.color,
fontSize: 9,
fontWeight: FontWeight.bold,
letterSpacing: 0.8,
),
),
const SizedBox(height: 2),
Text(
sector.archonWord.word.toUpperCase(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
);
}),
),
),
for (int r = 0; r < 33; r++)
Row(
children: [
for (int c = 0; c < 30; c++)
_buildCellWidget(r, c, chosenIndices[r * 30 + c]),
],
),
],
);
}
Widget _buildCellWidget(int row, int col, HouseCell? chosenCell) {
final isChosen = chosenCell != null;
final cell = widget.session.grid[row * 30 + col];
final isQuerent = widget.session.querentHouseIndex != null && cell.houseIndex == widget.session.querentHouseIndex;
return Container(
width: kCellSize,
height: kCellSize,
margin: const EdgeInsets.all(kCellMargin),
decoration: BoxDecoration(
color: isQuerent
? const Color(0xFF381E52)
: (isChosen
? const Color(0xFF2A224D)
: const Color(0xFF101323).withValues(alpha: 0.75)),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isQuerent
? const Color(0xFFFFD54F)
: (isChosen
? const Color(0xFFFFD54F)
: const Color(0xFF1E223D).withValues(alpha: 0.8)),
width: isQuerent ? 2.2 : (isChosen ? 1.5 : 0.6),
),
boxShadow: isQuerent
? [
BoxShadow(
color: const Color(0xFFFFD54F).withValues(alpha: 0.6),
blurRadius: 12,
spreadRadius: 1,
),
BoxShadow(
color: const Color(0xFFB388FF).withValues(alpha: 0.4),
blurRadius: 16,
spreadRadius: 2,
),
]
: (isChosen
? [
BoxShadow(
color: const Color(0xFFFFD54F).withValues(alpha: 0.35),
blurRadius: 10,
),
]
: null),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(10),
onTap: () {
HapticFeedback.lightImpact();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SymbolDetailScreen(word: cell.houseWord),
),
);
},
child: Padding(
padding: const EdgeInsets.all(3.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (isQuerent) ...[
Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: const Color(0xFFFFD54F),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'👑 КВЕРЕНТ',
style: TextStyle(
color: Colors.black,
fontSize: 7.5,
fontWeight: FontWeight.w900,
),
),
),
const SizedBox(height: 2),
] else if (isChosen) ...[
Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: const Color(0xFFFFD54F),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'#${chosenCell.selectionOrder}',
style: const TextStyle(
color: Colors.black,
fontSize: 9,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 2),
],
Text(
cell.houseWord.word,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: isChosen ? Colors.white : const Color(0xFF6B7280),
fontSize: 10.5,
fontWeight: isChosen ? FontWeight.bold : FontWeight.normal,
),
),
Text(
'#${(cell.houseIndex + 1).toString().padLeft(3, "0")}',
style: TextStyle(
color: isChosen
? const Color(0xFFFFD54F)
: const Color(0xFF374151),
fontSize: 8.5,
),
),
],
),
),
),
),
);
}
void _showHelpDialog(BuildContext context) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF15182D),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
title: const Row(
children: [
Icon(Icons.auto_awesome_mosaic_rounded, color: Color(0xFFFFD54F)),
SizedBox(width: 8),
Text('Сакральная Геометрия', style: TextStyle(color: Colors.white, fontSize: 17)),
],
),
content: const SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Полотно 33×30 образует единое координатное поле оракула из 990 домов и 10 архонтов.',
style: TextStyle(color: Color(0xFFCFD8DC), fontSize: 13, height: 1.4),
),
SizedBox(height: 12),
Text(
'• Фигура Расклада: золотые лучи соединяют открытые дома в порядке жребия, создавая сигил ситуации.',
style: TextStyle(color: Color(0xFFFFD54F), fontSize: 12.5, height: 1.4),
),
SizedBox(height: 8),
Text(
'• Цепочки Домов: неоновые векторы трассируют путь гостя каждого дома (до 9 звеньев). При замыкании возникает «Петля Судьбы», при попадании в имя Архонта — луч восходит к Небесному Престолу.',
style: TextStyle(color: Color(0xFF00E5FF), fontSize: 12.5, height: 1.4),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Понятно', style: TextStyle(color: Color(0xFFFFD54F))),
),
],
),
);
}
}
class _MatrixGeometryPainter extends CustomPainter {
final HousesSession session;
final GeometryViewMode viewMode;
final int selectedChainIndex;
final double pulseValue;
final List<Color> chainColors;
static const double kCellSize = 64.0;
static const double kCellMargin = 3.0;
static const double kHeaderHeight = 72.0;
_MatrixGeometryPainter({
required this.session,
required this.viewMode,
required this.selectedChainIndex,
required this.pulseValue,
required this.chainColors,
});
Offset _getCellCenter(int row, int col) {
final x = col * (kCellSize + kCellMargin * 2) + kCellMargin + kCellSize / 2;
final y = kHeaderHeight + row * (kCellSize + kCellMargin * 2) + kCellMargin + kCellSize / 2;
return Offset(x, y);
}
Offset _getArchonCenter(int sectorIndex) {
final startX = (sectorIndex * 3) * (kCellSize + kCellMargin * 2);
final width = 3 * (kCellSize + kCellMargin * 2);
return Offset(startX + width / 2, kHeaderHeight / 2);
}
@override
void paint(Canvas canvas, Size size) {
if (viewMode == GeometryViewMode.constellation) {
_paintConstellation(canvas);
} else if (viewMode == GeometryViewMode.allChains) {
for (int i = 0; i < session.chains.length; i++) {
_paintChain(canvas, session.chains[i], i + 1, false);
}
} else {
if (selectedChainIndex > 0 && selectedChainIndex <= session.chains.length) {
_paintChain(canvas, session.chains[selectedChainIndex - 1], selectedChainIndex, true);
}
}
_paintQuerentAnchor(canvas);
}
void _paintQuerentAnchor(Canvas canvas) {
if (session.querentHouseIndex == null) return;
final qRow = session.querentHouseIndex! ~/ 30;
final qCol = session.querentHouseIndex! % 30;
final qCenter = _getCellCenter(qRow, qCol);
final wavePaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color(0xFFFFD54F).withValues(alpha: 0.55 + 0.25 * math.sin(pulseValue * math.pi * 2));
canvas.drawCircle(qCenter, 22.0 + 4.0 * math.sin(pulseValue * math.pi * 2), wavePaint);
final auraPaint = Paint()
..style = PaintingStyle.fill
..color = const Color(0xFFB388FF).withValues(alpha: 0.20 + 0.08 * math.cos(pulseValue * math.pi * 2));
canvas.drawCircle(qCenter, 30.0, auraPaint);
if (viewMode == GeometryViewMode.constellation) {
final tetherPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1.4
..color = const Color(0xFFFFD54F).withValues(alpha: 0.35 + 0.15 * math.sin(pulseValue * math.pi * 2));
for (final h in session.selectedHouses) {
final hCenter = _getCellCenter(h.row, h.col);
canvas.drawLine(qCenter, hCenter, tetherPaint);
}
}
final coreGlow = Paint()
..style = PaintingStyle.fill
..color = const Color(0xFFFFD54F);
canvas.drawCircle(qCenter, 6.5, coreGlow);
canvas.drawCircle(qCenter, 2.5, Paint()..color = Colors.white);
}
void _paintConstellation(Canvas canvas) {
final houses = List<HouseCell>.from(session.selectedHouses)
..sort((a, b) => (a.selectionOrder ?? 0).compareTo(b.selectionOrder ?? 0));
if (houses.isEmpty) return;
final points = houses.map((h) => _getCellCenter(h.row, h.col)).toList();
if (points.length >= 3) {
final polyPath = Path()..moveTo(points.first.dx, points.first.dy);
for (int i = 1; i < points.length; i++) {
polyPath.lineTo(points[i].dx, points[i].dy);
}
polyPath.close();
final fillPaint = Paint()
..style = PaintingStyle.fill
..color = const Color(0xFFFFD54F).withValues(alpha: 0.10 + 0.04 * math.sin(pulseValue * math.pi * 2));
canvas.drawPath(polyPath, fillPaint);
}
final glowPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 6.0 + 1.5 * math.sin(pulseValue * math.pi * 2)
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..color = const Color(0xFFFFD54F).withValues(alpha: 0.35);
final corePaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.8
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..color = const Color(0xFFFFE082);
final linePath = Path()..moveTo(points.first.dx, points.first.dy);
for (int i = 1; i < points.length; i++) {
linePath.lineTo(points[i].dx, points[i].dy);
}
if (points.length >= 3) {
linePath.close();
}
canvas.drawPath(linePath, glowPaint);
canvas.drawPath(linePath, corePaint);
for (int i = 0; i < points.length; i++) {
final p = points[i];
final nodeGlow = Paint()
..style = PaintingStyle.fill
..color = const Color(0xFFFFD54F).withValues(alpha: 0.4 + 0.2 * math.sin(pulseValue * math.pi * 2 + i));
canvas.drawCircle(p, 12.0, nodeGlow);
final nodeCore = Paint()
..style = PaintingStyle.fill
..color = Colors.white;
canvas.drawCircle(p, 5.0, nodeCore);
}
}
void _paintChain(Canvas canvas, HouseChain chain, int chainIdx, bool isFocused) {
if (chain.steps.isEmpty) return;
final color = chainColors[(chainIdx - 1) % chainColors.length];
final glowWidth = isFocused ? 7.0 : 4.0;
final coreWidth = isFocused ? 3.0 : 2.0;
final glowPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = glowWidth
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..color = color.withValues(alpha: isFocused ? 0.45 : 0.25);
final corePaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = coreWidth
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..color = color;
final points = <Offset>[];
for (final step in chain.steps) {
points.add(_getCellCenter(step.house.row, step.house.col));
}
final path = Path()..moveTo(points.first.dx, points.first.dy);
for (int i = 1; i < points.length; i++) {
path.lineTo(points[i].dx, points[i].dy);
}
if (chain.endType == ChainEndType.archonThrone && chain.steps.last.destinationArchon != null) {
final archonCenter = _getArchonCenter(chain.steps.last.destinationArchon!.index);
path.lineTo(archonCenter.dx, archonCenter.dy);
points.add(archonCenter);
}
if (chain.endType == ChainEndType.loop && points.length >= 2) {
final lastP = points.last;
final haloPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 3.0
..color = const Color(0xFFFF5252);
canvas.drawCircle(lastP, 18.0 + 4.0 * math.sin(pulseValue * math.pi * 2), haloPaint);
}
canvas.drawPath(path, glowPaint);
canvas.drawPath(path, corePaint);
for (int i = 0; i < points.length; i++) {
final p = points[i];
final isStart = i == 0;
final isEnd = i == points.length - 1;
final nodePaint = Paint()
..style = PaintingStyle.fill
..color = isStart
? const Color(0xFFFFD54F)
: (isEnd ? const Color(0xFFFF5252) : color);
canvas.drawCircle(p, isStart || isEnd ? 6.5 : 4.0, nodePaint);
final centerDot = Paint()
..style = PaintingStyle.fill
..color = Colors.white;
canvas.drawCircle(p, isStart || isEnd ? 2.5 : 1.5, centerDot);
}
}
@override
bool shouldRepaint(covariant _MatrixGeometryPainter oldDelegate) {
return true;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -186,7 +186,7 @@ class _OracleChatScreenState extends State<OracleChatScreen> {
width: 1,
),
),
child: Text(
child: SelectableText(
message.text,
style: TextStyle(
color: message.isUser ? Colors.white : const Color(0xFFE5E7EB),
+608
View File
@@ -0,0 +1,608 @@
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';
class RunesDivinationScreen extends StatefulWidget {
final String? initialQuestion;
const RunesDivinationScreen({super.key, this.initialQuestion});
@override
State<RunesDivinationScreen> createState() => _RunesDivinationScreenState();
}
class _RunesDivinationScreenState extends State<RunesDivinationScreen> with SingleTickerProviderStateMixin {
late TabController _tabController;
final _questionController = TextEditingController();
final _repo = WordsRepository();
// 24 runes from canon
List<TraditionStation> _runes = [];
// Layout selection
DivinationLayout _selectedLayout = DivinationLayouts.runesLayouts[0]; // 1, 3, or 5
int _currentStepIndex = 0; // for multi-step layouts
// Dice state (for current step)
int? _d12First;
int? _d12Second;
bool _isRollingDice = false;
// Manual input state
bool _isManualReversed = false;
// Multi-item results
List<({TraditionStation rune, 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!;
}
_loadRunes();
}
void _loadRunes() {
final canon = TraditionsRepository().byId('runes');
if (canon != null) {
_runes = canon.stations.take(24).toList();
}
}
@override
void dispose() {
_tabController.dispose();
_questionController.dispose();
super.dispose();
}
int _calculateRuneIndexFromDice(int d1, int d2) {
final isOdd = (d1 % 2) != 0;
final runeNumber = isOdd ? d2 : (d2 + 12);
return (runeNumber - 1).clamp(0, 23);
}
void _onLayoutChanged(DivinationLayout layout) {
setState(() {
_selectedLayout = layout;
_currentStepIndex = 0;
_drawnResults.clear();
_d12First = null;
_d12Second = null;
_savedToSession = false;
});
}
Future<void> _rollDiceForCurrentStep() async {
if (_runes.isEmpty || _currentStepIndex >= _selectedLayout.count) return;
setState(() => _isRollingDice = true);
HapticFeedback.mediumImpact();
final rnd = Random();
for (int i = 0; i < 8; i++) {
await Future.delayed(const Duration(milliseconds: 50));
if (mounted) {
setState(() {
_d12First = rnd.nextInt(12) + 1;
_d12Second = rnd.nextInt(12) + 1;
});
}
}
final finalD1 = rnd.nextInt(12) + 1;
final finalD2 = rnd.nextInt(12) + 1;
final isRev = rnd.nextBool() && rnd.nextBool(); // 25%
final idx = _calculateRuneIndexFromDice(finalD1, finalD2);
final rune = _runes[idx];
final posTitle = _selectedLayout.positionTitles[_currentStepIndex];
if (mounted) {
setState(() {
_d12First = finalD1;
_d12Second = finalD2;
_isRollingDice = false;
_drawnResults.add((rune: rune, isReversed: isRev, positionTitle: posTitle));
_currentStepIndex++;
if (_currentStepIndex >= _selectedLayout.count) {
_saveToSession('dice');
}
});
HapticFeedback.heavyImpact();
}
}
void _applyManualChoice(int runeIndex) {
if (runeIndex < 0 || runeIndex >= _runes.length || _currentStepIndex >= _selectedLayout.count) return;
final rune = _runes[runeIndex];
final posTitle = _selectedLayout.positionTitles[_currentStepIndex];
setState(() {
_drawnResults.add((rune: rune, isReversed: _isManualReversed, positionTitle: posTitle));
_isManualReversed = false;
_currentStepIndex++;
if (_currentStepIndex >= _selectedLayout.count) {
_saveToSession('manual');
}
});
HapticFeedback.selectionClick();
}
void _resetSpread() {
setState(() {
_currentStepIndex = 0;
_drawnResults.clear();
_d12First = null;
_d12Second = null;
_savedToSession = false;
});
}
Future<void> _saveToSession(String inputMethod) async {
if (_drawnResults.isEmpty) return;
final items = _drawnResults.map((r) {
WordItem? linkedWord;
if (r.rune.primary != null) {
linkedWord = _repo.getWordById(r.rune.primary!.id) ?? _repo.getWordByName(r.rune.primary!.word);
}
return ReadingSignItem(
title: '${r.positionTitle}: ${r.rune.name}',
subtitle: r.rune.group,
symbol: r.rune.symbol,
lore: r.rune.lore,
isReversed: r.isReversed,
linkedWord: linkedWord,
);
}).toList();
final reading = SyncreticReading(
id: 'reading_${DateTime.now().millisecondsSinceEpoch}',
systemId: 'runes',
systemName: 'Руны Старшего Футарка (24)',
systemIcon: 'ᚱ',
timestamp: DateTime.now(),
layoutTitle: '${_selectedLayout.title} (${inputMethod == "dice" ? "2×d12" : "со стола"})',
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, 'runes'),
),
if (_drawnResults.isNotEmpty)
IconButton(
tooltip: 'Сбросить расклад',
icon: const Icon(Icons.refresh_rounded),
onPressed: _resetSpread,
),
],
title: const Row(
children: [
Text('ᚱ', style: TextStyle(fontSize: 22, color: Color(0xFFFFD54F))),
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.casino_rounded, size: 18), text: 'Жребий 2×d12'),
Tab(icon: Icon(Icons.touch_app_rounded, size: 18), text: 'Ввод со стола'),
],
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Question Input
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.runesLayouts.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),
// Controls Tabs
if (!isSpreadComplete)
SizedBox(
height: 310,
child: TabBarView(
controller: _tabController,
children: [
_buildDiceTab(),
_buildManualTabThreeAetts(),
],
),
),
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),
// Drawn Runes Cards List
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++) ...[
_buildResultRuneCard(_drawnResults[i], i + 1),
const SizedBox(height: 10),
],
],
],
),
),
);
}
Widget _buildDiceTab() {
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: [
const Text(
'БРОСОК 2×d12 (1..24 РАВНОМЕРНО)',
style: TextStyle(color: Color(0xFFFFD54F), fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 0.8),
),
const SizedBox(height: 6),
Text(
'Извлечение позиции: «' + _selectedLayout.positionTitles[_currentStepIndex] + '»',
textAlign: TextAlign.center,
style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 11.5),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildDieBox('d12 #1 (чет/нечет)', _d12First, isOddEven: true),
const SizedBox(width: 20),
_buildDieBox('d12 #2 (1..12)', _d12Second),
],
),
const SizedBox(height: 18),
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: _isRollingDice ? null : _rollDiceForCurrentStep,
icon: const Icon(Icons.casino_rounded, size: 20),
label: Text(
_isRollingDice ? 'Кости звенят...' : 'Бросить для шага ${_currentStepIndex + 1}',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13.5),
),
),
],
),
);
}
Widget _buildDieBox(String label, int? val, {bool isOddEven = false}) {
String sub = '';
if (val != null && isOddEven) {
sub = (val % 2 != 0) ? 'Нечет (1..12)' : 'Чет (13..24)';
}
return Column(
children: [
Container(
width: 68,
height: 68,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xFF232742),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.6), width: 1.5),
),
child: Text(
val != null ? '$val' : '?',
style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 26, fontWeight: FontWeight.w900),
),
),
const SizedBox(height: 6),
Text(label, style: const TextStyle(color: Color(0xFF90A4AE), fontSize: 10, fontWeight: FontWeight.bold)),
if (sub.isNotEmpty)
Text(sub, style: const TextStyle(color: Color(0xFFFFD54F), fontSize: 9, fontWeight: FontWeight.bold)),
],
);
}
/// Exact 3 Rows of 8 Runes: 1st Aett (1..8), 2nd Aett (9..16), 3rd Aett (17..24)
Widget _buildManualTabThreeAetts() {
const aettNames = [
'I ЭТТ ФРЕЙИ (ᚠ..ᚹ)',
'II ЭТТ ХЕЙМДАЛЛЯ (ᚺ..ᛏ)',
'III ЭТТ ТЮРА (ᛒ..ᛟ)',
];
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: 10.5, fontWeight: FontWeight.bold),
),
FilterChip(
label: const Text('Перевёрнутая ᛉ/ᛦ', style: TextStyle(fontSize: 10)),
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: 6),
// 3 Rows of 8 runes
for (int aett = 0; aett < 3; aett++) ...[
Padding(
padding: const EdgeInsets.only(top: 4, bottom: 2),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
aettNames[aett],
style: const TextStyle(color: Color(0xFF80DEEA), fontSize: 9.5, fontWeight: FontWeight.bold),
),
),
),
Row(
children: [
for (int col = 0; col < 8; col++) ...[
Builder(builder: (ctx) {
final idx = aett * 8 + col;
if (idx >= _runes.length) return const SizedBox.shrink();
final r = _runes[idx];
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: InkWell(
onTap: () => _applyManualChoice(idx),
borderRadius: BorderRadius.circular(6),
child: Container(
height: 38,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xFF1E2238),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFF2A2D4A)),
),
child: Text(
r.symbol,
style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 18, fontWeight: FontWeight.bold),
),
),
),
),
);
}),
],
],
),
],
],
),
);
}
Widget _buildResultRuneCard(({TraditionStation rune, bool isReversed, String positionTitle}) res, int index) {
final r = res.rune;
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: Text(
r.symbol,
style: const TextStyle(fontSize: 26, color: Color(0xFFFFD54F)),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
'#$index. ${res.positionTitle}: ${r.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(r.group, style: const TextStyle(color: Color(0xFF80DEEA), fontSize: 11)),
const SizedBox(height: 6),
Text(
r.lore,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Color(0xFFD1D5DB), fontSize: 12, height: 1.4),
),
],
),
),
],
),
);
}
}
+286 -58
View File
@@ -1,11 +1,16 @@
import '../models/syncretic_session_model.dart';
import '../services/oracle_master_prompt_generator.dart';
import 'symbol_detail_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/history_entry_model.dart';
import '../models/word_model.dart';
import '../services/ai_oracle_service.dart';
import '../services/settings_repository.dart';
import '../services/words_repository.dart';
import '../widgets/d10_dice_roller_widget.dart';
import '../widgets/gradient_star_badge.dart';
import '../widgets/interpreter_persona_picker.dart';
class SessionResultScreen extends StatefulWidget {
final OracleSession session;
@@ -35,6 +40,33 @@ class _SessionResultScreenState extends State<SessionResultScreen> {
_session = widget.session;
_repo.saveSession(_session);
// Auto-record this session into active Syncretic Day Session
final readingItems = _session.words.map((w) {
final fw = _repo.getWordByName(w.word) ?? _repo.getWordBySessionNumber(w.sessionNumber);
return ReadingSignItem(
title: '#${w.formattedSessionNumber} ${w.word.toUpperCase()}',
subtitle: w.category,
symbol: '🎲',
lore: w.symbolicMeaning,
linkedWord: fw,
);
}).toList();
_repo.addReadingToSyncreticSession(
SyncreticReading(
id: 'reading_${_session.id}',
systemId: 'cleromancy',
systemName: 'Оракул (Клеромантия)',
systemIcon: '🎲',
timestamp: _session.timestamp,
layoutTitle: '${_session.words.length} ${_session.words.length == 1 ? "знак" : "знаков"}',
inputMethod: 'dice',
items: readingItems,
question: _session.question,
),
question: _session.question,
);
// If initial interpretation is not yet done, trigger it
if (_session.aiInterpretation == null) {
_fetchInitialAi();
@@ -48,26 +80,32 @@ class _SessionResultScreenState extends State<SessionResultScreen> {
super.dispose();
}
Future<void> _fetchInitialAi() async {
Future<void> _fetchInitialAi({bool replaceOpening = true}) async {
setState(() => _isGeneratingInitialAi = true);
try {
final voice = SettingsRepository().persona;
final result = await _aiService.interpretSession(
words: _session.words,
question: _session.question ?? '',
);
if (mounted) {
final initialMessage = SessionChatMessage(
id: 'msg_0',
final labeled = '[${voice.title}] $result';
final message = SessionChatMessage(
id: 'msg_${DateTime.now().millisecondsSinceEpoch}',
sender: 'oracle',
text: result,
text: labeled,
timestamp: DateTime.now(),
);
final nextMessages = replaceOpening && _session.messages.isEmpty
? [message]
: [..._session.messages, message];
final updated = _session.copyWith(
aiInterpretation: result,
messages: _session.messages.isEmpty ? [initialMessage] : _session.messages,
aiInterpretation: labeled,
messages: nextMessages,
);
setState(() {
@@ -190,9 +228,140 @@ class _SessionResultScreenState extends State<SessionResultScreen> {
);
}
void _showMasterPromptDialog() {
HapticFeedback.mediumImpact();
final prompt = OracleMasterPromptGenerator.generateForSession(_session);
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: const Color(0xFF0F1222),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (ctx) {
return DraggableScrollableSheet(
initialChildSize: 0.85,
minChildSize: 0.5,
maxChildSize: 0.95,
expand: false,
builder: (context, scrollController) {
return Container(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 44,
height: 4,
decoration: BoxDecoration(
color: Colors.white24,
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 14),
Row(
children: [
const Icon(Icons.psychology_alt_rounded, color: Color(0xFFFFD54F), size: 24),
const SizedBox(width: 10),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Мастер-Промпт Сессии',
style: TextStyle(color: Colors.white, fontSize: 17, fontWeight: FontWeight.bold),
),
Text(
'Для отправки в Grok, Claude 3.5 Sonnet, Gemini 1.5 Pro, GPT-4o',
style: TextStyle(color: Color(0xFF90A4AE), fontSize: 11),
),
],
),
),
IconButton(
icon: const Icon(Icons.close_rounded, color: Color(0xFF90A4AE)),
onPressed: () => Navigator.pop(ctx),
),
],
),
const SizedBox(height: 12),
// Quick Actions Bar
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF1C223D),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'${prompt.length} символов • ${prompt.split(RegExp(r"\s+")).length} слов',
style: const TextStyle(color: Color(0xFF90CAF9), fontSize: 11),
),
),
const Spacer(),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFFD54F),
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(0xFF388E3C),
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),
// Scrollable Prompt Text Area
Expanded(
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF090A14),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF232742), width: 1.0),
),
child: SingleChildScrollView(
controller: scrollController,
child: SelectableText(
prompt,
style: const TextStyle(
color: Color(0xFFECEFF1),
fontFamily: 'monospace',
fontSize: 11.5,
height: 1.45,
),
),
),
),
),
],
),
);
},
);
},
);
}
void _copySessionToClipboard() {
final buf = StringBuffer();
buf.writeln('✨ Оракул 999 — Сессия Клеромантии ✨');
buf.writeln('✨ Оракул — Сессия Клеромантии ✨');
buf.writeln('Дата: ${_session.formattedDate}');
if (_session.question != null && _session.question!.isNotEmpty) {
buf.writeln('Вопрос: «${_session.question}»');
@@ -247,6 +416,11 @@ class _SessionResultScreenState extends State<SessionResultScreen> {
],
),
actions: [
IconButton(
tooltip: 'Мастер-промпт для нейросетей',
icon: const Icon(Icons.psychology_alt_rounded, color: Color(0xFFFFD54F)),
onPressed: _showMasterPromptDialog,
),
IconButton(
tooltip: 'Докинуть уточняющий знак',
icon: const Icon(Icons.add_circle_outline_rounded, color: Color(0xFFFFD54F)),
@@ -369,6 +543,48 @@ class _SessionResultScreenState extends State<SessionResultScreen> {
],
),
),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: InterpreterPersonaPicker(
compact: true,
onSelected: (_) {
if (_isGeneratingInitialAi || _isChatLoading) return;
_fetchInitialAi(replaceOpening: false);
},
),
),
const SizedBox(width: 8),
InkWell(
onTap: _showMasterPromptDialog,
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF2A1F45),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFBA68C8).withValues(alpha: 0.6)),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.auto_awesome_rounded, size: 14, color: Color(0xFFFFD54F)),
SizedBox(width: 5),
Text(
'Мастер-Промпт',
style: TextStyle(
color: Color(0xFFFFF9C4),
fontSize: 11.5,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
],
),
),
@@ -498,7 +714,6 @@ class _SessionResultScreenState extends State<SessionResultScreen> {
children: [
// Header: Sender icon & name
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isUser ? Icons.person_outline_rounded : Icons.psychology_rounded,
@@ -507,32 +722,66 @@ class _SessionResultScreenState extends State<SessionResultScreen> {
),
const SizedBox(width: 5),
Text(
isUser ? 'Вы' : 'Оракул 999',
isUser ? 'Вы' : 'Оракул',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w900,
color: isUser ? const Color(0xFFFFD54F) : const Color(0xFFE1BEE7),
),
),
const Spacer(),
IconButton(
tooltip: 'Копировать текст',
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
icon: Icon(
Icons.copy_rounded,
size: 15,
color: isUser ? const Color(0xFFFFD54F) : const Color(0xFFE1BEE7),
),
onPressed: () {
Clipboard.setData(ClipboardData(text: msg.text));
HapticFeedback.lightImpact();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Текст скопирован'),
duration: Duration(seconds: 1),
behavior: SnackBarBehavior.floating,
),
);
},
),
],
),
// Attached new word pill if present
if (msg.attachedWord != null) ...[
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: const Color(0xFF3B2B5E),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.4)),
),
child: Text(
'🎲 Добавлен знак: #${msg.attachedWord!.formattedSessionNumber} «${msg.attachedWord!.word.toUpperCase()}»',
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Color(0xFFFFF9C4),
InkWell(
onTap: () => _showWordDetails(msg.attachedWord!),
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: const Color(0xFF3B2B5E),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.5)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'🎲 Добавлен знак: #${msg.attachedWord!.formattedSessionNumber} «${msg.attachedWord!.word.toUpperCase()}»',
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Color(0xFFFFF9C4),
),
),
const SizedBox(width: 4),
const Icon(Icons.arrow_forward_ios_rounded, size: 10, color: Color(0xFFFFD54F)),
],
),
),
),
@@ -540,8 +789,7 @@ class _SessionResultScreenState extends State<SessionResultScreen> {
const SizedBox(height: 6),
// Message text
Text(
SelectableText(
msg.text,
style: TextStyle(
fontSize: 13.5,
@@ -589,40 +837,20 @@ class _SessionResultScreenState extends State<SessionResultScreen> {
}
void _showWordDetails(SessionWord word) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Row(
children: [
const GradientStar(size: 20),
const SizedBox(width: 8),
Text(
'#${word.formattedSessionNumber} ${word.word.toUpperCase()}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${word.partOfSpeech} • ${word.category}',
style: TextStyle(color: Theme.of(ctx).colorScheme.outline),
),
const SizedBox(height: 12),
Text(
word.symbolicMeaning,
style: const TextStyle(fontSize: 14, height: 1.45),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Закрыть'),
),
],
final fullWord = _repo.getWordByName(word.word) ??
_repo.getWordBySessionNumber(word.sessionNumber) ??
WordItem(
id: word.sessionNumber,
word: word.word,
partOfSpeech: word.partOfSpeech.isNotEmpty ? word.partOfSpeech : 'знак',
frequency: 0,
category: word.category,
description: word.symbolicMeaning,
);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SymbolDetailScreen(word: fullWord),
),
);
}
+614 -1
View File
@@ -1,10 +1,14 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/interpreter_persona.dart';
import '../services/settings_repository.dart';
import '../services/words_repository.dart';
import '../services/true_random_service.dart';
import '../widgets/gradient_star_badge.dart';
import 'manual_dive_screen.dart';
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
@@ -62,7 +66,11 @@ class SettingsScreen extends StatelessWidget {
),
const SizedBox(height: 10),
],
const SizedBox(height: 18),
const SizedBox(height: 22),
const _ManualDiveEntry(),
const SizedBox(height: 22),
const _SymbolDescriptionsBackupSection(),
const SizedBox(height: 24),
const _RandomOrgKeySection(),
],
);
@@ -312,3 +320,608 @@ class _RandomOrgKeySectionState extends State<_RandomOrgKeySection> {
);
}
}
class _ManualDiveEntry extends StatelessWidget {
const _ManualDiveEntry();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(18),
onTap: () {
HapticFeedback.selectionClick();
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const ManualDiveScreen()),
);
},
child: Ink(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF17192C),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.45)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ручной обогатитель (Паспорт 999)',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w900,
color: const Color(0xFFFFD54F),
),
),
const SizedBox(height: 6),
Text(
'Система окошек по 6 разделам и 78 предпромптов: копируй строки, вставляй ответы или разбирай Markdown целиком.',
style: theme.textTheme.bodyMedium?.copyWith(height: 1.35),
),
],
),
),
),
);
}
}
class _SymbolDescriptionsBackupSection extends StatefulWidget {
const _SymbolDescriptionsBackupSection();
@override
State<_SymbolDescriptionsBackupSection> createState() =>
_SymbolDescriptionsBackupSectionState();
}
class _SymbolDescriptionsBackupSectionState
extends State<_SymbolDescriptionsBackupSection> {
final _repo = WordsRepository();
Future<void> _exportCustomEdits() async {
final editsCount = _repo.customEditsCount;
final jsonStr = _repo.exportCustomEditsJson();
final defaultFileName = 'oracle_999_custom_symbols_${WordsRepository.utcDateStamp()}.json';
_showExportDialog(
title: 'Экспорт новых описаний',
subtitle: editsCount > 0
? 'Выгрузка $editsCount отредактированных символов со всеми статьями'
: 'Пользовательских правок пока нет, файл содержит структуру выгрузки',
fileName: defaultFileName,
content: jsonStr,
itemCount: editsCount,
);
}
Future<void> _exportFullLexicon() async {
final jsonStr = _repo.exportFullLexiconJson();
final defaultFileName = 'oracle_999_full_canon_1000_${WordsRepository.utcDateStamp()}.json';
_showExportDialog(
title: 'Экспорт полной базы (1000 символов)',
subtitle: 'Полный корпус всех 1000 знаков Оракула с каноном, статьями и формулами',
fileName: defaultFileName,
content: jsonStr,
itemCount: 1000,
);
}
Future<void> _showExportDialog({
required String title,
required String subtitle,
required String fileName,
required String content,
required int itemCount,
}) async {
HapticFeedback.lightImpact();
final bytes = utf8.encode(content).length;
final sizeKb = (bytes / 1024).toStringAsFixed(1);
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: const Color(0xFF151829),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (context) {
return Padding(
padding: EdgeInsets.fromLTRB(
20,
16,
20,
MediaQuery.of(context).viewInsets.bottom + 28,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Container(
width: 44,
height: 4,
decoration: BoxDecoration(
color: const Color(0xFFFFD54F).withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 16),
Row(
children: [
const Icon(Icons.file_download_outlined, color: Color(0xFFFFD54F), size: 26),
const SizedBox(width: 10),
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w900,
color: Color(0xFFFFF9C4),
),
),
),
],
),
const SizedBox(height: 6),
Text(
subtitle,
style: const TextStyle(fontSize: 13, color: Color(0xFF8C93B8), height: 1.35),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF0F111E),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFF2A2D4A)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Имя файла:', style: TextStyle(color: Color(0xFF8C93B8), fontSize: 12)),
Text(fileName, style: const TextStyle(color: Color(0xFFFFD54F), fontWeight: FontWeight.bold, fontSize: 12)),
],
),
const SizedBox(height: 6),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Объём данных:', style: TextStyle(color: Color(0xFF8C93B8), fontSize: 12)),
Text('$itemCount знаков • $sizeKb КБ', style: const TextStyle(color: Colors.white, fontSize: 12)),
],
),
],
),
),
const SizedBox(height: 18),
FilledButton.icon(
icon: const Icon(Icons.copy_rounded, color: Colors.black),
label: const Text(
'Скопировать весь JSON в буфер',
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black),
),
style: FilledButton.styleFrom(
backgroundColor: const Color(0xFFFFD54F),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
),
onPressed: () {
Clipboard.setData(ClipboardData(text: content));
HapticFeedback.mediumImpact();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.check_circle_rounded, color: Color(0xFFFFD54F), size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(
'Файл ($sizeKb КБ, $itemCount знаков) скопирован в буфер обмена! Перенесите его в новую версию через "Импорт".',
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
],
),
duration: const Duration(seconds: 4),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
);
},
),
const SizedBox(height: 10),
OutlinedButton.icon(
icon: const Icon(Icons.save_alt_rounded, color: Color(0xFFFFD54F)),
label: const Text(
'Сохранить в папку Загрузки (Download)',
style: TextStyle(fontWeight: FontWeight.bold, color: Color(0xFFFFD54F)),
),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
side: const BorderSide(color: Color(0xFFFFD54F), width: 1.2),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
),
onPressed: () async {
final savedPath = await _saveContentToDownloads(fileName, content);
if (!context.mounted) return;
Navigator.pop(context);
HapticFeedback.mediumImpact();
if (savedPath != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Файл успешно сохранён: $savedPath'),
backgroundColor: const Color(0xFF1E2238),
duration: const Duration(seconds: 4),
behavior: SnackBarBehavior.floating,
),
);
} else {
Clipboard.setData(ClipboardData(text: content));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Папка Download недоступна напрямую. Файл скопирован в буфер обмена!'),
duration: Duration(seconds: 4),
behavior: SnackBarBehavior.floating,
),
);
}
},
),
],
),
);
},
);
}
Future<String?> _saveContentToDownloads(String fileName, String content) async {
try {
final candidates = <String>[];
if (Platform.isAndroid) {
candidates.add('/storage/emulated/0/Download');
candidates.add('/sdcard/Download');
candidates.add('/storage/emulated/0/Documents');
} else if (Platform.isLinux || Platform.isMacOS) {
final home = Platform.environment['HOME'];
if (home != null) {
candidates.add('$home/Downloads');
candidates.add('$home/Documents');
}
} else if (Platform.isWindows) {
final userProfile = Platform.environment['USERPROFILE'];
if (userProfile != null) {
candidates.add('$userProfile/Downloads');
candidates.add('$userProfile/Documents');
}
}
for (final dirPath in candidates) {
final dir = Directory(dirPath);
if (dir.existsSync()) {
final file = File('${dir.path}/$fileName');
await file.writeAsString(content, flush: true);
return file.path;
}
}
for (final dirPath in candidates) {
try {
final dir = Directory(dirPath);
await dir.create(recursive: true);
final file = File('${dir.path}/$fileName');
await file.writeAsString(content, flush: true);
return file.path;
} catch (_) {}
}
return null;
} catch (_) {
return null;
}
}
Future<void> _openImportDialog() async {
final textController = TextEditingController();
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: const Color(0xFF151829),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (context) {
return StatefulBuilder(
builder: (context, setModalState) {
return Padding(
padding: EdgeInsets.fromLTRB(
20,
16,
20,
MediaQuery.of(context).viewInsets.bottom + 24,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Container(
width: 44,
height: 4,
decoration: BoxDecoration(
color: const Color(0xFFFFD54F).withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 16),
const Row(
children: [
Icon(Icons.file_upload_outlined, color: Color(0xFFFFD54F), size: 26),
SizedBox(width: 10),
Text(
'Импорт описаний в эту версию',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w900,
color: Color(0xFFFFF9C4),
),
),
],
),
const SizedBox(height: 6),
const Text(
'Вставьте JSON-файл описаний, выгруженный из предыдущей версии приложения.',
style: TextStyle(fontSize: 13, color: Color(0xFF8C93B8), height: 1.35),
),
const SizedBox(height: 14),
TextField(
controller: textController,
maxLines: 6,
style: const TextStyle(fontSize: 12.5, fontFamily: 'monospace'),
decoration: InputDecoration(
hintText: 'Вставьте JSON сюда...',
hintStyle: const TextStyle(color: Color(0xFF555B7A)),
filled: true,
fillColor: const Color(0xFF0F111E),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(14)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Color(0xFF2A2D4A)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Color(0xFFFFD54F)),
),
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
icon: const Icon(Icons.paste_rounded, size: 18),
label: const Text('Вставить из буфера'),
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFFFFD54F),
side: const BorderSide(color: Color(0xFFFFD54F), width: 1.1),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () async {
final data = await Clipboard.getData('text/plain');
if (data?.text != null) {
textController.text = data!.text!;
HapticFeedback.lightImpact();
}
},
),
),
const SizedBox(width: 10),
Expanded(
child: FilledButton.icon(
icon: const Icon(Icons.check_rounded, color: Colors.black, size: 18),
label: const Text('Применить', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black)),
style: FilledButton.styleFrom(
backgroundColor: const Color(0xFFFFD54F),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () async {
final raw = textController.text.trim();
if (raw.isEmpty) return;
final count = await _repo.importCustomEditsJson(raw);
if (!context.mounted) return;
Navigator.pop(context);
setState(() {});
HapticFeedback.heavyImpact();
if (count > 0) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.check_circle_rounded, color: Color(0xFFFFD54F), size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(
'Успешно импортировано $count описаний символов! Они активированы в базе.',
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
],
),
backgroundColor: const Color(0xFF1E2238),
duration: const Duration(seconds: 4),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Не удалось распознать формат JSON или нет данных о символах.'),
backgroundColor: Color(0xFF3E1E22),
duration: Duration(seconds: 3),
behavior: SnackBarBehavior.floating,
),
);
}
},
),
),
],
),
],
),
);
},
);
},
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final editsCount = _repo.customEditsCount;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF151829),
borderRadius: BorderRadius.circular(18),
border: Border.all(
color: const Color(0xFFFFD54F).withValues(alpha: 0.3),
width: 1.2,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 38,
height: 38,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xFF2A2D4A),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.auto_stories_outlined, color: Color(0xFFFFD54F), size: 20),
),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'База и статьи символов',
style: TextStyle(
fontWeight: FontWeight.w900,
fontSize: 16,
color: Color(0xFFFFF9C4),
),
),
Text(
'Перенос описаний между версиями',
style: TextStyle(fontSize: 12, color: Color(0xFF8C93B8)),
),
],
),
),
],
),
const SizedBox(height: 12),
Text(
'Выгрузите все отредактированные и обогащённые описания одним файлом JSON. В новой версии приложения нажмите «Импортировать» и вставьте файл — все ваши статьи мгновенно встанут на свои места.',
style: TextStyle(
fontSize: 13,
height: 1.4,
color: theme.colorScheme.onSurface.withValues(alpha: 0.8),
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFF0F111E),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF2A2D4A)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Expanded(
child: Text('Пользовательских статей/правок:', style: TextStyle(color: Color(0xFF8C93B8), fontSize: 12)),
),
const SizedBox(width: 8),
Text(
'$editsCount знаков',
style: const TextStyle(color: Color(0xFFFFD54F), fontWeight: FontWeight.bold, fontSize: 13),
),
],
),
),
const SizedBox(height: 14),
Row(
children: [
Expanded(
child: FilledButton.icon(
onPressed: _exportCustomEdits,
icon: const Icon(Icons.file_download_outlined, color: Colors.black, size: 18),
label: const Text(
'Выгрузить новые',
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black, fontSize: 12.5),
),
style: FilledButton.styleFrom(
backgroundColor: const Color(0xFFFFD54F),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: _openImportDialog,
icon: const Icon(Icons.file_upload_outlined, color: Color(0xFFFFD54F), size: 18),
label: const Text(
'Импортировать',
style: TextStyle(fontWeight: FontWeight.bold, color: Color(0xFFFFD54F), fontSize: 12.5),
),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xFFFFD54F), width: 1.1),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
),
],
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: TextButton.icon(
onPressed: _exportFullLexicon,
icon: const Icon(Icons.inventory_2_outlined, size: 16, color: Color(0xFF80DEEA)),
label: const Text(
'Выгрузить полный канон (все 1000 символов одним файлом)',
style: TextStyle(fontSize: 12, color: Color(0xFF80DEEA)),
),
),
),
],
),
);
}
}
File diff suppressed because it is too large Load Diff
+511
View File
@@ -0,0 +1,511 @@
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),
),
],
),
),
],
),
);
}
}
File diff suppressed because it is too large Load Diff