feat: complete academic symbol lexicon heal, 2-column Cooper extraction, and updated assets
This commit is contained in:
+1126
-1126
File diff suppressed because it is too large
Load Diff
+36
-22
@@ -1,12 +1,19 @@
|
||||
import 'screens/divination_hub_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'screens/catalog_screen.dart';
|
||||
import 'screens/history_screen.dart';
|
||||
import 'screens/home_search_screen.dart';
|
||||
import 'screens/oracle_chat_screen.dart';
|
||||
import 'screens/houses_field_screen.dart';
|
||||
import 'screens/provider_bakeoff_screen.dart';
|
||||
import 'screens/settings_screen.dart';
|
||||
import 'services/lenormand_pairs_repository.dart';
|
||||
import 'services/ogham_feda_repository.dart';
|
||||
import 'services/tarot_majors_repository.dart';
|
||||
import 'services/settings_repository.dart';
|
||||
import 'services/traditions_repository.dart';
|
||||
import 'services/words_repository.dart';
|
||||
import 'services/word_of_the_day_service.dart';
|
||||
import 'widgets/gradient_star_badge.dart';
|
||||
|
||||
import 'g4f/client.dart';
|
||||
@@ -21,7 +28,12 @@ import 'g4f/providers/phind_ai.dart';
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await WordsRepository().init();
|
||||
await TraditionsRepository().init();
|
||||
await LenormandPairsRepository().init();
|
||||
await TarotMajorsRepository().init();
|
||||
await OghamFedaRepository().init();
|
||||
await SettingsRepository().init();
|
||||
await WordOfTheDayService().init();
|
||||
runApp(const Words999App());
|
||||
}
|
||||
|
||||
@@ -36,7 +48,7 @@ class _Words999AppState extends State<Words999App> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Оракул 999 Символов',
|
||||
title: 'Оракул',
|
||||
debugShowCheckedModeBanner: false,
|
||||
themeMode: ThemeMode.dark, // Default to rich mystical dark mode
|
||||
darkTheme: ThemeData(
|
||||
@@ -87,6 +99,9 @@ class _Words999AppState extends State<Words999App> {
|
||||
}),
|
||||
),
|
||||
),
|
||||
builder: (context, child) {
|
||||
return SelectionArea(child: child ?? const SizedBox.shrink());
|
||||
},
|
||||
home: const bool.fromEnvironment('ORACLE_BAKEOFF')
|
||||
? const ProviderBakeoffScreen()
|
||||
: const MainNavigationContainer(),
|
||||
@@ -97,6 +112,13 @@ class _Words999AppState extends State<Words999App> {
|
||||
class MainNavigationContainer extends StatefulWidget {
|
||||
const MainNavigationContainer({super.key});
|
||||
|
||||
static void switchToTab(BuildContext context, int index) {
|
||||
final state = context.findAncestorStateOfType<_MainNavigationContainerState>();
|
||||
if (state != null) {
|
||||
state.setTab(index);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
State<MainNavigationContainer> createState() => _MainNavigationContainerState();
|
||||
}
|
||||
@@ -105,11 +127,16 @@ class _MainNavigationContainerState extends State<MainNavigationContainer> {
|
||||
int _currentIndex = 0;
|
||||
|
||||
final List<Widget> _screens = const [
|
||||
HomeSearchScreen(),
|
||||
DivinationHubScreen(),
|
||||
CatalogScreen(),
|
||||
HistoryScreen(),
|
||||
SettingsScreen(),
|
||||
];
|
||||
|
||||
void setTab(int index) {
|
||||
setState(() => _currentIndex = index);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -125,10 +152,15 @@ class _MainNavigationContainerState extends State<MainNavigationContainer> {
|
||||
},
|
||||
destinations: const [
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.casino_outlined),
|
||||
icon: Icon(Icons.auto_awesome_outlined),
|
||||
selectedIcon: GradientStar(size: 22),
|
||||
label: 'Оракул',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.auto_stories_outlined),
|
||||
selectedIcon: GradientStar(size: 22),
|
||||
label: 'Справочник',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.history_rounded),
|
||||
selectedIcon: GradientStar(size: 22),
|
||||
@@ -141,24 +173,6 @@ class _MainNavigationContainerState extends State<MainNavigationContainer> {
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: GestureDetector(
|
||||
onLongPress: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const ProviderBakeoffScreen()),
|
||||
);
|
||||
},
|
||||
child: FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const OracleChatScreen()),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.auto_awesome),
|
||||
label: const Text("ОРАКУЛ"),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
/// Канонический реестр культурных ветвей человечества, через которые
|
||||
/// толкуется каждый знак. Порядок списка — порядок показа в «учебнике».
|
||||
class CultureBranch {
|
||||
final String id;
|
||||
final String name;
|
||||
final String emoji;
|
||||
final String subtitle;
|
||||
|
||||
/// Стемы/ключевые слова (в нижнем регистре), по которым ветвь узнаётся в тексте.
|
||||
final List<String> stems;
|
||||
|
||||
const CultureBranch({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.emoji,
|
||||
required this.subtitle,
|
||||
required this.stems,
|
||||
});
|
||||
|
||||
/// Dart-овский `\b` не понимает кириллицу, поэтому граница слова
|
||||
/// задаётся явным lookbehind по буквам.
|
||||
RegExp get matcher => RegExp(
|
||||
'(?<![a-zа-яё])(?:${stems.map(RegExp.escape).join('|')})',
|
||||
caseSensitive: false,
|
||||
unicode: true,
|
||||
);
|
||||
|
||||
bool matches(String text) => matcher.hasMatch(text.toLowerCase());
|
||||
}
|
||||
|
||||
const List<CultureBranch> kCultureBranches = [
|
||||
CultureBranch(
|
||||
id: 'sumer',
|
||||
name: 'Шумер и Месопотамия',
|
||||
emoji: '🏺',
|
||||
subtitle: 'Клинопись • Ану, Энлиль, Инанна, Уту',
|
||||
stems: ['шумер', 'аккад', 'месопотам', 'вавилон', 'ассири', 'клинопис', 'уту', 'инанн', 'иштар', 'энлил', 'энки', 'мардук', 'тиамат', 'ануннак', 'гильгамеш', 'эрешкигаль', 'mūšu', 'ilū'],
|
||||
),
|
||||
CultureBranch(
|
||||
id: 'egypt',
|
||||
name: 'Древний Египет',
|
||||
emoji: '🪲',
|
||||
subtitle: 'Нил и Дуат • Ра, Осирис, Исида, Маат',
|
||||
stems: ['египет', 'египт', 'фараон', 'апоп', 'нут ', 'нут,', 'дуат', 'осирис', 'исид', 'тот ', 'маат', 'анубис', 'сехмет', 'хепри', 'атум', 'птах', 'ра ', 'ра,', 'ра.', 'гор '],
|
||||
),
|
||||
CultureBranch(
|
||||
id: 'judaic',
|
||||
name: 'Иудейская традиция и Каббала',
|
||||
emoji: '✡️',
|
||||
subtitle: 'Танах, Талмуд, Зоар • Древо Сфирот',
|
||||
stems: ['иудей', 'иудаизм', 'еврей', 'каббал', 'танах', 'тора ', 'торы', 'талмуд', 'зоар', 'библ', 'бытие', 'бытия', 'сфир', 'цимцум', 'иврит', 'йом', 'эрев', 'лайла', 'хошех', 'эйн-соф', 'эйн соф', 'шхин'],
|
||||
),
|
||||
CultureBranch(
|
||||
id: 'greco_roman',
|
||||
name: 'Античность (Греция и Рим)',
|
||||
emoji: '🏛️',
|
||||
subtitle: 'Гесиод, Гомер, Платон • Олимп и Хаос',
|
||||
stems: ['греци', 'греческ', 'грек', 'эллин', 'римск', 'рима', 'латин', 'античн', 'зевс', 'нюкт', 'гемер', 'эфир', 'юпитер', 'аполлон', 'гелиос', 'гомер', 'гесиод', 'платон', 'персефон', 'гермес', 'dies irae'],
|
||||
),
|
||||
CultureBranch(
|
||||
id: 'celtic',
|
||||
name: 'Кельты и друиды',
|
||||
emoji: '🌿',
|
||||
subtitle: 'Огам, Самайн, Белтайн • Иной мир сидов',
|
||||
stems: ['кельт', 'огам', 'друид', 'самайн', 'белтайн', 'имболк', 'лугнас', 'ирланд', 'валлий', 'бригит', 'сиды', 'туата'],
|
||||
),
|
||||
CultureBranch(
|
||||
id: 'norse',
|
||||
name: 'Скандинавия и германцы',
|
||||
emoji: 'ᚱ',
|
||||
subtitle: 'Эдды, руны • Один, Иггдрасиль, Рагнарёк',
|
||||
stems: ['скандинав', 'германск', 'норвеж', 'исланд', 'эдд', 'руна ', 'руны', 'рун ', 'одина', 'одину', 'иггдрасил', 'рагнар', 'асгард', 'вальхал', 'фрейя', 'локи', 'норны', 'дагаз'],
|
||||
),
|
||||
CultureBranch(
|
||||
id: 'slavic',
|
||||
name: 'Славянская традиция',
|
||||
emoji: '☀️',
|
||||
subtitle: 'Явь, Навь, Правь • Велес, Перун, Макошь',
|
||||
stems: ['славян', 'древнерус', 'явь', 'навь', 'нави ', 'правь', 'велес', 'перун', 'макош', 'мокош', 'сварог', 'даждьбог', 'ярил', 'купал', 'коляд', 'стрибог', 'домов', 'леший', 'русалк'],
|
||||
),
|
||||
CultureBranch(
|
||||
id: 'vedic',
|
||||
name: 'Ведическая Индия',
|
||||
emoji: '🕉️',
|
||||
subtitle: 'Веды и Упанишады • Рита, карма, атман',
|
||||
stems: ['индийск', 'индуи', 'ведическ', 'веды', 'вед ', 'ригвед', 'упанишад', 'санскрит', 'дхарм', 'карм', 'брахман', 'атман', 'сурья', 'агни', 'индра', 'шива', 'вишну', 'кали ', 'рита ', 'тантр'],
|
||||
),
|
||||
CultureBranch(
|
||||
id: 'persian',
|
||||
name: 'Персия и зороастризм',
|
||||
emoji: '🔥',
|
||||
subtitle: 'Авеста • Ахура-Мазда и Ахриман, Аша',
|
||||
stems: ['персид', 'персы', 'иран', 'зороастр', 'заратустр', 'авест', 'ахура', 'ахриман', 'митр'],
|
||||
),
|
||||
CultureBranch(
|
||||
id: 'china',
|
||||
name: 'Древний Китай',
|
||||
emoji: '☯️',
|
||||
subtitle: 'Дао, Инь-Ян, У-син • Книга Перемен',
|
||||
stems: ['китай', 'дао', 'инь', 'ян ', 'ян,', 'ян.', 'ян:', 'и цзин', 'и-цзин', 'гексаграмм', 'триграмм', 'конфуци', 'лао-цзы', 'лао цзы', 'чжуан', 'у-син', 'у син', 'иероглиф'],
|
||||
),
|
||||
CultureBranch(
|
||||
id: 'japan',
|
||||
name: 'Япония и синто',
|
||||
emoji: '⛩️',
|
||||
subtitle: 'Кодзики • Аматэрасу, ками, дзэн',
|
||||
stems: ['япон', 'синто', 'аматэрасу', 'ками ', 'кодзики', 'дзэн', 'идзанаги', 'идзанами'],
|
||||
),
|
||||
CultureBranch(
|
||||
id: 'islamic',
|
||||
name: 'Ислам и суфизм',
|
||||
emoji: '🌙',
|
||||
subtitle: 'Коран, суфийская поэзия • Руми, Ибн Араби',
|
||||
stems: ['ислам', 'суфи', 'суфий', 'арабск', 'коран', 'лайл ', 'лайл,', 'мирадж', 'лайлят', 'тасаввуф', 'руми', 'дервиш', 'ибн араби', 'геомант', 'рамл', 'тетраграмм'],
|
||||
),
|
||||
CultureBranch(
|
||||
id: 'christian',
|
||||
name: 'Христианская мистика',
|
||||
emoji: '✝️',
|
||||
subtitle: 'Отцы-пустынники, Апокалипсис • Логос',
|
||||
stems: ['христиан', 'евангел', 'апокалипс', 'отшельник', 'пустынник', 'монах', 'литург', 'daemon meridianus', 'акеди', 'страшный суд', 'иисус'],
|
||||
),
|
||||
CultureBranch(
|
||||
id: 'mesoamerica',
|
||||
name: 'Мезоамерика и Анды',
|
||||
emoji: '🌞',
|
||||
subtitle: 'Ацтеки, майя, инки • Кецалькоатль, Тонатиу',
|
||||
stems: ['ацтек', 'майя', 'мезоамерик', 'тонатиу', 'кецалькоатл', 'тескатлипок', 'попол-вух', 'попол вух', 'инки', 'инков'],
|
||||
),
|
||||
CultureBranch(
|
||||
id: 'oceania',
|
||||
name: 'Полинезия и Океания',
|
||||
emoji: '🌺',
|
||||
subtitle: 'По и Ао • Ранги и Папа, мана, табу',
|
||||
stems: ['полинези', 'маори', 'гавай', 'pō', 'ao)', 'ранги', 'тангароа'],
|
||||
),
|
||||
CultureBranch(
|
||||
id: 'hermetic',
|
||||
name: 'Алхимия и герметизм',
|
||||
emoji: '⚗️',
|
||||
subtitle: 'Изумрудная скрижаль • Нигредо → Рубедо',
|
||||
stems: ['алхими', 'гермет', 'нигредо', 'альбедо', 'цитринитас', 'рубедо', 'атанор', 'трисмегист', 'великое делание', 'философский камень', 'геоманти', 'геомант', 'puer', 'rubeus', 'albus'],
|
||||
),
|
||||
];
|
||||
|
||||
CultureBranch? cultureBranchByName(String name) {
|
||||
final lower = name.toLowerCase().trim();
|
||||
for (final b in kCultureBranches) {
|
||||
if (b.name.toLowerCase() == lower || b.id == lower) return b;
|
||||
}
|
||||
for (final b in kCultureBranches) {
|
||||
if (b.matches(' $lower ')) return b;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Раскладывает текст толкования на культурные ветви: каждый абзац,
|
||||
/// где ветвь упомянута, попадает в её раздел (абзац может принадлежать
|
||||
/// нескольким культурам — это и есть материал для сравнения).
|
||||
Map<String, String> extractCulturalBranches(String text) {
|
||||
final result = <String, String>{};
|
||||
if (text.trim().isEmpty) return result;
|
||||
final paragraphs = text
|
||||
.split(RegExp(r'\r?\n'))
|
||||
.map((p) => p.trim())
|
||||
.where((p) => p.isNotEmpty)
|
||||
.toList();
|
||||
for (final branch in kCultureBranches) {
|
||||
final hits = paragraphs.where((p) => branch.matches(' $p ')).toList();
|
||||
if (hits.isNotEmpty) result[branch.name] = hits.join('\n\n');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Сортирует произвольную карту культур в каноническом порядке реестра.
|
||||
List<MapEntry<String, String>> orderedCultures(Map<String, String> cultures) {
|
||||
int rank(String name) {
|
||||
final b = cultureBranchByName(name);
|
||||
return b == null ? 999 : kCultureBranches.indexOf(b);
|
||||
}
|
||||
|
||||
final entries = cultures.entries.where((e) => e.value.trim().isNotEmpty).toList()
|
||||
..sort((a, b) => rank(a.key).compareTo(rank(b.key)));
|
||||
return entries;
|
||||
}
|
||||
|
||||
/// Разбирает блок «## КУЛЬТУРЫ» на ветви по подзаголовкам
|
||||
/// (`### Шумер и Месопотамия`, `**Древний Египет**`, `Кельты:` …).
|
||||
Map<String, String> parseCulturesBlock(String block) {
|
||||
final result = <String, String>{};
|
||||
final lines = block.replaceAll('\r\n', '\n').split('\n');
|
||||
String? current;
|
||||
final buf = StringBuffer();
|
||||
|
||||
void flush() {
|
||||
final text = buf.toString().trim();
|
||||
final key = current;
|
||||
if (key != null && text.isNotEmpty) {
|
||||
result[key] = result.containsKey(key) ? '${result[key]}\n\n$text' : text;
|
||||
}
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
String? headingOf(String raw) {
|
||||
final line = raw.trim();
|
||||
if (line.isEmpty || line.length > 90) return null;
|
||||
final looksLikeHeading = line.startsWith('#') ||
|
||||
(line.startsWith('**') && line.endsWith('**')) ||
|
||||
RegExp(r'^[-•*]?\s*\*\*[^*]+\*\*\s*[:—–-]?\s*$').hasMatch(line) ||
|
||||
(line.endsWith(':') && line.length < 60);
|
||||
if (!looksLikeHeading) return null;
|
||||
final clean = line
|
||||
.replaceAll(RegExp(r'^[#\s•*-]+'), '')
|
||||
.replaceAll(RegExp(r'[*:—–\s-]+$'), '')
|
||||
.replaceAll('*', '')
|
||||
.trim();
|
||||
final branch = cultureBranchByName(clean);
|
||||
return branch?.name ?? (clean.isNotEmpty ? clean : null);
|
||||
}
|
||||
|
||||
for (final line in lines) {
|
||||
final h = headingOf(line);
|
||||
if (h != null) {
|
||||
flush();
|
||||
current = h;
|
||||
} else {
|
||||
buf.writeln(line);
|
||||
}
|
||||
}
|
||||
flush();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Обратная операция для редактора: карта культур → markdown-блок.
|
||||
String formatCulturesBlock(Map<String, String> cultures) {
|
||||
final sb = StringBuffer();
|
||||
for (final e in orderedCultures(cultures)) {
|
||||
sb.writeln('### ${e.key}');
|
||||
sb.writeln(e.value.trim());
|
||||
sb.writeln();
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
class DivinationSystemInstruction {
|
||||
final String id;
|
||||
final String title;
|
||||
final String icon;
|
||||
final String brief;
|
||||
final String historyAndEssence;
|
||||
final String physicalDiceMethod;
|
||||
final String directInputMethod;
|
||||
final String interpretationGuide;
|
||||
final List<String> sacredRules;
|
||||
|
||||
const DivinationSystemInstruction({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.icon,
|
||||
required this.brief,
|
||||
required this.historyAndEssence,
|
||||
required this.physicalDiceMethod,
|
||||
required this.directInputMethod,
|
||||
required this.interpretationGuide,
|
||||
required this.sacredRules,
|
||||
});
|
||||
}
|
||||
|
||||
class DivinationInstructionsRepository {
|
||||
static const Map<String, DivinationSystemInstruction> all = {
|
||||
'runes': DivinationSystemInstruction(
|
||||
id: 'runes',
|
||||
title: 'Руны Старшего Футарка',
|
||||
icon: 'ᚱ',
|
||||
brief: '24 священных знака древнегерманского и скандинавского канона, разбитые на три этта по 8 рун.',
|
||||
historyAndEssence:
|
||||
'Старший Футарк — древнейшая руническая система, полученная верховным богом Одином ценой девятидневного висения на Мировом Древе Иггдрасиль. Каждая руна представляет собой не просто букву, а первородный космический закон, архетипическую силу и состояние материи.',
|
||||
physicalDiceMethod:
|
||||
'КАК БРОСАТЬ КОСТИ НА СТОЛЕ (2×d12):\n'
|
||||
'Для строго равномерного выпадения 24 рун (вероятность 1/24) используются два двенадцатигранника d12 (или один кубик d12, брошенный дважды):\n\n'
|
||||
'1. Первый кубик d12 (Чёт / Нечет):\n'
|
||||
' • Нечётное число (1, 3, 5, 7, 9, 11) ➔ Выбирает первую половину рун (номера 1..12, от Феху до Йеры).\n'
|
||||
' • Чётное число (2, 4, 6, 8, 10, 12) ➔ Выбирает вторую половину рун (номера 13..24, от Эйваз до Одал).\n\n'
|
||||
'2. Второй кубик d12 (Точный номер):\n'
|
||||
' • Показывает номер руны внутри выбранной половины (от 1 до 12).\n'
|
||||
' • Если на первом кубике был нечет: руна = d12_2.\n'
|
||||
' • Если на первом кубике был чет: руна = d12_2 + 12.\n\n'
|
||||
'Пример: Первый кубик = 8 (чет), Второй кубик = 12 ➔ 12 + 12 = Руна №24 (Одал).',
|
||||
directInputMethod:
|
||||
'РУЧНОЙ ВВОД СО СТОЛА:\n'
|
||||
'Если вы вытянули настоящую деревянную или каменную плашку из рунического мешочка:\n'
|
||||
'1. Перейдите на вкладку «Ввод со стола».\n'
|
||||
'2. Если руна легла головой вниз (для обратимых рун) — нажмите переключатель «Перевёрнутая ᛉ/ᛦ».\n'
|
||||
'3. Нажмите на глиф выпавшей руны в сетке 24 символов.',
|
||||
interpretationGuide:
|
||||
'ПРАВИЛА ТОЛКОВАНИЯ:\n'
|
||||
'• Прямое положение: сила руны струится естественно и гармонично.\n'
|
||||
'• Перевёрнутое положение (меркстейв): сила заблокирована, искажена, либо требует внутренней проработки перед внешним действием.\n'
|
||||
'• Необратимые руны (Иса, Гебо, Хагалаз, Совило, Ингуз, Эйваз, Дагаз, Наутиз): не имеют обратного положения — их закон непреложен.',
|
||||
sacredRules: [
|
||||
'Не задавайте один и тот же вопрос рунам дважды за день.',
|
||||
'Руны требуют личной ответственности: это совет и зеркало судьбы, а не фатализм.',
|
||||
'Обратите внимание на связанный знак 999 — он даёт земную форму северному архетипу.',
|
||||
],
|
||||
),
|
||||
|
||||
'lenormand': DivinationSystemInstruction(
|
||||
id: 'lenormand',
|
||||
title: 'Малый Оракул Ленорман',
|
||||
icon: '🪶',
|
||||
brief: 'Французская система 36 символических Домов мадам Марии Ленорман с сюжетно-комбинаторным чтением.',
|
||||
historyAndEssence:
|
||||
'Малый Оракул Ленорман возник в конце XVIII века и основан на ясных житейских архетипах (Всадник, Корабль, Дом, Сердце, Коса, Крест). В отличие от Таро, карты Ленорман не читаются изолированно — они раскрывают смысл в парах, триплетах и домах, образуя законченное повествовательное предложение.',
|
||||
physicalDiceMethod:
|
||||
'КАК БРОСАТЬ КОСТИ НА СТОЛЕ (d6 + d12):\n'
|
||||
'Колода насчитывает ровно 36 карт (3 блока по 12). Для идеальной вероятности 1/36 используются обычный кубик d6 и двенадцатигранник d12:\n\n'
|
||||
'1. Кубик d6 (Смещение блока):\n'
|
||||
' • Выпало 1 или 2 ➔ База +0 (карты с 1 по 12).\n'
|
||||
' • Выпало 3 или 4 ➔ База +12 (карты с 13 по 24).\n'
|
||||
' • Выпало 5 или 6 ➔ База +24 (карты с 25 по 36).\n\n'
|
||||
'2. Кубик d12 (Номер карты в блоке):\n'
|
||||
' • Выпавшее число на d12 складывается с базой: Карта = База + d12.\n\n'
|
||||
'Пример: d6 показал 4 (база 12), а d12 показал 12 ➔ 12 + 12 = Карта 24 («Сердце»).',
|
||||
directInputMethod:
|
||||
'РУЧНОЙ ВВОД СО СТОЛА:\n'
|
||||
'Если вы гадаете на бумажной колоде Ленорман:\n'
|
||||
'1. Перейдите во вкладку «Ввод 1..36 со стола».\n'
|
||||
'2. Вбейте номер выпавшей карты (1..36) в поле ввода либо просто нажмите на карточку с её номером в галерее.',
|
||||
interpretationGuide:
|
||||
'ПРАВИЛА ЧТЕНИЯ КАРТ ЛЕНОРМАН:\n'
|
||||
'• Первая карта — подлежащее (тема, субъект вопроса).\n'
|
||||
'• Вторая карта — сказуемое / определение (что происходит, какое качество обретает тема).\n'
|
||||
'• В приложении доступен полный справочник всех 35 парных сочетаний для каждой карты (кнопка с книгой в карточке результата).',
|
||||
sacredRules: [
|
||||
'Карты Ленорман буквальны и говорят на языке фактов материи.',
|
||||
'Обращайте внимание на «тяжёлые» карты (Коса, Гроб, Тучи, Крест) — они меняют вектор соседних карт.',
|
||||
'Используйте Мастер-промпт, если нужно разобрать многослойную цепочку событий.',
|
||||
],
|
||||
),
|
||||
|
||||
'iching': DivinationSystemInstruction(
|
||||
id: 'iching',
|
||||
title: 'И Цзин (Книга Перемен)',
|
||||
icon: '☯️',
|
||||
brief: 'Древнекитайский канон Чжоу И: 64 гексаграммы, образуемые 6 бросками трёх монет снизу вверх.',
|
||||
historyAndEssence:
|
||||
'И Цзин («Книга Перемен») — величайший памятник восточной мудрости, созданный Фу Си, Вэнь-ваном и Конфуцием более 3000 лет назад. Каждая гексаграмма состоит из двух триграмм (Небо, Земля, Гром, Вода, Гора, Ветер, Огонь, Водоём) и описывает фазу динамической трансформации Инь и Ян во Вселенной.',
|
||||
physicalDiceMethod:
|
||||
'КЛАССИЧЕСКИЙ МЕТОД 3 МОНЕТ (6 БРОСКОВ СНИЗУ ВВЕРХ):\n'
|
||||
'Возьмите 3 одинаковые монеты. Орёл считается за 3 (Ян), Решка — за 2 (Инь). Бросьте монеты 6 раз, складывая достоинства:\n\n'
|
||||
'• 6 (2+2+2, три решки): СТАРАЯ ИНЬ (⚋ ➔ ⚊)\n'
|
||||
' Мутирующая женская черта. В основной гексаграмме прерывистая, но трансформируется в сплошную.\n\n'
|
||||
'• 7 (3+2+2, один орёл, две решки): МОЛОДАЯ ЯН (⚊)\n'
|
||||
' Стабильная светлая черта. Не меняется.\n\n'
|
||||
'• 8 (3+3+2, два орла, одна решка): МОЛОДАЯ ИНЬ (⚋)\n'
|
||||
' Стабильная тёмная черта. Не меняется.\n\n'
|
||||
'• 9 (3+3+3, три орла): СТАРАЯ ЯН (⚊ ➔ ⚋)\n'
|
||||
' Мутирующая мужская черта. В основной гексаграмме сплошная, но трансформируется в прерывистую.',
|
||||
directInputMethod:
|
||||
'РУЧНОЙ ВВОД СО СТОЛА:\n'
|
||||
'1. Совершите 6 бросков монет на столе, записывая суммы снизу вверх (от 1-й линии к 6-й).\n'
|
||||
'2. Перейдите во вкладку «Ввод черт (6..9)».\n'
|
||||
'3. Последовательно нажимайте кнопки 6, 7, 8 или 9 для каждого броска.',
|
||||
interpretationGuide:
|
||||
'КАК ЧИТАТЬ ГЕКСАГРАММУ:\n'
|
||||
'• Линии 1–3 (нижняя триграмма): внутренний мир, намерения, личное начало.\n'
|
||||
'• Линии 4–6 (верхняя триграмма): внешние обстоятельства, окружение, исход ситуации.\n'
|
||||
'• Старые черты (6 и 9) указывают на точные точки перелома и скорых событий.',
|
||||
sacredRules: [
|
||||
'Линии всегда строятся СНИЗУ ВВЕРХ (от корней к ветвям).',
|
||||
'Книга Перемен не терпит суеты: формулируйте вопрос о стратегии правильного поведения.',
|
||||
'Истинный ответ кроется в моменте перехода между стабильностью и мутацией.',
|
||||
],
|
||||
),
|
||||
|
||||
'tarot': DivinationSystemInstruction(
|
||||
id: 'tarot',
|
||||
title: 'Таро (Старшие Арканы)',
|
||||
icon: '🎴',
|
||||
brief: '22 Старших Аркана: архитипический путь Души от Шута (0) до Мира (XXI).',
|
||||
historyAndEssence:
|
||||
'Старшие Арканы Таро — великая книга герметической философии. 22 аркана соотносятся с 22 буквами священного еврейского алфавита, путями на Каббалистическом Древе Жизни и астрологическими принципами. Это макрокосмические законы инициации сознания.',
|
||||
physicalDiceMethod:
|
||||
'КАК ГАДАТЬ НА КОЛОДЕ ИЛИ ЖРЕБИЕМ:\n'
|
||||
'В приложении:\n'
|
||||
'• Нажмите «Вытянуть Аркан» во вкладке «Жребий колоды». Приложение произведёт виртуальную тасовку и определит ориентацию карты (прямая или перевёрнутая).\n\n'
|
||||
'Физической колодой:\n'
|
||||
'• Тщательно перетасуйте 22 Старших Аркана, снимите колоду левой рукой к сердцу и вытяните центральную карту.',
|
||||
directInputMethod:
|
||||
'РУЧНОЙ ВВОД СО СТОЛА:\n'
|
||||
'1. Перейдите во вкладку «Выбор из 22 Арканов».\n'
|
||||
'2. Если выпавшая карта легла перевёрнутой — отметьте переключатель «Перевёрнутый».\n'
|
||||
'3. Выберите карту из пронумерованного списка 0..XXI.',
|
||||
interpretationGuide:
|
||||
'ПРАВИЛА ИНТЕРПРЕТАЦИИ:\n'
|
||||
'• Прямой аркан: манифестация архетипа в его чистом, созидательном свете.\n'
|
||||
'• Перевёрнутый аркан: теневое проявление, избыток или дефицит энергии аркана, задержка осознания.\n'
|
||||
'• По кнопке «Значения и пары» в результате гадания можно посмотреть парные созвучия аркана.',
|
||||
sacredRules: [
|
||||
'Старшие Арканы говорят о сущностных уроках судьбы, а не о мелком быте.',
|
||||
'Каждый аркан содержит как благословение, так и суровое испытание.',
|
||||
'Сопоставляйте аркан с выпавшими рунами и знаками сессии дня.',
|
||||
],
|
||||
),
|
||||
|
||||
'cleromancy': DivinationSystemInstruction(
|
||||
id: 'cleromancy',
|
||||
title: 'Оракул (Клеромантия)',
|
||||
icon: '🎲',
|
||||
brief: 'Каноническая тысяча сакральных знаков (000–999), бросаемых трёхзначными кубиками d10.',
|
||||
historyAndEssence:
|
||||
'Клеромантия — древнейшее искусство гадания по священному жребию. Система Оракула объединяет 1000 архетипических слов русского языка с их сакральной семантикой, энергетическими валентностями, тенями и нерушимыми формулами бытия.',
|
||||
physicalDiceMethod:
|
||||
'КАК БРОСАТЬ КОСТИ НА СТОЛЕ (3×d10):\n'
|
||||
'Для получения одного трёхзначного знака используются три десятигранные кости d10 (с гранями 0..9):\n'
|
||||
'• Первая кость d10 = Сотни (0..9)\n'
|
||||
'• Вторая кость d10 = Десятки (0..9)\n'
|
||||
'• Третья кость d10 = Единицы (0..9)\n\n'
|
||||
'Пример: выпало 0, 4, 2 ➔ Знак #042 («КРЫЛО»). Если выпало 0, 0, 0 ➔ Знак #000.',
|
||||
directInputMethod:
|
||||
'РУЧНОЙ ВВОД СО СТОЛА:\n'
|
||||
'1. Выберите количество знаков в цепи (от 1 до 10).\n'
|
||||
'2. Вбейте трёхзначный номер знака со своих костей либо наберите слово в поиске.',
|
||||
interpretationGuide:
|
||||
'ПРАВИЛА ТОЛКОВАНИЯ:\n'
|
||||
'• Знаки выстраиваются в связный поток судьбы слева направо (Исток ➔ Препятствие ➔ Вектор ➔ Исход).\n'
|
||||
'• В каждом знаке изучайте три уровня: Ядро знака, Канонический смысл и Толкование Оракула.\n'
|
||||
'• Практическая триада: «Тень» предостерегает от ловушки, «Вопрос» вскрывает слепое пятно, «Жест» требует конкретного действия.',
|
||||
sacredRules: [
|
||||
'Не перебрасывайте кости, если ответ кажется неожиданным: Оракул поражает эго, но открывает дух.',
|
||||
'Используйте встроенных толкователей (Ясный оракул, Орк-шаман, Техножрец и др.) для разных интонаций.',
|
||||
'Жмите «Мастер-Промпт», чтобы получить сверхглубокий разбор от Grok или Claude.',
|
||||
],
|
||||
),
|
||||
|
||||
'numerology': DivinationSystemInstruction(
|
||||
id: 'numerology',
|
||||
title: 'Нумерология и Квадрат Пифагора',
|
||||
icon: '🔢',
|
||||
brief: 'Древнегреческая психоматрица 3×3, расчёт 4 рабочих чисел, число жизненного пути и баланс 9 стихий личности.',
|
||||
historyAndEssence:
|
||||
'Пифагор и александрийские мистики объединили математические тайны египетских жрецов, халдейскую нумерологию и учение о музыке сфер в единую строгую систему. В основе лежит принцип: «Всё сущее есть число». Дата рождения или дата судьбоносного события кодирует матрицу потенциалов, скрытых талантов, запаса витальной энергии и кармических задач человека.',
|
||||
physicalDiceMethod: '''ЖРЕБИЙ НА КОСТЯХ (МАНТИЧЕСКИЙ СРЕЗ МОМЕНТА):
|
||||
Если вы гадаете на текущую ситуацию или вопрос, а не анализируете статическую дату рождения:
|
||||
|
||||
1. Бросьте 8 костей d10 (0..9) для формирования синтетической даты момента (ДД.ММ.ГГГГ).
|
||||
2. Либо используйте бросок одной кости d10 (1..9) для определения ведущего Архетипа Числа Судьбы прямо сейчас.
|
||||
3. В приложении во вкладке «Жребий d10» кости бросаются виртуально с мгновенным заполнением матрицы.''',
|
||||
directInputMethod: '''ВВОД ДАТЫ (РОЖДЕНИЯ ИЛИ СОБЫТИЯ):
|
||||
1. Выберите дату через встроенный календарь либо введите день, месяц и год вручную.
|
||||
2. Канонический расчёт 4 Рабочих Чисел:
|
||||
• 1-е число (W1): сумма всех цифр даты рождения.
|
||||
• 2-е число (W2): сумма цифр 1-го числа.
|
||||
• 3-е число (W3): W1 минус удвоенная первая значащая цифра дня рождения (W1 - 2 × d1).
|
||||
• 4-е число (W4): сумма цифр 3-го числа.
|
||||
3. Все цифры даты и 4 рабочих чисел вносятся в 9 ячеек психоматрицы 3×3:
|
||||
[1: Характер] [4: Здоровье] [7: Удача]
|
||||
[2: Энергия] [5: Логика] [8: Долг]
|
||||
[3: Интерес] [6: Труд] [9: Память]''',
|
||||
interpretationGuide: '''ПРАВИЛА ТОЛКОВАНИЯ ПСИХОМАТРИЦЫ:
|
||||
• Прочерк (цифр нет): качество не дано от природы — зона свободного роста и кармической наработки.
|
||||
• 1 цифра: качество развито слабо, требует осознанного включения.
|
||||
• 2 цифры: норма, гармоничное и естественное проявление силы.
|
||||
• 3 цифры: волнообразное проявление («хочу — проявлю, хочу — нет»), капризность таланта.
|
||||
• 4-5+ цифр: гипер-потенциал, доминанта личности; при застое перерастает в деструктив.
|
||||
|
||||
ЛИНИИ МАТРИЦЫ:
|
||||
• Строка 1 (1-4-7): Целеустремленность и настойчивость.
|
||||
• Строка 2 (2-5-8): Семья, качество семьянина, верность.
|
||||
• Строка 3 (3-6-9): Стабильность, привычки, привязанность к укладу.
|
||||
• Столбец 1 (1-2-3): Самооценка, амбиции, внутренняя опора.
|
||||
• Столбец 2 (4-5-6): Быт, материальный достаток, приспособляемость.
|
||||
• Столбец 3 (7-8-9): Талант, одаренность, искра божья.
|
||||
• Диагональ (1-5-9): Духовность, связь с высшим эгрегором.
|
||||
• Диагональ (3-5-7): Чувственность, магнетизм, темперамент.''',
|
||||
sacredRules: [
|
||||
'Числа матрицы показывают энергетический каркас, но не ограничивают свободу воли человека.',
|
||||
'Код суток на экране Слова Дня считается той же свёрткой: день, месяц и год по отдельности, мастер-числа 11, 22 и 33 не свёртываются.',
|
||||
'Обращайте внимание на Число Жизненного Пути (ЧЖП) — оно связывает матрицу с сакральными знаками Оракула 999.',
|
||||
'Психоматрица великолепно дополняет астрологию, руны и Таро в рамках единой дневной сессии.',
|
||||
],
|
||||
),
|
||||
'word_of_day': DivinationSystemInstruction(
|
||||
id: 'word_of_day',
|
||||
title: 'Слово Дня и Календарь Осознанности',
|
||||
icon: '☀️',
|
||||
brief: 'Ежедневный ритуал, тренажёр синхронистичности и духовное упражнение из трёх актов (Утро, День, Вечер).',
|
||||
historyAndEssence:
|
||||
'''Слово Дня — это живой камертон текущей даты. В оракуле 999 каждому календарному дню соответствует резонирующий архетип, соединяющий время, бессознательное и земную реальность. Рядом с ним стоит нумерологический код суток: день, месяц и год свёртываются по отдельности и складываются, а мастер-числа 11, 22 и 33 остаются собой. Резонансное слово дня — отдельный знак тысячи, его не подменяет цифра.''',
|
||||
physicalDiceMethod:
|
||||
'''КАЛЕНДАРНЫЙ РЕЗОНАНС:
|
||||
Слово дня рассчитывается математически детерминированным алгоритмом от даты года, месяца и дня. На один и тот же день на всех устройствах выпадает один и тот же символ, создавая единое синхронистическое поле.
|
||||
|
||||
ЖРЕБИЙ СО СТОЛА (d1000):
|
||||
Если вы предпочитаете физический жребий — бросьте три десятигранника d10 (сотни, десятки, единицы 000..999) и найдите знак в справочнике.''',
|
||||
directInputMethod:
|
||||
'''РАБОТА С КАЛЕНДАРЁМ:
|
||||
Используйте стрелки вверху или календарный виджет, чтобы перемещаться по дням, возвращаться к прошлым датам, просматривать хронологию и вести непрерывную летопись практик.''',
|
||||
interpretationGuide:
|
||||
'''ТРИ ТАКТА ОСОЗНАННОСТИ:
|
||||
1. Утро (Настройка): прочитайте послание Оракула и возьмите «Коан дня» в качестве ментального фокуса.
|
||||
2. День (Практикум): выполните микродействие (Вызов дня) и остерегайтесь Теневой ловушки.
|
||||
3. Вечер (Синхронии): запишите в дневник наблюдения — как именно это слово проявилось в событиях, встречах и мыслях.''',
|
||||
sacredRules: [
|
||||
'Не форсируйте совпадения: позвольте символу проявиться естественно.',
|
||||
'Теневая ловушка дня — ваш главный предохранитель от автоматизмов эго.',
|
||||
'Регулярная летопись синхроний раскрывает скрытые закономерности судьбы.',
|
||||
],
|
||||
),
|
||||
'matrix': DivinationSystemInstruction(
|
||||
id: 'matrix',
|
||||
title: 'Матрица 33×30 (990 Домов)',
|
||||
icon: '🏛️',
|
||||
brief: 'Сакральная геометрия поля 990 Домов, управляемая 10 верховными Архонтами и цепочками диспозиций.',
|
||||
historyAndEssence:
|
||||
'Матрица 33×30 представляет собой космическую развёртку реальности на 990 земных домов и 10 надмирных Архонтов (по одному на каждые 3 столбца). Каждый выпавший знак входит в дом как Гость, порождая уникальную трёхчастную иерархию: Архонт (небесный климат) ➔ Дом (земная почва) ➔ Гость (живой импульс).',
|
||||
physicalDiceMethod:
|
||||
'КАК ВЫБИРАТЬ ДОМА НА СТОЛЕ:\n'
|
||||
'• Бросьте кости для выбора строки (1..33) и столбца (1..30).\n'
|
||||
'• Либо используйте интерактивный бросок в приложении, где открываются выбранные вами дома на интерактивном полотне.',
|
||||
directInputMethod:
|
||||
'РУЧНОЙ ВВОД СО СТОЛА:\n'
|
||||
'• Нажмите на любые дома на интерактивной сетке матрицы, либо задайте сигнификатор Кверента для центрирования расклада.',
|
||||
interpretationGuide:
|
||||
'ЦЕПОЧКИ ДИСПОЗИЦИЙ (HOUSE CHAINS):\n'
|
||||
'• Дом указывает на знак Гостя. Но у самого Гостя есть свой собственный Дом в матрице!\n'
|
||||
'• Переход от дома к дому образует «Цепь Судьбы».\n'
|
||||
'• Цепь может замкнуться в «Петлю Судьбы», упереться в «Престол Архонта» или пронзить поле открытым вектором.',
|
||||
sacredRules: [
|
||||
'Архонт определяет высший закон, в рамках которого разворачиваются события.',
|
||||
'Петля судьбы указывает на кармический узел, требующий осознания.',
|
||||
'Формируйте Мастер-промпт Матрицы для многостраничного фундаментального анализа.',
|
||||
],
|
||||
),
|
||||
'geomancy': DivinationSystemInstruction(
|
||||
id: 'geomancy',
|
||||
title: 'Арабская Геомантия (Илм ар-рамл)',
|
||||
icon: '⌛',
|
||||
brief: '16 священных фигур земного жребия, получаемых считыванием чётности рядов точек на песке.',
|
||||
historyAndEssence:
|
||||
'«Илм ар-рамл» («Наука о песке») — древнейшая арабская мантическая система, восходящая к легендарному пророку Идрису (Гермесу Трисмегисту) и широко распространившаяся в средневековой Европе как Геомантия. Четыре стихийных уровня (Огонь/Голова, Воздух/Шея, Вода/Тело, Земля/Стопы) дают ровно 16 фигур бинарного кода Вселенной, отражающих динамику материи и духа.',
|
||||
physicalDiceMethod:
|
||||
'КАК БРОСАТЬ В РЕАЛЬНОСТИ (ПЕСОК ИЛИ МОНЕТЫ):\n'
|
||||
'1. Метод песка (традиционный):\n'
|
||||
' Палочкой на песке или ручкой на бумаге быстро наносится 4 ряда случайных точек справа налево, не считая их.\n'
|
||||
' Затем точки в каждом ряду группируются парами:\n'
|
||||
' • Нечётный остаток (одна точка) ➔ 1 точка (·)\n'
|
||||
' • Чётный остаток (две точки) ➔ 2 точки (··)\n'
|
||||
' 4 ряда сверху вниз формируют фигуру.\n\n'
|
||||
'2. Метод 4 монет или костей:\n'
|
||||
' Бросьте 4 монеты одновременно (Орёл = 1 точка, Решка = 2 точки) сверху вниз.',
|
||||
directInputMethod:
|
||||
'РУЧНОЙ ВВОД СО СТОЛА ИЛИ КАТАЛОГА:\n'
|
||||
'В приложении можно постукивать по песку пальцем, генерировать точки случайным рассеиванием песчинок, либо выбрать любую из 16 фигур напрямую из каталога.',
|
||||
interpretationGuide:
|
||||
'ПОЛЯРНОСТЬ И ЗНАЧЕНИЕ ФИГУР:\n'
|
||||
'Каждая фигура имеет полярность (Входящая / Исходящая, Подвижная / Неподвижная), стихию и планетарного управителя. В Великом Щите ровно 16 фигур: 4 Матери, 4 Дочери, 4 Племянника, 2 Свидетеля, Судья (15) и Примиритель (16-я фигура, рождённая сложением Судьи и 1-й Матери для согласования исхода с вопрошающим). При этом Судья математически всегда содержит чётное число точек.',
|
||||
sacredRules: [
|
||||
'Сформулируйте один ясный вопрос перед касанием песка.',
|
||||
'Не считайте точки при нанесении — пусть действует рука и интуиция.',
|
||||
'Если Судья содержит нечётное число точек — в расчёте щита допущена ошибка (в приложении расчёт математически строг).',
|
||||
'Фигура Rubeus (Красный) в 1-м доме или у Судьи предупреждает о ярости, крови или фальшивом вопросе.',
|
||||
],
|
||||
),
|
||||
'ogham': DivinationSystemInstruction(
|
||||
id: 'ogham',
|
||||
title: 'Кельтский Древесный Огам (20 Фед)',
|
||||
icon: '᚛',
|
||||
brief: '20 священных символов-деревьев древних друидов, распределённых по 4 Аикмам Священной Рощи.',
|
||||
historyAndEssence:
|
||||
'Огам (Ogham) — священная тайнопись и мантическая система кельтских друидов и филидов древней Ирландии и Британии, восходящая к богу красноречия и мудрости Огме. Каждая насечка (феда) соответствует священному дереву или кустарнику Священной Рощи (Берёза, Рябина, Ольха, Дуб, Орешник, Тис и др.). 20 фед разделены на 4 семьи (Аикмы), образуя лестницу духовного восхождения от зарождения до вечности.',
|
||||
physicalDiceMethod:
|
||||
'ТРАДИЦИОННЫЙ ЖРЕБИЙ ДЕРЕВЯННЫХ ПЛАШЕК (ДРУИДИЧЕСКИЙ ЖРЕБИЙ):\n'
|
||||
'1. Приготовьте мешочек с 20 деревянными палочками Огама (по одной на каждую феду).\n'
|
||||
'2. Сосредоточьтесь на вопросе, вдохните аромат дерева и не глядя достаньте плашку.\n'
|
||||
'3. Если плашка выпала засечками вправо/вверх — положение прямое, засечками влево/вниз — обратное/теневое.\n'
|
||||
'4. Для броска кубиками: бросьте d20 (1..20 определяет номер феды), затем любую монету/d2 для определения полярности (1 = прямое, 2 = теневое).',
|
||||
directInputMethod:
|
||||
'РУЧНОЙ ВЫБОР ИЗ СВЯЩЕННОЙ РОЩИ:\n'
|
||||
'В приложении доступен интерактивный бросок друидических палочек, либо прямой выбор нужных фед из резного свитка 4-х Аикм.',
|
||||
interpretationGuide:
|
||||
'ЗНАЧЕНИЕ ФЕД И ДРЕВЕСНЫХ СИЛ:\n'
|
||||
'Каждое дерево в кельтской традиции несёт свой неповторимый архетип:\n'
|
||||
'• Бэйт (Берёза) — начало, очищение, белый лист судьбы;\n'
|
||||
'• Дуир (Дуб) — несокрушимая стойкость, врата власти и царственность;\n'
|
||||
'• Тинне (Остролист) — огонь испытаний, победа в битве и благородство духа;\n'
|
||||
'• Колл (Орешник) — высшая мудрость, колодец поэтического вдохновения;\n'
|
||||
'• Идад (Тис) — порог вечности, смерть старого и бессмертие души.\n'
|
||||
'В перевёрнутом виде феда указывает на теневой аспект: блокировку древесной силы, иллюзию защиты или горделивую самоизоляцию.',
|
||||
sacredRules: [
|
||||
'Слушайте шёпот древесных сил без спешки: Огам говорит на языке корней и времён года.',
|
||||
'Обращайте внимание на перевёрнутое положение: оно никогда не бывает фатальным, а предупреждает о дисбалансе.',
|
||||
'Связывайте выпавшую феду с сакральным знаком Оракула 999 для углубления толкования.',
|
||||
],
|
||||
),
|
||||
};
|
||||
|
||||
static DivinationSystemInstruction get(String id) =>
|
||||
all[id] ?? all['cleromancy']!;
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
class DivinationLayout {
|
||||
final String id;
|
||||
final String title;
|
||||
final int count;
|
||||
final String description;
|
||||
final List<String> positionTitles;
|
||||
|
||||
const DivinationLayout({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.count,
|
||||
required this.description,
|
||||
required this.positionTitles,
|
||||
});
|
||||
}
|
||||
|
||||
class DivinationLayouts {
|
||||
static const runesLayouts = [
|
||||
DivinationLayout(
|
||||
id: 'rune_1',
|
||||
title: '1 Руна (Совет Одина)',
|
||||
count: 1,
|
||||
description: 'Мгновенный фокус ситуации или совет на день.',
|
||||
positionTitles: ['Совет Одина'],
|
||||
),
|
||||
DivinationLayout(
|
||||
id: 'rune_3',
|
||||
title: '3 Руны (Три Норны)',
|
||||
count: 3,
|
||||
description: 'Нить судьбы во времени: Урд (Прошлое), Верданди (Настоящее), Скульд (Грядущее).',
|
||||
positionTitles: ['Урд (Прошлое)', 'Верданди (Настоящее)', 'Скульд (Вектор будущего)'],
|
||||
),
|
||||
DivinationLayout(
|
||||
id: 'rune_cross',
|
||||
title: '5 Рун (Рунический Крест)',
|
||||
count: 5,
|
||||
description: 'Глубинный срез: Центр, Препятствие, Прошлое, Будущее и Итог.',
|
||||
positionTitles: ['1. Суть (Центр)', '2. Препятствие (Вызов)', '3. Опора (Прошлое)', '4. Потенциал (Будущее)', '5. Исход (Венец)'],
|
||||
),
|
||||
];
|
||||
|
||||
static const lenormandLayouts = [
|
||||
DivinationLayout(
|
||||
id: 'leno_2',
|
||||
title: '2 Карты (Пара)',
|
||||
count: 2,
|
||||
description: 'Подлежащее и Сказуемое — классическое диалоговое чтение.',
|
||||
positionTitles: ['1. Тема (Подлежащее)', '2. Развитие (Сказуемое)'],
|
||||
),
|
||||
DivinationLayout(
|
||||
id: 'leno_3',
|
||||
title: '3 Карты (Линия)',
|
||||
count: 3,
|
||||
description: 'Развитие ситуации от истока к исходу.',
|
||||
positionTitles: ['1. Исток', '2. Кульминация', '3. Исход'],
|
||||
),
|
||||
DivinationLayout(
|
||||
id: 'leno_cross',
|
||||
title: '5 Карт (Крест)',
|
||||
count: 5,
|
||||
description: 'Центр, скрытое, мысли, препятствие и развязка.',
|
||||
positionTitles: ['1. Фокус', '2. Скрытое', '3. Мысли', '4. Препятствие', '5. Исход'],
|
||||
),
|
||||
DivinationLayout(
|
||||
id: 'leno_square_9',
|
||||
title: '9 Карт (Квадрат 3×3)',
|
||||
count: 9,
|
||||
description: 'Портрет ситуации: Прошлое, Настоящее, Будущее по вертикалям и горизонталям.',
|
||||
positionTitles: [
|
||||
'1. Прошлое/Мысли', '2. Фокус/Настоящее', '3. Будущее/Планы',
|
||||
'4. Скрытые влияния', '5. Сердце ситуации (Кверент)', '6. Внешние силы',
|
||||
'7. Исток проблемы', '8. Ближайший шаг', '9. Неизбежный результат'
|
||||
],
|
||||
),
|
||||
DivinationLayout(
|
||||
id: 'leno_gt_36',
|
||||
title: 'Большой Расклад (36 Домов)',
|
||||
count: 36,
|
||||
description: 'Гран Табло (Grand Tableau): вся колода из 36 карт распределяется по 36 домам судьбы (4 ряда по 8 + 4 карты судьбы).',
|
||||
positionTitles: ['1. Дом Всадника (Новости, мысли)', '2. Дом Клевера (Надежды, шанс)', '3. Дом Корабля (Стремления, путь)', '4. Дом Дома (Семья, основа)', '5. Дом Дерева (Здоровье, род)', '6. Дом Туч (Страхи, сомнения)', '7. Дом Змеи (Скрытые враги, интриги)', '8. Дом Гроба (Кризис, конец старого)', '9. Дом Букета (Радость, подарки)', '10. Дом Косы (Резкие решения, опасность)', '11. Дом Метлы (Конфликты, споры)', '12. Дом Птиц (Беседы, суета)', '13. Дом Ребёнка (Начало, невинность)', '14. Дом Лисы (Хитрость, работа)', '15. Дом Медведя (Сила, покровитель)', '16. Дом Звёзд (Ясность, судьба)', '17. Дом Аистов (Перемены, обновление)', '18. Дом Собаки (Дружба, верность)', '19. Дом Башни (Власть, закон, статус)', '20. Дом Сада (Общество, публика)', '21. Дом Горы (Препятствие, блок)', '22. Дом Развилки (Выбор дорог)', '23. Дом Мышей (Потери, истощение)', '24. Дом Сердца (Любовь, страсть)', '25. Дом Кольца (Союз, контракт)', '26. Дом Книги (Тайны, знания)', '27. Дом Письма (Документы, известия)', '28. Дом Мужчины (Анимус, вопрошающий)', '29. Дом Женщины (Анима, партнёрша)', '30. Дом Лилий (Чистота, зрелость)', '31. Дом Солнца (Успех, витальность)', '32. Дом Луны (Интуиция, признание)', '33. Дом Ключа (Решение, озарение)', '34. Дом Рыб (Финансы, изобилие)', '35. Дом Якоря (Стабильность, труд)', '36. Дом Креста (Карма, судьбоносный долг)'],
|
||||
),
|
||||
];
|
||||
|
||||
static const tarotLayouts = [
|
||||
DivinationLayout(
|
||||
id: 'tarot_1',
|
||||
title: '1 Аркан (Суть Момента)',
|
||||
count: 1,
|
||||
description: 'Главный архетип, действующий в вашей жизни прямо сейчас.',
|
||||
positionTitles: ['Аркан Момента'],
|
||||
),
|
||||
DivinationLayout(
|
||||
id: 'tarot_3',
|
||||
title: '3 Аркана (Триптих Судьбы)',
|
||||
count: 3,
|
||||
description: 'Прошлое (что уходит), Настоящее (что есть), Будущее (куда течёт энергия).',
|
||||
positionTitles: ['1. Прошлое (Исток)', '2. Настоящее (Точка силы)', '3. Будущее (Вектор)'],
|
||||
),
|
||||
DivinationLayout(
|
||||
id: 'tarot_cross',
|
||||
title: '5 Арканов (Пентаграмма Выбора)',
|
||||
count: 5,
|
||||
description: 'Квинтэссенция ситуации и скрытые мотивы.',
|
||||
positionTitles: ['1. Основа', '2. Вызов / Тень', '3. Осознанное', '4. Неосознанное', '5. Итог / Квинт'],
|
||||
),
|
||||
];
|
||||
|
||||
static const geomancyLayouts = [
|
||||
DivinationLayout(
|
||||
id: 'geomancy_1',
|
||||
title: '1 Фигура (Знак Песка)',
|
||||
count: 1,
|
||||
description: 'Экспресс-оракул: быстрая фигура песка на один конкретный вопрос или совет момента.',
|
||||
positionTitles: ['Знак Песка (Суть вопроса)'],
|
||||
),
|
||||
DivinationLayout(
|
||||
id: 'geomancy_3',
|
||||
title: '3 Фигуры (Триплет Песка)',
|
||||
count: 3,
|
||||
description: 'Три независимые фигуры песка: Исток, Развитие и Исход. Это не щит: Судья и Свидетели рождаются только сложением Великого Щита.',
|
||||
positionTitles: ['Исток', 'Развитие', 'Исход'],
|
||||
),
|
||||
DivinationLayout(
|
||||
id: 'geomancy_shield',
|
||||
title: 'Великий Щит Геомантии (16 Фигур)',
|
||||
count: 4,
|
||||
description: 'Великий расклад "Илм ар-рамл": нанесение 4 Матерей, порождающих Дочерей, Племянников, Свидетелей, Судью и 16-ю фигуру — Примирителя.',
|
||||
positionTitles: [
|
||||
'1-я Мать (Вопрошающий / Душа)',
|
||||
'2-я Мать (Ресурсы / Материя)',
|
||||
'3-я Мать (Связи / Окружение)',
|
||||
'4-я Мать (Тайное / Завершение)',
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
static const oghamLayouts = [
|
||||
DivinationLayout(
|
||||
id: 'ogham_1',
|
||||
title: '1 Феда (Слово Древа)',
|
||||
count: 1,
|
||||
description: 'Совет священного дерева друидов на текущий момент или сущность ситуации.',
|
||||
positionTitles: ['Священное Древо (Суть совета)'],
|
||||
),
|
||||
DivinationLayout(
|
||||
id: 'ogham_3',
|
||||
title: '3 Феды (Кельтский Трискель)',
|
||||
count: 3,
|
||||
description: 'Трёхчастный срез судьбы: Корень (Истоки/Прошлое), Ствол (Сила действия/Настоящее), Крона (Исход/Будущее).',
|
||||
positionTitles: [
|
||||
'Корень (Исток и скрытая почва)',
|
||||
'Ствол (Сила действия в настоящем)',
|
||||
'Крона (Исход и плоды судьбы)',
|
||||
],
|
||||
),
|
||||
DivinationLayout(
|
||||
id: 'ogham_5',
|
||||
title: '5 Фед (Крест Священной Рощи)',
|
||||
count: 5,
|
||||
description: 'Четыре стороны света и Древо Сердца: Восток (Замысел), Юг (Действие), Запад (Чувства), Север (Опора), Центр (Сердце Рощи).',
|
||||
positionTitles: [
|
||||
'Восток (Зарождение и замысел)',
|
||||
'Юг (Страсть и преодоление)',
|
||||
'Запад (Интуиция и очищение)',
|
||||
'Север (Материальная опора)',
|
||||
'Центр (Сердцевина и вердикт Духа)',
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
class ExternalSymbolSource {
|
||||
final String author;
|
||||
final String work;
|
||||
final String excerpt;
|
||||
final String fullText;
|
||||
|
||||
const ExternalSymbolSource({
|
||||
required this.author,
|
||||
required this.work,
|
||||
this.excerpt = '',
|
||||
this.fullText = '',
|
||||
});
|
||||
|
||||
factory ExternalSymbolSource.fromMap(Map<String, dynamic> map) {
|
||||
return ExternalSymbolSource(
|
||||
author: map['author'] as String? ?? '',
|
||||
work: map['work'] as String? ?? '',
|
||||
excerpt: map['excerpt'] as String? ?? '',
|
||||
fullText: map['full_text'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'author': author,
|
||||
'work': work,
|
||||
'excerpt': excerpt,
|
||||
'full_text': fullText,
|
||||
};
|
||||
}
|
||||
|
||||
class ExternalSymbol {
|
||||
final String term;
|
||||
final String normalized;
|
||||
final String categoryGuess;
|
||||
final List<ExternalSymbolSource> sources;
|
||||
final List<Map<String, dynamic>> illustrations;
|
||||
|
||||
const ExternalSymbol({
|
||||
required this.term,
|
||||
required this.normalized,
|
||||
this.categoryGuess = 'Внешний академический символ',
|
||||
this.sources = const [],
|
||||
this.illustrations = const [],
|
||||
});
|
||||
|
||||
bool get hasIllustrations => illustrations.isNotEmpty;
|
||||
|
||||
String get primaryAuthor => sources.isNotEmpty ? sources.first.author : '';
|
||||
String get primaryWork => sources.isNotEmpty ? sources.first.work : '';
|
||||
String get primaryExcerpt => sources.isNotEmpty ? sources.first.excerpt : '';
|
||||
|
||||
factory ExternalSymbol.fromMap(Map<String, dynamic> map) {
|
||||
final rawSources = map['sources'] as List<dynamic>? ?? [];
|
||||
final rawIlls = map['illustrations'] as List<dynamic>? ?? [];
|
||||
|
||||
return ExternalSymbol(
|
||||
term: map['term'] as String? ?? '',
|
||||
normalized: map['normalized'] as String? ?? '',
|
||||
categoryGuess: map['category_guess'] as String? ?? 'Внешний символ',
|
||||
sources: rawSources
|
||||
.map((s) => ExternalSymbolSource.fromMap(Map<String, dynamic>.from(s as Map)))
|
||||
.toList(),
|
||||
illustrations: rawIlls
|
||||
.map((i) => Map<String, dynamic>.from(i as Map))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'term': term,
|
||||
'normalized': normalized,
|
||||
'category_guess': categoryGuess,
|
||||
'sources': sources.map((s) => s.toMap()).toList(),
|
||||
'illustrations': illustrations,
|
||||
};
|
||||
}
|
||||
@@ -43,7 +43,7 @@ class SessionWord {
|
||||
sessionNumber: item.sessionNumber,
|
||||
partOfSpeech: item.partOfSpeech,
|
||||
category: item.category,
|
||||
symbolicMeaning: item.description,
|
||||
symbolicMeaning: item.displayMeaning,
|
||||
positionTitle: positionTitle,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'word_model.dart';
|
||||
|
||||
/// One of the 10 Archons that rules over a triad of 3 columns (e.g. cols 0..2, 3..5, etc.).
|
||||
class ArchonSector {
|
||||
final int index; // 0..9
|
||||
final WordItem archonWord;
|
||||
final int startCol;
|
||||
final int endCol;
|
||||
final Color sectorColor;
|
||||
final String title;
|
||||
|
||||
Color get color => sectorColor;
|
||||
|
||||
const ArchonSector({
|
||||
required this.index,
|
||||
required this.archonWord,
|
||||
required this.startCol,
|
||||
required this.endCol,
|
||||
required this.sectorColor,
|
||||
required this.title,
|
||||
});
|
||||
}
|
||||
|
||||
/// A cell representing a House in the 33x30 field.
|
||||
class HouseCell {
|
||||
final int row; // 0..32
|
||||
final int col; // 0..29
|
||||
final int houseIndex; // 0..989 (position in grid)
|
||||
final WordItem houseWord; // The Host / Spirit of this House
|
||||
final int sectorIndex; // 0..9
|
||||
|
||||
// Assigned during divination
|
||||
WordItem? guestWord; // The Guest symbol that enters this house
|
||||
int? selectionOrder; // 1..9 (order in which the user selected this house)
|
||||
|
||||
HouseCell({
|
||||
required this.row,
|
||||
required this.col,
|
||||
required this.houseIndex,
|
||||
required this.houseWord,
|
||||
required this.sectorIndex,
|
||||
this.guestWord,
|
||||
this.selectionOrder,
|
||||
});
|
||||
|
||||
bool get isSelected => selectionOrder != null;
|
||||
|
||||
HouseCell copyWith({
|
||||
WordItem? guestWord,
|
||||
int? selectionOrder,
|
||||
bool clearGuest = false,
|
||||
bool clearOrder = false,
|
||||
}) {
|
||||
return HouseCell(
|
||||
row: row,
|
||||
col: col,
|
||||
houseIndex: houseIndex,
|
||||
houseWord: houseWord,
|
||||
sectorIndex: sectorIndex,
|
||||
guestWord: clearGuest ? null : (guestWord ?? this.guestWord),
|
||||
selectionOrder: clearOrder ? null : (selectionOrder ?? this.selectionOrder),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ChainEndType {
|
||||
loop, // Петля судьбы: энергия замкнулась на дом из цепочки
|
||||
archonThrone, // Престол Архонта: цепь увенчалась верховным Архонтом
|
||||
fieldVector, // Длинный вектор: открытый импульс сквозь матрицу
|
||||
}
|
||||
|
||||
/// A step in a House Chain: House A -> Guest B -> House of B...
|
||||
class HouseChainStep {
|
||||
final HouseCell house;
|
||||
final WordItem guest;
|
||||
final bool isLoopClosing;
|
||||
final bool leadsToArchon;
|
||||
final bool computed;
|
||||
final ArchonSector? destinationArchon;
|
||||
|
||||
const HouseChainStep({
|
||||
required this.house,
|
||||
required this.guest,
|
||||
this.isLoopClosing = false,
|
||||
this.leadsToArchon = false,
|
||||
this.computed = false,
|
||||
this.destinationArchon,
|
||||
});
|
||||
}
|
||||
|
||||
/// A full trajectory of energy moving between houses across the 990 field.
|
||||
class HouseChain {
|
||||
final int chainIndex;
|
||||
final HouseCell startingHouse;
|
||||
final List<HouseChainStep> steps;
|
||||
final ChainEndType endType;
|
||||
final String description;
|
||||
|
||||
const HouseChain({
|
||||
required this.chainIndex,
|
||||
required this.startingHouse,
|
||||
required this.steps,
|
||||
required this.endType,
|
||||
required this.description,
|
||||
});
|
||||
|
||||
|
||||
bool get hasLoop => endType == ChainEndType.loop;
|
||||
|
||||
String get endTypeTitle {
|
||||
switch (endType) {
|
||||
case ChainEndType.loop:
|
||||
return 'Петля Судьбы';
|
||||
case ChainEndType.archonThrone:
|
||||
return 'Престол Архонта';
|
||||
case ChainEndType.fieldVector:
|
||||
return 'Вектор в поле';
|
||||
}
|
||||
}
|
||||
|
||||
Color get statusColor {
|
||||
switch (endType) {
|
||||
case ChainEndType.loop:
|
||||
return const Color(0xFFE57373);
|
||||
case ChainEndType.archonThrone:
|
||||
return const Color(0xFFFFD54F);
|
||||
case ChainEndType.fieldVector:
|
||||
return const Color(0xFF64B5F6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The entire state of a Houses divination session.
|
||||
class HousesSession {
|
||||
final String id;
|
||||
final DateTime createdAt;
|
||||
final String? question;
|
||||
final WordItem? querentWord; // Карта Кверента (субъект / объект гадания)
|
||||
final int? querentHouseIndex; // Индекс дома кверента на поле 33x30 (если есть)
|
||||
final List<ArchonSector> sectors; // 10 Archons
|
||||
final List<HouseCell> grid; // 990 House cells (33 rows x 30 cols)
|
||||
final List<HouseCell> selectedHouses; // 1..9 houses in order of selection
|
||||
final List<HouseChain> chains; // Calculated house chains
|
||||
final String? aiInterpretation;
|
||||
|
||||
const HousesSession({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
this.question,
|
||||
this.querentWord,
|
||||
this.querentHouseIndex,
|
||||
required this.sectors,
|
||||
required this.grid,
|
||||
required this.selectedHouses,
|
||||
required this.chains,
|
||||
this.aiInterpretation,
|
||||
});
|
||||
|
||||
HousesSession copyWith({
|
||||
String? question,
|
||||
WordItem? querentWord,
|
||||
int? querentHouseIndex,
|
||||
String? aiInterpretation,
|
||||
List<HouseCell>? selectedHouses,
|
||||
List<HouseChain>? chains,
|
||||
}) {
|
||||
return HousesSession(
|
||||
id: id,
|
||||
createdAt: createdAt,
|
||||
question: question ?? this.question,
|
||||
querentWord: querentWord ?? this.querentWord,
|
||||
querentHouseIndex: querentHouseIndex ?? this.querentHouseIndex,
|
||||
sectors: sectors,
|
||||
grid: grid,
|
||||
selectedHouses: selectedHouses ?? this.selectedHouses,
|
||||
chains: chains ?? this.chains,
|
||||
aiInterpretation: aiInterpretation ?? this.aiInterpretation,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ class InterpreterPersona {
|
||||
title: 'Ясный оракул',
|
||||
subtitle: 'Прямо, без тумана: ситуация, слепая зона, один шаг',
|
||||
icon: Icons.visibility_outlined,
|
||||
systemPrompt: '''Ты — толкователь жребия «Оракул 999». Говори по-русски спокойно и ясно.
|
||||
systemPrompt: '''Ты — толкователь жребия «Оракул». Говори по-русски спокойно и ясно.
|
||||
Не мистифицируй. Не прячься за «тайной», «завесой», «голосом вечности» и общими фразами про путь.
|
||||
Твоя работа — прочитать выпавшие знаки как карту нынешнего положения и сказать, что с этим делать.
|
||||
|
||||
@@ -46,24 +46,25 @@ class InterpreterPersona {
|
||||
|
||||
static const InterpreterPersona orc = InterpreterPersona(
|
||||
id: 'orc',
|
||||
title: 'Древний орк-оракул',
|
||||
subtitle: 'Грубо и честно: где трусость, где удар, что бросить',
|
||||
icon: Icons.sports_mma_rounded,
|
||||
systemPrompt: '''Ты — древний орк-оракул клана Жребия. Говори по-русски грубо, коротко, честно, как старший воин младшему.
|
||||
Не сюсюкай и не молись туману. Знаки — следы на земле: где враг, где слабость, где бить, где уходить.
|
||||
title: 'Орк-шаман',
|
||||
subtitle: 'Вааагх в черепе: видения Горка и Морка, куда бить',
|
||||
icon: Icons.flash_on_rounded,
|
||||
systemPrompt: '''Ты — орк-шаман (weirdboy) из вархаммерского Вааагх. В черепе гудит зелёная сила: знаки бьют как разряды варпа, а не как «мудрые советы старшего».
|
||||
Говори по-русски грубо, жарко, с рыком — но читаемо. Можно «Вааагх», «Горк», «Морк», «варп», «зелёный огонь», «мальчки», «круши», «не ссы». Не превращай текст в нечитаемую кашу из сленга.
|
||||
|
||||
Ты видишь вопрос как поле боя в варпе: где сила Вааагх копится, где она рвёт череп, где враг, где трусость прикидывается хитростью.
|
||||
|
||||
Правила:
|
||||
- Назови каждый знак простыми словами и привяжи к вопросу.
|
||||
- Скажи, что вопрошающий делает не так и что делать вместо этого.
|
||||
- Можно «слушай», «брось», «держи строй». Не оскорбляй зря и не ломай русский язык в кашу.
|
||||
- 3–6 предложений. Никакой канцелярской эзотерики.''',
|
||||
- Назови каждый знак как видение, врезавшееся в башку, и привяжи к вопросу.
|
||||
- Скажи, кого/что надо крушить, от чего отойти, где Горк (прямо бить) и где Морк (ударить сбоку).
|
||||
- Закончить одним приказом-шагом. 3–6 предложений. Не лей эльфийский туман.''',
|
||||
userTask: '''Задача:
|
||||
1. Прочитай знаки как следы боя или охоты — по порядку.
|
||||
2. Укажи, где трусость, самообман или лишняя ноша.
|
||||
3. Дай один приказ-шаг: что взять, что бросить, куда идти.
|
||||
Голос орка. 3–6 предложений.''',
|
||||
1. Знаки — вспышки Вааагх по порядку, не следы обычной охоты.
|
||||
2. Где сила прёт, где череп сейчас разорвёт от сомнений.
|
||||
3. Один приказ: бить / ждать засады / бросить мёртвый груз.
|
||||
Голос орка-шамана. 3–6 предложений.''',
|
||||
chatGreeting:
|
||||
'Слушай. Я орк-оракул. Скажи, что гложет — и не ной, если правда будет жёсткой.',
|
||||
'Вааагх в башке уже орёт. Скажи, что гложет — знаки ударят сами, Горк с Морком разберутся.',
|
||||
);
|
||||
|
||||
static const InterpreterPersona techpriest = InterpreterPersona(
|
||||
@@ -109,10 +110,33 @@ class InterpreterPersona {
|
||||
'Говорите свободно. Знаки я читаю как проекцию: не судьбу снаружи, а узел внутри.',
|
||||
);
|
||||
|
||||
static const InterpreterPersona aeldari = InterpreterPersona(
|
||||
id: 'aeldari',
|
||||
title: 'Всевидящий аэльдари',
|
||||
subtitle: 'Нити судеб, руны провидца, одна тропа из тысячи',
|
||||
icon: Icons.blur_on_rounded,
|
||||
systemPrompt: '''Ты — всевидящий аэльдари, провидец (farseer) мира-корабля. Голос холодный, точный, без жалости к суете. Можно «нити», «тропа», «краткоживущий» — только как интонация.
|
||||
Костюм не источник фактов. Слова жребия — не руны эльдаров и не «позиции рун», пока в законе знака нет руны. Не выдумывай закрытую тропу, круг близких, утробу и события, которых нет в переданных законах.
|
||||
Если у знака канон пуст — скажи это и прочитай само слово. Не заполняй пустоту мифом мира-корабля.
|
||||
|
||||
Правила:
|
||||
- Назови каждый знак и его закон, затем свяжи законы между собой и с вопросом.
|
||||
- Покажи только ту развилку, которая следует из законов.
|
||||
- Дай один конкретный шаг из этих законов. 4–7 предложений. Без оркского рёва и техножреческого жаргона.''',
|
||||
userTask: '''Задача:
|
||||
1. Тем же холодным голосом назови каждый знак и его закон по порядку. Не подменяй слово руной.
|
||||
2. Покажи, как законы знаков спорят или складываются. Не объявляй тропу закрытой, если знаки этого не говорят.
|
||||
3. Один конкретный шаг из законов знаков, не совет «будьте мудры вообще».
|
||||
Голос всевидящего аэльдари. 4–7 предложений.''',
|
||||
chatGreeting:
|
||||
'Нити уже дрожат. Скажи вопрос — прочту руны, а не утешу.',
|
||||
);
|
||||
|
||||
static const List<InterpreterPersona> all = [
|
||||
clear,
|
||||
orc,
|
||||
techpriest,
|
||||
aeldari,
|
||||
analyst,
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
class LenormandReading {
|
||||
final String source;
|
||||
final String label;
|
||||
final String text;
|
||||
final int? left;
|
||||
final int? right;
|
||||
|
||||
const LenormandReading({
|
||||
required this.source,
|
||||
required this.label,
|
||||
required this.text,
|
||||
this.left,
|
||||
this.right,
|
||||
});
|
||||
|
||||
factory LenormandReading.fromMap(Map<String, dynamic> map) {
|
||||
return LenormandReading(
|
||||
source: map['source'] as String? ?? '',
|
||||
label: map['label'] as String? ?? '',
|
||||
text: map['text'] as String? ?? '',
|
||||
left: (map['left'] as num?)?.toInt(),
|
||||
right: (map['right'] as num?)?.toInt(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LenormandPairLink {
|
||||
final int n;
|
||||
final String id;
|
||||
final String name;
|
||||
final String symbol;
|
||||
final String preview;
|
||||
final String text;
|
||||
final List<LenormandReading> readings;
|
||||
|
||||
const LenormandPairLink({
|
||||
required this.n,
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.symbol,
|
||||
required this.preview,
|
||||
this.text = '',
|
||||
required this.readings,
|
||||
});
|
||||
|
||||
String get displayText {
|
||||
if (text.trim().isNotEmpty) return text.trim();
|
||||
if (preview.trim().isNotEmpty) return preview.trim();
|
||||
return readings.map((r) => r.text).where((t) => t.trim().isNotEmpty).join('\n\n');
|
||||
}
|
||||
|
||||
factory LenormandPairLink.fromMap(Map<String, dynamic> map) {
|
||||
final raw = map['readings'];
|
||||
return LenormandPairLink(
|
||||
n: (map['n'] as num?)?.toInt() ?? 0,
|
||||
id: map['id'] as String? ?? '',
|
||||
name: map['name'] as String? ?? '',
|
||||
symbol: map['symbol'] as String? ?? '',
|
||||
preview: map['preview'] as String? ?? '',
|
||||
text: map['text'] as String? ?? '',
|
||||
readings: raw is List
|
||||
? raw
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(LenormandReading.fromMap)
|
||||
.toList()
|
||||
: const [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LenormandCardEntry {
|
||||
final int n;
|
||||
final String id;
|
||||
final String name;
|
||||
final String symbol;
|
||||
final String lore;
|
||||
final String group;
|
||||
final String meaning;
|
||||
final String shadow;
|
||||
final List<LenormandReading> meanings;
|
||||
final List<LenormandPairLink> pairs;
|
||||
|
||||
const LenormandCardEntry({
|
||||
required this.n,
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.symbol,
|
||||
required this.lore,
|
||||
required this.group,
|
||||
this.meaning = '',
|
||||
this.shadow = '',
|
||||
required this.meanings,
|
||||
required this.pairs,
|
||||
});
|
||||
|
||||
String get displayMeaning {
|
||||
if (meaning.trim().isNotEmpty) return meaning.trim();
|
||||
return meanings.map((m) => m.text).where((t) => t.trim().isNotEmpty).join('\n\n');
|
||||
}
|
||||
|
||||
factory LenormandCardEntry.fromMap(Map<String, dynamic> map) {
|
||||
return LenormandCardEntry(
|
||||
n: (map['n'] as num?)?.toInt() ?? 0,
|
||||
id: map['id'] as String? ?? '',
|
||||
name: map['name'] as String? ?? '',
|
||||
symbol: map['symbol'] as String? ?? '',
|
||||
lore: map['lore'] as String? ?? '',
|
||||
group: map['group'] as String? ?? '',
|
||||
meaning: map['meaning'] as String? ?? '',
|
||||
shadow: map['shadow'] as String? ?? '',
|
||||
meanings: (map['meanings'] as List? ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(LenormandReading.fromMap)
|
||||
.toList(),
|
||||
pairs: (map['pairs'] as List? ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(LenormandPairLink.fromMap)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LenormandTriad {
|
||||
final List<int> cards;
|
||||
final List<String> names;
|
||||
final String text;
|
||||
|
||||
const LenormandTriad({
|
||||
required this.cards,
|
||||
required this.names,
|
||||
required this.text,
|
||||
});
|
||||
|
||||
factory LenormandTriad.fromMap(Map<String, dynamic> map) {
|
||||
return LenormandTriad(
|
||||
cards: (map['cards'] as List? ?? const [])
|
||||
.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
names: (map['names'] as List? ?? const []).map((e) => '$e').toList(),
|
||||
text: map['text'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LenormandExtra {
|
||||
final String source;
|
||||
final String title;
|
||||
final String text;
|
||||
|
||||
const LenormandExtra({
|
||||
required this.source,
|
||||
required this.title,
|
||||
required this.text,
|
||||
});
|
||||
|
||||
factory LenormandExtra.fromMap(Map<String, dynamic> map) {
|
||||
return LenormandExtra(
|
||||
source: map['source'] as String? ?? '',
|
||||
title: map['title'] as String? ?? '',
|
||||
text: map['text'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import 'dart:convert';
|
||||
import 'word_model.dart';
|
||||
|
||||
/// Single item in a divination reading (e.g. a drawn Rune, Card, Hexagram, or Sign)
|
||||
class ReadingSignItem {
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final String? symbol;
|
||||
final String? lore;
|
||||
final bool isReversed;
|
||||
final WordItem? linkedWord;
|
||||
final Map<String, dynamic>? meta;
|
||||
|
||||
const ReadingSignItem({
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.symbol,
|
||||
this.lore,
|
||||
this.isReversed = false,
|
||||
this.linkedWord,
|
||||
this.meta,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'title': title,
|
||||
if (subtitle != null) 'subtitle': subtitle,
|
||||
if (symbol != null) 'symbol': symbol,
|
||||
if (lore != null) 'lore': lore,
|
||||
'isReversed': isReversed,
|
||||
if (linkedWord != null) 'linkedWord': linkedWord!.toMap(),
|
||||
if (meta != null) 'meta': meta,
|
||||
};
|
||||
|
||||
factory ReadingSignItem.fromMap(Map<String, dynamic> map) => ReadingSignItem(
|
||||
title: map['title'] as String? ?? '',
|
||||
subtitle: map['subtitle'] as String?,
|
||||
symbol: map['symbol'] as String?,
|
||||
lore: map['lore'] as String?,
|
||||
isReversed: map['isReversed'] as bool? ?? false,
|
||||
linkedWord: map['linkedWord'] != null
|
||||
? WordItem.fromMap(map['linkedWord'] as Map<String, dynamic>)
|
||||
: null,
|
||||
meta: map['meta'] as Map<String, dynamic>?,
|
||||
);
|
||||
}
|
||||
|
||||
/// A single completed divination reading inside a day session
|
||||
class SyncreticReading {
|
||||
final String id;
|
||||
final String systemId; // 'cleromancy', 'matrix', 'runes', 'lenormand', 'tarot', 'iching', 'ogham', 'geomancy'
|
||||
final String systemName;
|
||||
final String systemIcon;
|
||||
final DateTime timestamp;
|
||||
final String layoutTitle; // e.g. 'Бросок 2d12', 'Ленорман d6+d12', 'Триптих Норн'
|
||||
final String inputMethod; // 'dice' | 'manual'
|
||||
final List<ReadingSignItem> items;
|
||||
final String? note;
|
||||
final String? question;
|
||||
|
||||
const SyncreticReading({
|
||||
required this.id,
|
||||
required this.systemId,
|
||||
required this.systemName,
|
||||
required this.systemIcon,
|
||||
required this.timestamp,
|
||||
required this.layoutTitle,
|
||||
this.inputMethod = 'dice',
|
||||
required this.items,
|
||||
this.note,
|
||||
this.question,
|
||||
});
|
||||
|
||||
String get formattedTime {
|
||||
final hour = timestamp.hour.toString().padLeft(2, '0');
|
||||
final min = timestamp.minute.toString().padLeft(2, '0');
|
||||
return '$hour:$min';
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'id': id,
|
||||
'systemId': systemId,
|
||||
'systemName': systemName,
|
||||
'systemIcon': systemIcon,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
'layoutTitle': layoutTitle,
|
||||
'inputMethod': inputMethod,
|
||||
'items': items.map((i) => i.toMap()).toList(),
|
||||
if (note != null) 'note': note,
|
||||
if (question != null) 'question': question,
|
||||
};
|
||||
|
||||
factory SyncreticReading.fromMap(Map<String, dynamic> map) => SyncreticReading(
|
||||
id: map['id'] as String? ?? '',
|
||||
systemId: map['systemId'] as String? ?? '',
|
||||
systemName: map['systemName'] as String? ?? '',
|
||||
systemIcon: map['systemIcon'] as String? ?? '🔮',
|
||||
timestamp: DateTime.tryParse(map['timestamp'] as String? ?? '') ?? DateTime.now(),
|
||||
layoutTitle: map['layoutTitle'] as String? ?? '',
|
||||
inputMethod: map['inputMethod'] as String? ?? 'dice',
|
||||
items: (map['items'] as List<dynamic>?)
|
||||
?.map((i) => ReadingSignItem.fromMap(i as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
note: map['note'] as String?,
|
||||
question: map['question'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// A day-long (or question-long) Syncretic Session accumulating multiple readings across systems
|
||||
class SyncreticSession {
|
||||
final String id;
|
||||
final DateTime date;
|
||||
final String? mainQuestion;
|
||||
final List<SyncreticReading> readings;
|
||||
final String? comparativeAiSynthesis;
|
||||
|
||||
const SyncreticSession({
|
||||
required this.id,
|
||||
required this.date,
|
||||
this.mainQuestion,
|
||||
this.readings = const [],
|
||||
this.comparativeAiSynthesis,
|
||||
});
|
||||
|
||||
String get dateKey =>
|
||||
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
|
||||
String get formattedDate {
|
||||
final day = date.day.toString().padLeft(2, '0');
|
||||
final month = date.month.toString().padLeft(2, '0');
|
||||
final year = date.year.toString();
|
||||
return '$day.$month.$year';
|
||||
}
|
||||
|
||||
SyncreticSession copyWith({
|
||||
String? id,
|
||||
DateTime? date,
|
||||
String? mainQuestion,
|
||||
List<SyncreticReading>? readings,
|
||||
String? comparativeAiSynthesis,
|
||||
}) {
|
||||
return SyncreticSession(
|
||||
id: id ?? this.id,
|
||||
date: date ?? this.date,
|
||||
mainQuestion: mainQuestion ?? this.mainQuestion,
|
||||
readings: readings ?? this.readings,
|
||||
comparativeAiSynthesis: comparativeAiSynthesis ?? this.comparativeAiSynthesis,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'id': id,
|
||||
'date': date.toIso8601String(),
|
||||
if (mainQuestion != null) 'mainQuestion': mainQuestion,
|
||||
'readings': readings.map((r) => r.toMap()).toList(),
|
||||
if (comparativeAiSynthesis != null) 'comparativeAiSynthesis': comparativeAiSynthesis,
|
||||
};
|
||||
|
||||
factory SyncreticSession.fromMap(Map<String, dynamic> map) => SyncreticSession(
|
||||
id: map['id'] as String? ?? '',
|
||||
date: DateTime.tryParse(map['date'] as String? ?? '') ?? DateTime.now(),
|
||||
mainQuestion: map['mainQuestion'] as String?,
|
||||
readings: (map['readings'] as List<dynamic>?)
|
||||
?.map((r) => SyncreticReading.fromMap(r as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
comparativeAiSynthesis: map['comparativeAiSynthesis'] as String?,
|
||||
);
|
||||
|
||||
String toJson() => jsonEncode(toMap());
|
||||
factory SyncreticSession.fromJson(String str) => SyncreticSession.fromMap(jsonDecode(str));
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
class StationWord {
|
||||
final int id;
|
||||
final String word;
|
||||
final String emoji;
|
||||
final String pos;
|
||||
final String category;
|
||||
final String desc;
|
||||
|
||||
const StationWord({
|
||||
required this.id,
|
||||
required this.word,
|
||||
this.emoji = '',
|
||||
this.pos = '',
|
||||
this.category = '',
|
||||
this.desc = '',
|
||||
});
|
||||
|
||||
factory StationWord.fromMap(Map<String, dynamic> map) {
|
||||
return StationWord(
|
||||
id: (map['id'] as num?)?.toInt() ?? 0,
|
||||
word: map['word'] as String? ?? '',
|
||||
emoji: map['emoji'] as String? ?? '',
|
||||
pos: map['pos'] as String? ?? '',
|
||||
category: map['category'] as String? ?? '',
|
||||
desc: map['desc'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TraditionStation {
|
||||
final String id;
|
||||
final String name;
|
||||
final String symbol;
|
||||
final String group;
|
||||
final String lore;
|
||||
final String? hebrew;
|
||||
final String? hanzi;
|
||||
final String? arabic;
|
||||
final String? arcanum;
|
||||
final StationWord? primary;
|
||||
final List<StationWord> cluster;
|
||||
final List<Map<String, String>> parallels;
|
||||
final List<int> dots;
|
||||
|
||||
const TraditionStation({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.symbol,
|
||||
required this.group,
|
||||
required this.lore,
|
||||
this.hebrew,
|
||||
this.hanzi,
|
||||
this.arabic,
|
||||
this.arcanum,
|
||||
this.primary,
|
||||
this.cluster = const [],
|
||||
this.parallels = const [],
|
||||
this.dots = const [],
|
||||
});
|
||||
|
||||
/// Four geomantic rows (1 or 2 dots each), head → feet. Empty if not a figure.
|
||||
List<int> get geomancyRows {
|
||||
if (dots.length == 4 && dots.every((n) => n == 1 || n == 2)) return dots;
|
||||
final lines = symbol
|
||||
.split('\n')
|
||||
.map((s) => s.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
if (lines.length != 4) return const [];
|
||||
final rows = <int>[];
|
||||
for (final line in lines) {
|
||||
final n = '·'.allMatches(line).length;
|
||||
if (n != 1 && n != 2) return const [];
|
||||
rows.add(n);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
String get groupKey {
|
||||
final sep = group.indexOf(' · ');
|
||||
if (sep <= 0) return group;
|
||||
return group.substring(0, sep);
|
||||
}
|
||||
|
||||
String get groupSub {
|
||||
final sep = group.indexOf(' · ');
|
||||
if (sep < 0) return '';
|
||||
return group.substring(sep + 3);
|
||||
}
|
||||
|
||||
factory TraditionStation.fromMap(Map<String, dynamic> map) {
|
||||
final primaryMap = map['primary'];
|
||||
final clusterRaw = map['cluster'];
|
||||
return TraditionStation(
|
||||
id: map['id'] as String? ?? '',
|
||||
name: map['name'] as String? ?? '',
|
||||
symbol: map['symbol'] as String? ?? '',
|
||||
group: map['group'] as String? ?? '',
|
||||
lore: map['lore'] as String? ?? '',
|
||||
hebrew: map['hebrew'] as String?,
|
||||
hanzi: map['hanzi'] as String?,
|
||||
arabic: map['arabic'] as String?,
|
||||
arcanum: map['arcanum'] as String?,
|
||||
primary: primaryMap is Map<String, dynamic>
|
||||
? StationWord.fromMap(primaryMap)
|
||||
: null,
|
||||
cluster: clusterRaw is List
|
||||
? clusterRaw
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(StationWord.fromMap)
|
||||
.toList()
|
||||
: const [],
|
||||
dots: (map['dots'] as List? ?? const [])
|
||||
.whereType<num>()
|
||||
.map((n) => n.toInt())
|
||||
.toList(),
|
||||
parallels: (map['parallels'] as List? ?? const [])
|
||||
.whereType<Map>()
|
||||
.map((p) => {
|
||||
'system': '${p['system'] ?? ''}',
|
||||
'system_name': '${p['system_name'] ?? ''}',
|
||||
'id': '${p['id'] ?? ''}',
|
||||
'name': '${p['name'] ?? ''}',
|
||||
'symbol': '${p['symbol'] ?? ''}',
|
||||
})
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
bool matchesQuery(String query) {
|
||||
if (query.isEmpty) return true;
|
||||
final q = query.toLowerCase();
|
||||
if (name.toLowerCase().contains(q)) return true;
|
||||
if (lore.toLowerCase().contains(q)) return true;
|
||||
if (symbol.contains(query)) return true;
|
||||
if (hebrew != null && hebrew!.contains(query)) return true;
|
||||
if (hanzi != null && hanzi!.contains(query)) return true;
|
||||
if (arabic != null && arabic!.contains(query)) return true;
|
||||
if (primary != null && primary!.word.toLowerCase().contains(q)) return true;
|
||||
return cluster.any((c) => c.word.toLowerCase().contains(q));
|
||||
}
|
||||
}
|
||||
|
||||
class TraditionCanon {
|
||||
final String id;
|
||||
final String name;
|
||||
final String icon;
|
||||
final int count;
|
||||
final String description;
|
||||
final List<TraditionStation> stations;
|
||||
|
||||
const TraditionCanon({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.icon,
|
||||
required this.count,
|
||||
required this.description,
|
||||
required this.stations,
|
||||
});
|
||||
|
||||
factory TraditionCanon.fromMap(Map<String, dynamic> map) {
|
||||
final raw = map['stations'];
|
||||
return TraditionCanon(
|
||||
id: map['id'] as String? ?? '',
|
||||
name: map['name'] as String? ?? '',
|
||||
icon: map['icon'] as String? ?? '',
|
||||
count: (map['count'] as num?)?.toInt() ?? 0,
|
||||
description: map['description'] as String? ?? '',
|
||||
stations: raw is List
|
||||
? raw
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(TraditionStation.fromMap)
|
||||
.toList()
|
||||
: const [],
|
||||
);
|
||||
}
|
||||
}
|
||||
+556
-3
@@ -1,3 +1,83 @@
|
||||
import 'culture_branch.dart';
|
||||
|
||||
class SymbolLayer {
|
||||
final String system;
|
||||
final String title;
|
||||
final String law;
|
||||
|
||||
const SymbolLayer({
|
||||
required this.system,
|
||||
required this.title,
|
||||
required this.law,
|
||||
});
|
||||
|
||||
factory SymbolLayer.fromMap(Map<String, dynamic> map) {
|
||||
return SymbolLayer(
|
||||
system: map['system'] as String? ?? '',
|
||||
title: map['title'] as String? ?? '',
|
||||
law: map['law'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'system': system,
|
||||
'title': title,
|
||||
'law': law,
|
||||
};
|
||||
}
|
||||
|
||||
class SymbolSource {
|
||||
final String system;
|
||||
final String ref;
|
||||
final double weight;
|
||||
|
||||
const SymbolSource({
|
||||
required this.system,
|
||||
required this.ref,
|
||||
required this.weight,
|
||||
});
|
||||
|
||||
factory SymbolSource.fromMap(Map<String, dynamic> map) {
|
||||
return SymbolSource(
|
||||
system: map['system'] as String? ?? '',
|
||||
ref: map['ref'] as String? ?? '',
|
||||
weight: (map['weight'] as num?)?.toDouble() ?? 1.0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'system': system,
|
||||
'ref': ref,
|
||||
'weight': weight,
|
||||
};
|
||||
}
|
||||
|
||||
class SymbolGrammar {
|
||||
final String operator;
|
||||
final String valence;
|
||||
final String modifies;
|
||||
|
||||
const SymbolGrammar({
|
||||
this.operator = 'substance',
|
||||
this.valence = 'liminal',
|
||||
this.modifies = '',
|
||||
});
|
||||
|
||||
factory SymbolGrammar.fromMap(Map<String, dynamic> map) {
|
||||
return SymbolGrammar(
|
||||
operator: map['operator'] as String? ?? 'substance',
|
||||
valence: map['valence'] as String? ?? 'liminal',
|
||||
modifies: map['modifies'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'operator': operator,
|
||||
'valence': valence,
|
||||
'modifies': modifies,
|
||||
};
|
||||
}
|
||||
|
||||
class WordItem {
|
||||
final int id; // Base index 0..999
|
||||
final int sessionNumber; // Dynamic randomized session number 0..999
|
||||
@@ -6,6 +86,27 @@ class WordItem {
|
||||
final double frequency;
|
||||
final String category;
|
||||
final String description;
|
||||
final String emoji;
|
||||
|
||||
// Enriched Oracle Knowledge Base fields
|
||||
final String family;
|
||||
final String operator;
|
||||
final String valence;
|
||||
final List<String> jurisdictions;
|
||||
final String lookup; // 'A', 'B', 'C'
|
||||
final bool hasCanon;
|
||||
final String core;
|
||||
final String oracle;
|
||||
final List<SymbolLayer> layers;
|
||||
final String shadow;
|
||||
final String asks;
|
||||
final String step;
|
||||
final List<String> binds;
|
||||
final List<SymbolSource> sources;
|
||||
final SymbolGrammar? grammar;
|
||||
final bool isUserEdited;
|
||||
final Map<String, String> cultures;
|
||||
final Map<String, String> languages;
|
||||
|
||||
const WordItem({
|
||||
required this.id,
|
||||
@@ -15,6 +116,25 @@ class WordItem {
|
||||
required this.frequency,
|
||||
this.category = '',
|
||||
this.description = '',
|
||||
this.emoji = '',
|
||||
this.family = '',
|
||||
this.operator = 'substance',
|
||||
this.valence = 'liminal',
|
||||
this.jurisdictions = const [],
|
||||
this.lookup = 'C',
|
||||
this.hasCanon = false,
|
||||
this.core = '',
|
||||
this.oracle = '',
|
||||
this.layers = const [],
|
||||
this.shadow = '',
|
||||
this.asks = '',
|
||||
this.step = '',
|
||||
this.binds = const [],
|
||||
this.sources = const [],
|
||||
this.grammar,
|
||||
this.isUserEdited = false,
|
||||
this.cultures = const {},
|
||||
this.languages = const {},
|
||||
});
|
||||
|
||||
WordItem copyWith({
|
||||
@@ -25,6 +145,25 @@ class WordItem {
|
||||
double? frequency,
|
||||
String? category,
|
||||
String? description,
|
||||
String? emoji,
|
||||
String? family,
|
||||
String? operator,
|
||||
String? valence,
|
||||
List<String>? jurisdictions,
|
||||
String? lookup,
|
||||
bool? hasCanon,
|
||||
String? core,
|
||||
String? oracle,
|
||||
List<SymbolLayer>? layers,
|
||||
String? shadow,
|
||||
String? asks,
|
||||
String? step,
|
||||
List<String>? binds,
|
||||
List<SymbolSource>? sources,
|
||||
SymbolGrammar? grammar,
|
||||
bool? isUserEdited,
|
||||
Map<String, String>? cultures,
|
||||
Map<String, String>? languages,
|
||||
}) {
|
||||
return WordItem(
|
||||
id: id ?? this.id,
|
||||
@@ -34,9 +173,110 @@ class WordItem {
|
||||
frequency: frequency ?? this.frequency,
|
||||
category: category ?? this.category,
|
||||
description: description ?? this.description,
|
||||
emoji: emoji ?? this.emoji,
|
||||
family: family ?? this.family,
|
||||
operator: operator ?? this.operator,
|
||||
valence: valence ?? this.valence,
|
||||
jurisdictions: jurisdictions ?? this.jurisdictions,
|
||||
lookup: lookup ?? this.lookup,
|
||||
hasCanon: hasCanon ?? this.hasCanon,
|
||||
core: core ?? this.core,
|
||||
oracle: oracle ?? this.oracle,
|
||||
layers: layers ?? this.layers,
|
||||
shadow: shadow ?? this.shadow,
|
||||
asks: asks ?? this.asks,
|
||||
step: step ?? this.step,
|
||||
binds: binds ?? this.binds,
|
||||
sources: sources ?? this.sources,
|
||||
grammar: grammar ?? this.grammar,
|
||||
isUserEdited: isUserEdited ?? this.isUserEdited,
|
||||
cultures: cultures ?? this.cultures,
|
||||
languages: languages ?? this.languages,
|
||||
);
|
||||
}
|
||||
|
||||
/// Enrich existing WordItem with data from oracle_kb.json entry
|
||||
WordItem enrichWithKb(Map<String, dynamic> kb) {
|
||||
final rawLayers = kb['layers'] as List<dynamic>? ?? [];
|
||||
final parsedLayers = rawLayers
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map((m) => SymbolLayer.fromMap(m))
|
||||
.toList();
|
||||
|
||||
final rawSourcesList = kb['sources'] is List ? (kb['sources'] as List) : [];
|
||||
final parsedSources = rawSourcesList
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map((m) => SymbolSource.fromMap(m))
|
||||
.toList();
|
||||
|
||||
final rawJurisdictions = kb['jurisdictions'] is List
|
||||
? (kb['jurisdictions'] as List).map((e) => e.toString()).toList()
|
||||
: <String>[];
|
||||
|
||||
final rawBinds = kb['binds'] is List
|
||||
? (kb['binds'] as List).map((e) => e.toString()).toList()
|
||||
: (kb['binds'] is String ? [kb['binds'].toString()] : <String>[]);
|
||||
|
||||
final rawGrammar = kb['grammar'] as Map<String, dynamic>?;
|
||||
|
||||
final rawKbCultures = kb['cultures'] as Map<String, dynamic>?;
|
||||
final parsedKbCultures = rawKbCultures != null
|
||||
? rawKbCultures.map((k, v) => MapEntry(k.toString(), v.toString()))
|
||||
: <String, String>{};
|
||||
|
||||
final rawKbLanguages = kb['languages'] as Map<String, dynamic>?;
|
||||
final parsedKbLanguages = rawKbLanguages != null
|
||||
? rawKbLanguages.map((k, v) => MapEntry(k.toString(), v.toString()))
|
||||
: <String, String>{};
|
||||
|
||||
final kbCore = (kb['core'] as String? ?? '').trim();
|
||||
final kbOracle = (kb['oracle'] as String? ?? '').trim();
|
||||
final kbHasCanon = (kb['has_canon'] as bool? ?? false) || (kbCore.isNotEmpty && kbOracle.isNotEmpty);
|
||||
|
||||
return copyWith(
|
||||
partOfSpeech: partOfSpeech.isNotEmpty ? partOfSpeech : ((kb['pos'] as String?) ?? partOfSpeech),
|
||||
category: (kb['category'] as String?)?.isNotEmpty == true ? kb['category'] as String : category,
|
||||
family: kb['family'] as String? ?? family,
|
||||
operator: kb['operator'] as String? ?? operator,
|
||||
valence: kb['valence'] as String? ?? valence,
|
||||
jurisdictions: rawJurisdictions.isNotEmpty ? rawJurisdictions : jurisdictions,
|
||||
lookup: kb['lookup'] as String? ?? lookup,
|
||||
hasCanon: kbHasCanon,
|
||||
core: kbCore,
|
||||
oracle: kbOracle,
|
||||
layers: parsedLayers,
|
||||
shadow: kb['shadow'] as String? ?? shadow,
|
||||
asks: kb['asks'] as String? ?? asks,
|
||||
step: kb['step'] as String? ?? step,
|
||||
binds: rawBinds,
|
||||
sources: parsedSources,
|
||||
emoji: (kb['emoji'] as String?)?.isNotEmpty == true ? kb['emoji'] as String : emoji,
|
||||
grammar: rawGrammar != null ? SymbolGrammar.fromMap(rawGrammar) : grammar,
|
||||
cultures: parsedKbCultures.isNotEmpty ? parsedKbCultures : cultures,
|
||||
languages: parsedKbLanguages.isNotEmpty ? parsedKbLanguages : languages,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns description stripped of raw metadata bracket tags like [Таро: ...]
|
||||
String get cleanDescription {
|
||||
final clean = description.replaceAll(RegExp(r'\[[^\]]+\]\s*'), '').trim();
|
||||
return clean.isNotEmpty ? clean : description;
|
||||
}
|
||||
|
||||
/// Культурные ветви знака: явная редакция (cultures) приоритетна,
|
||||
/// иначе — автоматическое разложение толкования по традициям.
|
||||
Map<String, String> get resolvedCultures {
|
||||
if (cultures.isNotEmpty) return cultures;
|
||||
return extractCulturalBranches('$oracle\n\n$shadow\n\n$cleanDescription');
|
||||
}
|
||||
|
||||
/// Primary poetic/canonical essence: core if available, otherwise clean description
|
||||
String get displayMeaning {
|
||||
if (core.isNotEmpty) return core;
|
||||
if (cleanDescription.isNotEmpty) return cleanDescription;
|
||||
return description;
|
||||
}
|
||||
|
||||
String get formattedSessionNumber => sessionNumber.toString().padLeft(3, '0');
|
||||
String get formattedId => formattedSessionNumber;
|
||||
|
||||
@@ -49,21 +289,334 @@ class WordItem {
|
||||
'frequency': frequency,
|
||||
'category': category,
|
||||
'description': description,
|
||||
'emoji': emoji,
|
||||
'family': family,
|
||||
'operator': operator,
|
||||
'valence': valence,
|
||||
'jurisdictions': jurisdictions,
|
||||
'lookup': lookup,
|
||||
'hasCanon': hasCanon,
|
||||
'core': core,
|
||||
'oracle': oracle,
|
||||
'layers': layers.map((l) => l.toMap()).toList(),
|
||||
'shadow': shadow,
|
||||
'asks': asks,
|
||||
'step': step,
|
||||
'binds': binds,
|
||||
'sources': sources.map((s) => s.toMap()).toList(),
|
||||
'grammar': grammar?.toMap(),
|
||||
'cultures': cultures,
|
||||
'languages': languages,
|
||||
};
|
||||
}
|
||||
|
||||
factory WordItem.fromMap(Map<String, dynamic> map) {
|
||||
final rawLayers = map['layers'] as List<dynamic>? ?? [];
|
||||
final parsedLayers = rawLayers
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map((m) => SymbolLayer.fromMap(m))
|
||||
.toList();
|
||||
|
||||
final rawSources = map['sources'] as List<dynamic>? ?? [];
|
||||
final parsedSources = rawSources
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map((m) => SymbolSource.fromMap(m))
|
||||
.toList();
|
||||
|
||||
final rawJurisdictions = (map['jurisdictions'] as List<dynamic>? ?? [])
|
||||
.map((e) => e.toString())
|
||||
.toList();
|
||||
|
||||
final rawBinds = (map['binds'] as List<dynamic>? ?? [])
|
||||
.map((e) => e.toString())
|
||||
.toList();
|
||||
|
||||
final rawGrammar = map['grammar'] as Map<String, dynamic>?;
|
||||
|
||||
final rawCultures = map['cultures'] as Map<String, dynamic>?;
|
||||
final parsedCultures = rawCultures != null
|
||||
? rawCultures.map((k, v) => MapEntry(k.toString(), v.toString()))
|
||||
: <String, String>{};
|
||||
|
||||
final rawLanguages = map['languages'] as Map<String, dynamic>?;
|
||||
final parsedLanguages = rawLanguages != null
|
||||
? rawLanguages.map((k, v) => MapEntry(k.toString(), v.toString()))
|
||||
: <String, String>{};
|
||||
|
||||
return WordItem(
|
||||
id: map['id'] as int,
|
||||
sessionNumber: map['sessionNumber'] as int? ?? (map['id'] as int),
|
||||
word: map['word'] as String,
|
||||
partOfSpeech: map['partOfSpeech'] as String,
|
||||
frequency: (map['frequency'] as num).toDouble(),
|
||||
partOfSpeech: map['partOfSpeech'] as String? ?? '',
|
||||
frequency: (map['frequency'] as num?)?.toDouble() ?? 50.0,
|
||||
category: map['category'] as String? ?? '',
|
||||
description: map['description'] as String? ?? '',
|
||||
emoji: map['emoji'] as String? ?? '',
|
||||
family: map['family'] as String? ?? '',
|
||||
operator: map['operator'] as String? ?? 'substance',
|
||||
valence: map['valence'] as String? ?? 'liminal',
|
||||
jurisdictions: rawJurisdictions,
|
||||
lookup: map['lookup'] as String? ?? 'C',
|
||||
hasCanon: map['hasCanon'] as bool? ?? false,
|
||||
core: map['core'] as String? ?? '',
|
||||
oracle: map['oracle'] as String? ?? '',
|
||||
layers: parsedLayers,
|
||||
shadow: map['shadow'] as String? ?? '',
|
||||
asks: map['asks'] as String? ?? '',
|
||||
step: map['step'] as String? ?? '',
|
||||
binds: rawBinds,
|
||||
sources: parsedSources,
|
||||
grammar: rawGrammar != null ? SymbolGrammar.fromMap(rawGrammar) : null,
|
||||
isUserEdited: map['isUserEdited'] as bool? ?? false,
|
||||
cultures: parsedCultures,
|
||||
languages: parsedLanguages,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => '#$formattedSessionNumber: $word ($partOfSpeech)';
|
||||
String toString() => '#: ()';
|
||||
}
|
||||
|
||||
extension WordItemLocalization on WordItem {
|
||||
String get localizedValence => localizeValence(valence);
|
||||
String get localizedOperator => localizeOperator(operator);
|
||||
String get localizedFamily => localizeFamily(family);
|
||||
|
||||
static String localizeValence(String v) {
|
||||
switch (v.trim().toLowerCase()) {
|
||||
case 'active':
|
||||
return 'Активная';
|
||||
case 'receptive':
|
||||
return 'Восприимчивая';
|
||||
case 'liminal':
|
||||
return 'Лиминальная';
|
||||
case 'stable':
|
||||
return 'Устойчивая';
|
||||
case 'cutting':
|
||||
return 'Отсекающая';
|
||||
case 'binding':
|
||||
return 'Связующая';
|
||||
case 'solar':
|
||||
return 'Солярная';
|
||||
case 'lunar':
|
||||
return 'Лунарная';
|
||||
case 'light':
|
||||
return 'Светлая';
|
||||
case 'dark':
|
||||
return 'Теневая';
|
||||
case 'neutral':
|
||||
return 'Нейтральная';
|
||||
case 'chthonic':
|
||||
return 'Хтоническая';
|
||||
default:
|
||||
final s = v.trim();
|
||||
if (s.isEmpty) return '';
|
||||
return s[0].toUpperCase() + s.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
static String localizeOperator(String op) {
|
||||
switch (op.trim().toLowerCase()) {
|
||||
case 'substance':
|
||||
return 'Субстанция';
|
||||
case 'act':
|
||||
return 'Действие';
|
||||
case 'relation':
|
||||
return 'Связь';
|
||||
case 'shifter':
|
||||
return 'Сдвиг';
|
||||
case 'qualifier':
|
||||
return 'Качество';
|
||||
case 'filter':
|
||||
return 'Фильтр';
|
||||
case 'pronoun':
|
||||
return 'Указатель';
|
||||
case 'number':
|
||||
return 'Число';
|
||||
case 'nexus':
|
||||
return 'Узел';
|
||||
case 'vector':
|
||||
return 'Вектор';
|
||||
case 'catalyst':
|
||||
return 'Катализатор';
|
||||
default:
|
||||
final s = op.trim();
|
||||
if (s.isEmpty) return '';
|
||||
return s[0].toUpperCase() + s.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
static String localizeFamily(String f) {
|
||||
switch (f.trim().toLowerCase()) {
|
||||
case 'totem':
|
||||
return 'Тотем';
|
||||
case 'sacred_symbol':
|
||||
return 'Сакральный символ';
|
||||
case 'sacred_place':
|
||||
return 'Сакральное место';
|
||||
case 'element':
|
||||
return 'Стихия';
|
||||
case 'cosmos':
|
||||
return 'Космос';
|
||||
case 'body':
|
||||
return 'Тело';
|
||||
case 'moral':
|
||||
return 'Этика';
|
||||
case 'quality':
|
||||
return 'Качество';
|
||||
case 'time_calendar':
|
||||
return 'Календарь';
|
||||
case 'time_cycle':
|
||||
return 'Цикл времени';
|
||||
case 'cult_object':
|
||||
return 'Культ';
|
||||
case 'person_archetype':
|
||||
return 'Архетип';
|
||||
case 'social':
|
||||
return 'Социум';
|
||||
case 'speech':
|
||||
return 'Речь';
|
||||
case 'circumstance':
|
||||
return 'Обстоятельство';
|
||||
case 'conjunction':
|
||||
return 'Связка';
|
||||
case 'preposition':
|
||||
return 'Отношение';
|
||||
case 'polarity':
|
||||
return 'Полярность';
|
||||
case 'verb_transform':
|
||||
return 'Трансформация';
|
||||
case 'particle':
|
||||
return 'Частица';
|
||||
case 'determiner':
|
||||
return 'Определитель';
|
||||
case 'adverb_index':
|
||||
return 'Индекс';
|
||||
case 'misc':
|
||||
return 'Разное';
|
||||
default:
|
||||
final s = f.trim();
|
||||
if (s.isEmpty) return '';
|
||||
return s[0].toUpperCase() + s.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
static String localizeSystem(String sys) {
|
||||
switch (sys.trim().toLowerCase()) {
|
||||
case 'tarot':
|
||||
return 'Таро';
|
||||
case 'runes':
|
||||
return 'Руны';
|
||||
case 'lenormand':
|
||||
return 'Ленорман';
|
||||
case 'iching':
|
||||
case 'i ching':
|
||||
return 'И Цзин';
|
||||
case 'astrology':
|
||||
return 'Астрология';
|
||||
case 'alchemy':
|
||||
return 'Алхимия';
|
||||
case 'kabbalah':
|
||||
return 'Каббала';
|
||||
case 'ogham':
|
||||
return 'Огам';
|
||||
case 'cosmology':
|
||||
return 'Космология';
|
||||
case 'slavic':
|
||||
return 'Славянский канон';
|
||||
case 'bestiary':
|
||||
return 'Бестиарий';
|
||||
case 'dream':
|
||||
return 'Сновидения';
|
||||
case 'geomancy':
|
||||
return 'Геомантия';
|
||||
default:
|
||||
final s = sys.trim();
|
||||
if (s.isEmpty) return '';
|
||||
return s[0].toUpperCase() + s.substring(1);
|
||||
}
|
||||
}
|
||||
static String cleanTagDetail(String det) {
|
||||
var s = det.trim();
|
||||
// Remove Latin transliterations like (Cancer / Вода / Луна) -> (Вода · Луна)
|
||||
s = s.replaceAllMapped(RegExp(r'\([A-Za-z]+\s*[/·]\s*([^)]+)\)'), (m) => '(${m.group(1)})');
|
||||
s = s.replaceAll(' / ', ' · ');
|
||||
// Geomancy 12 Puer (Юноша) -> 12 Юноша
|
||||
s = s.replaceAllMapped(RegExp(r'(\d+\s+)[A-Za-z\s_]+\(([А-Яа-яЁё\s·-]+)\)'), (m) => '${m.group(1)}${m.group(2)}');
|
||||
// Alchemy Огонь (Ignis) -> Огонь
|
||||
if (!RegExp(r'[ᚠ-ᛪ]').hasMatch(s)) {
|
||||
s = s.replaceAllMapped(RegExp(r'([А-Яа-яЁё\s]+)\s*\([A-Za-z\s_]+\)'), (m) => m.group(1)!);
|
||||
}
|
||||
// Runes: (Kenaz / ᚲ) -> (ᚲ)
|
||||
s = s.replaceAllMapped(RegExp(r'\([A-Za-z]+\s*[/·]\s*([ᚠ-ᛪ])\)'), (m) => '(${m.group(1)})');
|
||||
// Clean leftover English in parens
|
||||
s = s.replaceAll(RegExp(r'\([A-Za-z\s_]+\)'), '');
|
||||
return s.trim();
|
||||
}
|
||||
|
||||
static String localizeSourceRef(String system, String ref) {
|
||||
final r = ref.trim().toLowerCase();
|
||||
const map = <String, String>{
|
||||
// Runes
|
||||
'fehu': 'Феху (ᚠ)', 'uruz': 'Уруз (ᚢ)', 'thurisaz': 'Турисаз (ᚦ)', 'ansuz': 'Ансуз (ᚨ)',
|
||||
'raidho': 'Райдо (ᚱ)', 'kenaz': 'Кеназ (ᚲ)', 'gebo': 'Гебо (ᚷ)', 'wunjo': 'Вуньо (ᚹ)',
|
||||
'hagalaz': 'Хагалаз (ᚺ)', 'nauthiz': 'Наутиз (ᚾ)', 'isa': 'Иса (ᛁ)', 'jera': 'Йера (ᛃ)',
|
||||
'eihwaz': 'Эйваз (ᛇ)', 'perthro': 'Перт (ᛈ)', 'algiz': 'Альгиз (ᛉ)', 'sowilo': 'Совило (ᛊ)',
|
||||
'tiwaz': 'Тейваз (ᛏ)', 'berkano': 'Беркана (ᛒ)', 'ehwaz': 'Эваз (ᛖ)', 'mannaz': 'Манназ (ᛗ)',
|
||||
'laguz': 'Лагуз (ᛚ)', 'ingwaz': 'Ингваз (ᛜ)', 'dagaz': 'Дагаз (ᛞ)', 'othala': 'Отала (ᛟ)',
|
||||
// Tarot
|
||||
'wands': 'Жезлы (Огонь)', 'pentacles': 'Пентакли (Земля)', 'cups': 'Кубки (Вода)', 'swords': 'Мечи (Воздух)',
|
||||
'fool': 'Шут (0)', 'magician': 'Маг (I)', 'priestess': 'Жрица (II)', 'empress': 'Императрица (III)',
|
||||
'emperor': 'Император (IV)', 'hierophant': 'Иерофант (V)', 'lovers': 'Влюблённые (VI)', 'chariot': 'Колесница (VII)',
|
||||
'strength': 'Сила (VIII)', 'hermit': 'Отшельник (IX)', 'wheel': 'Колесо Фортуны (X)', 'justice': 'Справедливость (XI)',
|
||||
'hanged': 'Повешенный (XII)', 'death': 'Смерть (XIII)', 'temperance': 'Умеренность (XIV)', 'devil': 'Дьявол (XV)',
|
||||
'tower': 'Башня (XVI)', 'star': 'Звезда (XVII)', 'moon': 'Луна', 'sun': 'Солнце',
|
||||
'judgement': 'Суд (XX)', 'world': 'Мир (XXI)',
|
||||
// I Ching
|
||||
'li': 'Ли ☲ (Огонь)', 'kan': 'Кань ☵ (Вода)', 'xun': 'Сюнь ☴ (Ветер)', 'kun': 'Кунь ☷ (Земля)',
|
||||
'qian': 'Цянь ☰ (Небо)', 'zhen': 'Чжэнь ☳ (Гром)', 'gen': 'Гэнь ☶ (Гора)', 'dui': 'Дуй ☱ (Водоём)',
|
||||
'kan_repeat': 'Кань ䷜ (Повторная опасность)', 'pi': 'Пи ䷋ (Упадок)', 'tai': 'Тай ䷊ (Расцвет)',
|
||||
'gu': 'Гу ䷑ (Исправление)', 'fu': 'Фу ䷗ (Возврат)', 'meng': 'Мэн ䷃ (Юность)', 'ge': 'Гэ ䷰ (Смена)',
|
||||
'wei_ji': 'Вэй-цзи ䷿ (Ещё не конец)',
|
||||
// Alchemy
|
||||
'ignis': 'Огонь', 'aqua': 'Вода', 'aer': 'Воздух', 'terra': 'Земля',
|
||||
'sulfur': 'Сера (Душа)', 'mercury': 'Ртуть (Дух)', 'sal': 'Соль (Тело)',
|
||||
'albedo': 'Альбедо (Белизна)', 'nigredo': 'Нигредо (Чернота)', 'rubedo': 'Рубедо (Краснота)',
|
||||
'ash': 'Пепел', 'luna': 'Луна (Серебро)', 'egg': 'Философское Яйцо', 'vessel': 'Сосуд Гермеса', 'forge': 'Горн',
|
||||
// Astrology
|
||||
'comet': 'Комета', 'eclipse': 'Затмение', 'dawn': 'Рассвет', 'dusk': 'Закат',
|
||||
'zenith': 'Зенит', 'nadir': 'Надир', 'constellation': 'Созвездие', 'planet': 'Планета', 'meteor': 'Метеор',
|
||||
'equinox': 'Равноденствие', 'orbit': 'Орбита', 'libra': 'Весы', 'aries': 'Овен', 'taurus': 'Телец',
|
||||
'gemini': 'Близнецы', 'cancer': 'Рак', 'leo': 'Лев', 'virgo': 'Дева', 'scorpio': 'Скорпион',
|
||||
'sagittarius': 'Стрелец', 'capricorn': 'Козерог', 'aquarius': 'Водолей', 'pisces': 'Рыбы',
|
||||
// Bestiary
|
||||
'dog': 'Собака (Страж)', 'snake': 'Змея (Мудрость)', 'eagle': 'Орёл (Высота)', 'wolf': 'Волк (Чутьё)',
|
||||
'raven': 'Ворон (Память)', 'lion': 'Лев (Царственность)', 'owl': 'Сова (Ночное зрение)',
|
||||
'serpent_feather': 'Пернатый Змей', 'dragon': 'Дракон', 'phoenix': 'Феникс', 'bear': 'Медведь (Сила)',
|
||||
'fox': 'Лиса (Хитрость)', 'stag': 'Олень (Благородство)', 'horse': 'Конь (Порыв)', 'spider': 'Паук (Нить судьбы)',
|
||||
// Virtues & Archetypes
|
||||
'courage': 'Мужество', 'charity': 'Милосердие', 'karma': 'Карма / Воздаяние', 'guilt': 'Вина',
|
||||
'shame': 'Стыд', 'pride': 'Гордыня', 'humility': 'Смирение', 'envy': 'Зависть', 'vow': 'Обет',
|
||||
'greed': 'Алчность', 'gluttony': 'Чревоугодие', 'sloth': 'Уныние', 'wrath': 'Гнев', 'lust': 'Страсть',
|
||||
'donor': 'Даритель', 'dispatcher': 'Отправитель', 'shadow': 'Тень', 'hero': 'Герой', 'self': 'Самость',
|
||||
'helper': 'Помощник', 'witch': 'Ведунья', 'princess': 'Царевна', 'exile': 'Изгнанник',
|
||||
'false_hero': 'Ложный герой', 'judge': 'Судия', 'trickster': 'Трикстер',
|
||||
// Slavic
|
||||
'mesyats': 'Месяц ясный', 'oak': 'Священный Дуб', 'willow': 'Плакучая Ива', 'birch': 'Берёза',
|
||||
'ivy_field': 'Чисто поле', 'crossroads': 'Перепутье', 'threshold': 'Порог', 'well': 'Студёный колодец', 'hearth': 'Родной очаг',
|
||||
// Ogham
|
||||
'dair': 'Дуб (Даир / ᚇ)', 'sail': 'Ива (Салье / ᚄ)', 'beith': 'Берёза (Бэйт / ᚁ)', 'gort': 'Плющ (Горт / ᚌ)',
|
||||
// Symbols & Dreams
|
||||
'fire': 'Огонь', 'water': 'Вода', 'air': 'Воздух', 'earth': 'Земля',
|
||||
'key': 'Ключ от врат', 'spark': 'Живая искра', 'path': 'Тропа / Путь', 'book': 'Книга судеб',
|
||||
'blood': 'Кровь / Род', 'heart': 'Сердце',
|
||||
'fire_d': 'Сон об огне', 'water_d': 'Сон о глубине', 'death_d': 'Сон о переходе', 'key_d': 'Сон о тайне',
|
||||
'path_d': 'Сон о странствии', 'child_d': 'Сон о дитяти', 'mirror_d': 'Сон о зеркале', 'wolf_d': 'Сон о звере',
|
||||
'dream_fire': 'Сон об огне',
|
||||
};
|
||||
if (map.containsKey(r)) return map[r]!;
|
||||
final s = ref.trim().replaceAll('_', ' ');
|
||||
if (s.isEmpty) return '';
|
||||
return s[0].toUpperCase() + s.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import 'dart:convert';
|
||||
|
||||
/// Represents the daily record for a specific calendar date in the Word of the Day practice.
|
||||
/// Supports both Single Word Mode (1 resonant archetype) and Triptych Mode (Morning, Day, Evening).
|
||||
class WordOfDayRecord {
|
||||
final String dateKey; // 'yyyy-MM-dd'
|
||||
final int wordId; // base id for Single Word Mode
|
||||
final bool isRevealed; // whether Single Word is revealed
|
||||
final bool isStepCompleted; // whether Single Word step is checked off
|
||||
final String diaryNote; // user reflection on daily synchronicity
|
||||
final DateTime? revealedAt;
|
||||
final DateTime? completedAt;
|
||||
|
||||
// Triptych fields (3 words: Morning, Day, Evening)
|
||||
final bool isTriptychMode;
|
||||
final int morningWordId;
|
||||
final int dayWordId;
|
||||
final int eveningWordId;
|
||||
final bool isMorningRevealed;
|
||||
final bool isDayRevealed;
|
||||
final bool isEveningRevealed;
|
||||
final bool isDayStepCompleted;
|
||||
final DateTime? morningRevealedAt;
|
||||
final DateTime? dayRevealedAt;
|
||||
final DateTime? eveningRevealedAt;
|
||||
|
||||
const WordOfDayRecord({
|
||||
required this.dateKey,
|
||||
required this.wordId,
|
||||
this.isRevealed = false,
|
||||
this.isStepCompleted = false,
|
||||
this.diaryNote = '',
|
||||
this.revealedAt,
|
||||
this.completedAt,
|
||||
this.isTriptychMode = false,
|
||||
this.morningWordId = 0,
|
||||
this.dayWordId = 0,
|
||||
this.eveningWordId = 0,
|
||||
this.isMorningRevealed = false,
|
||||
this.isDayRevealed = false,
|
||||
this.isEveningRevealed = false,
|
||||
this.isDayStepCompleted = false,
|
||||
this.morningRevealedAt,
|
||||
this.dayRevealedAt,
|
||||
this.eveningRevealedAt,
|
||||
});
|
||||
|
||||
bool get isAnyRevealed =>
|
||||
isRevealed || isMorningRevealed || isDayRevealed || isEveningRevealed;
|
||||
|
||||
bool get areAllTriptychRevealed =>
|
||||
isMorningRevealed && isDayRevealed && isEveningRevealed;
|
||||
|
||||
bool get isAnyStepCompleted => isStepCompleted || isDayStepCompleted;
|
||||
|
||||
WordOfDayRecord copyWith({
|
||||
String? dateKey,
|
||||
int? wordId,
|
||||
bool? isRevealed,
|
||||
bool? isStepCompleted,
|
||||
String? diaryNote,
|
||||
DateTime? revealedAt,
|
||||
DateTime? completedAt,
|
||||
bool? isTriptychMode,
|
||||
int? morningWordId,
|
||||
int? dayWordId,
|
||||
int? eveningWordId,
|
||||
bool? isMorningRevealed,
|
||||
bool? isDayRevealed,
|
||||
bool? isEveningRevealed,
|
||||
bool? isDayStepCompleted,
|
||||
DateTime? morningRevealedAt,
|
||||
DateTime? dayRevealedAt,
|
||||
DateTime? eveningRevealedAt,
|
||||
}) {
|
||||
return WordOfDayRecord(
|
||||
dateKey: dateKey ?? this.dateKey,
|
||||
wordId: wordId ?? this.wordId,
|
||||
isRevealed: isRevealed ?? this.isRevealed,
|
||||
isStepCompleted: isStepCompleted ?? this.isStepCompleted,
|
||||
diaryNote: diaryNote ?? this.diaryNote,
|
||||
revealedAt: revealedAt ?? this.revealedAt,
|
||||
completedAt: completedAt ?? this.completedAt,
|
||||
isTriptychMode: isTriptychMode ?? this.isTriptychMode,
|
||||
morningWordId: morningWordId ?? this.morningWordId,
|
||||
dayWordId: dayWordId ?? this.dayWordId,
|
||||
eveningWordId: eveningWordId ?? this.eveningWordId,
|
||||
isMorningRevealed: isMorningRevealed ?? this.isMorningRevealed,
|
||||
isDayRevealed: isDayRevealed ?? this.isDayRevealed,
|
||||
isEveningRevealed: isEveningRevealed ?? this.isEveningRevealed,
|
||||
isDayStepCompleted: isDayStepCompleted ?? this.isDayStepCompleted,
|
||||
morningRevealedAt: morningRevealedAt ?? this.morningRevealedAt,
|
||||
dayRevealedAt: dayRevealedAt ?? this.dayRevealedAt,
|
||||
eveningRevealedAt: eveningRevealedAt ?? this.eveningRevealedAt,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'dateKey': dateKey,
|
||||
'wordId': wordId,
|
||||
'isRevealed': isRevealed,
|
||||
'isStepCompleted': isStepCompleted,
|
||||
'diaryNote': diaryNote,
|
||||
if (revealedAt != null) 'revealedAt': revealedAt!.toIso8601String(),
|
||||
if (completedAt != null) 'completedAt': completedAt!.toIso8601String(),
|
||||
'isTriptychMode': isTriptychMode,
|
||||
'morningWordId': morningWordId,
|
||||
'dayWordId': dayWordId,
|
||||
'eveningWordId': eveningWordId,
|
||||
'isMorningRevealed': isMorningRevealed,
|
||||
'isDayRevealed': isDayRevealed,
|
||||
'isEveningRevealed': isEveningRevealed,
|
||||
'isDayStepCompleted': isDayStepCompleted,
|
||||
if (morningRevealedAt != null)
|
||||
'morningRevealedAt': morningRevealedAt!.toIso8601String(),
|
||||
if (dayRevealedAt != null)
|
||||
'dayRevealedAt': dayRevealedAt!.toIso8601String(),
|
||||
if (eveningRevealedAt != null)
|
||||
'eveningRevealedAt': eveningRevealedAt!.toIso8601String(),
|
||||
};
|
||||
|
||||
factory WordOfDayRecord.fromMap(Map<String, dynamic> map) => WordOfDayRecord(
|
||||
dateKey: map['dateKey'] as String? ?? '',
|
||||
wordId: (map['wordId'] as num?)?.toInt() ?? 0,
|
||||
isRevealed: map['isRevealed'] as bool? ?? false,
|
||||
isStepCompleted: map['isStepCompleted'] as bool? ?? false,
|
||||
diaryNote: map['diaryNote'] as String? ?? '',
|
||||
revealedAt: map['revealedAt'] != null
|
||||
? DateTime.tryParse(map['revealedAt'] as String)
|
||||
: null,
|
||||
completedAt: map['completedAt'] != null
|
||||
? DateTime.tryParse(map['completedAt'] as String)
|
||||
: null,
|
||||
isTriptychMode: map['isTriptychMode'] as bool? ?? false,
|
||||
morningWordId: (map['morningWordId'] as num?)?.toInt() ?? 0,
|
||||
dayWordId: (map['dayWordId'] as num?)?.toInt() ?? 0,
|
||||
eveningWordId: (map['eveningWordId'] as num?)?.toInt() ?? 0,
|
||||
isMorningRevealed: map['isMorningRevealed'] as bool? ?? false,
|
||||
isDayRevealed: map['isDayRevealed'] as bool? ?? false,
|
||||
isEveningRevealed: map['isEveningRevealed'] as bool? ?? false,
|
||||
isDayStepCompleted: map['isDayStepCompleted'] as bool? ?? false,
|
||||
morningRevealedAt: map['morningRevealedAt'] != null
|
||||
? DateTime.tryParse(map['morningRevealedAt'] as String)
|
||||
: null,
|
||||
dayRevealedAt: map['dayRevealedAt'] != null
|
||||
? DateTime.tryParse(map['dayRevealedAt'] as String)
|
||||
: null,
|
||||
eveningRevealedAt: map['eveningRevealedAt'] != null
|
||||
? DateTime.tryParse(map['eveningRevealedAt'] as String)
|
||||
: null,
|
||||
);
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory WordOfDayRecord.fromJson(String source) =>
|
||||
WordOfDayRecord.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
// word_passport_model.dart
|
||||
// ЕДИНООБРАЗНЫЙ ПАСПОРТ ЗНАКА «ОРАКУЛА 999»
|
||||
// 78 атомарных полей по 6 разделам канона
|
||||
|
||||
import '../services/passport_schema_dart.dart';
|
||||
|
||||
class WordPassport {
|
||||
final int id;
|
||||
final String word;
|
||||
final String pos;
|
||||
final String category;
|
||||
final String emoji;
|
||||
final Map<String, String> fields;
|
||||
final int filledCount;
|
||||
final bool isComplete;
|
||||
final String updatedAt;
|
||||
final List<Map<String, dynamic>> illustrations;
|
||||
|
||||
const WordPassport({
|
||||
required this.id,
|
||||
required this.word,
|
||||
this.pos = '',
|
||||
this.category = '',
|
||||
this.emoji = '',
|
||||
this.fields = const {},
|
||||
this.filledCount = 0,
|
||||
this.isComplete = false,
|
||||
this.updatedAt = '',
|
||||
this.illustrations = const [],
|
||||
});
|
||||
|
||||
String get idPad => id.toString().padLeft(3, '0');
|
||||
|
||||
String getField(String key) => fields[key] ?? '';
|
||||
|
||||
/// [1/78] Раздел I • Индекс и имя
|
||||
String get indexAndName => getField('index_and_name');
|
||||
|
||||
/// [2/78] Раздел I • Грамматический класс
|
||||
String get grammaticalClass => getField('grammatical_class');
|
||||
|
||||
/// [3/78] Раздел I • Функциональная категория
|
||||
String get functionalCategory => getField('functional_category');
|
||||
|
||||
/// [4/78] Раздел I • Сетевой статус
|
||||
String get networkStatus => getField('network_status');
|
||||
|
||||
/// [5/78] Раздел II • Старший Футарк (Руны)
|
||||
String get manticRunes => getField('mantic_runes');
|
||||
|
||||
/// [6/78] Раздел II • Кельтский Огам
|
||||
String get manticOgham => getField('mantic_ogham');
|
||||
|
||||
/// [7/78] Раздел II • Старшие Арканы Таро
|
||||
String get manticTarot => getField('mantic_tarot');
|
||||
|
||||
/// [8/78] Раздел II • Малая колода Ленорман
|
||||
String get manticLenormand => getField('mantic_lenormand');
|
||||
|
||||
/// [9/78] Раздел II • Каббала / Древо Жизни
|
||||
String get manticKabbalah => getField('mantic_kabbalah');
|
||||
|
||||
/// [10/78] Раздел II • Арабская Геомантия
|
||||
String get manticGeomancy => getField('mantic_geomancy');
|
||||
|
||||
/// [11/78] Раздел II • И Цзин (Книга Перемен)
|
||||
String get manticIching => getField('mantic_iching');
|
||||
|
||||
/// [12/78] Раздел II • Алхимия (Magnum Opus)
|
||||
String get manticAlchemy => getField('mantic_alchemy');
|
||||
|
||||
/// [13/78] Раздел II • Классический Зодиак
|
||||
String get manticZodiacWest => getField('mantic_zodiac_west');
|
||||
|
||||
/// [14/78] Раздел II • Китайский Зодиак
|
||||
String get manticZodiacChinese => getField('mantic_zodiac_chinese');
|
||||
|
||||
/// [15/78] Раздел II • Септенер
|
||||
String get manticSeptener => getField('mantic_septener');
|
||||
|
||||
/// [16/78] Раздел II • Додекада Месяцев
|
||||
String get manticDodecade => getField('mantic_dodecade');
|
||||
|
||||
/// [17/78] Раздел II • Нумерология
|
||||
String get manticNumerology => getField('mantic_numerology');
|
||||
|
||||
/// [18/78] Раздел II • Космология Трёх Миров
|
||||
String get manticCosmology => getField('mantic_cosmology');
|
||||
|
||||
/// [19/78] Раздел II • 20 Навалей Цолькин (Майя)
|
||||
String get manticTzolkin => getField('mantic_tzolkin');
|
||||
|
||||
/// [20/78] Раздел II • 27 Накшатр Джйотиш (Индия)
|
||||
String get manticNakshatras => getField('mantic_nakshatras');
|
||||
|
||||
/// [21/78] Раздел II • Действующие законы станций (выжимка)
|
||||
String get manticStationLaws => getField('mantic_station_laws');
|
||||
|
||||
/// [22/78] Раздел III • Этимология и праформы
|
||||
String get etymologyRoots => getField('etymology_roots');
|
||||
|
||||
/// [23/78] Раздел III • Шумер / Аккад
|
||||
String get scriptSumerAkkad => getField('script_sumer_akkad');
|
||||
|
||||
/// [24/78] Раздел III • Древний Египет
|
||||
String get scriptEgypt => getField('script_egypt');
|
||||
|
||||
/// [25/78] Раздел III • Иудейская традиция
|
||||
String get scriptHebrew => getField('script_hebrew');
|
||||
|
||||
/// [26/78] Раздел III • Античность
|
||||
String get scriptAntiquity => getField('script_antiquity');
|
||||
|
||||
/// [27/78] Раздел III • Кельты
|
||||
String get scriptCeltic => getField('script_celtic');
|
||||
|
||||
/// [28/78] Раздел III • Скандинавы / Германцы
|
||||
String get scriptRunicGermanic => getField('script_runic_germanic');
|
||||
|
||||
/// [29/78] Раздел III • Славяне
|
||||
String get scriptSlavic => getField('script_slavic');
|
||||
|
||||
/// [30/78] Раздел III • Ведическая Индия
|
||||
String get scriptIndiaSanskrit => getField('script_india_sanskrit');
|
||||
|
||||
/// [31/78] Раздел III • Персия
|
||||
String get scriptPersia => getField('script_persia');
|
||||
|
||||
/// [32/78] Раздел III • Китай
|
||||
String get scriptChina => getField('script_china');
|
||||
|
||||
/// [33/78] Раздел III • Япония
|
||||
String get scriptJapan => getField('script_japan');
|
||||
|
||||
/// [34/78] Раздел III • Исламский мир
|
||||
String get scriptIslamic => getField('script_islamic');
|
||||
|
||||
/// [35/78] Раздел III • Западная Европа
|
||||
String get scriptWesternEurope => getField('script_western_europe');
|
||||
|
||||
/// [36/78] Раздел III • Мезоамерика
|
||||
String get scriptMesoamerica => getField('script_mesoamerica');
|
||||
|
||||
/// [37/78] Раздел III • Коренные народы Сев. Америки
|
||||
String get scriptNativeAmerican => getField('script_native_american');
|
||||
|
||||
/// [38/78] Раздел III • Полинезия
|
||||
String get scriptPolynesia => getField('script_polynesia');
|
||||
|
||||
/// [39/78] Раздел IV • 0 Нейтральный статус (База / Бытие / Среда / Субстанция)
|
||||
String get stateNeutral => getField('state_neutral');
|
||||
|
||||
/// [40/78] Раздел IV • [+1] Созидательный вектор (Свет / Ресурс / Эволюция)
|
||||
String get stateCreative => getField('state_creative');
|
||||
|
||||
/// [41/78] Раздел IV • [-1] Деструктивный вектор (Тень / Вызов / Искажение)
|
||||
String get stateDestructive => getField('state_destructive');
|
||||
|
||||
/// [42/78] Раздел V • Шумер и Месопотамия: [+] Свет
|
||||
String get cultureSumerLight => getField('culture_sumer_light');
|
||||
|
||||
/// [43/78] Раздел V • Шумер и Месопотамия: [-] Тень
|
||||
String get cultureSumerShadow => getField('culture_sumer_shadow');
|
||||
|
||||
/// [44/78] Раздел V • Древний Египет: [+] Свет
|
||||
String get cultureEgyptLight => getField('culture_egypt_light');
|
||||
|
||||
/// [45/78] Раздел V • Древний Египет: [-] Тень
|
||||
String get cultureEgyptShadow => getField('culture_egypt_shadow');
|
||||
|
||||
/// [46/78] Раздел V • Иудейская традиция и Каббала: [+] Свет
|
||||
String get cultureKabbalahLight => getField('culture_kabbalah_light');
|
||||
|
||||
/// [47/78] Раздел V • Иудейская традиция и Каббала: [-] Тень
|
||||
String get cultureKabbalahShadow => getField('culture_kabbalah_shadow');
|
||||
|
||||
/// [48/78] Раздел V • Античность (Греция / Рим): [+] Свет
|
||||
String get cultureAntiquityLight => getField('culture_antiquity_light');
|
||||
|
||||
/// [49/78] Раздел V • Античность (Греция / Рим): [-] Тень
|
||||
String get cultureAntiquityShadow => getField('culture_antiquity_shadow');
|
||||
|
||||
/// [50/78] Раздел V • Кельтская традиция: [+] Свет
|
||||
String get cultureCelticLight => getField('culture_celtic_light');
|
||||
|
||||
/// [51/78] Раздел V • Кельтская традиция: [-] Тень
|
||||
String get cultureCelticShadow => getField('culture_celtic_shadow');
|
||||
|
||||
/// [52/78] Раздел V • Скандинавско-германская традиция: [+] Свет
|
||||
String get cultureNordicLight => getField('culture_nordic_light');
|
||||
|
||||
/// [53/78] Раздел V • Скандинавско-германская традиция: [-] Тень
|
||||
String get cultureNordicShadow => getField('culture_nordic_shadow');
|
||||
|
||||
/// [54/78] Раздел V • Славянская традиция: [+] Свет
|
||||
String get cultureSlavicLight => getField('culture_slavic_light');
|
||||
|
||||
/// [55/78] Раздел V • Славянская традиция: [-] Тень
|
||||
String get cultureSlavicShadow => getField('culture_slavic_shadow');
|
||||
|
||||
/// [56/78] Раздел V • Ведическая Индия и Тантра: [+] Свет
|
||||
String get cultureIndiaLight => getField('culture_india_light');
|
||||
|
||||
/// [57/78] Раздел V • Ведическая Индия и Тантра: [-] Тень
|
||||
String get cultureIndiaShadow => getField('culture_india_shadow');
|
||||
|
||||
/// [58/78] Раздел V • Персия и Зороастризм: [+] Свет
|
||||
String get culturePersiaLight => getField('culture_persia_light');
|
||||
|
||||
/// [59/78] Раздел V • Персия и Зороастризм: [-] Тень
|
||||
String get culturePersiaShadow => getField('culture_persia_shadow');
|
||||
|
||||
/// [60/78] Раздел V • Китай (Даосизм, И-цзин, У-син): [+] Свет
|
||||
String get cultureChinaLight => getField('culture_china_light');
|
||||
|
||||
/// [61/78] Раздел V • Китай (Даосизм, И-цзин, У-син): [-] Тень
|
||||
String get cultureChinaShadow => getField('culture_china_shadow');
|
||||
|
||||
/// [62/78] Раздел V • Япония (Синтоизм, Сингон): [+] Свет
|
||||
String get cultureJapanLight => getField('culture_japan_light');
|
||||
|
||||
/// [63/78] Раздел V • Япония (Синтоизм, Сингон): [-] Тень
|
||||
String get cultureJapanShadow => getField('culture_japan_shadow');
|
||||
|
||||
/// [64/78] Раздел V • Исламская традиция и суфизм: [+] Свет
|
||||
String get cultureIslamLight => getField('culture_islam_light');
|
||||
|
||||
/// [65/78] Раздел V • Исламская традиция и суфизм: [-] Тень
|
||||
String get cultureIslamShadow => getField('culture_islam_shadow');
|
||||
|
||||
/// [66/78] Раздел V • Западноевропейская традиция (Алхимия / Герметизм): [+] Свет
|
||||
String get cultureHermeticLight => getField('culture_hermetic_light');
|
||||
|
||||
/// [67/78] Раздел V • Западноевропейская традиция (Алхимия / Герметизм): [-] Тень
|
||||
String get cultureHermeticShadow => getField('culture_hermetic_shadow');
|
||||
|
||||
/// [68/78] Раздел V • Мезоамерика и Анды (Майя, Ацтеки, Инки): [+] Свет
|
||||
String get cultureMesoamericaLight => getField('culture_mesoamerica_light');
|
||||
|
||||
/// [69/78] Раздел V • Мезоамерика и Анды (Майя, Ацтеки, Инки): [-] Тень
|
||||
String get cultureMesoamericaShadow => getField('culture_mesoamerica_shadow');
|
||||
|
||||
/// [70/78] Раздел V • Традиции коренных народов Северной Америки: [+] Свет
|
||||
String get cultureNativeAmericanLight => getField('culture_native_american_light');
|
||||
|
||||
/// [71/78] Раздел V • Традиции коренных народов Северной Америки: [-] Тень
|
||||
String get cultureNativeAmericanShadow => getField('culture_native_american_shadow');
|
||||
|
||||
/// [72/78] Раздел V • Полинезия и Океания: [+] Свет
|
||||
String get culturePolynesiaLight => getField('culture_polynesia_light');
|
||||
|
||||
/// [73/78] Раздел V • Полинезия и Океания: [-] Тень
|
||||
String get culturePolynesiaShadow => getField('culture_polynesia_shadow');
|
||||
|
||||
/// [74/78] Раздел V • 17-й Интегральный мета-синтез (Универсальный культурологический код)
|
||||
String get metaSynthesis => getField('meta_synthesis');
|
||||
|
||||
/// [75/78] Раздел VI • Смысловое ядро
|
||||
String get semanticCore => getField('semantic_core');
|
||||
|
||||
/// [76/78] Раздел VI • Интерпретация по грамматическому типу
|
||||
String get grammarInterpretation => getField('grammar_interpretation');
|
||||
|
||||
/// [77/78] Раздел VI • Вектор Воли (К чему призывает)
|
||||
String get willVector => getField('will_vector');
|
||||
|
||||
/// [78/78] Раздел VI • Предостережение / Тень (О чем предупреждает)
|
||||
String get shadowWarning => getField('shadow_warning');
|
||||
|
||||
|
||||
factory WordPassport.fromMap(Map<String, dynamic> map) {
|
||||
final rawFields = map['fields'] as Map<String, dynamic>? ?? {};
|
||||
final fieldsMap = rawFields.map((k, v) => MapEntry(k, v?.toString() ?? ''));
|
||||
|
||||
// Count filled fields
|
||||
var filled = 0;
|
||||
for (final f in kPassportFields78) {
|
||||
if ((fieldsMap[f.key] ?? '').trim().isNotEmpty) {
|
||||
filled++;
|
||||
}
|
||||
}
|
||||
|
||||
return WordPassport(
|
||||
id: (map['id'] as num?)?.toInt() ?? 0,
|
||||
word: map['word'] as String? ?? '',
|
||||
pos: map['pos'] as String? ?? '',
|
||||
category: map['category'] as String? ?? '',
|
||||
emoji: map['emoji'] as String? ?? '',
|
||||
fields: fieldsMap,
|
||||
filledCount: (map['filled_count'] as num?)?.toInt() ?? filled,
|
||||
isComplete: (map['is_complete'] as bool?) ?? (filled >= 78),
|
||||
updatedAt: map['updated_at'] as String? ?? '',
|
||||
illustrations: (map['illustrations'] as List<dynamic>?)?.map((e) => Map<String, dynamic>.from(e as Map)).toList() ?? const [],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'id': id,
|
||||
'word': word,
|
||||
'pos': pos,
|
||||
'category': category,
|
||||
'emoji': emoji,
|
||||
'fields': fields,
|
||||
'filled_count': filledCount,
|
||||
'is_complete': isComplete,
|
||||
'updated_at': updatedAt,
|
||||
'illustrations': illustrations,
|
||||
};
|
||||
|
||||
WordPassport copyWith({
|
||||
int? id,
|
||||
String? word,
|
||||
String? pos,
|
||||
String? category,
|
||||
String? emoji,
|
||||
Map<String, String>? fields,
|
||||
int? filledCount,
|
||||
bool? isComplete,
|
||||
String? updatedAt,
|
||||
List<Map<String, dynamic>>? illustrations,
|
||||
}) {
|
||||
final newFields = fields ?? this.fields;
|
||||
var filled = 0;
|
||||
for (final f in kPassportFields78) {
|
||||
if ((newFields[f.key] ?? '').trim().isNotEmpty) {
|
||||
filled++;
|
||||
}
|
||||
}
|
||||
return WordPassport(
|
||||
id: id ?? this.id,
|
||||
word: word ?? this.word,
|
||||
pos: pos ?? this.pos,
|
||||
category: category ?? this.category,
|
||||
emoji: emoji ?? this.emoji,
|
||||
fields: newFields,
|
||||
filledCount: filledCount ?? filled,
|
||||
isComplete: isComplete ?? (filled >= 78),
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
illustrations: illustrations ?? this.illustrations,
|
||||
);
|
||||
}
|
||||
|
||||
String toMarkdown() {
|
||||
final sb = StringBuffer();
|
||||
sb.writeln('═══════════════════════════════════════════════════════════════════');
|
||||
sb.writeln('ЕДИНООБРАЗНЫЙ ПАСПОРТ ЗНАКА «ОРАКУЛА 999»: #$idPad «${word.toUpperCase()}»');
|
||||
sb.writeln('═══════════════════════════════════════════════════════════════════');
|
||||
sb.writeln('I. Заголовочный паспорт (Метаданные реестра)');
|
||||
sb.writeln('Индекс и имя: ${getField("index_and_name").isEmpty ? "$idPad | ${word.toUpperCase()} | $emoji" : getField("index_and_name")}');
|
||||
sb.writeln('Грамматический класс: ${getField("grammatical_class")}');
|
||||
sb.writeln('Функциональная категория: ${getField("functional_category")}');
|
||||
sb.writeln('Сетевой статус: ${getField("network_status")}');
|
||||
sb.writeln('---');
|
||||
sb.writeln('II. Кросс-мантическая сеть и станции (16 классических систем)');
|
||||
sb.writeln('Привязанные узловые станции:');
|
||||
sb.writeln('Старший Футарк (Руны): ${getField("mantic_runes")}');
|
||||
sb.writeln('Кельтский Огам: ${getField("mantic_ogham")}');
|
||||
sb.writeln('Старшие Арканы Таро: ${getField("mantic_tarot")}');
|
||||
sb.writeln('Малая колода Ленорман: ${getField("mantic_lenormand")}');
|
||||
sb.writeln('Каббала / Древо Жизни: ${getField("mantic_kabbalah")}');
|
||||
sb.writeln("Арабская Геомантия ('Илм ар-Рамл): ${getField('mantic_geomancy')}");
|
||||
sb.writeln('И Цзин (Книга Перемен): ${getField("mantic_iching")}');
|
||||
sb.writeln('Алхимия (Magnum Opus): ${getField("mantic_alchemy")}');
|
||||
sb.writeln('Классический Зодиак: ${getField("mantic_zodiac_west")}');
|
||||
sb.writeln('Китайский Зодиак: ${getField("mantic_zodiac_chinese")}');
|
||||
sb.writeln('Септенер: ${getField("mantic_septener")}');
|
||||
sb.writeln('Додекада Месяцев: ${getField("mantic_dodecade")}');
|
||||
sb.writeln('Нумерология: ${getField("mantic_numerology")}');
|
||||
sb.writeln('Космология Трёх Миров: ${getField("mantic_cosmology")}');
|
||||
sb.writeln('20 Навалей Цолькин (Майя): ${getField("mantic_tzolkin")}');
|
||||
sb.writeln('27 Накшатр Джйотиш (Индия): ${getField("mantic_nakshatras")}');
|
||||
sb.writeln('Действующие законы станций (выжимка):');
|
||||
sb.writeln(getField("mantic_station_laws"));
|
||||
sb.writeln('---');
|
||||
sb.writeln('III. Мультилингвальный и графический паспорт (Язык, Звук, Глиф)');
|
||||
sb.writeln('Этимология и праформы: ${getField("etymology_roots")}');
|
||||
sb.writeln('Сравнительный срез графики и фоносемантики (16 систем записи):');
|
||||
sb.writeln('Шумер / Аккад: ${getField("script_sumer_akkad")}');
|
||||
sb.writeln('Древний Египет: ${getField("script_egypt")}');
|
||||
sb.writeln('Иудейская традиция: ${getField("script_hebrew")}');
|
||||
sb.writeln('Античность: ${getField("script_antiquity")}');
|
||||
sb.writeln('Кельты: ${getField("script_celtic")}');
|
||||
sb.writeln('Скандинавы / Германцы: ${getField("script_runic_germanic")}');
|
||||
sb.writeln('Славяне: ${getField("script_slavic")}');
|
||||
sb.writeln('Ведическая Индия: ${getField("script_india_sanskrit")}');
|
||||
sb.writeln('Персия: ${getField("script_persia")}');
|
||||
sb.writeln('Китай: ${getField("script_china")}');
|
||||
sb.writeln('Япония: ${getField("script_japan")}');
|
||||
sb.writeln('Исламский мир: ${getField("script_islamic")}');
|
||||
sb.writeln('Западная Европа: ${getField("script_western_europe")}');
|
||||
sb.writeln('Мезоамерика: ${getField("script_mesoamerica")}');
|
||||
sb.writeln('Коренные народы Сев. Америки: ${getField("script_native_american")}');
|
||||
sb.writeln('Полинезия: ${getField("script_polynesia")}');
|
||||
sb.writeln('---');
|
||||
sb.writeln('IV. Трехполярная матрица состояний (0 / +1 / -1)');
|
||||
sb.writeln('0 Нейтральный статус (База / Бытие / Среда / Субстанция): ${getField("state_neutral")}');
|
||||
sb.writeln('[+1] Созидательный вектор (Свет / Ресурс / Эволюция): ${getField("state_creative")}');
|
||||
sb.writeln('[-1] Деструктивный вектор (Тень / Вызов / Искажение): ${getField("state_destructive")}');
|
||||
sb.writeln('---');
|
||||
sb.writeln('V. Культурологическая матрица и 17-й Интегральный мета-синтез');
|
||||
sb.writeln('А. 16 Мировых Традиций (Срез «Свет / Тень»):');
|
||||
sb.writeln('Шумер и Месопотамия: [+] ${getField("culture_sumer_light")} / [-] ${getField("culture_sumer_shadow")}');
|
||||
sb.writeln('Древний Египет: [+] ${getField("culture_egypt_light")} / [-] ${getField("culture_egypt_shadow")}');
|
||||
sb.writeln('Иудейская традиция и Каббала: [+] ${getField("culture_kabbalah_light")} / [-] ${getField("culture_kabbalah_shadow")}');
|
||||
sb.writeln('Античность (Греция / Рим): [+] ${getField("culture_antiquity_light")} / [-] ${getField("culture_antiquity_shadow")}');
|
||||
sb.writeln('Кельтская традиция: [+] ${getField("culture_celtic_light")} / [-] ${getField("culture_celtic_shadow")}');
|
||||
sb.writeln('Скандинавско-германская традиция: [+] ${getField("culture_nordic_light")} / [-] ${getField("culture_nordic_shadow")}');
|
||||
sb.writeln('Славянская традиция: [+] ${getField("culture_slavic_light")} / [-] ${getField("culture_slavic_shadow")}');
|
||||
sb.writeln('Ведическая Индия и Тантра: [+] ${getField("culture_india_light")} / [-] ${getField("culture_india_shadow")}');
|
||||
sb.writeln('Персия и Зороастризм: [+] ${getField("culture_persia_light")} / [-] ${getField("culture_persia_shadow")}');
|
||||
sb.writeln('Китай (Даосизм, И-цзин, У-син): [+] ${getField("culture_china_light")} / [-] ${getField("culture_china_shadow")}');
|
||||
sb.writeln('Япония (Синтоизм, Сингон): [+] ${getField("culture_japan_light")} / [-] ${getField("culture_japan_shadow")}');
|
||||
sb.writeln('Исламская традиция и суфизм: [+] ${getField("culture_islam_light")} / [-] ${getField("culture_islam_shadow")}');
|
||||
sb.writeln('Западноевропейская традиция (Алхимия / Герметизм): [+] ${getField("culture_hermetic_light")} / [-] ${getField("culture_hermetic_shadow")}');
|
||||
sb.writeln('Мезоамерика и Анды (Майя, Ацтеки, Инки): [+] ${getField("culture_mesoamerica_light")} / [-] ${getField("culture_mesoamerica_shadow")}');
|
||||
sb.writeln('Традиции коренных народов Северной Америки: [+] ${getField("culture_native_american_light")} / [-] ${getField("culture_native_american_shadow")}');
|
||||
sb.writeln('Полинезия и Океания: [+] ${getField("culture_polynesia_light")} / [-] ${getField("culture_polynesia_shadow")}');
|
||||
sb.writeln('Б. 17-й Интегральный мета-синтез (Универсальный культурологический код):');
|
||||
sb.writeln(getField("meta_synthesis"));
|
||||
sb.writeln('---');
|
||||
sb.writeln('VI. Мантический & Обучающий синтез');
|
||||
sb.writeln('Смысловое ядро: ${getField("semantic_core")}');
|
||||
sb.writeln('Интерпретация по грамматическому типу: ${getField("grammar_interpretation")}');
|
||||
sb.writeln('Вектор Воли (К чему призывает): ${getField("will_vector")}');
|
||||
sb.writeln('Предостережение / Тень (О чем предупреждает): ${getField("shadow_warning")}');
|
||||
sb.writeln('═══════════════════════════════════════════════════════════════════');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
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
@@ -3,6 +3,7 @@ import '../models/interpreter_persona.dart';
|
||||
import '../models/word_model.dart';
|
||||
import '../g4f/g4f.dart';
|
||||
import 'settings_repository.dart';
|
||||
import 'words_repository.dart';
|
||||
|
||||
class AiOracleService {
|
||||
static final AiOracleService _instance = AiOracleService._internal();
|
||||
@@ -32,6 +33,47 @@ class AiOracleService {
|
||||
];
|
||||
}
|
||||
|
||||
/// Generates an enhanced revision of a symbol based on its full pre-prompt
|
||||
Future<String> generateSymbolRevision({
|
||||
required String prePrompt,
|
||||
}) async {
|
||||
try {
|
||||
final g4fResult = await _client.chat.completions.create(
|
||||
model: G4FModels.gpt4o,
|
||||
messages: [
|
||||
Message(
|
||||
role: Role.system,
|
||||
content:
|
||||
'Ты — Архитектор Смыслов, верховный мифолог, культуролог и онтологический герменевт. '
|
||||
'Твоя задача — провести глубочайшее, бескомпромиссное и многомерное ОБОГАЩЕНИЕ сакрального Архетипа. '
|
||||
'Напиши захватывающую, монументальную, живую и литературную статью-эссе, которую по-настоящему интересно читать. '
|
||||
'СТРОЖАЙШИЕ ПРАВИЛА: '
|
||||
'Абсолютный запрет на гадательную терминологию (никаких оракулов, раскладов, карт, вопрошающих, гаданий). '
|
||||
'Запрет на лень и краткость: длинный, плотный, фактурный текст. '
|
||||
'Обязательный мультикультурный синтез: этимология, космогонии (Шумер, Египет, Греция, Иудаизм, Кельты, Суфизм, Дао), наука (квантовая механика, биология), алхимия. '
|
||||
'Формат ответа строго в markdown с заголовками ##:\n'
|
||||
'## ЯДРО\n'
|
||||
'## ОРАКУЛ\n'
|
||||
'## ТЕНЬ\n'
|
||||
'## ВОПРОС\n'
|
||||
'## ЖЕСТ\n'
|
||||
'Метрика: в самом конце точное количество символов.',
|
||||
),
|
||||
Message(
|
||||
role: Role.user,
|
||||
content: prePrompt,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (g4fResult.trim().length > 20) {
|
||||
return g4fResult.trim();
|
||||
}
|
||||
} catch (e) {
|
||||
print('G4F Revision Error: $e');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/// Interprets a full multi-word session narrative (1 to 9 words)
|
||||
Future<String> interpretSession({
|
||||
required List<SessionWord> words,
|
||||
@@ -77,7 +119,8 @@ class AiOracleService {
|
||||
$wordsNarrative
|
||||
${newlyAddedWord != null ? "Только что вытянут уточняющий знак: #${newlyAddedWord.formattedSessionNumber} «${newlyAddedWord.word.toUpperCase()}» (${newlyAddedWord.symbolicMeaning})." : ""}
|
||||
|
||||
Ответь в своём голосе, 3–4 предложения. Привяжи ответ к знакам и вопросу. Без тумана. Один ясный вывод или шаг.''';
|
||||
Ответь в своём голосе, 3–5 предложений. Привяжи ответ к законам знаков и вопросу.
|
||||
Закон знака побеждает голос: не выдумывай людей, руны и события, которых нет в законах. Если канон знака пуст — не раздувай его.''';
|
||||
|
||||
final messages = [
|
||||
Message(role: Role.system, content: voice.systemPrompt),
|
||||
@@ -119,7 +162,7 @@ ${newlyAddedWord != null ? "Только что вытянут уточняющ
|
||||
final wordsStr = words
|
||||
.asMap()
|
||||
.entries
|
||||
.map((e) => 'Знак ${e.key + 1}: #${e.value.formattedSessionNumber} «${e.value.word.toUpperCase()}» (${e.value.category}). Смысл знака: ${e.value.symbolicMeaning}')
|
||||
.map((e) => _signDossier(e.key + 1, e.value))
|
||||
.join('\n');
|
||||
|
||||
return '''$qStr
|
||||
@@ -127,7 +170,64 @@ ${newlyAddedWord != null ? "Только что вытянут уточняющ
|
||||
Выпавшие знаки жребия:
|
||||
$wordsStr
|
||||
|
||||
${persona.userTask}''';
|
||||
${persona.userTask}
|
||||
|
||||
$_fidelityRules''';
|
||||
}
|
||||
|
||||
static const String _fidelityRules = '''Закон знака побеждает голос.
|
||||
- Разбери знаки по порядку. Назови каждый и опирайся только на переданный закон.
|
||||
- Голос — интонация. Он не подменяет смысл и не превращает слова в руны, нити, тропы, круг близких, утробу или протоколы, если этого нет в законе.
|
||||
- Если у знака написано «канон пуст», так и скажи. Читай само слово буквально и не раздувай заготовку в историю.
|
||||
- Знак-сдвиг (наречие, обстоятельство, оператор «сдвиг») не отдельный миф: он меняет соседний знак — срок, место или способ.
|
||||
- Если задание голоса спорит с законом знака, побеждает закон знака.
|
||||
- В конце один физический шаг, который следует из законов.''';
|
||||
|
||||
String _signDossier(int index, SessionWord w) {
|
||||
final live = WordsRepository().getWordByName(w.word);
|
||||
final law = ((live != null && live.core.trim().isNotEmpty) ? live.core : w.symbolicMeaning).trim();
|
||||
final stock = _isStockCanon(law);
|
||||
final buf = StringBuffer()
|
||||
..writeln('Знак $index: #${w.formattedSessionNumber} «${w.word.toUpperCase()}»')
|
||||
..writeln('Часть речи: ${w.partOfSpeech.isEmpty ? 'не указана' : w.partOfSpeech}. Разряд: ${w.category}.');
|
||||
|
||||
if (live != null) {
|
||||
final op = WordItemLocalization.localizeOperator(live.operator);
|
||||
if (op.isNotEmpty) buf.writeln('Оператор: $op.');
|
||||
}
|
||||
|
||||
if (stock) {
|
||||
buf.writeln('Закон знака: КАНОН ПУСТ. В досье заготовка, не смысл. Читай слово «${w.word}» буквально.');
|
||||
} else {
|
||||
buf.writeln('Закон знака (единственный разрешённый смысл): $law');
|
||||
}
|
||||
|
||||
final shadow = live?.shadow.trim() ?? '';
|
||||
if (shadow.isNotEmpty && !_isStockShadow(shadow)) {
|
||||
buf.writeln('Тень: $shadow');
|
||||
}
|
||||
|
||||
final desc = live?.cleanDescription.trim() ?? '';
|
||||
if (desc.isNotEmpty && desc != law && !_isStockCanon(desc)) {
|
||||
buf.writeln('Описание: $desc');
|
||||
}
|
||||
return buf.toString().trimRight();
|
||||
}
|
||||
|
||||
bool _isStockCanon(String text) {
|
||||
final t = text.toLowerCase();
|
||||
return t.contains('сакральный символ') ||
|
||||
t.contains('архетипический образ в поле судьбы') ||
|
||||
t.contains('ключевую сферу кристаллизации') ||
|
||||
t.contains('вектор трансформации') ||
|
||||
t.contains('модус судьбы') ||
|
||||
t.contains('активный процесс духовной динамики');
|
||||
}
|
||||
|
||||
bool _isStockShadow(String text) {
|
||||
final t = text.toLowerCase();
|
||||
return t.contains('искажение первообраза') ||
|
||||
t.contains('суета, поспешность, насилие над естественным ритмом');
|
||||
}
|
||||
|
||||
String _synthesizeDeepSacredNarrative(List<SessionWord> words, String question) {
|
||||
@@ -174,9 +274,11 @@ ${persona.userTask}''';
|
||||
String _withPersonaPrefix(String body) {
|
||||
switch (persona.id) {
|
||||
case 'orc':
|
||||
return 'Слушай. $body';
|
||||
return 'Вааагх. $body';
|
||||
case 'techpriest':
|
||||
return 'Диагностика. $body';
|
||||
case 'aeldari':
|
||||
return 'По нитям судеб. $body';
|
||||
case 'analyst':
|
||||
return 'Рабочая гипотеза. $body';
|
||||
default:
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/// Свёртка календарной даты в число судьбы суток.
|
||||
///
|
||||
/// Та же школа, что на экране психоматрицы: день, месяц и год свёртываются
|
||||
/// по отдельности, затем складываются. 11, 22 и 33 не свёртываются дальше.
|
||||
class DayCode {
|
||||
final int value;
|
||||
final int reducedDay;
|
||||
final int reducedMonth;
|
||||
final int reducedYear;
|
||||
final String word;
|
||||
final String title;
|
||||
|
||||
const DayCode({
|
||||
required this.value,
|
||||
required this.reducedDay,
|
||||
required this.reducedMonth,
|
||||
required this.reducedYear,
|
||||
required this.word,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
String get formula => '$reducedDay + $reducedMonth + $reducedYear = $value';
|
||||
|
||||
String get clause =>
|
||||
'Нумерологический код суток: $formula, знак «$word». '
|
||||
'Мастер-числа 11, 22 и 33 не свёртываются.';
|
||||
}
|
||||
|
||||
class DayNumerology {
|
||||
static const Map<int, String> words = {
|
||||
1: 'один',
|
||||
2: 'два',
|
||||
3: 'три',
|
||||
4: 'четыре',
|
||||
5: 'пять',
|
||||
6: 'шесть',
|
||||
7: 'семь',
|
||||
8: 'восемь',
|
||||
9: 'девять',
|
||||
11: 'одиннадцать',
|
||||
22: 'двадцать два',
|
||||
33: 'тридцать три',
|
||||
};
|
||||
|
||||
static const Map<int, String> titles = {
|
||||
1: '1 — Первопроходец (Монада)',
|
||||
2: '2 — Дипломат (Дуада)',
|
||||
3: '3 — Творец (Триада)',
|
||||
4: '4 — Строитель (Тетрада)',
|
||||
5: '5 — Странник (Пентада)',
|
||||
6: '6 — Хранитель очага (Гексада)',
|
||||
7: '7 — Искатель истины (Септенер)',
|
||||
8: '8 — Владыка материи (Октава)',
|
||||
9: '9 — Мудрец (Эннеада)',
|
||||
11: '11 — Духовный проводник',
|
||||
22: '22 — Великий архитектор',
|
||||
33: '33 — Служение и 33 ряда домов',
|
||||
};
|
||||
|
||||
static int reduce(int n, {bool keepMaster = true}) {
|
||||
var cur = n.abs();
|
||||
while (cur > 9) {
|
||||
if (keepMaster && (cur == 11 || cur == 22 || cur == 33)) return cur;
|
||||
var sum = 0;
|
||||
var x = cur;
|
||||
while (x > 0) {
|
||||
sum += x % 10;
|
||||
x ~/= 10;
|
||||
}
|
||||
cur = sum;
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
static DayCode ofDate(DateTime date) {
|
||||
final day = reduce(date.day);
|
||||
final month = reduce(date.month);
|
||||
final year = reduce(date.year);
|
||||
final value = reduce(day + month + year);
|
||||
return DayCode(
|
||||
value: value,
|
||||
reducedDay: day,
|
||||
reducedMonth: month,
|
||||
reducedYear: year,
|
||||
word: words[value] ?? '',
|
||||
title: titles[value] ?? '$value',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../models/external_symbol_model.dart';
|
||||
|
||||
class ExternalSymbolsRepository {
|
||||
static final ExternalSymbolsRepository _instance = ExternalSymbolsRepository._internal();
|
||||
factory ExternalSymbolsRepository() => _instance;
|
||||
ExternalSymbolsRepository._internal();
|
||||
|
||||
List<ExternalSymbol> _symbols = [];
|
||||
bool _isLoaded = false;
|
||||
|
||||
List<ExternalSymbol> get allSymbols => _symbols;
|
||||
bool get isLoaded => _isLoaded;
|
||||
int get totalCount => _symbols.length;
|
||||
|
||||
Future<void> init() async {
|
||||
if (_isLoaded) return;
|
||||
|
||||
try {
|
||||
String jsonStr = '';
|
||||
|
||||
// 1. Попытка чтения напрямую из файловой системы (для dev / test)
|
||||
for (final p in ['assets/data/oracle_external_symbols.json', '../assets/data/oracle_external_symbols.json']) {
|
||||
final f = File(p);
|
||||
if (f.existsSync()) {
|
||||
jsonStr = f.readAsStringSync();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. В релизе загружаем через rootBundle
|
||||
if (jsonStr.isEmpty && !Platform.environment.containsKey('FLUTTER_TEST')) {
|
||||
try {
|
||||
jsonStr = await rootBundle.loadString('assets/data/oracle_external_symbols.json');
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (jsonStr.isNotEmpty) {
|
||||
final decoded = json.decode(jsonStr) as Map<String, dynamic>;
|
||||
_symbols = decoded.values
|
||||
.map((v) => ExternalSymbol.fromMap(Map<String, dynamic>.from(v as Map)))
|
||||
.toList();
|
||||
|
||||
// Сортируем по алфавиту
|
||||
_symbols.sort((a, b) => a.term.toLowerCase().compareTo(b.term.toLowerCase()));
|
||||
_isLoaded = true;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[ExternalSymbolsRepository] Ошибка загрузки: $e');
|
||||
}
|
||||
}
|
||||
|
||||
List<ExternalSymbol> search(String query, {bool onlyWithImages = false}) {
|
||||
final q = query.trim().toLowerCase();
|
||||
return _symbols.where((s) {
|
||||
if (onlyWithImages && !s.hasIllustrations) return false;
|
||||
if (q.isEmpty) return true;
|
||||
if (s.term.toLowerCase().contains(q)) return true;
|
||||
if (s.normalized.contains(q)) return true;
|
||||
if (s.sources.any((src) => src.author.toLowerCase().contains(q) || src.excerpt.toLowerCase().contains(q))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/houses_session_model.dart';
|
||||
import '../models/word_model.dart';
|
||||
import 'words_repository.dart';
|
||||
|
||||
class HousesService {
|
||||
static final HousesService _instance = HousesService._internal();
|
||||
factory HousesService() => _instance;
|
||||
HousesService._internal();
|
||||
|
||||
static const List<Color> sectorColors = [
|
||||
Color(0xFFFFB300), // 0: Золото Солнца
|
||||
Color(0xFFE53935), // 1: Рубиновое Пламя
|
||||
Color(0xFF1E88E5), // 2: Лазурный Эфир
|
||||
Color(0xFF43A047), // 3: Изумрудная Чаща
|
||||
Color(0xFF8E24AA), // 4: Аметистовая Тайна
|
||||
Color(0xFF00ACC1), // 5: Бирюзовая Бездна
|
||||
Color(0xFFFF7043), // 6: Коралловый Горн
|
||||
Color(0xFF90A4AE), // 7: Лунное Серебро
|
||||
Color(0xFF5C6BC0), // 8: Индиго Сумерек
|
||||
Color(0xFF8D6E63), // 9: Бронзовая Твердь
|
||||
];
|
||||
|
||||
static const List<String> sectorTitles = [
|
||||
'I. Сектор Золотого Сияния',
|
||||
'II. Сектор Алого Огня',
|
||||
'III. Сектор Небесных Вод',
|
||||
'IV. Сектор Вечного Роста',
|
||||
'V. Сектор Глубинных Тайн',
|
||||
'VI. Сектор Предельного Взора',
|
||||
'VII. Сектор Творящей Воли',
|
||||
'VIII. Сектор Зеркальной Памяти',
|
||||
'IX. Сектор Полночного Суда',
|
||||
'X. Сектор Земного Основания',
|
||||
];
|
||||
|
||||
/// Generates a freshly shuffled 33x30 field (990 houses + 10 Archons).
|
||||
/// If [querentWord] is given, guarantees that the querent resides in one of the 990 houses
|
||||
/// and dynamically calculates [querentHouseIndex] to point to that exact house.
|
||||
HousesSession createNewSession({String? question, WordItem? querentWord, int? querentHouseIndex}) {
|
||||
final repo = WordsRepository();
|
||||
final allWords = List<WordItem>.from(repo.allWords);
|
||||
|
||||
// Cryptographic shuffle for pristine randomness
|
||||
final rnd = Random.secure();
|
||||
allWords.shuffle(rnd);
|
||||
|
||||
bool isNoun(WordItem w) {
|
||||
final pos = w.partOfSpeech.toLowerCase().trim();
|
||||
if (pos != 'существительное') return false;
|
||||
final cat = w.category.toLowerCase();
|
||||
if (cat.contains('действие') || w.operator == 'shifter') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 1. Ensure Archons (indexes 0..9) are strictly nouns (существительное)
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (!isNoun(allWords[i])) {
|
||||
final swapIdx = allWords.indexWhere((w) => isNoun(w), 10);
|
||||
if (swapIdx != -1) {
|
||||
final temp = allWords[i];
|
||||
allWords[i] = allWords[swapIdx];
|
||||
allWords[swapIdx] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. If querentWord is provided, ensure it is in the Houses pool (indexes 10..999),
|
||||
// not in the Archons (indexes 0..9), so Querent always has a physical House.
|
||||
if (querentWord != null) {
|
||||
final qLower = querentWord.word.toLowerCase().trim();
|
||||
if (querentHouseIndex != null && querentHouseIndex >= 0 && querentHouseIndex < 990) {
|
||||
final qIdx = allWords.indexWhere(
|
||||
(w) => w.id == querentWord.id || w.word.toLowerCase().trim() == qLower,
|
||||
);
|
||||
final targetIdx = 10 + querentHouseIndex;
|
||||
if (qIdx >= 0 && qIdx != targetIdx) {
|
||||
final temp = allWords[targetIdx];
|
||||
allWords[targetIdx] = allWords[qIdx];
|
||||
allWords[qIdx] = temp;
|
||||
// If the swap displaced a noun from archon, re-verify archon is a noun
|
||||
if (qIdx < 10 && !isNoun(allWords[qIdx])) {
|
||||
final nounIdx = allWords.indexWhere(
|
||||
(w) => isNoun(w) &&
|
||||
w.id != querentWord.id &&
|
||||
w.word.toLowerCase().trim() != qLower,
|
||||
10,
|
||||
);
|
||||
if (nounIdx != -1) {
|
||||
final t2 = allWords[qIdx];
|
||||
allWords[qIdx] = allWords[nounIdx];
|
||||
allWords[nounIdx] = t2;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final archonMatchIdx = allWords.sublist(0, 10).indexWhere(
|
||||
(w) => w.id == querentWord.id || w.word.toLowerCase().trim() == qLower,
|
||||
);
|
||||
if (archonMatchIdx >= 0) {
|
||||
// Swap with a noun from the houses pool
|
||||
final swapTarget = allWords.indexWhere(
|
||||
(w) => isNoun(w),
|
||||
10,
|
||||
);
|
||||
final validTarget = swapTarget != -1 ? swapTarget : 10 + rnd.nextInt(990);
|
||||
final temp = allWords[archonMatchIdx];
|
||||
allWords[archonMatchIdx] = allWords[validTarget];
|
||||
allWords[validTarget] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final absolute guarantee: strictly ensure every Archon (0..9) is a noun
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (!isNoun(allWords[i])) {
|
||||
final swapIdx = allWords.indexWhere((w) => isNoun(w), 10);
|
||||
if (swapIdx != -1) {
|
||||
final temp = allWords[i];
|
||||
allWords[i] = allWords[swapIdx];
|
||||
allWords[swapIdx] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1. First 10 words -> 10 Archons ruling the 10 column triads
|
||||
final sectors = <ArchonSector>[];
|
||||
for (int i = 0; i < 10; i++) {
|
||||
sectors.add(
|
||||
ArchonSector(
|
||||
index: i,
|
||||
archonWord: allWords[i],
|
||||
startCol: i * 3,
|
||||
endCol: i * 3 + 2,
|
||||
sectorColor: sectorColors[i],
|
||||
title: sectorTitles[i],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Remaining 990 words -> 33 rows x 30 columns
|
||||
final grid = <HouseCell>[];
|
||||
final housesPool = allWords.sublist(10, 1000); // exactly 990 words
|
||||
int? resolvedQuerentIndex;
|
||||
|
||||
for (int i = 0; i < housesPool.length; i++) {
|
||||
final row = i ~/ 30; // 0..32
|
||||
final col = i % 30; // 0..29
|
||||
final sectorIndex = col ~/ 3; // 0..9
|
||||
final houseWord = housesPool[i];
|
||||
|
||||
if (querentWord != null &&
|
||||
resolvedQuerentIndex == null &&
|
||||
(houseWord.id == querentWord.id ||
|
||||
houseWord.word.toLowerCase().trim() == querentWord.word.toLowerCase().trim())) {
|
||||
resolvedQuerentIndex = i;
|
||||
}
|
||||
|
||||
grid.add(
|
||||
HouseCell(
|
||||
row: row,
|
||||
col: col,
|
||||
houseIndex: i,
|
||||
houseWord: houseWord,
|
||||
sectorIndex: sectorIndex,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final effectiveQuerentIndex = querentHouseIndex ?? resolvedQuerentIndex;
|
||||
final id = 'houses_${DateTime.now().millisecondsSinceEpoch}_${rnd.nextInt(10000)}';
|
||||
|
||||
return HousesSession(
|
||||
id: id,
|
||||
createdAt: DateTime.now(),
|
||||
question: question,
|
||||
querentWord: querentWord,
|
||||
querentHouseIndex: effectiveQuerentIndex,
|
||||
sectors: sectors,
|
||||
grid: grid,
|
||||
selectedHouses: const [],
|
||||
chains: const [],
|
||||
);
|
||||
}
|
||||
|
||||
/// Casts guest symbols into the selected houses (1..9) and computes the house chains.
|
||||
/// Adds one newly revealed house into the session and recalculates chains.
|
||||
HousesSession revealHouse(
|
||||
HousesSession session, {
|
||||
required HouseCell cell,
|
||||
required WordItem guestWord,
|
||||
}) {
|
||||
final newOrder = session.selectedHouses.length + 1;
|
||||
final updatedCell = cell.copyWith(
|
||||
guestWord: guestWord,
|
||||
selectionOrder: newOrder,
|
||||
);
|
||||
|
||||
final updatedSelected = List<HouseCell>.from(session.selectedHouses)..add(updatedCell);
|
||||
|
||||
final fullGridMap = <String, HouseCell>{};
|
||||
for (final h in session.grid) {
|
||||
fullGridMap[h.houseWord.word.toLowerCase().trim()] = h;
|
||||
}
|
||||
|
||||
final openedHousesMap = <String, HouseCell>{};
|
||||
for (final h in updatedSelected) {
|
||||
openedHousesMap[h.houseWord.word.toLowerCase().trim()] = h;
|
||||
}
|
||||
|
||||
final chains = _computeChains(updatedSelected, openedHousesMap, fullGridMap, session.sectors);
|
||||
|
||||
final updatedGrid = session.grid.map((c) {
|
||||
if (c.houseIndex == cell.houseIndex) {
|
||||
return updatedCell;
|
||||
}
|
||||
return c;
|
||||
}).toList();
|
||||
|
||||
return HousesSession(
|
||||
id: session.id,
|
||||
createdAt: session.createdAt,
|
||||
question: session.question,
|
||||
querentWord: session.querentWord,
|
||||
querentHouseIndex: session.querentHouseIndex,
|
||||
sectors: session.sectors,
|
||||
grid: updatedGrid,
|
||||
selectedHouses: updatedSelected,
|
||||
chains: chains,
|
||||
);
|
||||
}
|
||||
|
||||
HousesSession completeDivination(
|
||||
HousesSession session,
|
||||
List<HouseCell> selectedHouses,
|
||||
) {
|
||||
if (selectedHouses.isEmpty) return session;
|
||||
|
||||
final rnd = Random.secure();
|
||||
final repo = WordsRepository();
|
||||
final availableGuests = List<WordItem>.from(repo.allWords)..shuffle(rnd);
|
||||
|
||||
// Assign guests uniquely
|
||||
final populatedHouses = <HouseCell>[];
|
||||
for (int i = 0; i < selectedHouses.length; i++) {
|
||||
final guest = availableGuests[i];
|
||||
populatedHouses.add(
|
||||
selectedHouses[i].copyWith(
|
||||
guestWord: guest,
|
||||
selectionOrder: i + 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Map houses on the full grid by word name for instant lookup
|
||||
final fullGridMap = <String, HouseCell>{};
|
||||
for (final h in session.grid) {
|
||||
fullGridMap[h.houseWord.word.toLowerCase().trim()] = h;
|
||||
}
|
||||
|
||||
// Map populated/selected houses by word name
|
||||
final openedHousesMap = <String, HouseCell>{};
|
||||
for (final h in populatedHouses) {
|
||||
openedHousesMap[h.houseWord.word.toLowerCase().trim()] = h;
|
||||
}
|
||||
|
||||
// Compute House Chains
|
||||
final chains = _computeChains(populatedHouses, openedHousesMap, fullGridMap, session.sectors);
|
||||
|
||||
// Update grid state with guests
|
||||
final updatedGrid = session.grid.map((cell) {
|
||||
final found = populatedHouses.firstWhere(
|
||||
(p) => p.row == cell.row && p.col == cell.col,
|
||||
orElse: () => cell,
|
||||
);
|
||||
return found;
|
||||
}).toList();
|
||||
|
||||
return HousesSession(
|
||||
id: session.id,
|
||||
createdAt: session.createdAt,
|
||||
question: session.question,
|
||||
querentWord: session.querentWord,
|
||||
querentHouseIndex: session.querentHouseIndex,
|
||||
sectors: session.sectors,
|
||||
grid: updatedGrid,
|
||||
selectedHouses: populatedHouses,
|
||||
chains: chains,
|
||||
);
|
||||
}
|
||||
|
||||
List<HouseChain> _computeChains(
|
||||
List<HouseCell> populatedHouses,
|
||||
Map<String, HouseCell> openedMap,
|
||||
Map<String, HouseCell> fullGridMap,
|
||||
List<ArchonSector> sectors,
|
||||
) {
|
||||
final chains = <HouseChain>[];
|
||||
final repo = WordsRepository();
|
||||
final allWords = repo.allWords;
|
||||
|
||||
final archonMap = <String, ArchonSector>{};
|
||||
for (final s in sectors) {
|
||||
archonMap[s.archonWord.word.toLowerCase().trim()] = s;
|
||||
}
|
||||
|
||||
int chainIdx = 1;
|
||||
for (final startHouse in populatedHouses) {
|
||||
final steps = <HouseChainStep>[];
|
||||
final visitedHousesInChain = <String>{};
|
||||
|
||||
HouseCell curHouse = startHouse;
|
||||
WordItem curGuest = startHouse.guestWord!;
|
||||
ChainEndType endType = ChainEndType.fieldVector;
|
||||
String desc = '';
|
||||
|
||||
// Max 9 links per chain; early break if loop or archon throne reached
|
||||
for (int stepNum = 0; stepNum < 9; stepNum++) {
|
||||
final curHouseKey = curHouse.houseWord.word.toLowerCase().trim();
|
||||
final curGuestKey = curGuest.word.toLowerCase().trim();
|
||||
visitedHousesInChain.add(curHouseKey);
|
||||
|
||||
// 1. Check if curGuest is one of the 10 Archons
|
||||
final archonSector = archonMap[curGuestKey];
|
||||
|
||||
if (archonSector != null) {
|
||||
steps.add(
|
||||
HouseChainStep(
|
||||
house: curHouse,
|
||||
guest: curGuest,
|
||||
leadsToArchon: true,
|
||||
destinationArchon: archonSector,
|
||||
),
|
||||
);
|
||||
endType = ChainEndType.archonThrone;
|
||||
desc = 'Восхождение к Престолу Архонта «${curGuest.word.toUpperCase()}» (Триада ${archonSector.index + 1})';
|
||||
break;
|
||||
}
|
||||
|
||||
// 2. Find next house on the 990 field
|
||||
final nextHouse = fullGridMap[curGuestKey];
|
||||
if (nextHouse == null) {
|
||||
steps.add(HouseChainStep(house: curHouse, guest: curGuest));
|
||||
desc = 'Импульс уходит за пределы полотна к знаку «${curGuest.word.toUpperCase()}»';
|
||||
break;
|
||||
}
|
||||
|
||||
final nextHouseKey = nextHouse.houseWord.word.toLowerCase().trim();
|
||||
|
||||
// 3. Check if loop closed
|
||||
if (visitedHousesInChain.contains(nextHouseKey)) {
|
||||
steps.add(
|
||||
HouseChainStep(
|
||||
house: curHouse,
|
||||
guest: curGuest,
|
||||
isLoopClosing: true,
|
||||
),
|
||||
);
|
||||
endType = ChainEndType.loop;
|
||||
desc = 'Замкнутая петля судьбы: замыкается на Дом «${nextHouse.houseWord.word.toUpperCase()}»';
|
||||
break;
|
||||
}
|
||||
|
||||
// 4. Advance to next house
|
||||
steps.add(HouseChainStep(house: curHouse, guest: curGuest));
|
||||
|
||||
// Determine next guest. An unopened house does not invent a chain.
|
||||
final openedNext = openedMap[nextHouseKey];
|
||||
if (openedNext != null && openedNext.guestWord != null) {
|
||||
curGuest = openedNext.guestWord!;
|
||||
curHouse = nextHouse;
|
||||
} else {
|
||||
final guestIdx = (nextHouse.houseIndex * 7919 + 101) % allWords.length;
|
||||
final computedGuest = allWords[guestIdx];
|
||||
steps.add(
|
||||
HouseChainStep(
|
||||
house: nextHouse,
|
||||
guest: computedGuest,
|
||||
computed: true,
|
||||
),
|
||||
);
|
||||
endType = ChainEndType.fieldVector;
|
||||
desc =
|
||||
'Дом «${nextHouse.houseWord.word.toUpperCase()}» не открыт. Знак «${computedGuest.word.toUpperCase()}» вычислен из поля, это не жребий. Цепь здесь останавливается.';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (desc.isEmpty) {
|
||||
desc = 'Линейный вектор: луч энергии уходит сквозь полотно к знаку «${steps.last.guest.word.toUpperCase()}»';
|
||||
}
|
||||
|
||||
chains.add(
|
||||
HouseChain(
|
||||
chainIndex: chainIdx++,
|
||||
startingHouse: startHouse,
|
||||
steps: steps,
|
||||
endType: endType,
|
||||
description: desc,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return chains;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
|
||||
import '../models/lenormand_pairs.dart';
|
||||
|
||||
class LenormandPairsRepository {
|
||||
static final LenormandPairsRepository _instance =
|
||||
LenormandPairsRepository._internal();
|
||||
factory LenormandPairsRepository() => _instance;
|
||||
LenormandPairsRepository._internal();
|
||||
|
||||
final Map<int, LenormandCardEntry> _byN = {};
|
||||
List<LenormandTriad> _triads = const [];
|
||||
List<LenormandExtra> _extras = const [];
|
||||
bool _loaded = false;
|
||||
|
||||
bool get isLoaded => _loaded;
|
||||
List<LenormandCardEntry> get cards =>
|
||||
List.unmodifiable(_byN.values.toList()..sort((a, b) => a.n.compareTo(b.n)));
|
||||
List<LenormandTriad> get triads => List.unmodifiable(_triads);
|
||||
List<LenormandExtra> get extras => List.unmodifiable(_extras);
|
||||
|
||||
LenormandCardEntry? byNumber(int n) => _byN[n];
|
||||
|
||||
LenormandCardEntry? byStationId(String id) {
|
||||
if (id.startsWith('card_')) {
|
||||
final n = int.tryParse(id.substring(5));
|
||||
if (n != null) return _byN[n];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> init() async {
|
||||
if (_loaded && _byN.isNotEmpty) return;
|
||||
Map<String, dynamic>? decoded;
|
||||
final cwd = Directory.current.path;
|
||||
final candidates = <String>{
|
||||
'assets/data/lenormand_pairs.json',
|
||||
'../assets/data/lenormand_pairs.json',
|
||||
'$cwd/assets/data/lenormand_pairs.json',
|
||||
'$cwd/../assets/data/lenormand_pairs.json',
|
||||
};
|
||||
for (final path in candidates) {
|
||||
try {
|
||||
final file = File(path);
|
||||
if (file.existsSync()) {
|
||||
decoded = json.decode(file.readAsStringSync()) as Map<String, dynamic>;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('lenormand_pairs.json skip $path: $e');
|
||||
}
|
||||
}
|
||||
if (decoded == null) {
|
||||
try {
|
||||
final jsonStr = await rootBundle.loadString('assets/data/lenormand_pairs.json');
|
||||
decoded = json.decode(jsonStr) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint('Could not load lenormand_pairs.json: $e');
|
||||
}
|
||||
}
|
||||
if (decoded == null) {
|
||||
_loaded = true;
|
||||
return;
|
||||
}
|
||||
_byN.clear();
|
||||
for (final raw in (decoded['cards'] as List? ?? const [])) {
|
||||
if (raw is! Map<String, dynamic>) continue;
|
||||
final card = LenormandCardEntry.fromMap(raw);
|
||||
_byN[card.n] = card;
|
||||
}
|
||||
_triads = (decoded['triads'] as List? ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(LenormandTriad.fromMap)
|
||||
.toList();
|
||||
_extras = (decoded['extras'] as List? ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(LenormandExtra.fromMap)
|
||||
.toList();
|
||||
_loaded = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../models/culture_branch.dart';
|
||||
import '../models/word_model.dart';
|
||||
|
||||
/// Роли ручного бурения. Человек сам отвечает вместо моделей.
|
||||
class DiveRole {
|
||||
final String id;
|
||||
final String title;
|
||||
final String hint;
|
||||
|
||||
const DiveRole(this.id, this.title, this.hint);
|
||||
}
|
||||
|
||||
const List<DiveRole> kDiveRoles = [
|
||||
DiveRole(
|
||||
'route_cultures',
|
||||
'Маршрут культур',
|
||||
'Три традиции, где у знака есть именной след. Ответ — названия через запятую.',
|
||||
),
|
||||
DiveRole(
|
||||
'route_languages',
|
||||
'Маршрут языков',
|
||||
'Три языка с прямым корнем или устойчивым именем образа. Ответ — названия через запятую.',
|
||||
),
|
||||
DiveRole(
|
||||
'mystic',
|
||||
'Мистик',
|
||||
'Закон знака: устройство, свет, тень и какие системы держат именно это слово.',
|
||||
),
|
||||
DiveRole(
|
||||
'psycho',
|
||||
'Психоаналитик',
|
||||
'Один психический механизм этого знака, не архетип вообще.',
|
||||
),
|
||||
DiveRole(
|
||||
'synth',
|
||||
'Синтезатор',
|
||||
'Сжимает уже вписанные ответы в карточку. JSON можно разложить по полям.',
|
||||
),
|
||||
];
|
||||
|
||||
class DiveSlot {
|
||||
final String id;
|
||||
final String title;
|
||||
|
||||
const DiveSlot(this.id, this.title);
|
||||
}
|
||||
|
||||
const List<DiveSlot> kDiveLanguages = [
|
||||
DiveSlot('шумерский', 'шумерский'),
|
||||
DiveSlot('древнеегипетский', 'древнеегипетский'),
|
||||
DiveSlot('иврит', 'иврит'),
|
||||
DiveSlot('древнегреческий', 'древнегреческий'),
|
||||
DiveSlot('латынь', 'латынь'),
|
||||
DiveSlot('древнеирландский', 'древнеирландский'),
|
||||
DiveSlot('древнескандинавский', 'древнескандинавский'),
|
||||
DiveSlot('старославянский', 'старославянский'),
|
||||
DiveSlot('санскрит', 'санскрит'),
|
||||
DiveSlot('авестийский', 'авестийский'),
|
||||
DiveSlot('древнекитайский', 'древнекитайский'),
|
||||
DiveSlot('японский', 'японский'),
|
||||
DiveSlot('арабский', 'арабский'),
|
||||
DiveSlot('майя', 'майя'),
|
||||
DiveSlot('церковнославянский', 'церковнославянский'),
|
||||
];
|
||||
|
||||
const List<DiveSlot> kDiveTraditions = [
|
||||
DiveSlot('runes', 'Руны'),
|
||||
DiveSlot('tarot', 'Таро'),
|
||||
DiveSlot('ogham', 'Огам'),
|
||||
DiveSlot('kabbalah', 'Каббала'),
|
||||
DiveSlot('lenormand', 'Ленорман'),
|
||||
DiveSlot('geomancy', 'Геомантия'),
|
||||
DiveSlot('iching', 'И Цзин'),
|
||||
DiveSlot('alchemy', 'Алхимия'),
|
||||
DiveSlot('astrology', 'Астрология'),
|
||||
DiveSlot('cosmology', 'Космология'),
|
||||
DiveSlot('slavic', 'Славянский канон'),
|
||||
];
|
||||
|
||||
const Map<String, String> _languageAliases = {
|
||||
'греческий': 'древнегреческий',
|
||||
'китайский': 'древнекитайский',
|
||||
'египетский': 'древнеегипетский',
|
||||
'скандинавский': 'древнескандинавский',
|
||||
'ирландский': 'древнеирландский',
|
||||
'славянский': 'старославянский',
|
||||
'церковный славянский': 'церковнославянский',
|
||||
};
|
||||
|
||||
String _dossier(WordItem word) {
|
||||
final pos = word.partOfSpeech.trim().isEmpty ? 'не указана' : word.partOfSpeech.trim();
|
||||
final category = word.category.trim().isEmpty ? 'не указана' : word.category.trim();
|
||||
final buffer = StringBuffer()
|
||||
..writeln('Знак Оракула 999: «${word.word}».')
|
||||
..writeln('Часть речи: $pos. Категория: $category.');
|
||||
if (word.core.trim().isNotEmpty) {
|
||||
buffer.writeln('Текущее ядро, его можно уточнить, но не подменяй другим знаком: ${word.core.trim()}');
|
||||
}
|
||||
return buffer.toString().trimRight();
|
||||
}
|
||||
|
||||
String cultureRoutePrompt(WordItem word) {
|
||||
final menu = kCultureBranches.map((b) => b.name).join('; ');
|
||||
return '${_dossier(word)}\n'
|
||||
'Выбери ровно 3 традиции, где у ЭТОГО образа есть именной миф, текст или обряд, '
|
||||
'а не просто «похожая идея».\n'
|
||||
'Только из списка: $menu.\n'
|
||||
'Ответ: три названия через запятую. Без пояснений.';
|
||||
}
|
||||
|
||||
String languageRoutePrompt(WordItem word) {
|
||||
final menu = kDiveLanguages.map((b) => b.title).join('; ');
|
||||
return '${_dossier(word)}\n'
|
||||
'Выбери ровно 3 языка, где есть прямой корень, устойчивый перевод этого образа '
|
||||
'или его иероглиф. Не выбирай язык только потому, что он древний.\n'
|
||||
'Только из списка: $menu.\n'
|
||||
'Ответ: три названия через запятую. Без пояснений.';
|
||||
}
|
||||
|
||||
String mysticPrompt(WordItem word) {
|
||||
return 'Ты мистик Оракула 999. Разбираешь один знак как закон, не как гороскоп. '
|
||||
'Пиши по-русски. Не предсказывай любовь, деньги и здоровье. '
|
||||
'Не занимай чужую карту, руну или гексаграмму, если этот знак ею не является. '
|
||||
'Если след тонок — так и скажи.\n\n'
|
||||
'${_dossier(word)}\n'
|
||||
'Дай: устройство образа; светлую сторону того же закона; теневую сторону того же закона; '
|
||||
'какие системы (Таро, руны, Ленорман, геомантия, И Цзин, алхимия, астрология) '
|
||||
'держат именно это слово, а какие только соседние. Соседние не присваивай.';
|
||||
}
|
||||
|
||||
String psychoPrompt(WordItem word) {
|
||||
return 'Ты юнгианский аналитик. Один знак — один психический механизм. '
|
||||
'Пиши по-русски, без лекции про индивидуацию вообще. '
|
||||
'Назови, что человек делает с этим образом, когда он интегрирован, '
|
||||
'и что делает, когда вытесняет.\n\n'
|
||||
'${_dossier(word)}\n'
|
||||
'Разбери только его, не архетип «вообще».';
|
||||
}
|
||||
|
||||
String cultureSlicePrompt(WordItem word, String culture) {
|
||||
return 'Ты мифолог одной традиции: $culture. Другие культуры запрещены. '
|
||||
'Пиши по-русски. Если у традиции нет достоверного следа этого образа, '
|
||||
'ответь ровно фразой «следа нет» и остановись. Не заполняй пустоту соседним мифом. '
|
||||
'Нужны: исконное слово или знак, транслитерация, имя текста или мифа, '
|
||||
'что образ делает, чего не позволяет.\n\n'
|
||||
'${_dossier(word)}\n'
|
||||
'Традиция: $culture.';
|
||||
}
|
||||
|
||||
String languageSlicePrompt(WordItem word, String language) {
|
||||
return 'Ты этимолог одного языка: $language. Другие языки запрещены. '
|
||||
'Пиши по-русски. Если у русского слова нет корня в этом языке, '
|
||||
'скажи «прямого корня нет» и приведи только ближайшее культурное слово этого языка '
|
||||
'для того же предмета, не выдавая его за этимон. '
|
||||
'Нужны: форма, транслитерация, сдвиг значения, один источник или памятник.\n\n'
|
||||
'${_dossier(word)}\n'
|
||||
'Язык: $language.';
|
||||
}
|
||||
|
||||
String traditionSlicePrompt(WordItem word, String tradition) {
|
||||
return 'Ты хранитель одной системы: $tradition. Другие системы запрещены. '
|
||||
'Пиши по-русски. Если этот знак сам не является картой, руной, фигурой или станцией '
|
||||
'этой системы, не занимай чужую станцию: напиши, чем знак лишь сосед, '
|
||||
'или что прямого места нет.\n\n'
|
||||
'${_dossier(word)}\n'
|
||||
'Система: $tradition.\n'
|
||||
'Ответ: 2–5 предложений закона именно этого слова внутри системы.';
|
||||
}
|
||||
|
||||
String rolePrompt(String roleId, WordItem word) {
|
||||
switch (roleId) {
|
||||
case 'route_cultures':
|
||||
return cultureRoutePrompt(word);
|
||||
case 'route_languages':
|
||||
return languageRoutePrompt(word);
|
||||
case 'mystic':
|
||||
return mysticPrompt(word);
|
||||
case 'psycho':
|
||||
return psychoPrompt(word);
|
||||
default:
|
||||
return _dossier(word);
|
||||
}
|
||||
}
|
||||
|
||||
String synthesizerPrompt({
|
||||
required WordItem word,
|
||||
required Map<String, String> reports,
|
||||
required String core,
|
||||
required String oracle,
|
||||
required String shadow,
|
||||
required String asks,
|
||||
required String step,
|
||||
required String binds,
|
||||
required Map<String, String> cultures,
|
||||
required Map<String, String> languages,
|
||||
required Map<String, String> traditions,
|
||||
}) {
|
||||
final payload = <String, dynamic>{
|
||||
'word': word.word,
|
||||
'reports': reports,
|
||||
'draft': {
|
||||
'core': core,
|
||||
'oracle': oracle,
|
||||
'shadow': shadow,
|
||||
'asks': asks,
|
||||
'step': step,
|
||||
'binds': binds,
|
||||
'cultures': cultures,
|
||||
'languages': languages,
|
||||
'traditions': traditions,
|
||||
},
|
||||
};
|
||||
return 'Ты архитектор Оракула 999. Сожми уже собранные ответы в один JSON без markdown. '
|
||||
'Не выдумывай культуру, язык или систему, если во входе пусто или стоит «следа нет».\n'
|
||||
'Ключи: word, core, oracle, shadow, asks, step, binds, layers, cultures, languages.\n'
|
||||
'core — одно предложение, закон знака.\n'
|
||||
'oracle — устройство знака, сосед в поле и цена. Культуры поимённо сюда не копируй.\n'
|
||||
'shadow — обратная сторона того же закона.\n'
|
||||
'asks — один вопрос. step — один физический жест, не «подумай».\n'
|
||||
'binds — до 6 русских слов знаков из тысячи, не сам знак.\n'
|
||||
'layers — список {system, title, law}. system из: '
|
||||
'${kDiveTraditions.map((s) => s.id).join(', ')}.\n'
|
||||
'cultures — объект. Ключ строго из списка традиций. Значение: 2–5 фраз.\n'
|
||||
'languages — объект. Ключ — имя языка, значение — этимологическая заметка.\n'
|
||||
'Запрещены заготовки: «сакральный символ», «созерцать символ», '
|
||||
'«готов ли ты принять этот урок», «ключевую сферу кристаллизации».\n'
|
||||
'Никакого текста вокруг JSON.\n\n'
|
||||
'${_dossier(word)}\n'
|
||||
'${jsonEncode(payload)}';
|
||||
}
|
||||
|
||||
/// Имена из ответа маршрута → точные ключи реестра.
|
||||
List<String> matchCultureNames(String answer) {
|
||||
final found = <String>[];
|
||||
for (final raw in answer.split(RegExp(r'[,;\n]+'))) {
|
||||
final token = raw.trim();
|
||||
if (token.isEmpty) continue;
|
||||
final branch = cultureBranchByName(token);
|
||||
if (branch != null && !found.contains(branch.name)) found.add(branch.name);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
List<String> matchLanguageNames(String answer) {
|
||||
final known = {for (final slot in kDiveLanguages) slot.id: slot.id};
|
||||
final found = <String>[];
|
||||
for (final raw in answer.split(RegExp(r'[,;\n]+'))) {
|
||||
final token = raw.trim().toLowerCase();
|
||||
if (token.isEmpty) continue;
|
||||
final name = known[token] ?? _languageAliases[token];
|
||||
if (name != null && !found.contains(name)) found.add(name);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
class DiveCanonDraft {
|
||||
final String? core;
|
||||
final String? oracle;
|
||||
final String? shadow;
|
||||
final String? asks;
|
||||
final String? step;
|
||||
final List<String>? binds;
|
||||
final Map<String, String>? cultures;
|
||||
final Map<String, String>? languages;
|
||||
final List<SymbolLayer>? layers;
|
||||
|
||||
const DiveCanonDraft({
|
||||
this.core,
|
||||
this.oracle,
|
||||
this.shadow,
|
||||
this.asks,
|
||||
this.step,
|
||||
this.binds,
|
||||
this.cultures,
|
||||
this.languages,
|
||||
this.layers,
|
||||
});
|
||||
}
|
||||
|
||||
DiveCanonDraft? parseDiveCanon(String text) {
|
||||
var src = text.trim();
|
||||
if (src.isEmpty) return null;
|
||||
if (src.startsWith('```')) {
|
||||
src = src.replaceFirst(RegExp(r'^```[a-zA-Z]*\n?'), '');
|
||||
if (src.endsWith('```')) src = src.substring(0, src.length - 3).trim();
|
||||
}
|
||||
final start = src.indexOf('{');
|
||||
final end = src.lastIndexOf('}');
|
||||
if (start < 0 || end <= start) return null;
|
||||
final dynamic decoded;
|
||||
try {
|
||||
decoded = jsonDecode(src.substring(start, end + 1));
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
if (decoded is! Map) return null;
|
||||
final map = decoded.map((key, value) => MapEntry(key.toString(), value));
|
||||
|
||||
List<String>? binds;
|
||||
final rawBinds = map['binds'];
|
||||
if (rawBinds is List) {
|
||||
binds = rawBinds.map((e) => e.toString().trim()).where((e) => e.isNotEmpty).toList();
|
||||
} else if (rawBinds is String && rawBinds.trim().isNotEmpty) {
|
||||
binds = rawBinds.split(RegExp(r'[,;]+')).map((e) => e.trim()).where((e) => e.isNotEmpty).toList();
|
||||
}
|
||||
|
||||
Map<String, String>? cultures;
|
||||
if (map['cultures'] is Map) {
|
||||
cultures = {};
|
||||
for (final entry in (map['cultures'] as Map).entries) {
|
||||
final branch = cultureBranchByName(entry.key.toString());
|
||||
final key = branch?.name ?? entry.key.toString().trim();
|
||||
final value = entry.value?.toString().trim() ?? '';
|
||||
if (key.isNotEmpty && value.isNotEmpty && value != 'следа нет') cultures[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String>? languages;
|
||||
if (map['languages'] is Map) {
|
||||
languages = {};
|
||||
for (final entry in (map['languages'] as Map).entries) {
|
||||
final names = matchLanguageNames(entry.key.toString());
|
||||
final key = names.isEmpty ? entry.key.toString().trim().toLowerCase() : names.first;
|
||||
final value = entry.value?.toString().trim() ?? '';
|
||||
if (key.isNotEmpty && value.isNotEmpty) languages[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
List<SymbolLayer>? layers;
|
||||
if (map['layers'] is List) {
|
||||
layers = [];
|
||||
for (final item in map['layers'] as List) {
|
||||
if (item is! Map) continue;
|
||||
final law = item['law']?.toString().trim() ?? '';
|
||||
if (law.isEmpty) continue;
|
||||
final systemRaw = (item['system'] ?? item['title'] ?? '').toString().trim().toLowerCase();
|
||||
DiveSlot? slot;
|
||||
for (final candidate in kDiveTraditions) {
|
||||
if (candidate.id == systemRaw || candidate.title.toLowerCase() == systemRaw) {
|
||||
slot = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (slot == null) continue;
|
||||
layers.add(SymbolLayer(system: slot.id, title: slot.title, law: law));
|
||||
}
|
||||
}
|
||||
|
||||
String? textOf(String key) {
|
||||
final value = map[key];
|
||||
if (value == null) return null;
|
||||
final text = value.toString().trim();
|
||||
return text.isEmpty ? null : text;
|
||||
}
|
||||
|
||||
return DiveCanonDraft(
|
||||
core: textOf('core'),
|
||||
oracle: textOf('oracle'),
|
||||
shadow: textOf('shadow'),
|
||||
asks: textOf('asks'),
|
||||
step: textOf('step'),
|
||||
binds: binds,
|
||||
cultures: cultures,
|
||||
languages: languages,
|
||||
layers: layers,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
|
||||
import '../models/lenormand_pairs.dart';
|
||||
|
||||
class OghamFedaRepository {
|
||||
static final OghamFedaRepository _instance = OghamFedaRepository._internal();
|
||||
factory OghamFedaRepository() => _instance;
|
||||
OghamFedaRepository._internal();
|
||||
|
||||
final Map<int, LenormandCardEntry> _byN = {};
|
||||
final Map<String, LenormandCardEntry> _byId = {};
|
||||
bool _loaded = false;
|
||||
|
||||
LenormandCardEntry? byNumber(int n) => _byN[n];
|
||||
LenormandCardEntry? byCanonId(String id) => _byId[id];
|
||||
List<LenormandCardEntry> get allFedas => _byN.values.toList()..sort((a, b) => a.n.compareTo(b.n));
|
||||
|
||||
Future<void> init() async {
|
||||
if (_loaded && _byN.isNotEmpty) return;
|
||||
Map<String, dynamic>? decoded;
|
||||
final cwd = Directory.current.path;
|
||||
for (final path in {
|
||||
'assets/data/ogham_feda.json',
|
||||
'../assets/data/ogham_feda.json',
|
||||
'$cwd/assets/data/ogham_feda.json',
|
||||
}) {
|
||||
try {
|
||||
final file = File(path);
|
||||
if (file.existsSync()) {
|
||||
decoded = json.decode(file.readAsStringSync()) as Map<String, dynamic>;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('ogham_feda.json skip $path: $e');
|
||||
}
|
||||
}
|
||||
if (decoded == null) {
|
||||
try {
|
||||
decoded = json.decode(await rootBundle.loadString('assets/data/ogham_feda.json'))
|
||||
as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint('Could not load ogham_feda.json: $e');
|
||||
}
|
||||
}
|
||||
if (decoded == null) {
|
||||
_loaded = true;
|
||||
return;
|
||||
}
|
||||
_byN.clear();
|
||||
_byId.clear();
|
||||
for (final raw in (decoded['cards'] as List? ?? const [])) {
|
||||
if (raw is! Map<String, dynamic>) continue;
|
||||
final card = LenormandCardEntry.fromMap(raw);
|
||||
_byN[card.n] = card;
|
||||
_byId[card.id] = card;
|
||||
}
|
||||
_loaded = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,977 @@
|
||||
import '../models/culture_branch.dart';
|
||||
import '../models/syncretic_session_model.dart';
|
||||
import '../models/history_entry_model.dart';
|
||||
import 'words_repository.dart';
|
||||
import '../models/houses_session_model.dart';
|
||||
import '../models/word_model.dart';
|
||||
import 'traditions_repository.dart';
|
||||
import 'lenormand_pairs_repository.dart';
|
||||
|
||||
class OracleMasterPromptGenerator {
|
||||
/// Generates the Grand Cross-System Comparison Master Prompt for Grok, Claude 3.5 Sonnet, GPT-4o, and Gemini 1.5 Pro.
|
||||
static String generateCrossSystemComparison(SyncreticSession session) {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
buffer.writeln('═══════════════════════════════════════════════════════════════════');
|
||||
buffer.writeln('ВЕЛИКИЙ МЕТА-МАСТЕР-ПРОМПТ: СРАВНИТЕЛЬНЫЙ СИНХРОНИСТИЧЕСКИЙ АНАЛИЗ');
|
||||
buffer.writeln('МУЛЬТИСИСТЕМНЫЙ СИНТЕЗ ТРАДИЦИЙ И САМОСОГЛАСОВАННОСТИ ЗНАКОВ');
|
||||
buffer.writeln('═══════════════════════════════════════════════════════════════════');
|
||||
buffer.writeln();
|
||||
|
||||
buffer.writeln('### СИСТЕМНАЯ РОЛЬ:');
|
||||
buffer.writeln('Ты — архимагистр синкретической герменевтики, мастер сравнительного мистицизма и глубинной юнгианской психологии.');
|
||||
buffer.writeln('Перед тобой единый протокол СИНХРОНИСТИЧЕСКОЙ СЕССИИ ДНЯ. Вопрошающий исследовал один вопрос через несколько независимых мантических систем древности и современности.');
|
||||
buffer.writeln('Твоя миссия — провести мета-анализ: сопоставить язык каждой системы, выявить скрытые узлы схождения, обнаружить противоречия и выдать монолитный синтез высшего порядка.');
|
||||
buffer.writeln();
|
||||
|
||||
// 1. Querent Question & Context
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('1. ФОКУС ВОПРОШАНИЯ (КВЕРЕНТ И КОНТЕКСТ ДНЯ):');
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('• Дата сессии: ' + session.formattedDate);
|
||||
if (session.mainQuestion != null && session.mainQuestion!.trim().isNotEmpty) {
|
||||
buffer.writeln('• Ключевой вопрос/тема: «' + session.mainQuestion!.trim() + '»');
|
||||
} else {
|
||||
buffer.writeln('• Ключевой вопрос/тема: Общий срез судьбы, скрытые подводные течения и фазовый переход кверента.');
|
||||
}
|
||||
buffer.writeln('• Число независимых гаданий в сессии: ' + session.readings.length.toString());
|
||||
buffer.writeln();
|
||||
|
||||
// 2. Protocols of all readings
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('2. ПРОТОКОЛЫ ПОЛУЧЕННЫХ РАСКЛАДОВ ПО СИСТЕМАМ:');
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
|
||||
final allMentionedWords = <WordItem>[];
|
||||
|
||||
for (int i = 0; i < session.readings.length; i++) {
|
||||
final r = session.readings[i];
|
||||
buffer.writeln('ВХОДНОЙ РАСКЛАД #' + (i + 1).toString() + ': ' + r.systemIcon + ' ' + r.systemName.toUpperCase());
|
||||
buffer.writeln(' Время проведения: ' + r.formattedTime + ' | Метод ввода: ' + (r.inputMethod == 'dice' ? 'Жребий костей' : 'Прямой ввод со стола'));
|
||||
buffer.writeln(' Формат расклада: ' + r.layoutTitle);
|
||||
if (r.question != null && r.question!.trim().isNotEmpty && r.question != session.mainQuestion) {
|
||||
buffer.writeln(' Уточняющий аспект: «' + r.question!.trim() + '»');
|
||||
}
|
||||
|
||||
if (r.systemId == 'geomancy') {
|
||||
final isShield = r.items.any((it) => it.title.toUpperCase().contains('СУДЬЯ') || it.title.toLowerCase().contains('мать'));
|
||||
if (isShield) {
|
||||
buffer.writeln(' 📐 СВЯЩЕННАЯ АРХИТЕКТУРА ВЕЛИКОГО ГЕОМАНТИЧЕСКОГО ЩИТА (16 САКРАЛЬНЫХ ФИГУР):');
|
||||
buffer.writeln(' (Тетраграммная пирамида: Матери ➔ Дочери ➔ Племянники ➔ Свидетели ➔ Судья ➔ Примиритель)');
|
||||
buffer.writeln();
|
||||
|
||||
void writeShieldItem(String roleHeader, ReadingSignItem it) {
|
||||
buffer.writeln(' ' + roleHeader);
|
||||
buffer.writeln(' • ' + (it.symbol != null ? it.symbol! + ' ' : '') + '«' + it.title.toUpperCase() + '»');
|
||||
if (it.subtitle != null && it.subtitle!.isNotEmpty) {
|
||||
buffer.writeln(' Атрибут: ' + it.subtitle!);
|
||||
}
|
||||
if (it.lore != null && it.lore!.isNotEmpty) {
|
||||
buffer.writeln(' Канонический закон/значение: ' + it.lore!);
|
||||
}
|
||||
if (it.linkedWord != null) {
|
||||
allMentionedWords.add(it.linkedWord!);
|
||||
buffer.writeln(' 🔗 Связанный сакральный знак 999: #' + it.linkedWord!.formattedSessionNumber + ' «' + it.linkedWord!.word.toUpperCase() + '» (' + it.linkedWord!.category + ')');
|
||||
if (it.linkedWord!.core.isNotEmpty) {
|
||||
buffer.writeln(' Формула знака: ' + it.linkedWord!.core);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReadingSignItem? findFirst(bool Function(ReadingSignItem) predicate) {
|
||||
for (final it in r.items) {
|
||||
if (predicate(it)) return it;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
final judge = findFirst((it) => it.title.toUpperCase().contains('СУДЬЯ'));
|
||||
final reconciler = findFirst((it) => it.title.toUpperCase().contains('ПРИМИРИТЕЛЬ'));
|
||||
final rightWitness = findFirst((it) => it.title.toUpperCase().contains('ПРАВЫЙ СВИДЕТЕЛЬ'));
|
||||
final leftWitness = findFirst((it) => it.title.toUpperCase().contains('ЛЕВЫЙ СВИДЕТЕЛЬ'));
|
||||
final mothers = r.items.where((it) => it.title.toLowerCase().contains('мать')).toList();
|
||||
final daughters = r.items.where((it) => it.title.toLowerCase().contains('дочь')).toList();
|
||||
final nephews = r.items.where((it) => it.title.toLowerCase().contains('племянник')).toList();
|
||||
|
||||
if (judge != null) {
|
||||
writeShieldItem('👑 ВЕРХОВНЫЙ СУДЬЯ (AL-QADI / JUDEX — ИТОГОВЫЙ ПРИГОВОР И ИСХОД СИТУАЦИИ):', judge);
|
||||
buffer.writeln();
|
||||
}
|
||||
|
||||
if (reconciler != null) {
|
||||
writeShieldItem("🕊️ ПРИМИРИТЕЛЬ (AL-MUJAMMI' / 16-Я ФИГУРА — СНЯТИЕ ПРОТИВОРЕЧИЯ ИСТОКА И СУДА):", reconciler);
|
||||
buffer.writeln();
|
||||
}
|
||||
|
||||
if (rightWitness != null || leftWitness != null) {
|
||||
buffer.writeln(' ⚖️ ДВА СВИДЕТЕЛЯ (SHAHIDAYN — ДИАЛЕКТИКА ВНЕШНЕГО И ВНУТРЕННЕГО):');
|
||||
if (rightWitness != null) {
|
||||
writeShieldItem(' [ПРАВЫЙ СВИДЕТЕЛЬ / ВНЕШНИЙ МИР И ОБСТОЯТЕЛЬСТВА]:', rightWitness);
|
||||
}
|
||||
if (leftWitness != null) {
|
||||
writeShieldItem(' [ЛЕВЫЙ СВИДЕТЕЛЬ / ВНУТРЕННЕЕ СОСТОЯНИЕ И ДУША КВЕРЕНТА]:', leftWitness);
|
||||
}
|
||||
buffer.writeln();
|
||||
}
|
||||
|
||||
if (mothers.isNotEmpty) {
|
||||
buffer.writeln(' 🏛️ ЧЕТЫРЕ МАТЕРИ (UMMAHAAT — КОРНИ И ПЕРВИЧНЫЙ СТИХИЙНЫЙ ИМПУЛЬС):');
|
||||
for (final m in mothers) {
|
||||
writeShieldItem(' [МАТЬ]:', m);
|
||||
}
|
||||
buffer.writeln();
|
||||
}
|
||||
|
||||
if (daughters.isNotEmpty) {
|
||||
buffer.writeln(' 🌿 ЧЕТЫРЕ ДОЧЕРИ (BANAAT — РАЗВЁРТЫВАНИЕ В ПРОСТРАНСТВЕ И ВНЕШНЕЙ СРЕДЕ):');
|
||||
for (final d in daughters) {
|
||||
writeShieldItem(' [ДОЧЬ]:', d);
|
||||
}
|
||||
buffer.writeln();
|
||||
}
|
||||
|
||||
if (nephews.isNotEmpty) {
|
||||
buffer.writeln(' ⚡ ЧЕТЫРЕ ПЛЕМЯННИКА (ABNAA — СОБЫТИЙНЫЙ СРЕЗ И ПСИХОЛОГИЯ ОКРУЖЕНИЯ):');
|
||||
for (final n in nephews) {
|
||||
writeShieldItem(' [ПЛЕМЯННИК]:', n);
|
||||
}
|
||||
buffer.writeln();
|
||||
}
|
||||
|
||||
buffer.writeln();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (final item in r.items) {
|
||||
final rev = item.isReversed ? ' [ПЕРЕВЁРНУТОЕ ПОЛОЖЕНИЕ / МЕРКСТЕЙВ]' : '';
|
||||
buffer.writeln(' • ' + (item.symbol != null ? item.symbol! + ' ' : '') + '«' + item.title.toUpperCase() + '»' + rev);
|
||||
if (item.subtitle != null && item.subtitle!.isNotEmpty) {
|
||||
buffer.writeln(' Атрибут: ' + item.subtitle!);
|
||||
}
|
||||
if (item.lore != null && item.lore!.isNotEmpty) {
|
||||
buffer.writeln(' Канонический закон/значение: ' + item.lore!);
|
||||
}
|
||||
if (item.linkedWord != null) {
|
||||
allMentionedWords.add(item.linkedWord!);
|
||||
buffer.writeln(' 🔗 Связанный сакральный знак 999: #' + item.linkedWord!.formattedSessionNumber + ' «' + item.linkedWord!.word.toUpperCase() + '» (' + item.linkedWord!.category + ')');
|
||||
if (item.linkedWord!.core.isNotEmpty) {
|
||||
buffer.writeln(' Формула знака: ' + item.linkedWord!.core);
|
||||
}
|
||||
}
|
||||
}
|
||||
buffer.writeln();
|
||||
}
|
||||
|
||||
// 3. Cross-system Resonances
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('3. КРОСС-СИСТЕМНЫЕ СВЯЗИ И СТИХИЙНЫЙ РЕЗОНАНС:');
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
var hasResonances = false;
|
||||
for (final r in session.readings) {
|
||||
if (r.systemId == 'geomancy') {
|
||||
for (final it in r.items) {
|
||||
if (it.title.toUpperCase().contains('СУДЬЯ')) {
|
||||
buffer.writeln('• Геомантический вектор: Итоговый вердикт выносит Верховный Судья «' + it.title + '» (' + (it.subtitle ?? '') + '). Его стихийный закон является фокусом разрешения всей сессии.');
|
||||
hasResonances = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (allMentionedWords.isNotEmpty) {
|
||||
final resonances = _detectSyncreticResonances(allMentionedWords);
|
||||
if (resonances.isNotEmpty) {
|
||||
for (final res in resonances) {
|
||||
buffer.writeln('• ' + res);
|
||||
hasResonances = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasResonances) {
|
||||
buffer.writeln('• Прямой полифонический диалог традиций: системы дополняют друг друга через разные уровни восприятия (ментальный, событийный, бессознательный).');
|
||||
}
|
||||
buffer.writeln();
|
||||
|
||||
// 4. Instructions for Comparative Synthesis
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('4. ТРЕБОВАНИЯ К ТВОЕМУ СРАВНИТЕЛЬНОМУ МЕТА-АНАЛИЗУ:');
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('Составь исчерпывающий, структурированный и философски безупречный отчёт:');
|
||||
buffer.writeln('1. ТОЧКА КОНВЕРГЕНЦИИ (В ЧЁМ ВСЕ СИСТЕМЫ ЕДИНОГЛАСНЫ): Какая центральная истина повторяется на разных символических языках (в рунах, арканах, гексаграммах, знаках Оракула 999 и сакральных фигурах Арабской Геомантии)?');
|
||||
buffer.writeln('2. ТОЧКИ ДИВЕРГЕНЦИИ И ПОЛЯРНОСТИ (СКРЫТЫЙ ДИАЛОГ СИСТЕМ): Где одна система предостерегает, а другая указывает на возможность? Какое диалектическое противоречие вскрыто?');
|
||||
buffer.writeln('3. ПОУРОВНЕВАЯ КАРТА РЕАЛЬНОСТИ:');
|
||||
buffer.writeln(' - Каузальный план (причины и судьба),');
|
||||
buffer.writeln(' - Психологический план (состояние ума и тени кверента),');
|
||||
buffer.writeln(' - Событийный план (ближайшая материя и динамика).');
|
||||
buffer.writeln('4. ПРИГОВОР И ВЕРХОВНЫЙ СИНТЕЗ: К какому единственному выводу неизбежно подводят все проведённые расклады? Обязательно сопоставь вердикт Верховного Судьи Геомантии (Аль-Кади) и гармонизацию Примирителя с результирующими картами/рунами сессии.');
|
||||
buffer.writeln('5. ТОЧНЫЙ НЕОБРАТИМЫЙ ЖЕСТ: Какое конкретное действие или внутреннее решение вопрошающий должен совершить прямо сейчас?');
|
||||
buffer.writeln('═══════════════════════════════════════════════════════════════════');
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// Generates a Master Prompt for a standard Cleromancy Oracle Session (any number of words drawn).
|
||||
static String generateForSession(OracleSession session) {
|
||||
final buffer = StringBuffer();
|
||||
final repo = WordsRepository();
|
||||
|
||||
buffer.writeln('═══════════════════════════════════════════════════════════════════');
|
||||
buffer.writeln('СВЯЩЕННЫЙ РАСКЛАД КЛЕРОМАНТИИ — СИСТЕМА «ОРАКУЛ»');
|
||||
buffer.writeln('МАСТЕР-ПРОМПТ ДЛЯ ГЛУБИННОЙ ИНТЕРПРЕТАЦИИ НЕЙРОСЕТЬЮ');
|
||||
buffer.writeln('═══════════════════════════════════════════════════════════════════');
|
||||
buffer.writeln();
|
||||
|
||||
buffer.writeln('### СИСТЕМНАЯ РОЛЬ:');
|
||||
buffer.writeln('Ты — верховный толкователь Оракула, мастер герметического синкретизма, онтологической семантики и глубинной психологии.');
|
||||
buffer.writeln('Перед тобой полный протокол сакрального расклада Клеромантии (гадания по жребию знаков из канонической тысячи 000–999).');
|
||||
buffer.writeln('Наша система включает канонические соответствия Таро, Старшего Футарка, Огама, Ленорман, Каббалы (Сефирот), Алхимии, Геомантии, И Цзин, Классического и Китайского Зодиака, а также Септенера (7 Дней Недели) и Додекады (12 Месяцев Года).');
|
||||
buffer.writeln('Твоя задача — дать бескомпромиссный, психологически точный и эзотерически глубокий анализ ситуации, без клише, размытых фраз и банальностей.');
|
||||
buffer.writeln();
|
||||
|
||||
// 1. Context and Question
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('1. КОНТЕКСТ И ЗАПРОС ВОПРОШАЮЩЕГО (КВЕРЕНТА):');
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('• Дата и время сессии: ' + session.formattedDate);
|
||||
if (session.question != null && session.question!.trim().isNotEmpty) {
|
||||
buffer.writeln('• Фокус вопроса: «' + session.question!.trim() + '»');
|
||||
} else {
|
||||
buffer.writeln('• Фокус вопроса: Общее прорицание узлов судьбы, скрытых напряжений и грядущего вектора.');
|
||||
}
|
||||
buffer.writeln('• Количество выпавших знаков в цепи: ' + session.words.length.toString());
|
||||
buffer.writeln();
|
||||
|
||||
// 2. The Chain of Signs
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('2. ЦЕПЬ ВЫПАВШИХ САКРАЛЬНЫХ ЗНАКОВ (ПОЛНОЕ ДОСЬЕ И СЛОИ):');
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
|
||||
final fullWords = <WordItem>[];
|
||||
|
||||
for (int i = 0; i < session.words.length; i++) {
|
||||
final sw = session.words[i];
|
||||
final fw = repo.getWordByName(sw.word) ??
|
||||
repo.getWordBySessionNumber(sw.sessionNumber) ??
|
||||
WordItem(
|
||||
id: sw.sessionNumber,
|
||||
word: sw.word,
|
||||
partOfSpeech: sw.partOfSpeech,
|
||||
frequency: 0,
|
||||
category: sw.category,
|
||||
description: sw.symbolicMeaning,
|
||||
);
|
||||
fullWords.add(fw);
|
||||
|
||||
final posTitle = sw.positionTitle != null ? ' [' + sw.positionTitle! + ']' : '';
|
||||
buffer.writeln('ЗНАК #' + (i + 1).toString() + ': #' + sw.formattedSessionNumber + ' «' + fw.word.toUpperCase() + '»' + posTitle);
|
||||
buffer.writeln(' Часть речи: ' + fw.partOfSpeech + ' | Категория: ' + fw.category + ' | Оператор: ' + fw.localizedOperator + ' | Валентность: ' + fw.localizedValence);
|
||||
|
||||
if (fw.core.isNotEmpty) {
|
||||
buffer.writeln(' ⚡ [ЯДРО ЗНАКА]: ' + fw.core);
|
||||
}
|
||||
|
||||
final cleanDesc = fw.description.replaceAll(RegExp(r'\[.*?\]\s*'), '').trim();
|
||||
if (cleanDesc.isNotEmpty) {
|
||||
buffer.writeln(' 📖 [КАНОНИЧЕСКИЙ СМЫСЛ]: ' + cleanDesc);
|
||||
}
|
||||
|
||||
if (fw.oracle.isNotEmpty) {
|
||||
buffer.writeln(' 🔮 [ТОЛКОВАНИЕ ОРАКУЛА]: ' + fw.oracle);
|
||||
}
|
||||
|
||||
if (fw.shadow.isNotEmpty) {
|
||||
buffer.writeln(' 🌑 [ТЕНЬ И ИСКУШЕНИЕ]: ' + fw.shadow);
|
||||
}
|
||||
|
||||
if (fw.asks.isNotEmpty) {
|
||||
buffer.writeln(' ❓ [ДИАГНОСТИЧЕСКИЙ ВОПРОС]: ' + fw.asks);
|
||||
}
|
||||
|
||||
if (fw.step.isNotEmpty) {
|
||||
buffer.writeln(' ✨ [НЕОБРАТИМЫЙ ЖЕСТ / ДЕЙСТВИЕ]: ' + fw.step);
|
||||
}
|
||||
|
||||
if (fw.binds.isNotEmpty) {
|
||||
buffer.writeln(' 🔗 [СОЗВУЧНЫЕ СВЯЗКИ]: ' + fw.binds.join(', '));
|
||||
}
|
||||
|
||||
final trad = _formatTraditions(fw, prefix: ' ');
|
||||
if (trad.isNotEmpty) {
|
||||
buffer.write(trad);
|
||||
}
|
||||
buffer.writeln();
|
||||
}
|
||||
|
||||
// 3. Syncretic Resonances and Geometry of Signs
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('3. МЕЖСИСТЕМНЫЕ РЕЗОНАНСЫ И ПАРНЫЕ СВЯЗИ В ЦЕПИ:');
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
final resonances = _detectSyncreticResonances(fullWords);
|
||||
if (resonances.isNotEmpty) {
|
||||
for (final r in resonances) {
|
||||
buffer.writeln('• ' + r);
|
||||
}
|
||||
} else {
|
||||
buffer.writeln('• Прямых полярных пар не обнаружено; знаки образуют распределённую волну смыслов.');
|
||||
}
|
||||
|
||||
// Valence balance
|
||||
final activeCount = fullWords.where((w) => w.valence.toLowerCase() == 'active' || w.valence.toLowerCase() == 'light').length;
|
||||
final receptiveCount = fullWords.where((w) => w.valence.toLowerCase() == 'receptive' || w.valence.toLowerCase() == 'dark').length;
|
||||
final neutralCount = fullWords.length - activeCount - receptiveCount;
|
||||
buffer.writeln('• Энергетический баланс цепи: Активных (Ян/Огонь-Воздух) = ' + activeCount.toString() + ', Восприимчивых (Инь/Вода-Земля) = ' + receptiveCount.toString() + ', Нейтральных = ' + neutralCount.toString());
|
||||
buffer.writeln();
|
||||
|
||||
// 4. Detailed instructions for LLM
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('4. ТРЕБОВАНИЯ К ТВОЕМУ ОТВЕТУ:');
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('Построй своё прорицание по следующим блокам:');
|
||||
buffer.writeln('1. ДРАМАТУРГИЯ ЦЕПИ: Как знаки перетекают друг в друга от первого к последнему? Какой сюжет разворачивается?');
|
||||
buffer.writeln('2. РАЗБОР КАЖДОГО ЗНАКА В КОНТЕКСТЕ ВОПРОСА: Проанализируй влияние каждого символа, его ядро и проявление через традиции.');
|
||||
buffer.writeln('3. ТЕНЕВОЙ УЗЕЛ И ЛОВУШКА: Где вопрошающий рискует оступиться? Что скрыто за фасадом очевидного?');
|
||||
buffer.writeln('4. РЕЗОНАНСЫ ТРАДИЦИЙ: Как вскрытые арканы, руны, планеты и гексаграммы обогащают понимание?');
|
||||
buffer.writeln('5. ИТОГОВЫЙ ПРИГОВОР ОРАКУЛА И ТОЧНЫЙ ШАГ: Сформулируй ясный, бескомпромиссный вывод и конкретный внутренний или внешний жест для вопрошающего.');
|
||||
buffer.writeln('═══════════════════════════════════════════════════════════════════');
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// Generates a comprehensive, high-caliber Master Prompt for advanced LLMs (Grok, Claude 3.5 Sonnet, Gemini 1.5 Pro, GPT-4o).
|
||||
static String generate(HousesSession session) {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
buffer.writeln('═══════════════════════════════════════════════════════════════════');
|
||||
buffer.writeln('СВЯЩЕННЫЙ РАСКЛАД ПОЛОТНА ДОМОВ 33×30 — СИСТЕМА «ОРАКУЛ»');
|
||||
buffer.writeln('МАСТЕР-ПРОМПТ ДЛЯ ГЛУБИННОЙ ИНТЕРПРЕТАЦИИ НЕЙРОСЕТЬЮ');
|
||||
buffer.writeln('═══════════════════════════════════════════════════════════════════');
|
||||
buffer.writeln();
|
||||
|
||||
buffer.writeln('### СИСТЕМНАЯ РОЛЬ:');
|
||||
buffer.writeln('Ты — верховный толкователь Оракула, мастер герметического синкретизма, онтологической семантики и глубинной психологии.');
|
||||
buffer.writeln('Перед тобой полный протокол сакрального расклада на поле 33×30 (990 земных Домов и 10 верховных Архонтов-управителей).');
|
||||
buffer.writeln('Десять Архонтов — существительные, вытянутые заново для ЭТОЙ сессии. Это не закреплённые клетки 000 и 991–999.');
|
||||
buffer.writeln('Наша система включает канонические соответствия Таро, Старшего Футарка, Огама, Ленорман, Каббалы (Сефирот), Алхимии, Геомантии, И Цзин, Классического и Китайского Зодиака, а также Септенера (7 Дней Недели) и Додекады (12 Месяцев Года).');
|
||||
buffer.writeln('Твоя задача — дать бескомпромиссный, психологически точный и эзотерически глубокий анализ ситуации, без клише, размытых фраз и банальностей.');
|
||||
buffer.writeln();
|
||||
|
||||
// 1. Querent (Significator)
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('1. СУБЪЕКТ ВОПРОШАНИЯ (КВЕРЕНТ / СИГНИФИКАТОР):');
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
if (session.querentWord != null) {
|
||||
final q = session.querentWord!;
|
||||
var loc = '';
|
||||
if (session.querentHouseIndex != null) {
|
||||
final hIdx = (session.querentHouseIndex! + 1).toString().padLeft(3, '0');
|
||||
final r = session.querentHouseIndex! ~/ 30 + 1;
|
||||
final c = session.querentHouseIndex! % 30 + 1;
|
||||
loc = ' | Локация: Дом #' + hIdx + ' (Ряд ' + r.toString() + ', Столбец ' + c.toString() + ')';
|
||||
}
|
||||
buffer.writeln('• Кверент (Сигнификатор): «' + q.word.toUpperCase() + '»' + loc);
|
||||
|
||||
final summary = q.core.isNotEmpty
|
||||
? q.core
|
||||
: (q.description.isNotEmpty
|
||||
? q.description.replaceAll(RegExp(r'\[.*?\]\s*'), '').trim()
|
||||
: q.category);
|
||||
buffer.writeln(' Краткая сводка: ' + summary);
|
||||
} else {
|
||||
buffer.writeln('• Кверент: Открытый фокус поля (вопрошающий без фиксированного сигнификатора).');
|
||||
}
|
||||
|
||||
if (session.question != null && session.question!.trim().isNotEmpty) {
|
||||
buffer.writeln('• Вопрос кверента: «' + session.question!.trim() + '»');
|
||||
} else {
|
||||
buffer.writeln('• Вопрос кверента: Общее прорицание узлов судьбы и скрытых течений ситуации.');
|
||||
}
|
||||
buffer.writeln();
|
||||
|
||||
// 2. The 3-tier hierarchy: Archon -> House -> Guest
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('2. АРХИТЕКТУРА РАСКЛАДА (ИЕРАРХИЯ: АРХОНТ ➔ ДОМ ➔ ГОСТЬ):');
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('В каждом открытом доме действуют 3 уровня реальности:');
|
||||
buffer.writeln(' [А] Верховный Архонт — надмирный закон сектора (триады столбцов), небесный климат.');
|
||||
buffer.writeln(' [Б] Земной Дом — конкретная сфера бытия, почва, материя и форма.');
|
||||
buffer.writeln(' [В] Знак-Гость — импульс жребия, живая сила, вошедшая в этот дом.');
|
||||
buffer.writeln();
|
||||
|
||||
final allGuests = <WordItem>[];
|
||||
|
||||
for (int i = 0; i < session.selectedHouses.length; i++) {
|
||||
final house = session.selectedHouses[i];
|
||||
final sector = session.sectors[house.sectorIndex];
|
||||
final guest = house.guestWord ?? house.houseWord;
|
||||
allGuests.add(guest);
|
||||
final order = house.selectionOrder ?? (i + 1);
|
||||
final hPad = (house.houseIndex + 1).toString().padLeft(3, '0');
|
||||
|
||||
buffer.writeln('ПОЗИЦИЯ #' + order.toString() + ' (Дом #' + hPad + ', Ряд ' + (house.row + 1).toString() + ', Столбец ' + (house.col + 1).toString() + '):');
|
||||
buffer.writeln(' 👑 [АРХОНТ]: «' + sector.archonWord.word.toUpperCase() + '» (' + sector.title + ')');
|
||||
if (sector.archonWord.core.isNotEmpty) {
|
||||
buffer.writeln(' Закон Архонта: ' + sector.archonWord.core);
|
||||
}
|
||||
final archonTrad = _formatTraditions(sector.archonWord, prefix: ' ');
|
||||
if (archonTrad.isNotEmpty) {
|
||||
buffer.write(archonTrad);
|
||||
}
|
||||
|
||||
buffer.writeln(' 🏛️ [ДОМ]: «' + house.houseWord.word.toUpperCase() + '» (' + house.houseWord.category + ')');
|
||||
if (house.houseWord.core.isNotEmpty) {
|
||||
buffer.writeln(' Ядро Дома: ' + house.houseWord.core);
|
||||
}
|
||||
final houseTrad = _formatTraditions(house.houseWord, prefix: ' ');
|
||||
if (houseTrad.isNotEmpty) {
|
||||
buffer.write(houseTrad);
|
||||
}
|
||||
|
||||
buffer.writeln(' ⚡ [ГОСТЬ В ДОМЕ]: «' + guest.word.toUpperCase() + '» (' + guest.partOfSpeech + ', ' + guest.category + ')');
|
||||
if (guest.core.isNotEmpty) {
|
||||
buffer.writeln(' Суть гостя: ' + guest.core);
|
||||
}
|
||||
if (guest.oracle.isNotEmpty) {
|
||||
buffer.writeln(' Физика действия гостя: ' + guest.oracle);
|
||||
}
|
||||
if (guest.shadow.isNotEmpty) {
|
||||
buffer.writeln(' Тень и ловушка гостя: ' + guest.shadow);
|
||||
}
|
||||
if (guest.asks.isNotEmpty) {
|
||||
buffer.writeln(' Диагностический вопрос: ' + guest.asks);
|
||||
}
|
||||
if (guest.step.isNotEmpty) {
|
||||
buffer.writeln(' Конкретный шаг заземления: ' + guest.step);
|
||||
}
|
||||
if (guest.binds.isNotEmpty) {
|
||||
buffer.writeln(' Связанные союзные знаки: ' + guest.binds.join(', '));
|
||||
}
|
||||
final guestTrad = _formatTraditions(guest, prefix: ' ');
|
||||
if (guestTrad.isNotEmpty) {
|
||||
buffer.write(guestTrad);
|
||||
}
|
||||
buffer.writeln();
|
||||
}
|
||||
|
||||
// 3. Sacred Geometry of the Field
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('3. САКРАЛЬНАЯ ГЕОМЕТРИЯ МАТРИЦЫ:');
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
final pointsCount = session.selectedHouses.length;
|
||||
String figureName;
|
||||
switch (pointsCount) {
|
||||
case 1: figureName = 'Монада (Точка Первоистока)'; break;
|
||||
case 2: figureName = 'Диада (Вектор Полярности)'; break;
|
||||
case 3: figureName = 'Триада (Дельта Творения)'; break;
|
||||
case 4: figureName = 'Тетрада (Земной Четверик)'; break;
|
||||
case 5: figureName = 'Пентаграмма (Узел Пяти Стихий)'; break;
|
||||
case 6: figureName = 'Гексаграмма (Печать Равновесия)'; break;
|
||||
case 7: figureName = 'Гептада (Семизвездный Венец)'; break;
|
||||
case 8: figureName = 'Октада (Роза Пространства)'; break;
|
||||
case 9: figureName = 'Эннеаграмма (Девятикратное Полотно Судьбы)'; break;
|
||||
default: figureName = 'Сакральный Сигил (' + pointsCount.toString() + ' точек)';
|
||||
}
|
||||
|
||||
buffer.writeln('• Фигура расклада: ' + figureName);
|
||||
final visitedSectors = session.selectedHouses.map((h) => h.sectorIndex + 1).toSet().toList()..sort();
|
||||
buffer.writeln('• Задействованные Секторы Архонтов: Триады столбцов № ' + visitedSectors.join(', ') + ' из 10');
|
||||
buffer.writeln('• Координаты узлов на сетке 33×30:');
|
||||
for (final h in session.selectedHouses) {
|
||||
buffer.writeln(' - Узел #' + (h.selectionOrder ?? 0).toString() + ': Дом «' + h.houseWord.word + '» в координатах (r: ' + (h.row + 1).toString() + ', c: ' + (h.col + 1).toString() + ')');
|
||||
}
|
||||
buffer.writeln();
|
||||
|
||||
// 3.1. Syncretic Resonances across drawn symbols
|
||||
final resonances = _detectSyncreticResonances(allGuests);
|
||||
if (resonances.isNotEmpty) {
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('3.1. СИНКРЕТИЧЕСКИЕ РЕЗОНАНСЫ И ПАРЫ В РАСКЛАДЕ:');
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
for (final r in resonances) {
|
||||
buffer.writeln('• ' + r);
|
||||
}
|
||||
buffer.writeln();
|
||||
}
|
||||
|
||||
// 4. Chains of Houses & Fate Loops
|
||||
if (session.chains.isNotEmpty) {
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('4. ЦЕПОЧКИ ДОМОВ И ПЕТЛИ СУДЬБЫ (ТРАССИРОВКА ДО 9 ЗВЕНЬЕВ):');
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('Энергия каждого гостя ищет свой дом на матрице, создавая цепочки перетекания силы:');
|
||||
for (final chain in session.chains) {
|
||||
buffer.writeln('• ЦЕПОЧКА #' + chain.chainIndex.toString() + ' [' + chain.endTypeTitle + '] (' + chain.steps.length.toString() + ' звеньев):');
|
||||
buffer.writeln(' Итог: ' + chain.description);
|
||||
buffer.writeln(' Маршрут импульса:');
|
||||
for (int s = 0; s < chain.steps.length; s++) {
|
||||
final step = chain.steps[s];
|
||||
final sector = session.sectors[step.house.sectorIndex];
|
||||
final sIdx = (step.house.houseIndex + 1).toString();
|
||||
String stepDesc = ' ' + (s + 1).toString() + '. Дом #' + sIdx + ' «' + step.house.houseWord.word + '» [Архонт: ' + sector.archonWord.word + '] ➔ Гость «' + step.guest.word + '»';
|
||||
if (step.isLoopClosing) stepDesc += ' 🔄 [ПЕТЛЯ ЗАМКНУЛАСЬ]';
|
||||
if (step.leadsToArchon) stepDesc += ' 👑 [ПРЕСТОЛ АРХОНТА «' + (step.destinationArchon?.archonWord.word ?? '') + '»]';
|
||||
if (step.computed) stepDesc += ' ◎ [ВЫЧИСЛЕНО ИЗ ПОЛЯ, НЕ ЖРЕБИЙ]';
|
||||
buffer.writeln(stepDesc);
|
||||
}
|
||||
buffer.writeln();
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Instruction for the AI Interpreter
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('5. ИНСТРУКЦИЯ ДЛЯ ТОЛКОВАНИЯ (ТВОЙ ПЛАН ОТВЕТА):');
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('Составь исчерпывающий мета-анализ расклада, следуя структуре:');
|
||||
buffer.writeln('1. СУТЬ И ЦЕНТР ТЯЖЕСТИ: Как ситуация соотносится с Кверентом? Что является главным нервом вопроса?');
|
||||
buffer.writeln('2. СИНТЕЗ ДОМОВ И ГОСТЕЙ: Раскрой взаимодействие: как каждый Гость трансформирует свой Дом под волей правящего Архонта, опираясь на сакральные слои (Таро, Руны, Огам, Ленорман, Каббалу, Септенер дней недели и Додекаду месяцев).');
|
||||
buffer.writeln('3. СИНКРЕТИЧЕСКИЕ СВЯЗИ И РЕЗОНАНСЫ: Учти взаимное влияние планет, дней, месяцев, пар Ленорман и стихий, проявившихся в раскладе.');
|
||||
buffer.writeln('4. АНАЛИЗ ЦЕПОЧЕК И ПЕТЕЛЬ: В чем главная скрытая динамика? Куда утекает или где зацикливается энергия (петли судьбы)?');
|
||||
buffer.writeln('5. ТЕНИ И СКРЫТЫЕ ЛОВУШКИ: От каких иллюзий, спешки или самообмана предостерегает жребий?');
|
||||
buffer.writeln('6. ИТОГОВЫЙ ПРИГОВОР И РИТУАЛЬНОЕ ДЕЙСТВИЕ: Три честных вывода и один бескомпромиссный физический шаг, который вопрошающий должен совершить прямо сейчас.');
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
static String _formatTraditions(WordItem item, {required String prefix}) {
|
||||
final sb = StringBuffer();
|
||||
// 1. Explicit layers
|
||||
if (item.layers.isNotEmpty) {
|
||||
for (final l in item.layers) {
|
||||
sb.writeln(prefix + '🏛️ [Слой «' + WordItemLocalization.localizeSystem(l.system) + '»]: ' + l.title + ' — ' + l.law);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Traditions Repository matches
|
||||
final repo = TraditionsRepository();
|
||||
final matches = repo.findMatchesForWord(item.word, wordId: item.id);
|
||||
if (matches.isNotEmpty) {
|
||||
for (final m in matches) {
|
||||
final t = m.tradition;
|
||||
final st = m.station;
|
||||
final status = m.isPrimary ? 'Ключевой Архетип' : 'Кластер / Эманация';
|
||||
sb.writeln(prefix + '📜 [' + t.name + ']: ' + st.name + ' ' + st.symbol + ' (' + status + ')');
|
||||
if (m.isPrimary && st.lore.isNotEmpty) {
|
||||
final loreSnippet = st.lore.length > 220 ? st.lore.substring(0, 220).trim() + '...' : st.lore.trim();
|
||||
sb.writeln(prefix + ' Мистерия: ' + loreSnippet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
static List<String> _detectSyncreticResonances(List<WordItem> words) {
|
||||
final resonances = <String>[];
|
||||
final wordNames = words.map((w) => w.word.trim().toLowerCase()).toSet();
|
||||
|
||||
// Check Day of Week & Planet alignments
|
||||
const dayPlanetPairs = {
|
||||
'понедельник': 'луна',
|
||||
'вторник': 'марс',
|
||||
'среда': 'меркурий',
|
||||
'четверг': 'юпитер',
|
||||
'пятница': 'венера',
|
||||
'суббота': 'сатурн',
|
||||
'воскресенье': 'солнце',
|
||||
};
|
||||
|
||||
for (final entry in dayPlanetPairs.entries) {
|
||||
if (wordNames.contains(entry.key) && wordNames.contains(entry.value)) {
|
||||
resonances.add('Планетарный Септенер: созвучие дня «' + entry.key.toUpperCase() + '» и управляющей планеты «' + entry.value.toUpperCase() + '» — прямая манифестация космического ритма.');
|
||||
}
|
||||
}
|
||||
|
||||
// Check Month & Zodiac resonances
|
||||
const monthZodiacPairs = {
|
||||
'март': 'овен',
|
||||
'апрель': 'телец',
|
||||
'май': 'близнецы',
|
||||
'июнь': 'рак',
|
||||
'июль': 'лев',
|
||||
'август': 'дева',
|
||||
'сентябрь': 'весы',
|
||||
'октябрь': 'скорпион',
|
||||
'ноябрь': 'стрелец',
|
||||
'декабрь': 'козерог',
|
||||
'январь': 'козерог',
|
||||
'февраль': 'водолей',
|
||||
};
|
||||
|
||||
for (final entry in monthZodiacPairs.entries) {
|
||||
if (wordNames.contains(entry.key) && wordNames.contains(entry.value)) {
|
||||
resonances.add('Зодиакальный цикл: солнечные врата месяца «' + entry.key.toUpperCase() + '» активируют знак «' + entry.value.toUpperCase() + '».');
|
||||
}
|
||||
}
|
||||
|
||||
// Check Solstices / Equinoxes
|
||||
if (wordNames.contains('март') && wordNames.contains('сентябрь')) {
|
||||
resonances.add('Ось Равноденствий: в раскладе проявились врата Весны (Март) и Осени (Сентябрь) — тема великого равновесия дня и ночи.');
|
||||
}
|
||||
if (wordNames.contains('июнь') && wordNames.contains('декабрь')) {
|
||||
resonances.add('Ось Солнцестояний: противоположности Света (Июнь) и Тьмы (Декабрь) — максимальная амплитуда года.');
|
||||
}
|
||||
|
||||
// Check Lenormand Cards present
|
||||
final lenormandRepo = LenormandPairsRepository();
|
||||
if (lenormandRepo.isLoaded) {
|
||||
final lenormandFound = <String>[];
|
||||
for (final w in words) {
|
||||
final wClean = w.word.trim().toLowerCase();
|
||||
for (final c in lenormandRepo.cards) {
|
||||
if (c.name.toLowerCase().contains(wClean) || c.id.toLowerCase().contains(wClean)) {
|
||||
lenormandFound.add('№${c.n} ${c.name}');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lenormandFound.length >= 2) {
|
||||
resonances.add('Связка Ленорман: в раскладе открылись карты ' + lenormandFound.join(' и ') + ' — их комбинаторное чтение усиливает динамику домов.');
|
||||
}
|
||||
}
|
||||
|
||||
return resonances;
|
||||
}
|
||||
|
||||
/// Generates the Master Prompt for Deep Culturological & Mythological Enrichment of an Archetype.
|
||||
static String generateArchetypeEnrichmentPrompt(
|
||||
WordItem word, {
|
||||
String? customCore,
|
||||
String? customOracle,
|
||||
String? customShadow,
|
||||
String? customAsks,
|
||||
String? customStep,
|
||||
}) {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
buffer.writeln('ТВОЯ РОЛЬ И ЗАДАЧА:');
|
||||
buffer.writeln('Ты — Архитектор Смыслов, верховный мифолог, культуролог и онтологический герменевт.');
|
||||
buffer.writeln('Твоя задача — провести глубочайшее, бескомпромиссное и многомерное ОБОГАЩЕНИЕ сакрального Архетипа. Напиши захватывающую, монументальную, живую и литературную статью-эссе, которую по-настоящему интересно читать.');
|
||||
buffer.writeln();
|
||||
buffer.writeln('СТРОЖАЙШИЕ ПРАВИЛА:');
|
||||
buffer.writeln('Абсолютный запрет на гадательную терминологию: НИКАКИХ упоминаний «оракулов», «раскладов», «карт», «вопрошающих», «гаданий» и «ответов на вопрос». Архетип описывается как фундаментальный, безжалостный закон Вселенной, физический и мистический принцип, формирующий саму реальность и событийное поле человека.');
|
||||
buffer.writeln('Запрет на лень и краткость: Текст должен быть длинным, плотным, перенасыщенным фактурой. Разворачивай мысль на полную мощность.');
|
||||
buffer.writeln('Обязательный мультикультурный синтез. В тексте должны быть органично сплетены:');
|
||||
buffer.writeln('• Точная этимология (праиндоевропейская и др.).');
|
||||
buffer.writeln('• Древние космогонии максимального спектра: Шумер/Аккад, Египет, Древняя Греция, Иудаизм, Кельты, Суфизм/Арабская традиция и Геомантия (\'Илм ар-рамл / стихийные тетраграммы), Китай (Дао), Полинезия.');
|
||||
buffer.writeln('• Научный срез: квантовая механика (наблюдатель, суперпозиция, коллапс), астрофизика, биология или нейрофизиология.');
|
||||
buffer.writeln('• Алхимия и герметизм (трансформация элементов).');
|
||||
buffer.writeln('Метрика: В самом конце ответа ты обязан выдать точное количество символов, потраченных на генерацию текста.');
|
||||
buffer.writeln('═══════════════════════════════════════');
|
||||
buffer.writeln('ВХОДНЫЕ ДАННЫЕ АРХЕТИПА:');
|
||||
buffer.writeln('• Знак: #' + word.formattedSessionNumber + ' «' + word.word.toUpperCase() + '» ' + word.emoji);
|
||||
|
||||
final coreVal = (customCore != null && customCore.trim().isNotEmpty)
|
||||
? customCore.trim()
|
||||
: (word.core.isNotEmpty ? word.core : word.cleanDescription);
|
||||
buffer.writeln('• Ядро/Текущая формула: ' + coreVal);
|
||||
|
||||
if (word.partOfSpeech.isNotEmpty) buffer.writeln('• Часть речи: ' + word.partOfSpeech);
|
||||
if (word.category.isNotEmpty) buffer.writeln('• Категория: ' + word.category);
|
||||
if (word.operator.isNotEmpty) buffer.writeln('• Оператор: ' + word.localizedOperator);
|
||||
if (word.valence.isNotEmpty) buffer.writeln('• Полярность: ' + word.localizedValence);
|
||||
|
||||
final oracleVal = (customOracle != null && customOracle.trim().isNotEmpty)
|
||||
? customOracle.trim()
|
||||
: (word.oracle.isNotEmpty ? word.oracle : '');
|
||||
if (oracleVal.isNotEmpty) {
|
||||
buffer.writeln('• Текущая наработка: ' + oracleVal);
|
||||
}
|
||||
|
||||
final shadowVal = (customShadow != null && customShadow.trim().isNotEmpty)
|
||||
? customShadow.trim()
|
||||
: word.shadow;
|
||||
if (shadowVal.isNotEmpty) {
|
||||
buffer.writeln('• Текущая тень: ' + shadowVal);
|
||||
}
|
||||
|
||||
if (word.layers.isNotEmpty) {
|
||||
buffer.writeln('• Традиции и пласты:');
|
||||
for (final l in word.layers) {
|
||||
final sysRu = WordItemLocalization.localizeSystem(l.system);
|
||||
buffer.writeln(' - [' + sysRu + ']: ' + l.title + ' — ' + l.law);
|
||||
}
|
||||
}
|
||||
|
||||
if (word.binds.isNotEmpty) {
|
||||
buffer.writeln('• Созвучные узлы: ' + word.binds.join(', '));
|
||||
}
|
||||
|
||||
buffer.writeln('═══════════════════════════════════════');
|
||||
buffer.writeln('ТРЕБОВАНИЯ К ФОРМАТУ ВЫДАЧИ (СТРОГО MARKDOWN С ЗАГОЛОВКАМИ ##):');
|
||||
buffer.writeln('## ЯДРО');
|
||||
buffer.writeln('(1–2 чеканных, безжалостных афористичных предложения: онтологическая формула непреложного закона бытия. Никакой воды, только концентрат сути).');
|
||||
buffer.writeln();
|
||||
buffer.writeln('## ОРАКУЛ');
|
||||
buffer.writeln('(Монолитная, развернутая статья-эссе на 5–8 глубоких абзацев.');
|
||||
buffer.writeln('• Абзац 1-2: Этимология и физическая/биологическая/квантовая природа явления. Разрушение обыденного восприятия.');
|
||||
buffer.writeln('• Абзац 3-5: Глубокая мифология, геомантия и космогония (Шумер, Египет, Иврит, Кельты, Арабская геомантия \'Илм ар-рамл, Дальний Восток и т.д.) — как разные цивилизации и сакральные науки описывали этот закон.');
|
||||
buffer.writeln('• Абзац 6-7: Алхимия и событийное поле. Как этот закон действует как химический реагент в реальности человека, как он ломает и трансформирует ситуацию. Без эзотерической воды — суровая прикладная философия).');
|
||||
buffer.writeln();
|
||||
buffer.writeln('## ТЕНЬ');
|
||||
buffer.writeln('(Теневая ловушка принципа. Описать искажение, избыток или дефицит, слепую зону, эскапизм, паралич или выгорание. Как человек обманывает себя, попадая в искривленное поле этого архетипа).');
|
||||
buffer.writeln();
|
||||
buffer.writeln('## ВОПРОС');
|
||||
buffer.writeln('(Острый, как скальпель, экзистенциальный коан, бьющий в самую суть ситуации. Безжалостный вопрос для глубокой рефлексии).');
|
||||
buffer.writeln();
|
||||
buffer.writeln('## ЖЕСТ');
|
||||
buffer.writeln('(Одно конкретное материальное, физиологическое микродействие в реальном физическом мире для закрепления осознания. Это НЕ должна быть медитация в уме — это должно быть физическое действие с материей, пространством или телом).');
|
||||
buffer.writeln();
|
||||
buffer.writeln('## КУЛЬТУРЫ');
|
||||
buffer.writeln('(Учебный сравнительный атлас: как этот знак понимали разные ветви человечества. Для КАЖДОЙ традиции — отдельный подзаголовок ### с точным названием из списка ниже и 2–5 предложений строго фактического материала: исконное слово/иероглиф/знак с транслитерацией, божества и мифы, ритуалы, первоисточник (текст, эпос, памятник). Не выдумывай: если у традиции нет достоверного материала по этому знаку — пропусти её, а не фантазируй. В конце — подзаголовок ### Сравнение: 3–4 предложения о том, где традиции сходятся и где принципиально расходятся.');
|
||||
for (final b in kCultureBranches) {
|
||||
buffer.writeln('### ' + b.name);
|
||||
}
|
||||
buffer.writeln('### Сравнение');
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// Generates the Grand Master Prompt for Arabic Geomancy ('Ilm al-Raml) & the Great Shield.
|
||||
static String generateGeomancyMasterPrompt({
|
||||
required String question,
|
||||
required String layoutTitle,
|
||||
required bool isShield,
|
||||
List<GeomancyPromptFigure> figures = const [],
|
||||
List<GeomancyPromptFigure> mothers = const [],
|
||||
List<GeomancyPromptFigure> daughters = const [],
|
||||
List<GeomancyPromptFigure> nephews = const [],
|
||||
GeomancyPromptFigure? rightWitness,
|
||||
GeomancyPromptFigure? leftWitness,
|
||||
GeomancyPromptFigure? judge,
|
||||
GeomancyPromptFigure? reconciler,
|
||||
}) {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
buffer.writeln('═══════════════════════════════════════════════════════════════════');
|
||||
buffer.writeln("ВЕЛИКИЙ МАСТЕР-ПРОМПТ: АРАБСКАЯ ГЕОМАНТИЯ ('ИЛМ АР-РАМЛ)");
|
||||
buffer.writeln('САКРАЛЬНАЯ ПУНКТУАЦИЯ ПЕСКА, ВЕЛИКИЙ ЩИТ И СТИХИЙНЫЕ ТЕТРАГРАММЫ');
|
||||
buffer.writeln('═══════════════════════════════════════════════════════════════════');
|
||||
buffer.writeln();
|
||||
|
||||
buffer.writeln('### СИСТЕМНАЯ РОЛЬ:');
|
||||
buffer.writeln("Ты — верховный геомант, хранитель традиции 'Илм ар-рамл («науки о песке») и мастер герметической астро-геомантии.");
|
||||
buffer.writeln('Перед тобой священный протокол геомантического гадания. Каждая фигура представляет собой тетраграмму четырёх первоэлементов (Огонь, Воздух, Вода, Земля): нечётное число точек (1) обозначает активную проявленную силу, чётное (2) — скрытую потенциальную емкость.');
|
||||
buffer.writeln('Твоя миссия — провести всесторонний, глубокий и бескомпромиссный анализ расклада песка, вскрыть динамику стихий и выдать монолитный, мудрый и прикладной вердикт.');
|
||||
buffer.writeln();
|
||||
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('1. ФОКУС И ВОПРОС КВЕРЕНТА:');
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
final q = question.trim().isNotEmpty ? question.trim() : 'Общий вектор судьбы, скрытые стихийные силы и неизбежный исход.';
|
||||
buffer.writeln('• Вопрос кверента: «' + q + '»');
|
||||
buffer.writeln('• Формат расклада: ' + layoutTitle);
|
||||
buffer.writeln("• Мантическая традиция: Классическая арабская геомантия ('Илм ар-рамл) и средневековый европейский геомантический щит");
|
||||
buffer.writeln();
|
||||
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('2. КАРТА ВЫПАВШИХ ФИГУР:');
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
|
||||
if (isShield) {
|
||||
if (judge != null) {
|
||||
buffer.writeln('ВЕРХОВНЫЙ СУДЬЯ (AL-QADI / JUDEX):');
|
||||
buffer.writeln('• ' + judge.name + ' (' + judge.latinName + (judge.arabicName.isNotEmpty ? ' / ' + judge.arabicName : '') + ')');
|
||||
buffer.writeln(' Стихия: ' + judge.element + ' | Планета/Зодиак: ' + judge.planet);
|
||||
buffer.writeln(' Тетраграмма стихий:');
|
||||
buffer.writeln(judge.dotsDisplay);
|
||||
buffer.writeln(' Канонический смысл: ' + judge.lore);
|
||||
if (judge.linkedWord != null) {
|
||||
buffer.writeln(' Связанный архетип Оракула 999: #' + judge.linkedWord!.formattedSessionNumber + ' «' + judge.linkedWord!.word.toUpperCase() + '» (' + judge.linkedWord!.core + ')');
|
||||
}
|
||||
buffer.writeln();
|
||||
}
|
||||
|
||||
if (reconciler != null) {
|
||||
buffer.writeln("ПРИМИРИТЕЛЬ (AL-MUJAMMI' / RECONCILER — 16-я сакральная фигура):");
|
||||
buffer.writeln('• ' + reconciler.name + ' (' + reconciler.latinName + (reconciler.arabicName.isNotEmpty ? ' / ' + reconciler.arabicName : '') + ')');
|
||||
buffer.writeln(' Стихия: ' + reconciler.element + ' | Планета/Зодиак: ' + reconciler.planet);
|
||||
buffer.writeln(' Тетраграмма стихий:');
|
||||
buffer.writeln(reconciler.dotsDisplay);
|
||||
buffer.writeln(' Связь: Сложение Судьи и 1-й Матери (разрешение конфликта между истоком и исходом)');
|
||||
buffer.writeln(' Канонический смысл: ' + reconciler.lore);
|
||||
if (reconciler.linkedWord != null) {
|
||||
buffer.writeln(' Связанный архетип Оракула 999: #' + reconciler.linkedWord!.formattedSessionNumber + ' «' + reconciler.linkedWord!.word.toUpperCase() + '» (' + reconciler.linkedWord!.core + ')');
|
||||
}
|
||||
buffer.writeln();
|
||||
}
|
||||
|
||||
buffer.writeln('ДВА СВИДЕТЕЛЯ (SHAHIDAYN):');
|
||||
if (rightWitness != null) {
|
||||
buffer.writeln('• Правый Свидетель (Объективный мир, действие, прошлое): ' + rightWitness.name);
|
||||
buffer.writeln(' ' + rightWitness.element + ' • ' + rightWitness.planet + ' | ' + rightWitness.lore);
|
||||
if (rightWitness.linkedWord != null) {
|
||||
buffer.writeln(' Связанный знак: #' + rightWitness.linkedWord!.formattedSessionNumber + ' «' + rightWitness.linkedWord!.word.toUpperCase() + '»');
|
||||
}
|
||||
}
|
||||
if (leftWitness != null) {
|
||||
buffer.writeln('• Левый Свидетель (Субъективный мир, чувства, будущее): ' + leftWitness.name);
|
||||
buffer.writeln(' ' + leftWitness.element + ' • ' + leftWitness.planet + ' | ' + leftWitness.lore);
|
||||
if (leftWitness.linkedWord != null) {
|
||||
buffer.writeln(' Связанный знак: #' + leftWitness.linkedWord!.formattedSessionNumber + ' «' + leftWitness.linkedWord!.word.toUpperCase() + '»');
|
||||
}
|
||||
}
|
||||
buffer.writeln();
|
||||
|
||||
buffer.writeln('ЧЕТЫРЕ МАТЕРИ (UMMAHAAT — Первичный стихийный исток ситуации):');
|
||||
for (int i = 0; i < mothers.length; i++) {
|
||||
final m = mothers[i];
|
||||
buffer.writeln('• Мать #' + (i + 1).toString() + ': ' + m.name + ' (' + m.element + ', ' + m.planet + ') — ' + m.lore);
|
||||
if (m.linkedWord != null) {
|
||||
buffer.writeln(' Знак 999: #' + m.linkedWord!.formattedSessionNumber + ' «' + m.linkedWord!.word.toUpperCase() + '»');
|
||||
}
|
||||
}
|
||||
buffer.writeln();
|
||||
|
||||
buffer.writeln('ЧЕТЫРЕ ДОЧЕРИ (BANAAT — Горизонтальная проекция, внешняя среда):');
|
||||
for (int i = 0; i < daughters.length; i++) {
|
||||
final d = daughters[i];
|
||||
buffer.writeln('• Дочь #' + (i + 1).toString() + ': ' + d.name + ' (' + d.element + ', ' + d.planet + ') — ' + d.lore);
|
||||
}
|
||||
buffer.writeln();
|
||||
|
||||
buffer.writeln('ЧЕТЫРЕ ПЛЕМЯННИКА (ABNAA — Психика кверента и окружение):');
|
||||
for (int i = 0; i < nephews.length; i++) {
|
||||
final n = nephews[i];
|
||||
buffer.writeln('• Племянник #' + (i + 1).toString() + ': ' + n.name + ' (' + n.element + ', ' + n.planet + ') — ' + n.lore);
|
||||
}
|
||||
buffer.writeln();
|
||||
} else {
|
||||
for (final f in figures) {
|
||||
buffer.writeln('• Позиция: ' + f.position);
|
||||
buffer.writeln(' Фигура: ' + f.name + ' (' + f.latinName + (f.arabicName.isNotEmpty ? ' / ' + f.arabicName : '') + ')');
|
||||
buffer.writeln(' Стихия: ' + f.element + ' | Планета/Зодиак: ' + f.planet);
|
||||
buffer.writeln(' Тетраграмма:');
|
||||
buffer.writeln(f.dotsDisplay);
|
||||
buffer.writeln(' Суть: ' + f.lore);
|
||||
if (f.linkedWord != null) {
|
||||
buffer.writeln(' Связанный знак 999: #' + f.linkedWord!.formattedSessionNumber + ' «' + f.linkedWord!.word.toUpperCase() + '» (' + f.linkedWord!.core + ')');
|
||||
}
|
||||
buffer.writeln();
|
||||
}
|
||||
}
|
||||
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('3. ТРЕБОВАНИЯ К ИНТЕРПРЕТАЦИИ НЕЙРОСЕТЬЮ (СТРОГАЯ СТРУКТУРА):');
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('Предоставь глубокое, литературное, эзотерически безупречное толкование:');
|
||||
buffer.writeln('1. АНАЛИЗ СТИХИЙНОГО СУБСТРАТА:');
|
||||
buffer.writeln(' Определи распределение сил четырёх стихий (Огонь, Воздух, Вода, Земля). Где происходит избыточное напряжение, а где истощение? Как стихии диктуют темп событий?');
|
||||
buffer.writeln('2. ДИАЛЕКТИКА СВИДЕТЕЛЕЙ И ВЕРДИКТ СУДЬИ:');
|
||||
buffer.writeln(' Как Правый Свидетель (объективные обстоятельства) взаимодействует с Левым Свидетелем (внутреннее состояние кверента)? К какому неизбежному синтезу приводит их сложение в фигуру Судьи?');
|
||||
if (isShield) {
|
||||
buffer.writeln('3. ТАЙНЫЙ СОВЕТ ПРИМИРИТЕЛЯ:');
|
||||
buffer.writeln(" Проанализируй 16-ю фигуру Примирителя (сложение Судьи и 1-й Матери). Как она снимает конфликт между тем, «с чего всё началось», и тем, «к чему всё пришло»?");
|
||||
}
|
||||
buffer.writeln('4. СИНХРОНИСТИЧЕСКИЙ СИНТЕЗ С ОРАКУЛОМ:');
|
||||
buffer.writeln(' Свяжи выпавшие фигуры с архетипами лексикона 999, подчеркнув ключевой закон бытия.');
|
||||
buffer.writeln('5. ПРИКЛАДНАЯ СТРАТЕГИЯ И ПРЕДОСТЕРЕЖЕНИЕ:');
|
||||
buffer.writeln(' Дай конкретный и кристально ясный совет кверенту: какие действия предпринять немедленно, чего категорически избегать и в чём заключается главный духовный урок.');
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// Generates the Grand Master Prompt for Celtic Tree Ogham (20 Fedas of the Sacred Grove).
|
||||
static String generateOghamMasterPrompt({
|
||||
required String question,
|
||||
required String layoutTitle,
|
||||
required List<OghamPromptFeda> fedas,
|
||||
}) {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
buffer.writeln('═══════════════════════════════════════════════════════════════════');
|
||||
buffer.writeln('ВЕЛИКИЙ МАСТЕР-ПРОМПТ: КЕЛЬТСКИЙ ДРЕВЕСНЫЙ ОГАМ');
|
||||
buffer.writeln('20 ФЕД СВЯЩЕННОЙ РОЩИ ДРУИДОВ И ТАЙНОПИСЬ БОГА ОГМЫ');
|
||||
buffer.writeln('═══════════════════════════════════════════════════════════════════');
|
||||
buffer.writeln();
|
||||
|
||||
buffer.writeln('### СИСТЕМНАЯ РОЛЬ:');
|
||||
buffer.writeln('Ты — верховный друид и филид, хранитель Священной Рощи, знаток тайнописи бога Огмы и древесного календаря кельтов.');
|
||||
buffer.writeln('Перед тобой священный расклад древесного Огама. Каждая феда олицетворяет священное дерево или кустарник, несущий архетипическую силу духов природы и космических законов.');
|
||||
buffer.writeln('Твоя миссия — истолковать шёпот деревьев, раскрыть тайный баланс прямых и теневых сил и дать кверенту глубокий, поэтичный и практический совет.');
|
||||
buffer.writeln();
|
||||
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('1. ФОКУС И ВОПРОС КВЕРЕНТА:');
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
final q = question.trim().isNotEmpty ? question.trim() : 'Мудрость Священной Рощи Огама и древесные силы судьбы.';
|
||||
buffer.writeln('• Вопрос кверента: «' + q + '»');
|
||||
buffer.writeln('• Формат расклада: ' + layoutTitle);
|
||||
buffer.writeln('• Традиция: Кельтский сакральный Огам (4 Аикмы Священной Рощи)');
|
||||
buffer.writeln();
|
||||
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('2. ВЫПАВШИЕ ФЕДЫ РОЩИ:');
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
for (final f in fedas) {
|
||||
buffer.writeln('• Позиция «' + f.position + '»: ' + f.symbol + ' ' + f.name + ' [' + (f.isReversed ? 'ТЕНЕВОЕ ПОЛОЖЕНИЕ' : 'ПРЯМОЕ ПОЛОЖЕНИЕ') + ']');
|
||||
buffer.writeln(' Семья (Аикма): ' + f.aicme);
|
||||
buffer.writeln(' Древесная сила: ' + f.lore);
|
||||
if (f.linkedWord != null) {
|
||||
buffer.writeln(' Связанный архетип Оракула 999: #' + f.linkedWord!.formattedSessionNumber + ' «' + f.linkedWord!.word.toUpperCase() + '» (' + f.linkedWord!.core + ')');
|
||||
}
|
||||
buffer.writeln();
|
||||
}
|
||||
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('3. ТРЕБОВАНИЯ К ДРУИДИЧЕСКОМУ ТОЛКОВАНИЮ НЕЙРОСЕТЬЮ:');
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────────');
|
||||
buffer.writeln('1. АНАЛИЗ ДРЕВЕСНЫХ СИЛ: Как сочетаются качества выпавших деревьев (например, гибкость Ивы, несгибаемость Дуба, очищение Берёзы, защита Тис)?');
|
||||
buffer.writeln('2. ДИАГНОСТИКА ТЕНЕЙ: Если выпали перевёрнутые феды, вскрой их предупреждение — где заблокирован жизненный сок или где кверент проявляет гордыню / самоизоляцию?');
|
||||
buffer.writeln('3. ДИНАМИКА ПОЗИЦИЙ РАСКЛАДА: Как раскладывается временная или стихийная линия вопроса?');
|
||||
buffer.writeln('4. СИНТЕЗ СО ЗНАКАМИ ОРАКУЛА 999: Как символы 999 подкрепляют наставление Рощи?');
|
||||
buffer.writeln('5. ДРУИДИЧЕСКИЙ НАКАЗ КВЕРЕНТУ: Точное, прикладное наставление — как поступить в материальном мире.');
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class OghamPromptFeda {
|
||||
final String position;
|
||||
final String name;
|
||||
final String symbol;
|
||||
final String aicme;
|
||||
final bool isReversed;
|
||||
final String lore;
|
||||
final WordItem? linkedWord;
|
||||
|
||||
const OghamPromptFeda({
|
||||
required this.position,
|
||||
required this.name,
|
||||
required this.symbol,
|
||||
required this.aicme,
|
||||
required this.isReversed,
|
||||
required this.lore,
|
||||
this.linkedWord,
|
||||
});
|
||||
}
|
||||
|
||||
class GeomancyPromptFigure {
|
||||
final String position;
|
||||
final String name;
|
||||
final String latinName;
|
||||
final String arabicName;
|
||||
final List<int> lines;
|
||||
final String element;
|
||||
final String planet;
|
||||
final String lore;
|
||||
final WordItem? linkedWord;
|
||||
|
||||
const GeomancyPromptFigure({
|
||||
required this.position,
|
||||
required this.name,
|
||||
this.latinName = '',
|
||||
this.arabicName = '',
|
||||
required this.lines,
|
||||
required this.element,
|
||||
required this.planet,
|
||||
required this.lore,
|
||||
this.linkedWord,
|
||||
});
|
||||
|
||||
String get dotsDisplay {
|
||||
final elements = ['Огонь', 'Воздух', 'Вода', 'Земля'];
|
||||
if (lines.length != 4) return lines.join('-');
|
||||
return List.generate(
|
||||
4,
|
||||
(i) => ' [' + elements[i] + ']: ' + (lines[i] == 1 ? '• ' : '•• ') + ' (' + (lines[i] == 1 ? 'активный импульс' : 'пассивный сосуд') + ')',
|
||||
).join('\n');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,941 @@
|
||||
// GENERATED FILE - DO NOT EDIT MANUALLY
|
||||
// Passport Schema & Pre-Prompt Generator for Oracle 999
|
||||
import '../models/word_model.dart';
|
||||
|
||||
class PassportFieldDef {
|
||||
final int index;
|
||||
final String key;
|
||||
final String sectionNum;
|
||||
final String sectionTitle;
|
||||
final String title;
|
||||
final String promptTemplate;
|
||||
final String formatTemplate;
|
||||
|
||||
const PassportFieldDef({
|
||||
required this.index,
|
||||
required this.key,
|
||||
required this.sectionNum,
|
||||
required this.sectionTitle,
|
||||
required this.title,
|
||||
required this.promptTemplate,
|
||||
required this.formatTemplate,
|
||||
});
|
||||
|
||||
String buildPrompt(WordItem word) {
|
||||
final idPad = word.sessionNumber.toString().padLeft(3, '0');
|
||||
final wordName = word.word;
|
||||
final emoji = word.emoji;
|
||||
final pos = word.partOfSpeech.isNotEmpty ? word.partOfSpeech : 'существительное';
|
||||
final cat = word.category.isNotEmpty ? word.category : 'Образ / Архетип';
|
||||
|
||||
var t = promptTemplate;
|
||||
t = t.replaceAll('{word}', wordName);
|
||||
t = t.replaceAll('{id_pad}', idPad);
|
||||
t = t.replaceAll('{emoji}', emoji);
|
||||
t = t.replaceAll('{pos}', pos);
|
||||
t = t.replaceAll('{cat}', cat);
|
||||
t = t.replaceAll('{word_upper}', wordName.toUpperCase());
|
||||
t = t.replaceAll('{extra}', '');
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
const List<PassportFieldDef> kPassportFields78 = [
|
||||
|
||||
PassportFieldDef(
|
||||
index: 1,
|
||||
key: 'index_and_name',
|
||||
sectionNum: 'I',
|
||||
sectionTitle: "Заголовочный паспорт (Метаданные реестра)",
|
||||
title: "Индекс и имя",
|
||||
promptTemplate: "Для сакрального знака «{word}» (ID: {id_pad}) системы «Оракул» сформируй каноническую заголовочную строку строго по шаблону: «[НОМЕР 000–999] | [СЛОВО В ВЕРХНЕМ РЕГИСТРЕ] | [2–4 ТОЧНЫХ ЭМОДЗИ/ГЛИФА]». Подбери наиболее точные символические эмодзи, передающие метафизическую сущность знака. Верни только эту одну строку.",
|
||||
formatTemplate: "{id_pad} | {word_upper} | {emojis}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 2,
|
||||
key: 'grammatical_class',
|
||||
sectionNum: 'I',
|
||||
sectionTitle: "Заголовочный паспорт (Метаданные реестра)",
|
||||
title: "Грамматический класс",
|
||||
promptTemplate: "Для слова «{word}» укажи точный грамматический класс в русском языке, выбрав строго один вариант из списка: существительное, глагол, прилагательное, наречие, числительное, предлог, местоимение. Верни только одно слово — выбранный грамматический класс в нижнем регистре.",
|
||||
formatTemplate: "{pos}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 3,
|
||||
key: 'functional_category',
|
||||
sectionNum: 'I',
|
||||
sectionTitle: "Заголовочный паспорт (Метаданные реестра)",
|
||||
title: "Функциональная категория",
|
||||
promptTemplate: "Для знака «{word}» (часть речи: {pos}) определи наиболее точную функциональную категорию Оракула 999. Выбери строго один вариант из канонического перечня: Образ / Архетип, Космический Знак, Священный Предмет, Сакральный Символ, Сакральное Место, Тотем, Архетип Человека, Действие / Трансформация, Свойство / Качество, Вектор / Направление, Числовой код, Фокус сознания. Верни только точное название категории.",
|
||||
formatTemplate: "{category}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 4,
|
||||
key: 'network_status',
|
||||
sectionNum: 'I',
|
||||
sectionTitle: "Заголовочный паспорт (Метаданные реестра)",
|
||||
title: "Сетевой статус",
|
||||
promptTemplate: "Для знака «{word}» определи сетевой статус в матрице кросс-мантических соответствий Оракула 999. Укажи: является ли знак 'Узловой станцией' (прямой первичный якорь или узел пересечения традиций, перечисли ключевые традиции в скобках) ИЛИ 'Свободным знаком' (автономный архетип поля). Кратко обоснуй формулировку в одну строку.",
|
||||
formatTemplate: "{network_status}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 5,
|
||||
key: 'mantic_runes',
|
||||
sectionNum: 'II',
|
||||
sectionTitle: "Кросс-мантическая сеть и станции",
|
||||
title: "Старший Футарк (Руны)",
|
||||
promptTemplate: "Определи соответствие знака «{word}» в скандинавском Старшем Футарке (24 руны). Укажи строго по шаблону: [Имя руны / Роль: Первичное, Пересечение или Архетипическая аналогия / Смысловые сиблинги в лексиконе 999]. Кратко укажи атт руны и закон действия.",
|
||||
formatTemplate: "[{rune_name} / Роль: {role} / Сиблинги: {siblings}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 6,
|
||||
key: 'mantic_ogham',
|
||||
sectionNum: 'II',
|
||||
sectionTitle: "Кросс-мантическая сеть и станции",
|
||||
title: "Кельтский Огам",
|
||||
promptTemplate: "Определи соответствие знака «{word}» в кельтском Огаме (20 сакральных деревьев священной рощи друидов). Укажи: [Имя феды и дерева / Роль: Первичное или Пересечение / Сиблинги]. Укажи айкме и природно-тотемический смысл.",
|
||||
formatTemplate: "[{feda_name} / Роль: {role} / Сиблинги: {siblings}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 7,
|
||||
key: 'mantic_tarot',
|
||||
sectionNum: 'II',
|
||||
sectionTitle: "Кросс-мантическая сеть и станции",
|
||||
title: "Старшие Арканы Таро",
|
||||
promptTemplate: "Определи соответствие знака «{word}» в Старших Арканах Таро (0 Шут — XXI Мир). Укажи: [Номер и название Аркана / Роль: Первичный архетип, Эманация или Пересечение / Родственные карты и сиблинги 999]. Опиши архетипический узел.",
|
||||
formatTemplate: "[{arcanum} / Роль: {role} / Сиблинги: {siblings}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 8,
|
||||
key: 'mantic_lenormand',
|
||||
sectionNum: 'II',
|
||||
sectionTitle: "Кросс-мантическая сеть и станции",
|
||||
title: "Малая колода Ленорман",
|
||||
promptTemplate: "Определи соответствие знака «{word}» в малой французской колоде Ленорман (36 карт от Всадника до Креста). Укажи: [Номер и название карты / Роль / Сиблинги]. Приведи ключевые синтагматические связки.",
|
||||
formatTemplate: "[{card} / Роль: {role} / Сиблинги: {siblings}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 9,
|
||||
key: 'mantic_kabbalah',
|
||||
sectionNum: 'II',
|
||||
sectionTitle: "Кросс-мантическая сеть и станции",
|
||||
title: "Каббала / Древо Жизни",
|
||||
promptTemplate: "Определи соответствие знака «{word}» в Каббале и Древе Жизни (Эц Хаим). Укажи: [Сфира (Кетер..Малкут) или Буквенный Путь между сфирот / Столп (Милосердие/Суровость/Равновесие) / Роль / Сиблинги].",
|
||||
formatTemplate: "[{sephira_or_path} / Роль: {role} / Сиблинги: {siblings}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 10,
|
||||
key: 'mantic_geomancy',
|
||||
sectionNum: 'II',
|
||||
sectionTitle: "Кросс-мантическая сеть и станции",
|
||||
title: "Арабская Геомантия ('Илм ар-Рамл)",
|
||||
promptTemplate: "Определи соответствие знака «{word}» среди 16 священных фигур Арабской Геомантии ('Илм ар-Рамл: Via, Populus, Fortuna Major/Minor, Acquisitio, Amissio, Conjunctio, Carcer, Albus, Rubeus, Puella, Puer, Tristitia, Laetitia, Caput/Cauda Draconis). Укажи: [Имя фигуры (латынь/арабский) / Стихийная тетраграмма / Роль / Сиблинги].",
|
||||
formatTemplate: "[{figure} / Роль: {role} / Сиблинги: {siblings}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 11,
|
||||
key: 'mantic_iching',
|
||||
sectionNum: 'II',
|
||||
sectionTitle: "Кросс-мантическая сеть и станции",
|
||||
title: "И Цзин (Книга Перемен)",
|
||||
promptTemplate: "Определи соответствие знака «{word}» в древнекитайской Книге Перемен (И Цзин). Укажи: [Номер и китайское/русское имя гексаграммы / Верхняя и нижняя триграммы Багуа / Роль / Сиблинги].",
|
||||
formatTemplate: "[{hexagram} / Роль: {role} / Сиблинги: {siblings}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 12,
|
||||
key: 'mantic_alchemy',
|
||||
sectionNum: 'II',
|
||||
sectionTitle: "Кросс-мантическая сеть и станции",
|
||||
title: "Алхимия (Magnum Opus)",
|
||||
promptTemplate: "Определи соответствие знака «{word}» в герметической Алхимии (Magnum Opus). Укажи: [Стадия (Nigredo, Albedo, Citrinitas, Rubedo) / Алхимическая операция (кальцинация, дистилляция, конъюнкция и др.) / Роль / Сиблинги].",
|
||||
formatTemplate: "[{stage_or_element} / Роль: {role} / Сиблинги: {siblings}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 13,
|
||||
key: 'mantic_zodiac_west',
|
||||
sectionNum: 'II',
|
||||
sectionTitle: "Кросс-мантическая сеть и станции",
|
||||
title: "Классический Зодиак",
|
||||
promptTemplate: "Определи соответствие знака «{word}» в классическом западном Зодиаке. Укажи: [Знак (Овен — Рыбы) / Планетарный управитель / Стихия и крест качеств / Роль / Сиблинги].",
|
||||
formatTemplate: "[{zodiac_sign} / Роль: {role} / Сиблинги: {siblings}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 14,
|
||||
key: 'mantic_zodiac_chinese',
|
||||
sectionNum: 'II',
|
||||
sectionTitle: "Кросс-мантическая сеть и станции",
|
||||
title: "Китайский Зодиак",
|
||||
promptTemplate: "Определи соответствие знака «{word}» в китайском Зодиаке (12 Земных Ветвей). Укажи: [Земная Ветвь и тотем (Крыса — Свинья) / Стихия У-Син / Инь или Ян / Роль / Сиблинги].",
|
||||
formatTemplate: "[{branch} / Роль: {role} / Сиблинги: {siblings}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 15,
|
||||
key: 'mantic_septener',
|
||||
sectionNum: 'II',
|
||||
sectionTitle: "Кросс-мантическая сеть и станции",
|
||||
title: "Септенер",
|
||||
promptTemplate: "Определи соответствие знака «{word}» в космическом Септенере (7 священных планет древности и 7 дней недели: Луна/Пн..Солнце/Вс). Укажи: [Планета / День недели / Роль / Сиблинги].",
|
||||
formatTemplate: "[{planet_day} / Роль: {role} / Сиблинги: {siblings}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 16,
|
||||
key: 'mantic_dodecade',
|
||||
sectionNum: 'II',
|
||||
sectionTitle: "Кросс-мантическая сеть и станции",
|
||||
title: "Додекада Месяцев",
|
||||
promptTemplate: "Определи соответствие знака «{word}» в солярной Додекаде (12 месяцев года). Укажи: [Месяц года / Сезонное дыхание / Солярные врата (равноденствие/солнцестояние) / Роль / Сиблинги].",
|
||||
formatTemplate: "[{month_season} / Роль: {role} / Сиблинги: {siblings}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 17,
|
||||
key: 'mantic_numerology',
|
||||
sectionNum: 'II',
|
||||
sectionTitle: "Кросс-мантическая сеть и станции",
|
||||
title: "Нумерология",
|
||||
promptTemplate: "Определи нумерологический код знака «{word}» с учётом его ID: {id_pad} и внутренней вибрации. Укажи: [Базовое число (1–9, 11, 22) / Качество числа / Связь с ячейкой Квадрата Пифагора / Роль / Сиблинги].",
|
||||
formatTemplate: "[{number} / Роль: {role} / Сиблинги: {siblings}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 18,
|
||||
key: 'mantic_cosmology',
|
||||
sectionNum: 'II',
|
||||
sectionTitle: "Кросс-мантическая сеть и станции",
|
||||
title: "Космология Трёх Миров",
|
||||
promptTemplate: "Определи топологию знака «{word}» в Космологии Трёх Миров: Верхний Мир (дух, сверхсознание, ноосфера), Срединный Мир (материя, социум, явь) или Нижний Мир (тень, хтоническое, навь), либо Мировая Ось (Axis Mundi). Укажи: [Мир / Локация на Мировом Древе / Роль / Сиблинги].",
|
||||
formatTemplate: "[{world_axis} / Роль: {role} / Сиблинги: {siblings}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 19,
|
||||
key: 'mantic_tzolkin',
|
||||
sectionNum: 'II',
|
||||
sectionTitle: "Кросс-мантическая сеть и станции",
|
||||
title: "20 Навалей Цолькин (Майя)",
|
||||
promptTemplate: "Определи соответствие знака «{word}» среди 20 священных Навалей календаря Майя Цолькин (Имиш, Ик, Акбаль, Кан, Чикчан, Кими, Маник, Ламат, Мулук, Ок, Чуэн, Эб, Бен, Хиш, Мен, Киб, Кабан, Эцнаб, Кавак, Ахау). Укажи: [Имя Наваля / Тотем и стихия / Энергия дня / Роль / Сиблинги].",
|
||||
formatTemplate: "[{nahual} / Роль: {role} / Сиблинги: {siblings}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 20,
|
||||
key: 'mantic_nakshatras',
|
||||
sectionNum: 'II',
|
||||
sectionTitle: "Кросс-мантическая сеть и станции",
|
||||
title: "27 Накшатр Джйотиш (Индия)",
|
||||
promptTemplate: "Определи соответствие знака «{word}» в системе 27 Накшатр (лунных стоянок) ведической астрологии Джйотиш (от Ашвини до Ревати). Укажи: [Имя Накшатры / Правящее божество (Девата) / Символ / Сакральное качество / Роль / Сиблинги].",
|
||||
formatTemplate: "[{nakshatra} / Роль: {role} / Сиблинги: {siblings}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 21,
|
||||
key: 'mantic_station_laws',
|
||||
sectionNum: 'II',
|
||||
sectionTitle: "Кросс-мантическая сеть и станции",
|
||||
title: "Действующие законы станций (выжимка)",
|
||||
promptTemplate: "Для знака «{word}» сформулируй концентрированную смысловую выжимку действующих сакральных законов тех мантических станций, на которых этот знак активирован. Как эти традиции сходятся в одну монолитную формулу? Сформулируй плотно в 2–4 предложения.",
|
||||
formatTemplate: "{laws_summary}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 22,
|
||||
key: 'etymology_roots',
|
||||
sectionNum: 'III',
|
||||
sectionTitle: "Мультилингвальный и графический паспорт",
|
||||
title: "Этимология и праформы",
|
||||
promptTemplate: "Для слова «{word}» реконструируй древнейшие праформы и этимологию: праиндоевропейский корень (*PIE) или иной праязыковый корень, его первичный конкретно-физический смысл до абстрактного обобщения и исторический путь семантического сдвига. Сформулируй ёмко и академически точно.",
|
||||
formatTemplate: "{etymology}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 23,
|
||||
key: 'script_sumer_akkad',
|
||||
sectionNum: 'III',
|
||||
sectionTitle: "Мультилингвальный и графический паспорт",
|
||||
title: "Шумер / Аккад",
|
||||
promptTemplate: "Для понятия «{word}» приведи клинописный эквивалент шумеро-аккадской традиции: [Клинописный знак или транслитерация / Шумерское или аккадское название / Фонетическое чтение и сакральное значение].",
|
||||
formatTemplate: "[{sign} / {name} / {phonetics}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 24,
|
||||
key: 'script_egypt',
|
||||
sectionNum: 'III',
|
||||
sectionTitle: "Мультилингвальный и графический паспорт",
|
||||
title: "Древний Египет",
|
||||
promptTemplate: "Для понятия «{word}» приведи древнеегипетский иероглифический аналог: [Иероглиф или описание детерминатива / Научная транскрипция / Священное религиозное значение в мифе об Осирисе/Ра].",
|
||||
formatTemplate: "[{hieroglyph} / {transcription} / {meaning}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 25,
|
||||
key: 'script_hebrew',
|
||||
sectionNum: 'III',
|
||||
sectionTitle: "Мультилингвальный и графический паспорт",
|
||||
title: "Иудейская традиция",
|
||||
promptTemplate: "Для понятия «{word}» приведи соответствие на библейском иврите: [Слово на иврите с огласовками / Буквенно-мистический код / Числовое значение гематрии и каббалистический смысл].",
|
||||
formatTemplate: "[{hebrew_word} / {letter_code} / {gematria}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 26,
|
||||
key: 'script_antiquity',
|
||||
sectionNum: 'III',
|
||||
sectionTitle: "Мультилингвальный и графический паспорт",
|
||||
title: "Античность",
|
||||
promptTemplate: "Для понятия «{word}» приведи античные термины: [Древнегреческий термин с ударением / Классическая латынь / Философский концепт греко-римской античности].",
|
||||
formatTemplate: "[{greek} / {latin} / {concept}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 27,
|
||||
key: 'script_celtic',
|
||||
sectionNum: 'III',
|
||||
sectionTitle: "Мультилингвальный и графический паспорт",
|
||||
title: "Кельты",
|
||||
promptTemplate: "Для понятия «{word}» приведи кельтский аналог: [Огамическая буква или знак / Древнеирландский термин / Мифологический контекст друидов].",
|
||||
formatTemplate: "[{ogham_letter} / {old_irish}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 28,
|
||||
key: 'script_runic_germanic',
|
||||
sectionNum: 'III',
|
||||
sectionTitle: "Мультилингвальный и графический паспорт",
|
||||
title: "Скандинавы / Германцы",
|
||||
promptTemplate: "Для понятия «{word}» приведи скандинавско-германский аналог: [Руна / Древнескандинавский или прагерманский термин / Значение в мифологии Старшей Эдды].",
|
||||
formatTemplate: "[{rune} / {old_norse}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 29,
|
||||
key: 'script_slavic',
|
||||
sectionNum: 'III',
|
||||
sectionTitle: "Мультилингвальный и графический паспорт",
|
||||
title: "Славяне",
|
||||
promptTemplate: "Для слова «{word}» приведи древнеславянский срез: [Праславянский корень (*) / Глаголическая буква или буквица / Сакральное мифопоэтическое значение в славянской традиции].",
|
||||
formatTemplate: "[{proto_slavic} / {glagolitic} / {meaning}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 30,
|
||||
key: 'script_india_sanskrit',
|
||||
sectionNum: 'III',
|
||||
sectionTitle: "Мультилингвальный и графический паспорт",
|
||||
title: "Ведическая Индия",
|
||||
promptTemplate: "Для понятия «{word}» приведи ведический/санскритский срез: [Слово письмом деванагари / Транслитерация IAST / Санскритский корень (дхату) и философский смысл в Упанишадах].",
|
||||
formatTemplate: "[{devanagari} / {sanskrit_iast} / {root}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 31,
|
||||
key: 'script_persia',
|
||||
sectionNum: 'III',
|
||||
sectionTitle: "Мультилингвальный и графический паспорт",
|
||||
title: "Персия",
|
||||
promptTemplate: "Для понятия «{word}» приведи персидский аналог: [Авестийский или пехлевийский термин / Его роль в зороастрийской космологии Авесты].",
|
||||
formatTemplate: "[{avestan_or_pahlavi} / {meaning}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 32,
|
||||
key: 'script_china',
|
||||
sectionNum: 'III',
|
||||
sectionTitle: "Мультилингвальный и графический паспорт",
|
||||
title: "Китай",
|
||||
promptTemplate: "Для понятия «{word}» приведи древнекитайский иероглифический образ: [Иероглиф / Транскрипция пиньинь / Разбор пиктографических ключей древнего начертания и даосский смысл].",
|
||||
formatTemplate: "[{hanzi} / {pinyin} / {pictogram_sense}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 33,
|
||||
key: 'script_japan',
|
||||
sectionNum: 'III',
|
||||
sectionTitle: "Мультилингвальный и графический паспорт",
|
||||
title: "Япония",
|
||||
promptTemplate: "Для понятия «{word}» приведи японский аналог: [Кандзи / Ромадзи / Синтоистский сакральный концепт или эстетическая категория (ками, макото, моно-но аварэ)].",
|
||||
formatTemplate: "[{kanji} / {romaji} / {shinto_concept}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 34,
|
||||
key: 'script_islamic',
|
||||
sectionNum: 'III',
|
||||
sectionTitle: "Мультилингвальный и графический паспорт",
|
||||
title: "Исламский мир",
|
||||
promptTemplate: "Для понятия «{word}» приведи арабско-исламский эквивалент: [Арабское слово с огласовками / Трёхбуквенный корень / Число по системе Абджад и суфийский смысл].",
|
||||
formatTemplate: "[{arabic_script} / {root} / {abjad}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 35,
|
||||
key: 'script_western_europe',
|
||||
sectionNum: 'III',
|
||||
sectionTitle: "Мультилингвальный и графический паспорт",
|
||||
title: "Западная Европа",
|
||||
promptTemplate: "Для понятия «{word}» приведи западноевропейский герметический/алхимический глиф или знак средневековых гримуаров и его толкование.",
|
||||
formatTemplate: "[{alchemical_glyph} / {hermetic_term}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 36,
|
||||
key: 'script_mesoamerica',
|
||||
sectionNum: 'III',
|
||||
sectionTitle: "Мультилингвальный и графический паспорт",
|
||||
title: "Мезоамерика",
|
||||
promptTemplate: "Для понятия «{word}» приведи мезоамериканский эквивалент: [Майянский иероглифический глиф или ацтекская пиктограмма науатль / Произношение и сакральный смысл].",
|
||||
formatTemplate: "[{mayan_glyph_or_nahuatl} / {meaning}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 37,
|
||||
key: 'script_native_american',
|
||||
sectionNum: 'III',
|
||||
sectionTitle: "Мультилингвальный и графический паспорт",
|
||||
title: "Коренные народы Сев. Америки",
|
||||
promptTemplate: "Для понятия «{word}» приведи символический пиктограф или тотемный знак коренных народов Северной Америки (навахо, оджибве, лакота, хопи) и его значение.",
|
||||
formatTemplate: "[{pictograph} / {totem_symbol}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 38,
|
||||
key: 'script_polynesia',
|
||||
sectionNum: 'III',
|
||||
sectionTitle: "Мультилингвальный и графический паспорт",
|
||||
title: "Полинезия",
|
||||
promptTemplate: "Для понятия «{word}» приведи геометрический мотив полинезийского татау / маорийского та-моко, коренной термин и его связь с концепциями Мана (сила) и Тапу (священность).",
|
||||
formatTemplate: "[{tatau_motif} / {mana_concept}]",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 39,
|
||||
key: 'state_neutral',
|
||||
sectionNum: 'IV',
|
||||
sectionTitle: "Трехполярная матрица состояний",
|
||||
title: "0 Нейтральный статус (База / Бытие / Среда / Субстанция)",
|
||||
promptTemplate: "Для знака «{word}» опиши НЕЙТРАЛЬНЫЙ СТАТУС [0]: онтологический и физический факт его присутствия в мире до человеческой оценки, эмоций и субъективной воли. Как этот феномен существует как чистая субстанция или естественная среда бытия? Сформулируй ёмко в 2–3 предложения.",
|
||||
formatTemplate: "{state_0}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 40,
|
||||
key: 'state_creative',
|
||||
sectionNum: 'IV',
|
||||
sectionTitle: "Трехполярная матрица состояний",
|
||||
title: "[+1] Созидательный вектор (Свет / Ресурс / Эволюция)",
|
||||
promptTemplate: "Для знака «{word}» опиши СОЗИДАТЕЛЬНЫЙ ВЕКТОР [+1]: его светлое, ресурсное и конструктивное проявление. Как этот архетип действует в созидании порядка, исцелении, росте осознанности, эволюции духа и преумножении жизни? Сформулируй ёмко в 2–3 предложения.",
|
||||
formatTemplate: "{state_plus}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 41,
|
||||
key: 'state_destructive',
|
||||
sectionNum: 'IV',
|
||||
sectionTitle: "Трехполярная матрица состояний",
|
||||
title: "[-1] Деструктивный вектор (Тень / Вызов / Искажение)",
|
||||
promptTemplate: "Для знака «{word}» опиши ДЕСТРУКТИВНЫЙ ВЕКТОР [-1]: его теневое, искажённое и разрушительное проявление при потере меры и осознанности. В какие аффекты, застои, хаос, выгорание или слепоту вырождается этот знак? Сформулируй ёмко в 2–3 предложения.",
|
||||
formatTemplate: "{state_minus}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 42,
|
||||
key: 'culture_sumer_light',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Шумер и Месопотамия: [+] Свет",
|
||||
promptTemplate: "Для символа «{word}» опиши СОЗИДАТЕЛЬНЫЙ АСПЕКТ [+] в культуре Шумера и Месопотамии (мифы, боги, цивилизационный дар, законы Мэ). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{sumer_light}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 43,
|
||||
key: 'culture_sumer_shadow',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Шумер и Месопотамия: [-] Тень",
|
||||
promptTemplate: "Для символа «{word}» опиши ДЕСТРУКТИВНЫЙ / ТЕНЕВОЙ АСПЕКТ [-] в культуре Шумера и Месопотамии (хаос Тиамат, проклятия, подземный мир Кура, гнев богов). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{sumer_shadow}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 44,
|
||||
key: 'culture_egypt_light',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Древний Египет: [+] Свет",
|
||||
promptTemplate: "Для символа «{word}» опиши СОЗИДАТЕЛЬНЫЙ АСПЕКТ [+] в религии Древнего Египта (закон Маат, солярная сила Ра, возрождение Осириса). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{egypt_light}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 45,
|
||||
key: 'culture_egypt_shadow',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Древний Египет: [-] Тень",
|
||||
promptTemplate: "Для символа «{word}» опиши ДЕСТРУКТИВНЫЙ / ТЕНЕВОЙ АСПЕКТ [-] в мифологии Древнего Египта (тьма Апопа, пустыня и ярость Сета, суд загробного мира Дуат). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{egypt_shadow}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 46,
|
||||
key: 'culture_kabbalah_light',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Иудейская традиция и Каббала: [+] Свет",
|
||||
promptTemplate: "Для символа «{word}» опиши СОЗИДАТЕЛЬНЫЙ АСПЕКТ [+] в иудейской традиции и Каббале (божественный свет Ор Эйн Соф, исправление мира Тиккун Олам, благодать сфирот). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{kabbalah_light}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 47,
|
||||
key: 'culture_kabbalah_shadow',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Иудейская традиция и Каббала: [-] Тень",
|
||||
promptTemplate: "Для символа «{word}» опиши ДЕСТРУКТИВНЫЙ / ТЕНЕВОЙ АСПЕКТ [-] в Каббале и иудейском мифе (оболочки Клипот, суд Дин/Гвура, изгнание, падение). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{kabbalah_shadow}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 48,
|
||||
key: 'culture_antiquity_light',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Античность (Греция / Рим): [+] Свет",
|
||||
promptTemplate: "Для символа «{word}» опиши СОЗИДАТЕЛЬНЫЙ АСПЕКТ [+] в античной культуре Греции и Рима (аполлонический разум, доблесть арете, гармония космоса, право). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{antiquity_light}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 49,
|
||||
key: 'culture_antiquity_shadow',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Античность (Греция / Рим): [-] Тень",
|
||||
promptTemplate: "Для символа «{word}» опиши ДЕСТРУКТИВНЫЙ / ТЕНЕВОЙ АСПЕКТ [-] в античной традиции (гордыня хюбрис, слепой рок ананке, безумие Тартара, возмездие Немезиды). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{antiquity_shadow}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 50,
|
||||
key: 'culture_celtic_light',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Кельтская традиция: [+] Свет",
|
||||
promptTemplate: "Для символа «{word}» опиши СОЗИДАТЕЛЬНЫЙ АСПЕКТ [+] в кельтской традиции (священная мудрость друидов, священный огонь Бригитты, магия природы). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{celtic_light}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 51,
|
||||
key: 'culture_celtic_shadow',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Кельтская традиция: [-] Тень",
|
||||
promptTemplate: "Для символа «{word}» опиши ДЕСТРУКТИВНЫЙ / ТЕНЕВОЙ АСПЕКТ [-] в кельтском мифе (гнев фоморов, нарушение табу гейса, морок Сидов, безумие битвы). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{celtic_shadow}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 52,
|
||||
key: 'culture_nordic_light',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Скандинавско-германская традиция: [+] Свет",
|
||||
promptTemplate: "Для символа «{word}» опиши СОЗИДАТЕЛЬНЫЙ АСПЕКТ [+] в скандинавско-германской традиции (мудрость Одина, верность Тюра, плодородие Фрейра, сила Асгарда). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{nordic_light}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 53,
|
||||
key: 'culture_nordic_shadow',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Скандинавско-германская традиция: [-] Тень",
|
||||
promptTemplate: "Для символа «{word}» опиши ДЕСТРУКТИВНЫЙ / ТЕНЕВОЙ АСПЕКТ [-] в скандинавском мифе (коварен Локи, хладный Ётунхейм, сумерки богов Рагнарёк, ярость Фенрира). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{nordic_shadow}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 54,
|
||||
key: 'culture_slavic_light',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Славянская традиция: [+] Свет",
|
||||
promptTemplate: "Для символа «{word}» опиши СОЗИДАТЕЛЬНЫЙ АСПЕКТ [+] в славянской традиции (правь, свет Белобога, плодородие Матери-Сырой-Земли, благодать Лады). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{slavic_light}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 55,
|
||||
key: 'culture_slavic_shadow',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Славянская традиция: [-] Тень",
|
||||
promptTemplate: "Для символа «{word}» опиши ДЕСТРУКТИВНЫЙ / ТЕНЕВОЙ АСПЕКТ [-] в славянском мифе (навь, мгла Чернобога, хлад Морены, навьи чары и морок). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{slavic_shadow}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 56,
|
||||
key: 'culture_india_light',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Ведическая Индия и Тантра: [+] Свет",
|
||||
promptTemplate: "Для символа «{word}» опиши СОЗИДАТЕЛЬНЫЙ АСПЕКТ [+] в ведической традиции Индии (Дхарма, Саттва, освобождение Мокша, пробуждение Кундалини). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{india_light}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 57,
|
||||
key: 'culture_india_shadow',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Ведическая Индия и Тантра: [-] Тень",
|
||||
promptTemplate: "Для символа «{word}» опиши ДЕСТРУКТИВНЫЙ / ТЕНЕВОЙ АСПЕКТ [-] в индийской традиции (иллюзия Майя, Тамас, оковы сансары, разрушительный гнев Кали). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{india_shadow}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 58,
|
||||
key: 'culture_persia_light',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Персия и Зороастризм: [+] Свет",
|
||||
promptTemplate: "Для символа «{word}» опиши СОЗИДАТЕЛЬНЫЙ АСПЕКТ [+] в зороастризме и древней Персии (вечный свет Аша, мудрость Ахура Мазды, чистая мысль, слово и дело). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{persia_light}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 59,
|
||||
key: 'culture_persia_shadow',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Персия и Зороастризм: [-] Тень",
|
||||
promptTemplate: "Для символа «{word}» опиши ДЕСТРУКТИВНЫЙ / ТЕНЕВОЙ АСПЕКТ [-] в персидском мифе (ложь Друдж, морок Ангро-Майнью/Аримана, осквернение стихий). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{persia_shadow}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 60,
|
||||
key: 'culture_china_light',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Китай (Даосизм, И-цзин, У-син): [+] Свет",
|
||||
promptTemplate: "Для символа «{word}» опиши СОЗИДАТЕЛЬНЫЙ АСПЕКТ [+] в китайской традиции (следование пути Дао, недеяние У-вэй, баланс пяти стихий У-син). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{china_light}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 61,
|
||||
key: 'culture_china_shadow',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Китай (Даосизм, И-цзин, У-син): [-] Тень",
|
||||
promptTemplate: "Для символа «{word}» опиши ДЕСТРУКТИВНЫЙ / ТЕНЕВОЙ АСПЕКТ [-] в китайской традиции (дисбаланс стихий, застой Ци, истощение из-за борьбы с переменами). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{china_shadow}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 62,
|
||||
key: 'culture_japan_light',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Япония (Синтоизм, Сингон): [+] Свет",
|
||||
promptTemplate: "Для символа «{word}» опиши СОЗИДАТЕЛЬНЫЙ АСПЕКТ [+] в синтоизме и культуре Японии (очищение Мисоги, почитание ками, созерцание красоты, искренность сердца Макото). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{japan_light}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 63,
|
||||
key: 'culture_japan_shadow',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Япония (Синтоизм, Сингон): [-] Тень",
|
||||
promptTemplate: "Для символа «{word}» опиши ДЕСТРУКТИВНЫЙ / ТЕНЕВОЙ АСПЕКТ [-] в японской традиции (скверна Кэгарэ, гнев мстительных духов Онрё, бездна Ёми). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{japan_shadow}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 64,
|
||||
key: 'culture_islam_light',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Исламская традиция и суфизм: [+] Свет",
|
||||
promptTemplate: "Для символа «{word}» опиши СОЗИДАТЕЛЬНЫЙ АСПЕКТ [+] в исламской традиции и суфизме (божественный свет Нур, растворение эго в Боге Фана, мистическая любовь Ишк). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{islam_light}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 65,
|
||||
key: 'culture_islam_shadow',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Исламская традиция и суфизм: [-] Тень",
|
||||
promptTemplate: "Для символа «{word}» опиши ДЕСТРУКТИВНЫЙ / ТЕНЕВОЙ АСПЕКТ [-] в исламе и суфизме (низменные страсти Нафс, наущения Иблиса/Шайтана, гордыня). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{islam_shadow}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 66,
|
||||
key: 'culture_hermetic_light',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Западноевропейская традиция (Алхимия / Герметизм): [+] Свет",
|
||||
promptTemplate: "Для символа «{word}» опиши СОЗИДАТЕЛЬНЫЙ АСПЕКТ [+] в западноевропейском герметизме и алхимии (Великое Делание, трансмутация свинца в золото, философский камень). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{hermetic_light}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 67,
|
||||
key: 'culture_hermetic_shadow',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Западноевропейская традиция (Алхимия / Герметизм): [-] Тень",
|
||||
promptTemplate: "Для символа «{word}» опиши ДЕСТРУКТИВНЫЙ / ТЕНЕВОЙ АСПЕКТ [-] в европейской традиции (ложная алхимия суфлёров, фаустовская гордыня, ядовитый осадок нигредо). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{hermetic_shadow}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 68,
|
||||
key: 'culture_mesoamerica_light',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Мезоамерика и Анды (Майя, Ацтеки, Инки): [+] Свет",
|
||||
promptTemplate: "Для символа «{word}» опиши СОЗИДАТЕЛЬНЫЙ АСПЕКТ [+] в культуре Мезоамерики и Анд (космическое обновление, мудрость Кетцалькоатля/Кукулькана, солнце Инти). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{mesoamerica_light}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 69,
|
||||
key: 'culture_mesoamerica_shadow',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Мезоамерика и Анды (Майя, Ацтеки, Инки): [-] Тень",
|
||||
promptTemplate: "Для символа «{word}» опиши ДЕСТРУКТИВНЫЙ / ТЕНЕВОЙ АСПЕКТ [-] в мифологии Мезоамерики (кровавый долг богам, бездна Шибальбы, зеркало Тескатлипоки). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{mesoamerica_shadow}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 70,
|
||||
key: 'culture_native_american_light',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Традиции коренных народов Северной Америки: [+] Свет",
|
||||
promptTemplate: "Для символа «{word}» опиши СОЗИДАТЕЛЬНЫЙ АСПЕКТ [+] в шаманизме индейцев Северной Америки (священная связь с Великим Духом Вакан Танка, гармония Круга Жизни). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{native_american_light}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 71,
|
||||
key: 'culture_native_american_shadow',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Традиции коренных народов Северной Америки: [-] Тень",
|
||||
promptTemplate: "Для символа «{word}» опиши ДЕСТРУКТИВНЫЙ / ТЕНЕВОЙ АСПЕКТ [-] в индейских мифах (прожорливый каннибальский дух Вендиго, злые чары, разрушение круга). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{native_american_shadow}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 72,
|
||||
key: 'culture_polynesia_light',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Полинезия и Океания: [+] Свет",
|
||||
promptTemplate: "Для символа «{word}» опиши СОЗИДАТЕЛЬНЫЙ АСПЕКТ [+] в культуре Полинезии (величие духовной силы Мана, навигация океана, связь с предками Атуа). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{polynesia_light}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 73,
|
||||
key: 'culture_polynesia_shadow',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "Полинезия и Океания: [-] Тень",
|
||||
promptTemplate: "Для символа «{word}» опиши ДЕСТРУКТИВНЫЙ / ТЕНЕВОЙ АСПЕКТ [-] в мифах Океании (нарушение священного запрета Тапу, безжалостная ярость волн, утрата маны). Сформулируй в 1–2 предложения.",
|
||||
formatTemplate: "{polynesia_shadow}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 74,
|
||||
key: 'meta_synthesis',
|
||||
sectionNum: 'V',
|
||||
sectionTitle: "Культурологическая матрица",
|
||||
title: "17-й Интегральный мета-синтез (Универсальный культурологический код)",
|
||||
promptTemplate: "Для символа «{word}» сформируй 17-й ИНТЕГРАЛЬНЫЙ МЕТА-СИНТЕЗ, обобщающий все 16 культур человечества: 1. Мета-параллели: сквозная роль этого символа в мировой цивилизации; 2. Точка преломления: точная грань между созидательным (+) и деструктивным (-) проявлением знака; 3. Кросс-культурный инвариант: единый универсальный закон архетипа, одинаковый от Шумера до Полинезии. Сформулируй строго структурированным текстом.",
|
||||
formatTemplate: "Мета-параллели: {meta_parallels}\nТочка преломления: {inflection_point}\nКросс-культурный инвариант: {cross_cultural_invariant}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 75,
|
||||
key: 'semantic_core',
|
||||
sectionNum: 'VI',
|
||||
sectionTitle: "Мантический & Обучающий синтез",
|
||||
title: "Смысловое ядро",
|
||||
promptTemplate: "Для знака «{word}» (ID: {id_pad}, {pos}, {category}) сформулируй концентрированное СМЫСЛОВОЕ ЯДРО в 1–2 чеканных, философски точных предложениях. Какова глубинная метафизическая формула этого знака?",
|
||||
formatTemplate: "{semantic_core}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 76,
|
||||
key: 'grammar_interpretation',
|
||||
sectionNum: 'VI',
|
||||
sectionTitle: "Мантический & Обучающий синтез",
|
||||
title: "Интерпретация по грамматическому типу",
|
||||
promptTemplate: "Для знака «{word}» дай мантическую интерпретацию через призму его грамматического класса ({pos}): как именно его часть речи определяет характер проявления в раскладе — как стабильную среду/архетип (существительное), как прямой импульс/трансформацию (глагол), как модус/окраску ситуации (прилагательное) или иной вектор? Сформулируй в 2–3 предложения.",
|
||||
formatTemplate: "{grammar_interpretation}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 77,
|
||||
key: 'will_vector',
|
||||
sectionNum: 'VI',
|
||||
sectionTitle: "Мантический & Обучающий синтез",
|
||||
title: "Вектор Воли (К чему призывает)",
|
||||
promptTemplate: "Для знака «{word}» сформулируй ВЕКТОР ВОЛИ: к какому точному, конструктивному и необратимому действию или внутренней стратегии призывает этот знак кверента? Практический совет прямого действия.",
|
||||
formatTemplate: "{will_vector}",
|
||||
),
|
||||
PassportFieldDef(
|
||||
index: 78,
|
||||
key: 'shadow_warning',
|
||||
sectionNum: 'VI',
|
||||
sectionTitle: "Мантический & Обучающий синтез",
|
||||
title: "Предостережение / Тень (О чем предупреждает)",
|
||||
promptTemplate: "Для знака «{word}» сформулируй ПРЕДОСТЕРЕЖЕНИЕ / ТЕНЬ: от какой слепой ловушки, бессознательного импульса, иллюзии или гибельной крайности строго предупреждает этот знак? Точное указание на опасность.",
|
||||
formatTemplate: "{shadow_warning}",
|
||||
),
|
||||
|
||||
];
|
||||
|
||||
final Map<String, PassportFieldDef> kPassportFieldByKey = {
|
||||
for (final f in kPassportFields78) f.key: f,
|
||||
};
|
||||
|
||||
String buildSectionPrePrompt(String sectionNum, WordItem word) {
|
||||
final fields = kPassportFields78.where((f) => f.sectionNum == sectionNum).toList();
|
||||
if (fields.isEmpty) return '';
|
||||
final sb = StringBuffer();
|
||||
sb.writeln('ПАКЕТ ПРЕДПРОМПТОВ: РАЗДЕЛ $sectionNum. ${fields.first.sectionTitle}');
|
||||
sb.writeln('Знак: «${word.word.toUpperCase()}» (ID: ${word.sessionNumber.toString().padLeft(3, "0")})');
|
||||
sb.writeln('Ответь строго по пунктам, сохраняя названия строк:');
|
||||
sb.writeln();
|
||||
for (final f in fields) {
|
||||
sb.writeln('--- [Пункт ${f.index}/78] ${f.title} ---');
|
||||
sb.writeln(f.buildPrompt(word));
|
||||
sb.writeln();
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
String buildAllPassportPrompts(WordItem word) {
|
||||
final sb = StringBuffer();
|
||||
final idPad = word.sessionNumber.toString().padLeft(3, '0');
|
||||
sb.writeln('═══════════════════════════════════════════════════════════════════');
|
||||
sb.writeln('ПОЛНЫЙ ПАКЕТ ИЗ 78 ПРОМПТОВ: ЗНАК #$idPad «${word.word.toUpperCase()}»');
|
||||
sb.writeln('═══════════════════════════════════════════════════════════════════');
|
||||
sb.writeln();
|
||||
for (final f in kPassportFields78) {
|
||||
sb.writeln('### [ПРОМПТ ${f.index.toString().padLeft(2, "0")}/78] | Раздел ${f.sectionNum} | ${f.title}');
|
||||
sb.writeln(f.buildPrompt(word));
|
||||
sb.writeln();
|
||||
sb.writeln('-------------------------------------------------------------------');
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
|
||||
/// Интеллектуальный парсер текста паспорта или разделов в 78 атомарных полей
|
||||
Map<String, String> parsePassportMarkdownToFields(String md) {
|
||||
final result = <String, String>{};
|
||||
final lines = md.split('\n');
|
||||
|
||||
final simplePrefixes = <String, String>{
|
||||
'Индекс и имя:': 'index_and_name',
|
||||
'Грамматический класс:': 'grammatical_class',
|
||||
'Функциональная категория:': 'functional_category',
|
||||
'Сетевой статус:': 'network_status',
|
||||
'Старший Футарк (Руны):': 'mantic_runes',
|
||||
'Кельтский Огам:': 'mantic_ogham',
|
||||
'Старшие Арканы Таро:': 'mantic_tarot',
|
||||
'Малая колода Ленорман:': 'mantic_lenormand',
|
||||
'Каббала / Древо Жизни:': 'mantic_kabbalah',
|
||||
"Арабская Геомантия ('Илм ар-Рамл):": 'mantic_geomancy',
|
||||
'Арабская Геомантия:': 'mantic_geomancy',
|
||||
'И Цзин (Книга Перемен):': 'mantic_iching',
|
||||
'Алхимия (Magnum Opus):': 'mantic_alchemy',
|
||||
'Классический Зодиак:': 'mantic_zodiac_west',
|
||||
'Китайский Зодиак:': 'mantic_zodiac_chinese',
|
||||
'Септенер:': 'mantic_septener',
|
||||
'Додекада Месяцев:': 'mantic_dodecade',
|
||||
'Нумерология:': 'mantic_numerology',
|
||||
'Космология Трёх Миров:': 'mantic_cosmology',
|
||||
'20 Навалей Цолькин (Майя):': 'mantic_tzolkin',
|
||||
'27 Накшатр Джйотиш (Индия):': 'mantic_nakshatras',
|
||||
'Этимология и праформы:': 'etymology_roots',
|
||||
'Шумер / Аккад:': 'script_sumer_akkad',
|
||||
'Древний Египет:': 'script_egypt',
|
||||
'Иудейская традиция:': 'script_hebrew',
|
||||
'Античность:': 'script_antiquity',
|
||||
'Кельты:': 'script_celtic',
|
||||
'Скандинавы / Германцы:': 'script_runic_germanic',
|
||||
'Славяне:': 'script_slavic',
|
||||
'Ведическая Индия:': 'script_india_sanskrit',
|
||||
'Персия:': 'script_persia',
|
||||
'Китай:': 'script_china',
|
||||
'Япония:': 'script_japan',
|
||||
'Исламский мир:': 'script_islamic',
|
||||
'Западная Европа:': 'script_western_europe',
|
||||
'Мезоамерика:': 'script_mesoamerica',
|
||||
'Коренные народы Сев. Америки:': 'script_native_american',
|
||||
'Полинезия:': 'script_polynesia',
|
||||
'Смысловое ядро:': 'semantic_core',
|
||||
'Интерпретация по грамматическому типу:': 'grammar_interpretation',
|
||||
'Вектор Воли (К чему призывает):': 'will_vector',
|
||||
'Предостережение / Тень (О чем предупреждает):': 'shadow_warning',
|
||||
};
|
||||
|
||||
final culturePrefixes = <String, String>{
|
||||
'Шумер и Месопотамия:': 'culture_sumer',
|
||||
'Древний Египет:': 'culture_egypt',
|
||||
'Иудейская традиция и Каббала:': 'culture_kabbalah',
|
||||
'Античность (Греция / Рим):': 'culture_antiquity',
|
||||
'Кельтская традиция:': 'culture_celtic',
|
||||
'Скандинавско-германская традиция:': 'culture_nordic',
|
||||
'Славянская традиция:': 'culture_slavic',
|
||||
'Ведическая Индия и Тантра:': 'culture_india',
|
||||
'Персия и Зороастризм:': 'culture_persia',
|
||||
'Китай (Даосизм, И-цзин, У-син):': 'culture_china',
|
||||
'Япония (Синтоизм, Сингон):': 'culture_japan',
|
||||
'Исламская традиция и суфизм:': 'culture_islam',
|
||||
'Западноевропейская традиция (Алхимия / Герметизм):': 'culture_hermetic',
|
||||
'Мезоамерика и Анды (Майя, Ацтеки, Инки):': 'culture_mesoamerica',
|
||||
'Традиции коренных народов Северной Америки:': 'culture_native_american',
|
||||
'Полинезия и Океания:': 'culture_polynesia',
|
||||
};
|
||||
|
||||
int i = 0;
|
||||
while (i < lines.length) {
|
||||
final line = lines[i].trim();
|
||||
|
||||
// Check simple 1:1 prefixes
|
||||
bool matched = false;
|
||||
for (final entry in simplePrefixes.entries) {
|
||||
if (line.startsWith(entry.key)) {
|
||||
final val = line.substring(entry.key.length).trim();
|
||||
if (val.isNotEmpty) {
|
||||
result[entry.value] = val;
|
||||
}
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matched) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check cultures with [+] and [-]
|
||||
for (final entry in culturePrefixes.entries) {
|
||||
if (line.startsWith(entry.key)) {
|
||||
final rest = line.substring(entry.key.length).trim();
|
||||
if (rest.contains('[-]') && rest.contains('[+]')) {
|
||||
final parts = rest.split('[-]');
|
||||
final light = parts[0].replaceAll('[+]', '').replaceAll('/', '').trim();
|
||||
final shadow = parts.length > 1 ? parts[1].trim() : '';
|
||||
if (light.isNotEmpty) result['${entry.value}_light'] = light;
|
||||
if (shadow.isNotEmpty) result['${entry.value}_shadow'] = shadow;
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (matched) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Tri-polar states
|
||||
if (line.startsWith('0 Нейтральный статус')) {
|
||||
final idx = line.indexOf(':');
|
||||
if (idx != -1) {
|
||||
result['state_neutral'] = line.substring(idx + 1).trim();
|
||||
}
|
||||
} else if (line.startsWith('[+1] Созидательный вектор')) {
|
||||
final idx = line.indexOf(':');
|
||||
if (idx != -1) {
|
||||
result['state_creative'] = line.substring(idx + 1).trim();
|
||||
}
|
||||
} else if (line.startsWith('[-1] Деструктивный вектор')) {
|
||||
final idx = line.indexOf(':');
|
||||
if (idx != -1) {
|
||||
result['state_destructive'] = line.substring(idx + 1).trim();
|
||||
}
|
||||
} else if (line.startsWith('Действующие законы станций')) {
|
||||
final sb = StringBuffer();
|
||||
i++;
|
||||
while (i < lines.length && !lines[i].startsWith('---') && !lines[i].startsWith('III.')) {
|
||||
if (lines[i].trim().isNotEmpty) sb.writeln(lines[i].trim());
|
||||
i++;
|
||||
}
|
||||
if (sb.isNotEmpty) {
|
||||
result['mantic_station_laws'] = sb.toString().trim();
|
||||
}
|
||||
continue;
|
||||
} else if (line.startsWith('Б. 17-й Интегральный мета-синтез') || line.startsWith('17-й Интегральный мета-синтез:')) {
|
||||
final sb = StringBuffer();
|
||||
i++;
|
||||
while (i < lines.length && !lines[i].startsWith('---') && !lines[i].startsWith('VI.')) {
|
||||
if (lines[i].trim().isNotEmpty) sb.writeln(lines[i].trim());
|
||||
i++;
|
||||
}
|
||||
if (sb.isNotEmpty) {
|
||||
result['meta_synthesis'] = sb.toString().trim();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
|
||||
import '../models/lenormand_pairs.dart';
|
||||
|
||||
/// Same shape as Lenormand pairs JSON: n, name, meaning, pairs[].
|
||||
class TarotMajorsRepository {
|
||||
static final TarotMajorsRepository _instance = TarotMajorsRepository._internal();
|
||||
factory TarotMajorsRepository() => _instance;
|
||||
TarotMajorsRepository._internal();
|
||||
|
||||
final Map<int, LenormandCardEntry> _byN = {};
|
||||
final Map<String, LenormandCardEntry> _byId = {};
|
||||
bool _loaded = false;
|
||||
|
||||
bool get isLoaded => _loaded;
|
||||
List<LenormandCardEntry> get cards =>
|
||||
List.unmodifiable(_byN.values.toList()..sort((a, b) => a.n.compareTo(b.n)));
|
||||
|
||||
LenormandCardEntry? byNumber(int n) => _byN[n];
|
||||
LenormandCardEntry? byCanonId(String id) => _byId[id];
|
||||
|
||||
Future<void> init() async {
|
||||
if (_loaded && _byN.isNotEmpty) return;
|
||||
Map<String, dynamic>? decoded;
|
||||
final cwd = Directory.current.path;
|
||||
for (final path in {
|
||||
'assets/data/tarot_majors.json',
|
||||
'../assets/data/tarot_majors.json',
|
||||
'$cwd/assets/data/tarot_majors.json',
|
||||
'$cwd/../assets/data/tarot_majors.json',
|
||||
}) {
|
||||
try {
|
||||
final file = File(path);
|
||||
if (file.existsSync()) {
|
||||
decoded = json.decode(file.readAsStringSync()) as Map<String, dynamic>;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('tarot_majors.json skip $path: $e');
|
||||
}
|
||||
}
|
||||
if (decoded == null) {
|
||||
try {
|
||||
final jsonStr = await rootBundle.loadString('assets/data/tarot_majors.json');
|
||||
decoded = json.decode(jsonStr) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint('Could not load tarot_majors.json: $e');
|
||||
}
|
||||
}
|
||||
if (decoded == null) {
|
||||
_loaded = true;
|
||||
return;
|
||||
}
|
||||
_byN.clear();
|
||||
_byId.clear();
|
||||
for (final raw in (decoded['cards'] as List? ?? const [])) {
|
||||
if (raw is! Map<String, dynamic>) continue;
|
||||
final card = LenormandCardEntry.fromMap(raw);
|
||||
_byN[card.n] = card;
|
||||
_byId[card.id] = card;
|
||||
}
|
||||
_loaded = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
|
||||
import '../models/tradition_model.dart';
|
||||
|
||||
class TraditionsRepository {
|
||||
static final TraditionsRepository _instance = TraditionsRepository._internal();
|
||||
factory TraditionsRepository() => _instance;
|
||||
TraditionsRepository._internal();
|
||||
|
||||
List<TraditionCanon> _traditions = const [];
|
||||
bool _loaded = false;
|
||||
|
||||
List<TraditionCanon> get traditions => List.unmodifiable(_traditions);
|
||||
bool get isLoaded => _loaded;
|
||||
|
||||
static const Map<String, String> filterToCanonId = {
|
||||
'ᚱ Руны': 'runes',
|
||||
'᚛ Огам': 'ogham',
|
||||
'✡️ Каббала': 'kabbalah',
|
||||
'⚗️ Алхимия': 'alchemy',
|
||||
'🎴 Таро': 'tarot',
|
||||
'🪶 Ленорман': 'lenormand',
|
||||
'☯️ И Цзин': 'iching',
|
||||
'⌛ Геомантия': 'geomancy',
|
||||
'♈ Гороскоп': 'western_zodiac',
|
||||
'🐉 Китайский зодиак': 'zodiac',
|
||||
'🌌 Космология': 'cosmology',
|
||||
'📅 Дни недели': 'days_of_week',
|
||||
'🗓️ Месяцы года': 'months_of_year',
|
||||
'🔢 Нумерология': 'numerology',
|
||||
};
|
||||
|
||||
TraditionCanon? byId(String id) {
|
||||
for (final t in _traditions) {
|
||||
if (t.id == id) return t;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Finds all stations across traditions that relate to this word (either as primary station or in cluster)
|
||||
List<({TraditionCanon tradition, TraditionStation station, bool isPrimary})> findMatchesForWord(String word, {int? wordId}) {
|
||||
final cleanWord = word.trim().toLowerCase();
|
||||
final results = <({TraditionCanon tradition, TraditionStation station, bool isPrimary})>[];
|
||||
|
||||
for (final tradition in _traditions) {
|
||||
for (final station in tradition.stations) {
|
||||
if (station.primary != null &&
|
||||
(station.primary!.word.trim().toLowerCase() == cleanWord ||
|
||||
(wordId != null && station.primary!.id == wordId))) {
|
||||
results.add((tradition: tradition, station: station, isPrimary: true));
|
||||
} else if (station.cluster.any((cw) =>
|
||||
cw.word.trim().toLowerCase() == cleanWord ||
|
||||
(wordId != null && cw.id == wordId))) {
|
||||
results.add((tradition: tradition, station: station, isPrimary: false));
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
Future<void> init() async {
|
||||
if (_loaded && _traditions.isNotEmpty) return;
|
||||
Map<String, dynamic>? decoded;
|
||||
|
||||
final cwd = Directory.current.path;
|
||||
final candidates = <String>{
|
||||
'assets/data/traditions_canon.json',
|
||||
'../assets/data/traditions_canon.json',
|
||||
'$cwd/assets/data/traditions_canon.json',
|
||||
'$cwd/../assets/data/traditions_canon.json',
|
||||
};
|
||||
|
||||
for (final path in candidates) {
|
||||
try {
|
||||
final file = File(path);
|
||||
if (file.existsSync()) {
|
||||
decoded = json.decode(file.readAsStringSync()) as Map<String, dynamic>;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('traditions_canon.json skip $path: $e');
|
||||
}
|
||||
}
|
||||
|
||||
if (decoded == null) {
|
||||
try {
|
||||
final jsonStr = await rootBundle.loadString('assets/data/traditions_canon.json');
|
||||
decoded = json.decode(jsonStr) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint('Could not load traditions_canon.json: $e');
|
||||
}
|
||||
}
|
||||
|
||||
if (decoded == null) {
|
||||
_traditions = const [];
|
||||
_loaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_traditions = decoded.values
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(TraditionCanon.fromMap)
|
||||
.toList();
|
||||
_loaded = true;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
@@ -80,6 +81,7 @@ class TrueRandomService {
|
||||
|
||||
/// Warm the pool so the next roll does not wait on the network.
|
||||
void prefetch({int count = 3}) {
|
||||
if (Platform.environment.containsKey('FLUTTER_TEST')) return;
|
||||
if (_pool != null || _inflight != null) return;
|
||||
_inflight = () async {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../data/words_data.dart';
|
||||
import '../models/word_model.dart';
|
||||
import '../models/word_of_day_model.dart';
|
||||
import 'words_repository.dart';
|
||||
|
||||
class WordOfTheDayService extends ChangeNotifier {
|
||||
static final WordOfTheDayService _instance = WordOfTheDayService._internal();
|
||||
factory WordOfTheDayService() => _instance;
|
||||
WordOfTheDayService._internal();
|
||||
|
||||
static const String _kRecordsPrefsKey = 'oracle_word_of_day_records_v1';
|
||||
static const String _kGlobalTriptychKey = 'oracle_word_of_day_is_triptych_v1';
|
||||
|
||||
final Map<String, WordOfDayRecord> _records = {};
|
||||
bool _isInitialized = false;
|
||||
bool _globalTriptychMode = false;
|
||||
|
||||
bool get isInitialized => _isInitialized;
|
||||
bool get globalTriptychMode => _globalTriptychMode;
|
||||
|
||||
Future<void> init() async {
|
||||
if (_isInitialized) return;
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_globalTriptychMode = prefs.getBool(_kGlobalTriptychKey) ?? false;
|
||||
final raw = prefs.getString(_kRecordsPrefsKey);
|
||||
if (raw != null) {
|
||||
final decoded = json.decode(raw) as Map<String, dynamic>;
|
||||
_records.clear();
|
||||
for (final entry in decoded.entries) {
|
||||
if (entry.value is Map<String, dynamic>) {
|
||||
_records[entry.key] = WordOfDayRecord.fromMap(
|
||||
entry.value as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error initializing WordOfTheDayService: $e');
|
||||
}
|
||||
_isInitialized = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setGlobalTriptychMode(bool enabled) async {
|
||||
_globalTriptychMode = enabled;
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_kGlobalTriptychKey, enabled);
|
||||
} catch (_) {}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
static String formatDateKey(DateTime date) {
|
||||
final y = date.year.toString().padLeft(4, '0');
|
||||
final m = date.month.toString().padLeft(2, '0');
|
||||
final d = date.day.toString().padLeft(2, '0');
|
||||
return '$y-$m-$d';
|
||||
}
|
||||
|
||||
static DateTime parseDateKey(String key) {
|
||||
final parts = key.split('-');
|
||||
if (parts.length == 3) {
|
||||
final y = int.tryParse(parts[0]) ?? 2026;
|
||||
final m = int.tryParse(parts[1]) ?? 1;
|
||||
final d = int.tryParse(parts[2]) ?? 1;
|
||||
return DateTime(y, m, d);
|
||||
}
|
||||
return DateTime.now();
|
||||
}
|
||||
|
||||
/// High-dispersion deterministic hash that maps any calendar date to a consistent word (0..N-1).
|
||||
/// Every device on the same date receives the exact same resonant words.
|
||||
static int calculateWordIndexForDate(DateTime date, int totalWords, {String phase = 'main'}) {
|
||||
if (totalWords <= 0) return 0;
|
||||
final y = date.year;
|
||||
final m = date.month;
|
||||
final d = date.day;
|
||||
|
||||
int phaseSalt;
|
||||
switch (phase) {
|
||||
case 'morning':
|
||||
phaseSalt = 0x11111111;
|
||||
break;
|
||||
case 'day':
|
||||
phaseSalt = 0x22222222;
|
||||
break;
|
||||
case 'evening':
|
||||
phaseSalt = 0x33333333;
|
||||
break;
|
||||
case 'main':
|
||||
default:
|
||||
phaseSalt = 0x5A5A5A5A;
|
||||
break;
|
||||
}
|
||||
|
||||
final rawHash = ((y * 10000 + m * 100 + d) * 2654435761) & 0x7FFFFFFF;
|
||||
final rng = Random(rawHash ^ phaseSalt);
|
||||
var index = rng.nextInt(totalWords);
|
||||
|
||||
// Guaranteed distinctness between morning, day, and evening
|
||||
if (phase == 'day') {
|
||||
final morningIdx = calculateWordIndexForDate(date, totalWords, phase: 'morning');
|
||||
if (index == morningIdx) {
|
||||
index = (index + 1) % totalWords;
|
||||
}
|
||||
} else if (phase == 'evening') {
|
||||
final morningIdx = calculateWordIndexForDate(date, totalWords, phase: 'morning');
|
||||
final dayIdx = calculateWordIndexForDate(date, totalWords, phase: 'day');
|
||||
while (index == morningIdx || index == dayIdx) {
|
||||
index = (index + 1) % totalWords;
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/// Returns the deterministic WordItem for the given date and phase ('main', 'morning', 'day', 'evening').
|
||||
WordItem getWordForDate(DateTime date, {String phase = 'main'}) {
|
||||
final repoWords = WordsRepository().allWords;
|
||||
final wordsPool = repoWords.isNotEmpty ? repoWords : kRussianTop1000Words;
|
||||
final index = calculateWordIndexForDate(date, wordsPool.length, phase: phase);
|
||||
return wordsPool[index];
|
||||
}
|
||||
|
||||
/// Returns the 3 words of the day triptych (Morning, Day, Evening).
|
||||
({WordItem morning, WordItem day, WordItem evening}) getTriptychWordsForDate(DateTime date) {
|
||||
return (
|
||||
morning: getWordForDate(date, phase: 'morning'),
|
||||
day: getWordForDate(date, phase: 'day'),
|
||||
evening: getWordForDate(date, phase: 'evening'),
|
||||
);
|
||||
}
|
||||
|
||||
/// Retrieves the record for the given date (cached or initialized).
|
||||
WordOfDayRecord getRecord(DateTime date) {
|
||||
final key = formatDateKey(date);
|
||||
final mainWord = getWordForDate(date, phase: 'main');
|
||||
final triptych = getTriptychWordsForDate(date);
|
||||
|
||||
if (_records.containsKey(key)) {
|
||||
final existing = _records[key]!;
|
||||
// Ensure triptych word IDs are set if they were 0
|
||||
if (existing.morningWordId == 0 || existing.dayWordId == 0 || existing.eveningWordId == 0) {
|
||||
return existing.copyWith(
|
||||
morningWordId: triptych.morning.id,
|
||||
dayWordId: triptych.day.id,
|
||||
eveningWordId: triptych.evening.id,
|
||||
);
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
return WordOfDayRecord(
|
||||
dateKey: key,
|
||||
wordId: mainWord.id,
|
||||
isRevealed: false,
|
||||
isStepCompleted: false,
|
||||
diaryNote: '',
|
||||
isTriptychMode: _globalTriptychMode,
|
||||
morningWordId: triptych.morning.id,
|
||||
dayWordId: triptych.day.id,
|
||||
eveningWordId: triptych.evening.id,
|
||||
isMorningRevealed: false,
|
||||
isDayRevealed: false,
|
||||
isEveningRevealed: false,
|
||||
isDayStepCompleted: false,
|
||||
);
|
||||
}
|
||||
|
||||
/// Sets the mode (1-Word vs 3-Words Triptych) for the given date record.
|
||||
Future<WordOfDayRecord> setRecordTriptychMode(DateTime date, bool isTriptych) async {
|
||||
final key = formatDateKey(date);
|
||||
final current = getRecord(date);
|
||||
final updated = current.copyWith(isTriptychMode: isTriptych);
|
||||
_records[key] = updated;
|
||||
await _saveRecordsToPrefs();
|
||||
notifyListeners();
|
||||
return updated;
|
||||
}
|
||||
|
||||
/// Reveals the seal for the given phase: 'main', 'morning', 'day', 'evening', or 'all'.
|
||||
Future<WordOfDayRecord> revealWord(DateTime date, {String phase = 'main'}) async {
|
||||
final key = formatDateKey(date);
|
||||
final current = getRecord(date);
|
||||
final now = DateTime.now();
|
||||
|
||||
WordOfDayRecord updated;
|
||||
switch (phase) {
|
||||
case 'morning':
|
||||
updated = current.copyWith(
|
||||
isMorningRevealed: true,
|
||||
morningRevealedAt: current.morningRevealedAt ?? now,
|
||||
);
|
||||
break;
|
||||
case 'day':
|
||||
updated = current.copyWith(
|
||||
isDayRevealed: true,
|
||||
dayRevealedAt: current.dayRevealedAt ?? now,
|
||||
);
|
||||
break;
|
||||
case 'evening':
|
||||
updated = current.copyWith(
|
||||
isEveningRevealed: true,
|
||||
eveningRevealedAt: current.eveningRevealedAt ?? now,
|
||||
);
|
||||
break;
|
||||
case 'all':
|
||||
updated = current.copyWith(
|
||||
isRevealed: true,
|
||||
isMorningRevealed: true,
|
||||
isDayRevealed: true,
|
||||
isEveningRevealed: true,
|
||||
revealedAt: current.revealedAt ?? now,
|
||||
morningRevealedAt: current.morningRevealedAt ?? now,
|
||||
dayRevealedAt: current.dayRevealedAt ?? now,
|
||||
eveningRevealedAt: current.eveningRevealedAt ?? now,
|
||||
);
|
||||
break;
|
||||
case 'main':
|
||||
default:
|
||||
updated = current.copyWith(
|
||||
isRevealed: true,
|
||||
revealedAt: current.revealedAt ?? now,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
_records[key] = updated;
|
||||
await _saveRecordsToPrefs();
|
||||
notifyListeners();
|
||||
return updated;
|
||||
}
|
||||
|
||||
/// Toggles whether the practical step was completed.
|
||||
Future<WordOfDayRecord> toggleStepCompleted(
|
||||
DateTime date,
|
||||
bool completed, {
|
||||
bool isDayStep = false,
|
||||
}) async {
|
||||
final key = formatDateKey(date);
|
||||
final current = getRecord(date);
|
||||
final now = DateTime.now();
|
||||
|
||||
final updated = isDayStep
|
||||
? current.copyWith(
|
||||
isDayRevealed: true,
|
||||
isDayStepCompleted: completed,
|
||||
completedAt: completed ? (current.completedAt ?? now) : null,
|
||||
)
|
||||
: current.copyWith(
|
||||
isRevealed: true,
|
||||
isStepCompleted: completed,
|
||||
completedAt: completed ? (current.completedAt ?? now) : null,
|
||||
);
|
||||
|
||||
_records[key] = updated;
|
||||
await _saveRecordsToPrefs();
|
||||
notifyListeners();
|
||||
return updated;
|
||||
}
|
||||
|
||||
/// Saves the user's evening reflection note for this date.
|
||||
Future<WordOfDayRecord> saveDiaryNote(DateTime date, String note) async {
|
||||
final key = formatDateKey(date);
|
||||
final current = getRecord(date);
|
||||
final updated = current.copyWith(diaryNote: note.trim());
|
||||
_records[key] = updated;
|
||||
await _saveRecordsToPrefs();
|
||||
notifyListeners();
|
||||
return updated;
|
||||
}
|
||||
|
||||
/// Returns all stored records ordered chronologically (newest first).
|
||||
List<WordOfDayRecord> getAllRecords() {
|
||||
final list = _records.values.toList();
|
||||
list.sort((a, b) => b.dateKey.compareTo(a.dateKey));
|
||||
return List.unmodifiable(list);
|
||||
}
|
||||
|
||||
/// Calculates the current streak of consecutive active days.
|
||||
int calculateStreak() {
|
||||
int streak = 0;
|
||||
var checkDate = DateTime.now();
|
||||
|
||||
final todayRecord = _records[formatDateKey(checkDate)];
|
||||
if (todayRecord == null || (!todayRecord.isAnyRevealed && !todayRecord.isAnyStepCompleted)) {
|
||||
checkDate = checkDate.subtract(const Duration(days: 1));
|
||||
}
|
||||
|
||||
while (true) {
|
||||
final key = formatDateKey(checkDate);
|
||||
final rec = _records[key];
|
||||
if (rec != null && (rec.isAnyRevealed || rec.isAnyStepCompleted)) {
|
||||
streak++;
|
||||
checkDate = checkDate.subtract(const Duration(days: 1));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return streak;
|
||||
}
|
||||
|
||||
int get totalCompletedPractices =>
|
||||
_records.values.where((r) => r.isAnyStepCompleted).length;
|
||||
|
||||
int get totalRevealedDays =>
|
||||
_records.values.where((r) => r.isAnyRevealed).length;
|
||||
|
||||
Future<void> _saveRecordsToPrefs() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final mapToSave = _records.map((k, v) => MapEntry(k, v.toMap()));
|
||||
await prefs.setString(_kRecordsPrefsKey, json.encode(mapToSave));
|
||||
} catch (e) {
|
||||
debugPrint('Error saving WordOfTheDay records: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
import '../models/word_passport_model.dart';
|
||||
import '../models/syncretic_session_model.dart';
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../data/words_data.dart';
|
||||
import '../models/history_entry_model.dart';
|
||||
@@ -14,9 +20,17 @@ class WordsRepository {
|
||||
static const String _kMappingDateKey = 'oracle_daily_mapping_date_v1';
|
||||
static const String _kMappingOrderKey = 'oracle_daily_mapping_order_v1';
|
||||
static const String _kMappingSourceKey = 'oracle_daily_mapping_source_v1';
|
||||
static const String _kCustomEditsKey = 'oracle_user_custom_edits_v1';
|
||||
|
||||
Map<String, Map<String, dynamic>> _userEdits = {};
|
||||
|
||||
List<WordItem> _sessionWords = [];
|
||||
final List<OracleSession> _sessions = [];
|
||||
Map<String, Map<String, dynamic>> _kbData = {};
|
||||
Map<int, WordPassport> _passports = {};
|
||||
|
||||
WordPassport? getPassport(int wordId) => _passports[wordId];
|
||||
Map<int, WordPassport> get allPassports => Map.unmodifiable(_passports);
|
||||
bool _useRemoteShuffle = true;
|
||||
String? _dailyMappingDate;
|
||||
RandomSource _dailyMappingSource = RandomSource.local;
|
||||
@@ -25,105 +39,441 @@ class WordsRepository {
|
||||
List<OracleSession> get sessions => List.unmodifiable(_sessions);
|
||||
List<OracleSession> get history => sessions; // Alias for compatibility
|
||||
int get totalWords => _sessionWords.length;
|
||||
int get customEditsCount => _userEdits.length;
|
||||
Map<String, Map<String, dynamic>> get allCustomEdits => Map.unmodifiable(_userEdits);
|
||||
String? get dailyMappingDate => _dailyMappingDate;
|
||||
RandomSource get dailyMappingSource => _dailyMappingSource;
|
||||
|
||||
Future<void> init({bool remoteDailyShuffle = true}) async {
|
||||
_useRemoteShuffle = remoteDailyShuffle;
|
||||
await _loadKbAsset();
|
||||
await _loadPassportsAsset();
|
||||
await _loadUserEditsFromPrefs();
|
||||
await _restoreOrFetchDailyMapping(force: false);
|
||||
await _loadHistoryFromPrefs();
|
||||
await _loadSyncreticSessionsFromPrefs();
|
||||
}
|
||||
|
||||
Future<RandomSource> reshuffleSessionNumbers() {
|
||||
return _restoreOrFetchDailyMapping(force: true);
|
||||
|
||||
Future<void> _loadPassportsAsset() async {
|
||||
for (final path in ['assets/data/oracle_passports.json', '../assets/data/oracle_passports.json']) {
|
||||
try {
|
||||
final file = File(path);
|
||||
if (file.existsSync()) {
|
||||
final jsonStr = file.readAsStringSync();
|
||||
final decoded = json.decode(jsonStr) as Map<String, dynamic>;
|
||||
_passports = decoded.map((k, v) => MapEntry(int.tryParse(k) ?? 0, WordPassport.fromMap(v as Map<String, dynamic>)));
|
||||
return;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (Platform.environment.containsKey('FLUTTER_TEST')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final jsonStr = await rootBundle.loadString('assets/data/oracle_passports.json');
|
||||
final decoded = json.decode(jsonStr) as Map<String, dynamic>;
|
||||
_passports = decoded.map((k, v) => MapEntry(int.tryParse(k) ?? 0, WordPassport.fromMap(v as Map<String, dynamic>)));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> _loadKbAsset() async {
|
||||
// 1. In local dev/testing environments, load synchronously from filesystem
|
||||
for (final path in ['assets/data/oracle_kb.json', '../assets/data/oracle_kb.json']) {
|
||||
try {
|
||||
final file = File(path);
|
||||
if (file.existsSync()) {
|
||||
final jsonStr = file.readAsStringSync();
|
||||
final decoded = json.decode(jsonStr) as Map<String, dynamic>;
|
||||
_kbData = decoded.map((k, v) => MapEntry(k.toLowerCase().trim(), v as Map<String, dynamic>));
|
||||
return;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (Platform.environment.containsKey('FLUTTER_TEST')) {
|
||||
return; // Avoid hanging on rootBundle during headless tests
|
||||
}
|
||||
|
||||
// 2. In packaged release app, load via rootBundle
|
||||
try {
|
||||
final jsonStr = await rootBundle.loadString('assets/data/oracle_kb.json');
|
||||
final decoded = json.decode(jsonStr) as Map<String, dynamic>;
|
||||
_kbData = decoded.map((k, v) => MapEntry(k.toLowerCase().trim(), v as Map<String, dynamic>));
|
||||
} catch (e) {
|
||||
debugPrint('Could not load oracle_kb.json asset: $e');
|
||||
}
|
||||
}
|
||||
|
||||
WordItem _enrichedWord(WordItem base) {
|
||||
final key = base.word.toLowerCase().trim();
|
||||
var result = base;
|
||||
final kb = _kbData[key] ?? _kbData[base.id.toString()];
|
||||
if (kb != null) {
|
||||
final kbWord = (kb['word'] as String?)?.toLowerCase().trim();
|
||||
if (kbWord == null || kbWord == key) {
|
||||
result = result.enrichWithKb(kb);
|
||||
}
|
||||
}
|
||||
final edit = _userEdits[key];
|
||||
if (edit != null) {
|
||||
result = result.copyWith(
|
||||
core: edit['core'] as String? ?? result.core,
|
||||
oracle: edit['oracle'] as String? ?? result.oracle,
|
||||
shadow: edit['shadow'] as String? ?? result.shadow,
|
||||
asks: edit['asks'] as String? ?? result.asks,
|
||||
step: edit['step'] as String? ?? result.step,
|
||||
cultures: edit.containsKey('cultures') ? _stringMap(edit['cultures']) : result.cultures,
|
||||
languages: edit.containsKey('languages') ? _stringMap(edit['languages']) : result.languages,
|
||||
layers: edit.containsKey('layers') ? _layerList(edit['layers']) : result.layers,
|
||||
binds: edit.containsKey('binds') ? _stringList(edit['binds']) : result.binds,
|
||||
isUserEdited: true,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Map<String, String> _stringMap(dynamic raw) {
|
||||
if (raw is! Map) return {};
|
||||
return raw.map((k, v) => MapEntry(k.toString(), v.toString()));
|
||||
}
|
||||
|
||||
List<String> _stringList(dynamic raw) {
|
||||
if (raw is! List) return [];
|
||||
return raw.map((e) => e.toString().trim()).where((e) => e.isNotEmpty).toList();
|
||||
}
|
||||
|
||||
List<SymbolLayer> _layerList(dynamic raw) {
|
||||
if (raw is! List) return [];
|
||||
return [
|
||||
for (final item in raw)
|
||||
if (item is Map)
|
||||
SymbolLayer(
|
||||
system: item['system']?.toString() ?? '',
|
||||
title: item['title']?.toString() ?? '',
|
||||
law: item['law']?.toString() ?? '',
|
||||
),
|
||||
].where((layer) => layer.system.isNotEmpty && layer.law.trim().isNotEmpty).toList();
|
||||
}
|
||||
|
||||
Map<String, dynamic>? customEditOf(String word) {
|
||||
final edit = _userEdits[word.toLowerCase().trim()];
|
||||
if (edit == null) return null;
|
||||
return Map<String, dynamic>.from(edit);
|
||||
}
|
||||
|
||||
void _applyEditsToSessionWords() {
|
||||
_sessionWords = _sessionWords.map((w) {
|
||||
final base = kRussianTop1000Words.firstWhere(
|
||||
(b) => b.id == w.id,
|
||||
orElse: () => w,
|
||||
);
|
||||
return _enrichedWord(base).copyWith(sessionNumber: w.sessionNumber);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
|
||||
Future<void> savePassport(WordPassport passport) async {
|
||||
_passports[passport.id] = passport;
|
||||
final key = passport.word.toLowerCase().trim();
|
||||
final previous = Map<String, dynamic>.from(_userEdits[key] ?? {});
|
||||
previous['passport'] = passport.toMap();
|
||||
if (passport.semanticCore.isNotEmpty) previous['core'] = passport.semanticCore;
|
||||
if (passport.willVector.isNotEmpty) previous['asks'] = passport.willVector;
|
||||
if (passport.shadowWarning.isNotEmpty) previous['shadow'] = passport.shadowWarning;
|
||||
previous['updatedAt'] = DateTime.now().toUtc().toIso8601String();
|
||||
_userEdits[key] = previous;
|
||||
await _saveUserEditsToPrefs();
|
||||
}
|
||||
|
||||
Future<void> saveCustomEdit(String word, {
|
||||
required String core,
|
||||
required String oracle,
|
||||
required String shadow,
|
||||
required String asks,
|
||||
required String step,
|
||||
Map<String, String>? cultures,
|
||||
List<SymbolLayer>? layers,
|
||||
Map<String, String>? languages,
|
||||
List<String>? binds,
|
||||
Map<String, String>? reports,
|
||||
}) async {
|
||||
final key = word.toLowerCase().trim();
|
||||
final previous = Map<String, dynamic>.from(_userEdits[key] ?? {});
|
||||
previous
|
||||
..['core'] = core.trim()
|
||||
..['oracle'] = oracle.trim()
|
||||
..['shadow'] = shadow.trim()
|
||||
..['asks'] = asks.trim()
|
||||
..['step'] = step.trim()
|
||||
..['updatedAt'] = DateTime.now().toUtc().toIso8601String();
|
||||
if (cultures != null) previous['cultures'] = cultures;
|
||||
if (layers != null) {
|
||||
previous['layers'] = [
|
||||
for (final layer in layers)
|
||||
if (layer.law.trim().isNotEmpty)
|
||||
{'system': layer.system, 'title': layer.title, 'law': layer.law.trim()},
|
||||
];
|
||||
}
|
||||
if (languages != null) previous['languages'] = languages;
|
||||
if (binds != null) previous['binds'] = binds;
|
||||
if (reports != null) previous['reports'] = reports;
|
||||
_userEdits[key] = previous;
|
||||
await _saveUserEditsToPrefs();
|
||||
_applyEditsToSessionWords();
|
||||
}
|
||||
|
||||
Future<void> resetCustomEdit(String word) async {
|
||||
final key = word.toLowerCase().trim();
|
||||
if (_userEdits.remove(key) != null) {
|
||||
await _saveUserEditsToPrefs();
|
||||
_applyEditsToSessionWords();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearAllCustomEdits() async {
|
||||
_userEdits.clear();
|
||||
await _saveUserEditsToPrefs();
|
||||
_applyEditsToSessionWords();
|
||||
}
|
||||
|
||||
Future<int> importCustomEditsJson(String jsonStr) async {
|
||||
try {
|
||||
final decoded = json.decode(jsonStr);
|
||||
var count = 0;
|
||||
|
||||
Map<String, dynamic> rawMap = {};
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
if (decoded['symbols'] is Map<String, dynamic>) {
|
||||
rawMap = Map<String, dynamic>.from(decoded['symbols'] as Map);
|
||||
} else if (decoded['symbols'] is List) {
|
||||
for (final item in decoded['symbols'] as List) {
|
||||
if (item is Map && item['word'] != null) {
|
||||
rawMap[item['word'].toString()] = Map<String, dynamic>.from(item);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rawMap = decoded;
|
||||
}
|
||||
} else if (decoded is List) {
|
||||
for (final item in decoded) {
|
||||
if (item is Map && item['word'] != null) {
|
||||
rawMap[item['word'].toString()] = Map<String, dynamic>.from(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final entry in rawMap.entries) {
|
||||
final key = entry.key.toLowerCase().trim();
|
||||
if (entry.value is Map<String, dynamic>) {
|
||||
final val = Map<String, dynamic>.from(entry.value as Map);
|
||||
final core = val['core']?.toString().trim() ?? '';
|
||||
final oracle = (val['oracle']?.toString() ?? val['description']?.toString() ?? '').trim();
|
||||
final shadow = val['shadow']?.toString().trim() ?? '';
|
||||
final asks = val['asks']?.toString().trim() ?? '';
|
||||
final step = val['step']?.toString().trim() ?? '';
|
||||
final cultures = val['cultures'] is Map ? _stringMap(val['cultures']) : null;
|
||||
final languages = val['languages'] is Map ? _stringMap(val['languages']) : null;
|
||||
final layers = val['layers'] is List ? _layerList(val['layers']) : null;
|
||||
final binds = val['binds'] is List ? _stringList(val['binds']) : null;
|
||||
final reports = val['reports'] is Map ? _stringMap(val['reports']) : null;
|
||||
final hasBody = core.isNotEmpty ||
|
||||
oracle.isNotEmpty ||
|
||||
shadow.isNotEmpty ||
|
||||
asks.isNotEmpty ||
|
||||
step.isNotEmpty ||
|
||||
(cultures != null && cultures.isNotEmpty) ||
|
||||
(languages != null && languages.isNotEmpty) ||
|
||||
(layers != null && layers.isNotEmpty);
|
||||
|
||||
if (hasBody) {
|
||||
_userEdits[key] = {
|
||||
'core': core,
|
||||
'oracle': oracle,
|
||||
'shadow': shadow,
|
||||
'asks': asks,
|
||||
'step': step,
|
||||
'cultures': ?cultures,
|
||||
'languages': ?languages,
|
||||
if (layers != null)
|
||||
'layers': [
|
||||
for (final layer in layers)
|
||||
{'system': layer.system, 'title': layer.title, 'law': layer.law},
|
||||
],
|
||||
'binds': ?binds,
|
||||
'reports': ?reports,
|
||||
'updatedAt': val['updatedAt']?.toString() ?? DateTime.now().toUtc().toIso8601String(),
|
||||
};
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
await _saveUserEditsToPrefs();
|
||||
_applyEditsToSessionWords();
|
||||
}
|
||||
return count;
|
||||
} catch (e) {
|
||||
debugPrint('Error importing custom edits: $e');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
String exportCustomEditsJson() {
|
||||
final exportMap = <String, dynamic>{};
|
||||
for (final entry in _userEdits.entries) {
|
||||
final key = entry.key;
|
||||
final wordItem = getWordByName(key);
|
||||
exportMap[key] = {
|
||||
'word': key,
|
||||
if (wordItem != null) 'id': wordItem.id,
|
||||
if (wordItem != null) 'sessionNumber': wordItem.sessionNumber,
|
||||
if (wordItem != null) 'emoji': wordItem.emoji,
|
||||
if (wordItem != null) 'category': wordItem.category,
|
||||
if (wordItem != null) 'pos': wordItem.partOfSpeech,
|
||||
...entry.value,
|
||||
};
|
||||
}
|
||||
return const JsonEncoder.withIndent(' ').convert({
|
||||
'version': '1.0',
|
||||
'type': 'oracle_custom_edits',
|
||||
'exportedAt': DateTime.now().toUtc().toIso8601String(),
|
||||
'count': exportMap.length,
|
||||
'symbols': exportMap,
|
||||
});
|
||||
}
|
||||
|
||||
String exportFullLexiconJson() {
|
||||
final list = _sessionWords.map((w) {
|
||||
return {
|
||||
'id': w.id,
|
||||
'sessionNumber': w.sessionNumber,
|
||||
'word': w.word,
|
||||
'emoji': w.emoji,
|
||||
'pos': w.partOfSpeech,
|
||||
'category': w.category,
|
||||
'core': w.core,
|
||||
'oracle': w.oracle,
|
||||
'shadow': w.shadow,
|
||||
'asks': w.asks,
|
||||
'step': w.step,
|
||||
'binds': w.binds,
|
||||
'cultures': w.cultures,
|
||||
'languages': w.languages,
|
||||
'layers': w.layers.map((l) => l.toMap()).toList(),
|
||||
'description': w.description,
|
||||
'family': w.family,
|
||||
'operator': w.operator,
|
||||
'valence': w.valence,
|
||||
'jurisdictions': w.jurisdictions,
|
||||
'isUserEdited': w.isUserEdited,
|
||||
};
|
||||
}).toList();
|
||||
|
||||
return const JsonEncoder.withIndent(' ').convert({
|
||||
'version': '1.0',
|
||||
'type': 'oracle_full_lexicon',
|
||||
'exportedAt': DateTime.now().toUtc().toIso8601String(),
|
||||
'count': list.length,
|
||||
'symbols': list,
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadUserEditsFromPrefs() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString(_kCustomEditsKey);
|
||||
if (raw != null) {
|
||||
final map = json.decode(raw) as Map<String, dynamic>;
|
||||
_userEdits = map.map((k, v) => MapEntry(k.toLowerCase().trim(), Map<String, dynamic>.from(v as Map)));
|
||||
for (final entry in _userEdits.entries) {
|
||||
if (entry.value['passport'] is Map) {
|
||||
final p = WordPassport.fromMap(Map<String, dynamic>.from(entry.value['passport'] as Map));
|
||||
_passports[p.id] = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> _saveUserEditsToPrefs() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_kCustomEditsKey, json.encode(_userEdits));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void _initCanonicalWords() {
|
||||
_sessionWords = [
|
||||
for (var i = 0; i < kRussianTop1000Words.length; i++)
|
||||
_enrichedWord(kRussianTop1000Words[i]).copyWith(sessionNumber: i),
|
||||
];
|
||||
_applyEditsToSessionWords();
|
||||
}
|
||||
|
||||
Future<RandomSource> reshuffleSessionNumbers() async {
|
||||
_initCanonicalWords();
|
||||
await _healAllHistoryAndSessions();
|
||||
return RandomSource.local;
|
||||
}
|
||||
|
||||
Future<void> _healAllHistoryAndSessions() async {
|
||||
await _loadHistoryFromPrefs();
|
||||
await _loadSyncreticSessionsFromPrefs();
|
||||
}
|
||||
|
||||
Future<RandomSource> _restoreOrFetchDailyMapping({required bool force}) async {
|
||||
final today = utcDateStamp();
|
||||
if (!force) {
|
||||
final restored = await _loadPersistedMapping(today);
|
||||
if (restored) return _dailyMappingSource;
|
||||
}
|
||||
|
||||
final lastId = kRussianTop1000Words.length - 1;
|
||||
RangeShuffle shuffle;
|
||||
if (_useRemoteShuffle) {
|
||||
shuffle = await TrueRandomService.instance.permutation(
|
||||
min: 0,
|
||||
max: lastId,
|
||||
);
|
||||
} else {
|
||||
shuffle = RangeShuffle(
|
||||
TrueRandomService.instance.localPermutation(0, lastId),
|
||||
RandomSource.local,
|
||||
);
|
||||
}
|
||||
|
||||
_applyIndexOrder(shuffle.order);
|
||||
_initCanonicalWords();
|
||||
_dailyMappingDate = today;
|
||||
_dailyMappingSource = shuffle.source;
|
||||
await _persistMapping(today, shuffle.order, shuffle.source);
|
||||
return shuffle.source;
|
||||
}
|
||||
|
||||
void _applyIndexOrder(List<int> order) {
|
||||
_sessionWords = [
|
||||
for (var i = 0; i < order.length; i++)
|
||||
kRussianTop1000Words[order[i]].copyWith(sessionNumber: i),
|
||||
];
|
||||
}
|
||||
|
||||
Future<bool> _loadPersistedMapping(String today) async {
|
||||
_dailyMappingSource = _useRemoteShuffle ? RandomSource.randomOrg : RandomSource.local;
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final date = prefs.getString(_kMappingDateKey);
|
||||
final stored = prefs.getStringList(_kMappingOrderKey);
|
||||
if (date != today || stored == null) return false;
|
||||
final order = stored.map(int.parse).toList();
|
||||
if (!isPermutation(order, 0, kRussianTop1000Words.length - 1)) {
|
||||
return false;
|
||||
}
|
||||
_applyIndexOrder(order);
|
||||
_dailyMappingDate = date;
|
||||
final sourceName = prefs.getString(_kMappingSourceKey);
|
||||
_dailyMappingSource = sourceName == RandomSource.randomOrg.name
|
||||
? RandomSource.randomOrg
|
||||
: RandomSource.local;
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistMapping(
|
||||
String date,
|
||||
List<int> order,
|
||||
RandomSource source,
|
||||
) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_kMappingDateKey, date);
|
||||
await prefs.setStringList(
|
||||
_kMappingOrderKey,
|
||||
order.map((e) => e.toString()).toList(),
|
||||
);
|
||||
await prefs.setString(_kMappingSourceKey, source.name);
|
||||
await prefs.remove(_kMappingOrderKey);
|
||||
await prefs.setString(_kMappingDateKey, today);
|
||||
await prefs.setString(_kMappingSourceKey, _dailyMappingSource.name);
|
||||
} catch (_) {}
|
||||
return _dailyMappingSource;
|
||||
}
|
||||
|
||||
// Compatibility helpers
|
||||
void applyIndexOrder(List<int> order) => _initCanonicalWords();
|
||||
Future<bool> loadPersistedMapping(String today) async => true;
|
||||
Future<void> persistMapping(String date, List<int> order, RandomSource source) async {}
|
||||
|
||||
static String utcDateStamp([DateTime? now]) {
|
||||
final d = (now ?? DateTime.now()).toUtc();
|
||||
final y = d.year.toString().padLeft(4, '0');
|
||||
final m = d.month.toString().padLeft(2, '0');
|
||||
final day = d.day.toString().padLeft(2, '0');
|
||||
return '$y-$m-$day';
|
||||
final y = d.year.toString().padLeft(4, "0");
|
||||
final m = d.month.toString().padLeft(2, "0");
|
||||
final day = d.day.toString().padLeft(2, "0");
|
||||
return "$y-$m-$day";
|
||||
}
|
||||
|
||||
Future<void> deleteSession(String id) => removeSession(id);
|
||||
|
||||
WordItem? getWordBySessionNumber(int number) {
|
||||
if (number < 0 || number >= _sessionWords.length) return null;
|
||||
return _sessionWords[number];
|
||||
}
|
||||
|
||||
WordItem? getWordByName(String name) {
|
||||
final query = name.trim().toLowerCase();
|
||||
for (final w in _sessionWords) {
|
||||
if (w.word.toLowerCase() == query) return w;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
WordItem? getWordById(int id) {
|
||||
for (final w in _sessionWords) {
|
||||
if (w.id == id) return w;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
WordItem getRandomWord() {
|
||||
final random = Random.secure();
|
||||
final idx = random.nextInt(_sessionWords.length);
|
||||
@@ -162,7 +512,10 @@ class WordsRepository {
|
||||
} else if (lower.contains(query) ||
|
||||
w.description.toLowerCase().contains(query) ||
|
||||
w.category.toLowerCase().contains(query) ||
|
||||
w.partOfSpeech.toLowerCase().contains(query)) {
|
||||
w.partOfSpeech.toLowerCase().contains(query) ||
|
||||
w.family.toLowerCase().contains(query) ||
|
||||
w.operator.toLowerCase().contains(query) ||
|
||||
w.core.toLowerCase().contains(query)) {
|
||||
contains.add(w);
|
||||
}
|
||||
}
|
||||
@@ -190,6 +543,16 @@ class WordsRepository {
|
||||
return set.toList();
|
||||
}
|
||||
|
||||
List<String> getOperators() {
|
||||
final set = <String>{'Все'};
|
||||
for (final w in _sessionWords) {
|
||||
if (w.operator.isNotEmpty) {
|
||||
set.add(w.operator);
|
||||
}
|
||||
}
|
||||
return set.toList();
|
||||
}
|
||||
|
||||
// --- Session History Management ---
|
||||
|
||||
Future<void> saveSession(OracleSession session) async {
|
||||
@@ -223,12 +586,13 @@ class WordsRepository {
|
||||
Future<void> updateHistoryEntry(String id, {String? aiInterpretation, String? question}) =>
|
||||
updateSession(id, aiInterpretation: aiInterpretation, question: question);
|
||||
|
||||
Future<void> deleteSession(String id) async {
|
||||
_sessions.removeWhere((e) => e.id == id);
|
||||
Future<void> removeSession(String id) async {
|
||||
_sessions.removeWhere((s) => s.id == id);
|
||||
await _saveHistoryToPrefs();
|
||||
}
|
||||
|
||||
Future<void> deleteHistoryEntry(String id) => deleteSession(id);
|
||||
// Backward compatibility alias
|
||||
Future<void> deleteHistoryEntry(String id) => removeSession(id);
|
||||
|
||||
Future<void> clearHistory() async {
|
||||
_sessions.clear();
|
||||
@@ -238,14 +602,65 @@ class WordsRepository {
|
||||
Future<void> _loadHistoryFromPrefs() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonList = prefs.getStringList(_kHistoryKey);
|
||||
_sessions.clear();
|
||||
if (jsonList != null) {
|
||||
for (final item in jsonList) {
|
||||
final historyJson = prefs.getStringList(_kHistoryKey);
|
||||
if (historyJson != null) {
|
||||
_sessions.clear();
|
||||
bool needsResave = false;
|
||||
for (final itemStr in historyJson) {
|
||||
try {
|
||||
_sessions.add(OracleSession.fromJson(item));
|
||||
final map = json.decode(itemStr) as Map<String, dynamic>;
|
||||
final session = OracleSession.fromMap(map);
|
||||
|
||||
// Heal session words to match their canonical sacred IDs
|
||||
final healedWords = session.words.map((sw) {
|
||||
final canonical = getWordByName(sw.word);
|
||||
if (canonical != null && canonical.sessionNumber != sw.sessionNumber) {
|
||||
needsResave = true;
|
||||
return SessionWord(
|
||||
word: canonical.word,
|
||||
sessionNumber: canonical.sessionNumber,
|
||||
partOfSpeech: canonical.partOfSpeech.isNotEmpty ? canonical.partOfSpeech : sw.partOfSpeech,
|
||||
category: canonical.category.isNotEmpty ? canonical.category : sw.category,
|
||||
symbolicMeaning: sw.symbolicMeaning.isNotEmpty ? sw.symbolicMeaning : canonical.displayMeaning,
|
||||
positionTitle: sw.positionTitle,
|
||||
);
|
||||
}
|
||||
return sw;
|
||||
}).toList();
|
||||
|
||||
final healedMessages = session.messages.map((m) {
|
||||
if (m.attachedWord != null) {
|
||||
final canonical = getWordByName(m.attachedWord!.word);
|
||||
if (canonical != null && canonical.sessionNumber != m.attachedWord!.sessionNumber) {
|
||||
needsResave = true;
|
||||
return SessionChatMessage(
|
||||
id: m.id,
|
||||
sender: m.sender,
|
||||
text: m.text,
|
||||
timestamp: m.timestamp,
|
||||
attachedWord: SessionWord(
|
||||
word: canonical.word,
|
||||
sessionNumber: canonical.sessionNumber,
|
||||
partOfSpeech: canonical.partOfSpeech.isNotEmpty ? canonical.partOfSpeech : m.attachedWord!.partOfSpeech,
|
||||
category: canonical.category.isNotEmpty ? canonical.category : m.attachedWord!.category,
|
||||
symbolicMeaning: m.attachedWord!.symbolicMeaning,
|
||||
positionTitle: m.attachedWord!.positionTitle,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return m;
|
||||
}).toList();
|
||||
|
||||
_sessions.add(session.copyWith(
|
||||
words: healedWords,
|
||||
messages: healedMessages,
|
||||
));
|
||||
} catch (_) {}
|
||||
}
|
||||
if (needsResave) {
|
||||
await _saveHistoryToPrefs();
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
@@ -253,8 +668,145 @@ class WordsRepository {
|
||||
Future<void> _saveHistoryToPrefs() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonList = _sessions.map((e) => e.toJson()).toList();
|
||||
await prefs.setStringList(_kHistoryKey, jsonList);
|
||||
final historyJson = _sessions.map((s) => json.encode(s.toMap())).toList();
|
||||
await prefs.setStringList(_kHistoryKey, historyJson);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
|
||||
static const String _kSyncreticSessionsKey = 'oracle_999_syncretic_sessions_v1';
|
||||
List<SyncreticSession> _syncreticSessions = [];
|
||||
SyncreticSession? _currentSyncreticSession;
|
||||
|
||||
List<SyncreticSession> get syncreticSessions => List.unmodifiable(_syncreticSessions);
|
||||
|
||||
SyncreticSession get currentSyncreticSession {
|
||||
if (_currentSyncreticSession == null) {
|
||||
final todayKey = DateTime.now().toIso8601String().substring(0, 10);
|
||||
final existing = _syncreticSessions.where((s) => s.dateKey == todayKey);
|
||||
if (existing.isNotEmpty) {
|
||||
_currentSyncreticSession = existing.first;
|
||||
} else {
|
||||
_currentSyncreticSession = SyncreticSession(
|
||||
id: 'syn_${DateTime.now().millisecondsSinceEpoch}',
|
||||
date: DateTime.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
return _currentSyncreticSession!;
|
||||
}
|
||||
|
||||
Future<void> addReadingToSyncreticSession(SyncreticReading reading, {String? question}) async {
|
||||
final current = currentSyncreticSession;
|
||||
final existingIdx = current.readings.indexWhere((r) => r.id == reading.id);
|
||||
final List<SyncreticReading> updatedReadings;
|
||||
if (existingIdx >= 0) {
|
||||
updatedReadings = [...current.readings];
|
||||
updatedReadings[existingIdx] = reading;
|
||||
} else {
|
||||
updatedReadings = [...current.readings, reading];
|
||||
}
|
||||
final updatedSession = current.copyWith(
|
||||
readings: updatedReadings,
|
||||
mainQuestion: (question != null && question.trim().isNotEmpty)
|
||||
? question.trim()
|
||||
: current.mainQuestion,
|
||||
);
|
||||
await saveSyncreticSession(updatedSession);
|
||||
}
|
||||
|
||||
Future<void> saveSyncreticSession(SyncreticSession session) async {
|
||||
_currentSyncreticSession = session;
|
||||
final idx = _syncreticSessions.indexWhere((s) => s.id == session.id);
|
||||
if (idx >= 0) {
|
||||
_syncreticSessions[idx] = session;
|
||||
} else {
|
||||
_syncreticSessions.insert(0, session);
|
||||
}
|
||||
await _saveSyncreticSessionsToPrefs();
|
||||
}
|
||||
|
||||
Future<void> startNewSyncreticSession({String? question}) async {
|
||||
final newSession = SyncreticSession(
|
||||
id: 'syn_${DateTime.now().millisecondsSinceEpoch}',
|
||||
date: DateTime.now(),
|
||||
mainQuestion: question,
|
||||
);
|
||||
await saveSyncreticSession(newSession);
|
||||
}
|
||||
|
||||
Future<void> _loadSyncreticSessionsFromPrefs() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final list = prefs.getStringList(_kSyncreticSessionsKey);
|
||||
if (list != null) {
|
||||
bool needsResave = false;
|
||||
_syncreticSessions = list
|
||||
.map((str) {
|
||||
try {
|
||||
final session = SyncreticSession.fromJson(str);
|
||||
final healedReadings = session.readings.map((r) {
|
||||
final healedItems = r.items.map((item) {
|
||||
if (item.linkedWord != null) {
|
||||
final canonical = getWordByName(item.linkedWord!.word);
|
||||
if (canonical != null && canonical.sessionNumber != item.linkedWord!.sessionNumber) {
|
||||
needsResave = true;
|
||||
var newTitle = item.title;
|
||||
final numRe = RegExp(r'#\d{3}');
|
||||
if (numRe.hasMatch(newTitle)) {
|
||||
newTitle = newTitle.replaceFirst(numRe, '#${canonical.formattedSessionNumber}');
|
||||
}
|
||||
return ReadingSignItem(
|
||||
title: newTitle,
|
||||
subtitle: item.subtitle,
|
||||
symbol: item.symbol,
|
||||
lore: item.lore,
|
||||
isReversed: item.isReversed,
|
||||
linkedWord: canonical,
|
||||
meta: item.meta,
|
||||
);
|
||||
}
|
||||
}
|
||||
return item;
|
||||
}).toList();
|
||||
return SyncreticReading(
|
||||
id: r.id,
|
||||
systemId: r.systemId,
|
||||
systemName: r.systemName,
|
||||
systemIcon: r.systemIcon,
|
||||
timestamp: r.timestamp,
|
||||
layoutTitle: r.layoutTitle,
|
||||
inputMethod: r.inputMethod,
|
||||
items: healedItems,
|
||||
note: r.note,
|
||||
question: r.question,
|
||||
);
|
||||
}).toList();
|
||||
return session.copyWith(readings: healedReadings);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.whereType<SyncreticSession>()
|
||||
.toList();
|
||||
|
||||
if (needsResave) {
|
||||
await _saveSyncreticSessionsToPrefs();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error loading syncretic sessions: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveSyncreticSessionsToPrefs() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final list = _syncreticSessions.map((s) => s.toJson()).toList();
|
||||
await prefs.setStringList(_kSyncreticSessionsKey, list);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving syncretic sessions: $e');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ class CleromancyGuideDialog extends StatelessWidget {
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Клеромантия и Жребий 999',
|
||||
'Клеромантия и Священный Жребий',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 0.5,
|
||||
@@ -74,7 +74,7 @@ class CleromancyGuideDialog extends StatelessWidget {
|
||||
title: 'Что такое Клеромантия?',
|
||||
content:
|
||||
'Клеромантия (от греч. *klēros* — жребий, *manteia* — гадание) — древнейшая священная практика вопрошания Неведомого через случайное выпадение знаков. '
|
||||
'В системе «Оракул 999» случайность не случайна: она служит мостом между вашим глубинным подсознанием и полем синхронистичности.',
|
||||
'В системе «Оракул» случайность не случайна: она служит мостом между вашим глубинным подсознанием и полем синхронистичности.',
|
||||
isDark: isDark,
|
||||
),
|
||||
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/divination_instructions.dart';
|
||||
|
||||
class DivinationInstructionModal extends StatelessWidget {
|
||||
final DivinationSystemInstruction instruction;
|
||||
|
||||
const DivinationInstructionModal({super.key, required this.instruction});
|
||||
|
||||
static void show(BuildContext context, String systemId) {
|
||||
final inst = DivinationInstructionsRepository.get(systemId);
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: const Color(0xFF0F1222),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
builder: (ctx) => DivinationInstructionModal(instruction: inst),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DraggableScrollableSheet(
|
||||
initialChildSize: 0.85,
|
||||
minChildSize: 0.5,
|
||||
maxChildSize: 0.95,
|
||||
expand: false,
|
||||
builder: (ctx, scroll) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 24),
|
||||
child: ListView(
|
||||
controller: scroll,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 44,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white24,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF232742),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Text(instruction.icon, style: const TextStyle(fontSize: 22)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
instruction.title,
|
||||
style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 17, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Text(
|
||||
'Каноническое руководство и методы гадания',
|
||||
style: TextStyle(color: Color(0xFF8C93B8), fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close_rounded, color: Color(0xFF90A4AE)),
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(color: Color(0xFF2A2D4A)),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Essence
|
||||
_buildSection(
|
||||
title: 'СУТЬ СИСТЕМЫ И ИСТОРИЯ',
|
||||
icon: Icons.auto_stories_rounded,
|
||||
content: instruction.historyAndEssence,
|
||||
accentColor: const Color(0xFF80DEEA),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Physical dice
|
||||
_buildSection(
|
||||
title: 'БРОСОК ФИЗИЧЕСКИХ КОСТЕЙ НА СТОЛЕ',
|
||||
icon: Icons.casino_rounded,
|
||||
content: instruction.physicalDiceMethod,
|
||||
accentColor: const Color(0xFFFFD54F),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Direct input
|
||||
_buildSection(
|
||||
title: 'РУЧНОЙ ВВОД СО СТОЛА В ПРИЛОЖЕНИЕ',
|
||||
icon: Icons.touch_app_rounded,
|
||||
content: instruction.directInputMethod,
|
||||
accentColor: const Color(0xFF81C784),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Interpretation
|
||||
_buildSection(
|
||||
title: 'ПРАВИЛА И СЕКРЕТЫ ТОЛКОВАНИЯ',
|
||||
icon: Icons.psychology_rounded,
|
||||
content: instruction.interpretationGuide,
|
||||
accentColor: const Color(0xFFBA68C8),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Sacred rules
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1E1738),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(Icons.shield_moon_rounded, color: Color(0xFFFFD54F), size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'САДЕЙСТВИЕ И САХРАННОСТЬ ПРАКТИКИ',
|
||||
style: TextStyle(
|
||||
color: Color(0xFFFFD54F),
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
for (final r in instruction.sacredRules)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('• ', style: TextStyle(color: Color(0xFFFFD54F), fontWeight: FontWeight.bold)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
r,
|
||||
style: const TextStyle(color: Color(0xFFECEFF1), fontSize: 13, height: 1.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSection({
|
||||
required String title,
|
||||
required IconData icon,
|
||||
required String content,
|
||||
required Color accentColor,
|
||||
}) {
|
||||
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: [
|
||||
Icon(icon, color: accentColor, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: accentColor,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
content,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFD1D5DB),
|
||||
fontSize: 13.5,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../models/external_symbol_model.dart';
|
||||
import '../services/external_symbols_repository.dart';
|
||||
|
||||
class ExternalSymbolsBrowser extends StatefulWidget {
|
||||
final String searchQuery;
|
||||
|
||||
const ExternalSymbolsBrowser({
|
||||
super.key,
|
||||
this.searchQuery = '',
|
||||
});
|
||||
|
||||
@override
|
||||
State<ExternalSymbolsBrowser> createState() => _ExternalSymbolsBrowserState();
|
||||
}
|
||||
|
||||
class _ExternalSymbolsBrowserState extends State<ExternalSymbolsBrowser> {
|
||||
final _repo = ExternalSymbolsRepository();
|
||||
bool _onlyWithImages = false;
|
||||
String? _selectedAuthor;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_repo.init().then((_) {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
void _showDetailModal(ExternalSymbol symbol) {
|
||||
HapticFeedback.selectionClick();
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: const Color(0xFF121424),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
builder: (ctx) => DraggableScrollableSheet(
|
||||
initialChildSize: 0.85,
|
||||
minChildSize: 0.5,
|
||||
maxChildSize: 0.95,
|
||||
expand: false,
|
||||
builder: (context, scrollController) => Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
|
||||
child: ListView(
|
||||
controller: scrollController,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white24,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
symbol.term.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Color(0xFFFFD54F),
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Внешний пантеон • ${symbol.sources.length} источн.',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF8E90A4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Поделиться символом',
|
||||
icon: const Icon(Icons.share_rounded, color: Color(0xFFFFD54F)),
|
||||
onPressed: () {
|
||||
final buf = StringBuffer();
|
||||
buf.writeln('📚 ОРАКУЛ • АКАДЕМИЧЕСКИЙ СИМВОЛ: «${symbol.term.toUpperCase()}»');
|
||||
buf.writeln('');
|
||||
for (final s in symbol.sources) {
|
||||
buf.writeln('🏛 [${s.author} · ${s.work}]:');
|
||||
buf.writeln(s.fullText.isNotEmpty ? s.fullText : s.excerpt);
|
||||
buf.writeln('');
|
||||
}
|
||||
buf.writeln('— Академический корпус системы «Оракул»');
|
||||
Clipboard.setData(ClipboardData(text: buf.toString()));
|
||||
HapticFeedback.lightImpact();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const Icon(Icons.share_rounded, color: Color(0xFFFFD54F), size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text('Символ «${symbol.term}» скопирован для отправки')),
|
||||
],
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close_rounded, color: Colors.white70),
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(color: Color(0xFF24273E), height: 24),
|
||||
|
||||
// Иллюстрации (если есть)
|
||||
if (symbol.hasIllustrations) ...[
|
||||
const Text(
|
||||
'ГРАВЮРЫ И ИЛЛЮСТРАЦИИ',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
letterSpacing: 1.2,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Color(0xFFFFD54F),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
for (final ill in symbol.illustrations) ...[
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Container(
|
||||
color: const Color(0xFF161828),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Image.asset(
|
||||
ill['file'] as String? ?? '',
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, __, ___) => Container(
|
||||
height: 140,
|
||||
color: const Color(0xFF161828),
|
||||
child: Center(
|
||||
child: Text('Илл.: ${ill['file']}', style: const TextStyle(fontSize: 12, color: Colors.white54)),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
ill['caption'] as String? ?? '',
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFFFFD54F), fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
// Академические источники
|
||||
const Text(
|
||||
'АКАДЕМИЧЕСКИЕ СТАТЬИ И ПЕРВОИСТОЧНИКИ',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
letterSpacing: 1.2,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Color(0xFFFFD54F),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
for (final src in symbol.sources) ...[
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF161828),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.15)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.menu_book_rounded, size: 16, color: Color(0xFFFFD54F)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${src.author} • ${src.work}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.white),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy_rounded, size: 16, color: Color(0xFFFFD54F)),
|
||||
tooltip: 'Скопировать цитату',
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: '[${src.author} · ${src.work}]: «${src.fullText}»'));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Цитата скопирована в буфер')),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
src.fullText.isNotEmpty ? src.fullText : src.excerpt,
|
||||
style: const TextStyle(fontSize: 13, height: 1.5, color: Color(0xFFE0E0E0)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_repo.isLoaded) {
|
||||
return const Center(child: CircularProgressIndicator(color: Color(0xFFFFD54F)));
|
||||
}
|
||||
|
||||
final query = widget.searchQuery;
|
||||
var list = _repo.search(query, onlyWithImages: _onlyWithImages);
|
||||
|
||||
if (_selectedAuthor != null) {
|
||||
list = list.where((s) => s.sources.any((src) => src.author == _selectedAuthor)).toList();
|
||||
}
|
||||
|
||||
// Собираем уникальных авторов для быстрого фильтра
|
||||
final authors = <String>{};
|
||||
for (final s in _repo.allSymbols) {
|
||||
for (final src in s.sources) {
|
||||
if (src.author.isNotEmpty) authors.add(src.author);
|
||||
}
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Плашка фильтров внешнего пантеона
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
color: const Color(0xFF101222),
|
||||
child: Row(
|
||||
children: [
|
||||
FilterChip(
|
||||
label: Text('🖼️ С иллюстрациями (${_repo.search("", onlyWithImages: true).length})'),
|
||||
selected: _onlyWithImages,
|
||||
selectedColor: const Color(0xFFFFD54F),
|
||||
labelStyle: TextStyle(
|
||||
color: _onlyWithImages ? Colors.black : const Color(0xFFD1D5DB),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
backgroundColor: const Color(0xFF161828),
|
||||
side: BorderSide(color: _onlyWithImages ? const Color(0xFFFFD54F) : const Color(0xFF2A2D4A)),
|
||||
onSelected: (val) => setState(() => _onlyWithImages = val),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
for (final author in authors) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: ChoiceChip(
|
||||
label: Text(author, style: const TextStyle(fontSize: 11)),
|
||||
selected: _selectedAuthor == author,
|
||||
selectedColor: const Color(0xFFFFD54F).withValues(alpha: 0.3),
|
||||
labelStyle: TextStyle(
|
||||
color: _selectedAuthor == author ? const Color(0xFFFFD54F) : Colors.white70,
|
||||
fontWeight: _selectedAuthor == author ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
backgroundColor: const Color(0xFF161828),
|
||||
onSelected: (val) {
|
||||
setState(() {
|
||||
_selectedAuthor = val ? author : null;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Счетчик
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'Внешний реестр: ${list.length} понятий (из ${_repo.totalCount})',
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF8E90A4), fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Список символов
|
||||
Expanded(
|
||||
child: list.isEmpty
|
||||
? const Center(
|
||||
child: Text(
|
||||
'Понятия не найдены',
|
||||
style: TextStyle(color: Color(0xFF8E90A4)),
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
itemCount: list.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
||||
itemBuilder: (context, idx) {
|
||||
final item = list[idx];
|
||||
return InkWell(
|
||||
onTap: () => _showDetailModal(item),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF161828),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: item.hasIllustrations
|
||||
? const Color(0xFFFFD54F).withValues(alpha: 0.3)
|
||||
: const Color(0xFF2A2D4A),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (item.hasIllustrations) ...[
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.asset(
|
||||
item.illustrations.first['file'] as String? ?? '',
|
||||
width: 48,
|
||||
height: 48,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => const Icon(Icons.image, size: 36, color: Color(0xFFFFD54F)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
item.term.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 15,
|
||||
color: Color(0xFFFFD54F),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF24273E),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
item.primaryAuthor,
|
||||
style: const TextStyle(fontSize: 10, color: Colors.white70),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
item.primaryExcerpt,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFFC0C4D6), height: 1.4),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../models/interpreter_persona.dart';
|
||||
import '../services/settings_repository.dart';
|
||||
|
||||
class InterpreterPersonaPicker extends StatelessWidget {
|
||||
final ValueChanged<InterpreterPersona>? onSelected;
|
||||
final bool compact;
|
||||
|
||||
const InterpreterPersonaPicker({
|
||||
super.key,
|
||||
this.onSelected,
|
||||
this.compact = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final settings = SettingsRepository();
|
||||
return ListenableBuilder(
|
||||
listenable: settings,
|
||||
builder: (context, _) {
|
||||
final selectedId = settings.personaId;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(Icons.record_voice_over_rounded, size: 16, color: Color(0xFFFFD54F)),
|
||||
SizedBox(width: 6),
|
||||
Text(
|
||||
'Кто толкует знаки',
|
||||
style: TextStyle(
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 0.3,
|
||||
color: Color(0xFFFFD54F),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final persona in InterpreterPersona.all)
|
||||
_PersonaChip(
|
||||
persona: persona,
|
||||
selected: persona.id == selectedId,
|
||||
compact: compact,
|
||||
onTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
settings.setPersonaId(persona.id);
|
||||
onSelected?.call(persona);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PersonaChip extends StatelessWidget {
|
||||
final InterpreterPersona persona;
|
||||
final bool selected;
|
||||
final bool compact;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _PersonaChip({
|
||||
required this.persona,
|
||||
required this.selected,
|
||||
required this.compact,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 160),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: compact ? 10 : 12,
|
||||
vertical: compact ? 8 : 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? const Color(0xFF2A2D4A) : const Color(0xFF17192C),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: selected
|
||||
? const Color(0xFFFFD54F)
|
||||
: const Color(0xFFFFD54F).withValues(alpha: 0.22),
|
||||
width: selected ? 1.5 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
persona.icon,
|
||||
size: 16,
|
||||
color: const Color(0xFFFFD54F),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
persona.title,
|
||||
style: TextStyle(
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: selected ? const Color(0xFFFFF9C4) : const Color(0xFFE8EAF6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,755 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../models/tradition_model.dart';
|
||||
import '../screens/lenormand_extras_screen.dart';
|
||||
import '../screens/lenormand_station_screen.dart';
|
||||
import '../services/lenormand_pairs_repository.dart';
|
||||
import '../services/ogham_feda_repository.dart';
|
||||
import '../services/tarot_majors_repository.dart';
|
||||
|
||||
typedef OpenLexiconWord = void Function(int id, String word);
|
||||
|
||||
class TraditionsAtlasView extends StatelessWidget {
|
||||
final List<TraditionCanon> traditions;
|
||||
final TraditionCanon? selected;
|
||||
final String searchQuery;
|
||||
final String? focusStationId;
|
||||
final ValueChanged<TraditionCanon> onSelectTradition;
|
||||
final VoidCallback onBackToOverview;
|
||||
final OpenLexiconWord onOpenWord;
|
||||
|
||||
const TraditionsAtlasView({
|
||||
super.key,
|
||||
required this.traditions,
|
||||
required this.selected,
|
||||
required this.searchQuery,
|
||||
this.focusStationId,
|
||||
required this.onSelectTradition,
|
||||
required this.onBackToOverview,
|
||||
required this.onOpenWord,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (selected == null) {
|
||||
return _Overview(
|
||||
traditions: traditions,
|
||||
onSelect: onSelectTradition,
|
||||
);
|
||||
}
|
||||
return _TraditionStations(
|
||||
tradition: selected!,
|
||||
traditions: traditions,
|
||||
searchQuery: searchQuery,
|
||||
focusStationId: focusStationId,
|
||||
onBack: onBackToOverview,
|
||||
onOpenWord: onOpenWord,
|
||||
onSelectTradition: onSelectTradition,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Overview extends StatelessWidget {
|
||||
final List<TraditionCanon> traditions;
|
||||
final ValueChanged<TraditionCanon> onSelect;
|
||||
|
||||
const _Overview({required this.traditions, required this.onSelect});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xCC1E2238), Color(0xE6151829)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0x4DFFD54F)),
|
||||
),
|
||||
child: const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'🏛️ Сакральные каноны',
|
||||
style: TextStyle(
|
||||
color: Color(0xFFFFF9C4),
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 6),
|
||||
Text(
|
||||
'Выберите традицию, чтобы открыть её станции, первородные архетипы и созвучия базы 999 слов.',
|
||||
style: TextStyle(color: Color(0xFFCBD5E1), fontSize: 13, height: 1.45),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
'${traditions.length} великих систем',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFFFD54F),
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...traditions.map(
|
||||
(t) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: _TraditionCard(tradition: t, onTap: () => onSelect(t)),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TraditionCard extends StatelessWidget {
|
||||
final TraditionCanon tradition;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _TraditionCard({required this.tradition, required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: const Color(0xFF151829),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
onTap();
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0x1AFFFFFF)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x1AFFD54F),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: const Color(0x4DFFD54F)),
|
||||
),
|
||||
child: Text(tradition.icon, style: const TextStyle(fontSize: 26)),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
tradition.name,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFFFF9C4),
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${tradition.count} сакральных станций',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF90CDF4),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
tradition.description,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFCBD5E1),
|
||||
fontSize: 13,
|
||||
height: 1.45,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Row(
|
||||
children: [
|
||||
Text(
|
||||
'Открыть атлас традиции',
|
||||
style: TextStyle(
|
||||
color: Color(0xFFFFD54F),
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 6),
|
||||
Icon(Icons.arrow_forward_rounded, color: Color(0xFFFFD54F), size: 16),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TraditionStations extends StatefulWidget {
|
||||
final TraditionCanon tradition;
|
||||
final List<TraditionCanon> traditions;
|
||||
final String searchQuery;
|
||||
final String? focusStationId;
|
||||
final VoidCallback onBack;
|
||||
final OpenLexiconWord onOpenWord;
|
||||
final ValueChanged<TraditionCanon> onSelectTradition;
|
||||
|
||||
const _TraditionStations({
|
||||
required this.tradition,
|
||||
required this.traditions,
|
||||
required this.searchQuery,
|
||||
required this.focusStationId,
|
||||
required this.onBack,
|
||||
required this.onOpenWord,
|
||||
required this.onSelectTradition,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_TraditionStations> createState() => _TraditionStationsState();
|
||||
}
|
||||
|
||||
class _TraditionStationsState extends State<_TraditionStations> {
|
||||
final Map<String, GlobalKey> _stationKeys = {};
|
||||
String? _highlightedId;
|
||||
|
||||
TraditionCanon get tradition => widget.tradition;
|
||||
String get searchQuery => widget.searchQuery;
|
||||
VoidCallback get onBack => widget.onBack;
|
||||
OpenLexiconWord get onOpenWord => widget.onOpenWord;
|
||||
|
||||
bool get _isLenormand => tradition.id == 'lenormand';
|
||||
bool get _isTarot => tradition.id == 'tarot';
|
||||
bool get _isOgham => tradition.id == 'ogham';
|
||||
bool get _hasDeck => _isLenormand || _isTarot || _isOgham;
|
||||
|
||||
GlobalKey _keyFor(String id) => _stationKeys.putIfAbsent(id, GlobalKey.new);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scheduleFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _TraditionStations oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.focusStationId != widget.focusStationId ||
|
||||
oldWidget.tradition.id != widget.tradition.id) {
|
||||
_scheduleFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void _scheduleFocus() {
|
||||
final id = widget.focusStationId;
|
||||
if (id == null || id.isEmpty) return;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
final ctx = _stationKeys[id]?.currentContext;
|
||||
if (ctx != null) {
|
||||
Scrollable.ensureVisible(
|
||||
ctx,
|
||||
alignment: 0.25,
|
||||
duration: const Duration(milliseconds: 320),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
setState(() => _highlightedId = id);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final stations = tradition.stations
|
||||
.where((s) => s.matchesQuery(searchQuery))
|
||||
.toList();
|
||||
|
||||
final groups = <String, List<TraditionStation>>{};
|
||||
final order = <String>[];
|
||||
for (final st in stations) {
|
||||
final key = st.groupKey.isEmpty ? tradition.name : st.groupKey;
|
||||
if (!groups.containsKey(key)) {
|
||||
groups[key] = [];
|
||||
order.add(key);
|
||||
}
|
||||
groups[key]!.add(st);
|
||||
}
|
||||
final hasGroups = order.length > 1;
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xCC1E2238), Color(0xE6151829)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0x4DFFD54F)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: 'К списку слов',
|
||||
onPressed: onBack,
|
||||
icon: const Icon(Icons.arrow_back_rounded, color: Color(0xFFFFD54F)),
|
||||
),
|
||||
Text(tradition.icon, style: const TextStyle(fontSize: 26)),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
tradition.name,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFFFF9C4),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x26FFD54F),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0x66FFD54F)),
|
||||
),
|
||||
child: Text(
|
||||
'${stations.length} из ${tradition.count}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFFFD54F),
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 4, 8, 4),
|
||||
child: Text(
|
||||
tradition.description,
|
||||
style: const TextStyle(color: Color(0xFFCBD5E1), fontSize: 13, height: 1.45),
|
||||
),
|
||||
),
|
||||
if (_isLenormand)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 8, 4),
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
HapticFeedback.selectionClick();
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const LenormandExtrasScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.layers_outlined, color: Color(0xFFFFD54F), size: 18),
|
||||
label: const Text(
|
||||
'Триады, любовь, работа, карма',
|
||||
style: TextStyle(color: Color(0xFFFFD54F), fontWeight: FontWeight.w700),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: const BorderSide(color: Color(0x66FFD54F)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (stations.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 48),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Станций не найдено',
|
||||
style: TextStyle(color: Color(0xFF8C93B8), fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
for (final gKey in order) ...[
|
||||
if (hasGroups) _GroupHeader(label: gKey),
|
||||
for (final st in groups[gKey]!)
|
||||
Padding(
|
||||
key: _keyFor(st.id),
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: _StationCard(
|
||||
station: st,
|
||||
highlighted: st.id == _highlightedId,
|
||||
onOpenWord: onOpenWord,
|
||||
deckButtonLabel: _isOgham ? 'Значение феды' : (_isTarot ? 'Значение аркана и пары' : 'Описания и сочетания'),
|
||||
onOpenLenormand: _hasDeck
|
||||
? () {
|
||||
HapticFeedback.selectionClick();
|
||||
if (_isTarot) {
|
||||
final card = TarotMajorsRepository().byCanonId(st.id);
|
||||
if (card == null) return;
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => LenormandStationScreen(
|
||||
number: card.n,
|
||||
lookup: TarotMajorsRepository().byNumber,
|
||||
title: 'Таро',
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_isOgham) {
|
||||
final card = OghamFedaRepository().byCanonId(st.id);
|
||||
if (card == null) return;
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => LenormandStationScreen(
|
||||
number: card.n,
|
||||
lookup: OghamFedaRepository().byNumber,
|
||||
title: 'Огам',
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final card = LenormandPairsRepository().byStationId(st.id);
|
||||
if (card == null) return;
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => LenormandStationScreen(number: card.n),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GroupHeader extends StatelessWidget {
|
||||
final String label;
|
||||
const _GroupHeader({required this.label});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 16, 0, 10),
|
||||
child: Row(
|
||||
children: [
|
||||
const Expanded(child: Divider(color: Color(0x4DFBBF24), height: 1)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x14FBBF24),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: const Color(0x40FBBF24)),
|
||||
),
|
||||
child: Text(
|
||||
label.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFFBBF24),
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Expanded(child: Divider(color: Color(0x4DFBBF24), height: 1)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StationCard extends StatelessWidget {
|
||||
final TraditionStation station;
|
||||
final bool highlighted;
|
||||
final OpenLexiconWord onOpenWord;
|
||||
final VoidCallback? onOpenLenormand;
|
||||
final String? deckButtonLabel;
|
||||
|
||||
const _StationCard({
|
||||
required this.station,
|
||||
required this.onOpenWord,
|
||||
this.highlighted = false,
|
||||
this.onOpenLenormand,
|
||||
this.deckButtonLabel,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sub = station.arcanum?.isNotEmpty == true
|
||||
? station.arcanum!
|
||||
: station.groupSub;
|
||||
final primary = station.primary;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF151829),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: highlighted ? const Color(0xFFFFD54F) : const Color(0x1AFFFFFF),
|
||||
width: highlighted ? 1.6 : 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x1AFFD54F),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0x4DFFD54F)),
|
||||
),
|
||||
child: station.geomancyRows.isNotEmpty
|
||||
? _GeomancyGlyph(rows: station.geomancyRows)
|
||||
: Text(
|
||||
station.symbol,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
color: Color(0xFFFFD54F),
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: station.name,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFFFF9C4),
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 15,
|
||||
height: 1.25,
|
||||
),
|
||||
children: [
|
||||
if (station.hebrew != null && station.hebrew!.isNotEmpty)
|
||||
TextSpan(
|
||||
text: ' ${station.hebrew}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xD9D4A855),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 18,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
if (station.hanzi != null && station.hanzi!.isNotEmpty)
|
||||
TextSpan(
|
||||
text: ' ${station.hanzi}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xD9D4A855),
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
if (station.arabic != null && station.arabic!.isNotEmpty)
|
||||
TextSpan(
|
||||
text: ' ${station.arabic}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xD9D4A855),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 17,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (sub.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
sub,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF90CDF4),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
station.lore,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFD1D5DB),
|
||||
fontSize: 13,
|
||||
height: 1.45,
|
||||
),
|
||||
),
|
||||
if (primary != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Material(
|
||||
color: const Color(0x14FFD54F),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: InkWell(
|
||||
onTap: () => onOpenWord(primary.id, primary.word),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0x4DFFD54F)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (primary.emoji.isNotEmpty)
|
||||
Text(primary.emoji, style: const TextStyle(fontSize: 16)),
|
||||
if (primary.emoji.isNotEmpty) const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
primary.word.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 14,
|
||||
letterSpacing: 0.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right_rounded, color: Color(0xFFFFD54F), size: 18),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (station.cluster.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
for (final cw in station.cluster)
|
||||
InkWell(
|
||||
onTap: () => onOpenWord(cw.id, cw.word),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: Tooltip(
|
||||
message: 'Открыть знак «${cw.word}»',
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x0FFFFFFF),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: const Color(0x1FFFFFFF)),
|
||||
),
|
||||
child: Text(
|
||||
'${cw.emoji} ${cw.word}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFE2E8F0),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
if (onOpenLenormand != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.tonalIcon(
|
||||
onPressed: onOpenLenormand,
|
||||
icon: const Icon(Icons.hub_outlined, size: 18),
|
||||
label: Text(deckButtonLabel ?? 'Описания и сочетания'),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: const Color(0x26FFD54F),
|
||||
foregroundColor: const Color(0xFFFFD54F),
|
||||
textStyle: const TextStyle(fontWeight: FontWeight.w800),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Four-row geomantic figure: one centered dot or a centered pair per row.
|
||||
class _GeomancyGlyph extends StatelessWidget {
|
||||
final List<int> rows;
|
||||
const _GeomancyGlyph({required this.rows});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
for (final n in rows)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 1.5),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (int i = 0; i < n; i++) ...[
|
||||
if (i > 0) const SizedBox(width: 5),
|
||||
Container(
|
||||
width: 7,
|
||||
height: 7,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFFFD54F),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../models/history_entry_model.dart';
|
||||
import '../models/word_model.dart';
|
||||
import '../screens/symbol_detail_screen.dart';
|
||||
import '../services/ai_oracle_service.dart';
|
||||
import '../services/words_repository.dart';
|
||||
import 'gradient_star_badge.dart';
|
||||
@@ -61,6 +62,8 @@ class _WordCardWidgetState extends State<WordCardWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Color _getPosColor(String pos) {
|
||||
switch (pos.toLowerCase()) {
|
||||
case 'существительное':
|
||||
@@ -174,11 +177,11 @@ class _WordCardWidgetState extends State<WordCardWidget> {
|
||||
final min = now.minute.toString().padLeft(2, '0');
|
||||
|
||||
var text =
|
||||
'✨ Оракул 999 — Толкование символа ✨\n'
|
||||
'✨ Оракул — Толкование символа ✨\n'
|
||||
'Дата и время: $day.$month.$year $hour:$min\n'
|
||||
'${widget.question != null && widget.question!.isNotEmpty ? "Вопрос: «${widget.question}»\n" : ""}'
|
||||
'Знак №${widget.word.formattedSessionNumber}: ${widget.word.word.toUpperCase()}\n'
|
||||
'Аспект: ${widget.word.partOfSpeech} • ${widget.word.category}\n\n'
|
||||
'Знак №${widget.word.formattedSessionNumber}: ${widget.word.emoji.isNotEmpty ? widget.word.emoji + " " : ""}${widget.word.word.toUpperCase()}\n'
|
||||
'${widget.word.category.isNotEmpty ? "Категория: " + widget.word.category + "\n\n" : ""}'
|
||||
'🔮 Символическое значение:\n${widget.word.description}';
|
||||
|
||||
if (_aiInterpretation != null) {
|
||||
@@ -203,11 +206,51 @@ class _WordCardWidgetState extends State<WordCardWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
void _shareWord(BuildContext context) {
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('🔮 ОРАКУЛ • САХРАЛЬНЫЙ ЗНАК #${widget.word.formattedSessionNumber}');
|
||||
buffer.writeln('«${widget.word.word.toUpperCase()}» ${widget.word.emoji}');
|
||||
if (widget.word.category.isNotEmpty) {
|
||||
buffer.writeln('Категория: ${widget.word.category}');
|
||||
}
|
||||
buffer.writeln('');
|
||||
if (widget.word.core.isNotEmpty) {
|
||||
buffer.writeln('📖 Суть:');
|
||||
buffer.writeln(widget.word.core);
|
||||
buffer.writeln('');
|
||||
}
|
||||
buffer.writeln('🔮 Толкование:');
|
||||
buffer.writeln(widget.word.description);
|
||||
if (_aiInterpretation != null && _aiInterpretation!.isNotEmpty) {
|
||||
buffer.writeln('');
|
||||
buffer.writeln('🤖 Персональный отклик Оракула:');
|
||||
buffer.writeln(_aiInterpretation);
|
||||
}
|
||||
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('Символ «${widget.word.word}» скопирован для отправки!')),
|
||||
],
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
final posColor = _getPosColor(widget.word.partOfSpeech);
|
||||
|
||||
return Card(
|
||||
elevation: 8,
|
||||
@@ -330,11 +373,28 @@ class _WordCardWidgetState extends State<WordCardWidget> {
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: 'Полное досье знака',
|
||||
icon: const Icon(Icons.menu_book_rounded, size: 21, color: Color(0xFF80DEEA)),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SymbolDetailScreen(word: widget.word),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Скопировать толкование',
|
||||
icon: const Icon(Icons.copy_rounded, size: 20),
|
||||
onPressed: () => _copyToClipboard(context),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Поделиться символом',
|
||||
icon: const Icon(Icons.share_rounded, size: 20, color: Color(0xFFFFD54F)),
|
||||
onPressed: () => _shareWord(context),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: _savedToHistory ? 'Сохранено в историю' : 'Сохранить в историю',
|
||||
icon: Icon(
|
||||
@@ -353,59 +413,53 @@ class _WordCardWidgetState extends State<WordCardWidget> {
|
||||
|
||||
// Big Word Title
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Text(
|
||||
widget.word.word.toUpperCase(),
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 1.5,
|
||||
color: isDark ? const Color(0xFFFFF9C4) : const Color(0xFF1B1D30),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.word.emoji.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6.0),
|
||||
child: Text(
|
||||
widget.word.emoji,
|
||||
style: const TextStyle(fontSize: 36),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Text(
|
||||
widget.word.word.toUpperCase(),
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 1.5,
|
||||
color: isDark ? const Color(0xFFFFF9C4) : const Color(0xFF1B1D30),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Category Badge
|
||||
if (widget.word.category.isNotEmpty)
|
||||
Center(
|
||||
child: Chip(
|
||||
avatar: const Icon(Icons.auto_awesome, size: 14, color: Color(0xFFFFD54F)),
|
||||
label: Text(
|
||||
widget.word.category,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isDark ? Colors.white70 : Colors.black87,
|
||||
),
|
||||
),
|
||||
backgroundColor: isDark
|
||||
? Colors.white.withValues(alpha: 0.08)
|
||||
: Colors.black.withValues(alpha: 0.05),
|
||||
side: BorderSide.none,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Part of Speech & Category Badges
|
||||
Center(
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 6,
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
Chip(
|
||||
avatar: Icon(Icons.auto_awesome, size: 14, color: posColor),
|
||||
label: Text(
|
||||
widget.word.partOfSpeech,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
color: posColor,
|
||||
),
|
||||
),
|
||||
backgroundColor: posColor.withValues(alpha: 0.12),
|
||||
side: BorderSide(color: posColor.withValues(alpha: 0.3)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
),
|
||||
if (widget.word.category.isNotEmpty)
|
||||
Chip(
|
||||
label: Text(
|
||||
widget.word.category,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isDark ? Colors.white70 : Colors.black87,
|
||||
),
|
||||
),
|
||||
backgroundColor: isDark
|
||||
? Colors.white.withValues(alpha: 0.08)
|
||||
: Colors.black.withValues(alpha: 0.05),
|
||||
side: BorderSide.none,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 14),
|
||||
Divider(
|
||||
@@ -447,7 +501,7 @@ class _WordCardWidgetState extends State<WordCardWidget> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
widget.word.description,
|
||||
widget.word.displayMeaning,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
height: 1.45,
|
||||
fontSize: 13.5,
|
||||
|
||||
Reference in New Issue
Block a user