feat: complete academic symbol lexicon heal, 2-column Cooper extraction, and updated assets
This commit is contained in:
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),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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))),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user