Files
999/lib/screens/home_search_screen.dart

639 lines
24 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/history_entry_model.dart';
import '../models/word_model.dart';
import '../services/true_random_service.dart';
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 {
const HomeSearchScreen({super.key});
@override
State<HomeSearchScreen> createState() => _HomeSearchScreenState();
}
class _HomeSearchScreenState extends State<HomeSearchScreen> {
final _repo = WordsRepository();
final _questionController = TextEditingController();
final _countController = TextEditingController(text: '1');
final _diceRollerKey = GlobalKey<D10DiceRollerWidgetState>();
int _wordsCount = 1; // 1 to 9 words in the session
int _activeWordIndex = 0; // 0.._wordsCount-1
List<String> _wordsDigits = ['']; // 3-digit strings for each word
bool _showKeypad = true;
@override
void initState() {
super.initState();
_countController.addListener(_onCountChanged);
}
@override
void dispose() {
_countController.removeListener(_onCountChanged);
_countController.dispose();
_questionController.dispose();
super.dispose();
}
void _onCountChanged() {
final text = _countController.text.trim();
final count = int.tryParse(text);
if (count != null && count >= 1 && count <= 9) {
if (count != _wordsCount) {
setState(() {
_wordsCount = count;
_adjustDigitsList();
});
}
}
}
void _adjustDigitsList() {
final current = List<String>.from(_wordsDigits);
if (current.length < _wordsCount) {
while (current.length < _wordsCount) {
current.add('');
}
} else if (current.length > _wordsCount) {
current.removeRange(_wordsCount, current.length);
}
_wordsDigits = current;
if (_activeWordIndex >= _wordsCount) {
_activeWordIndex = _wordsCount - 1;
}
}
void _onKeypadTap(String val) {
HapticFeedback.lightImpact();
if (val == 'C') {
setState(() {
_wordsDigits[_activeWordIndex] = '';
});
return;
}
if (val == '⌫') {
final current = _wordsDigits[_activeWordIndex];
if (current.isNotEmpty) {
setState(() {
_wordsDigits[_activeWordIndex] = current.substring(0, current.length - 1);
});
} else if (_activeWordIndex > 0) {
setState(() {
_activeWordIndex--;
});
}
return;
}
// Append digit to current active word (up to 3 digits)
final current = _wordsDigits[_activeWordIndex];
String newDigits;
if (current.length >= 3) {
newDigits = val;
} else {
newDigits = current + val;
}
setState(() {
_wordsDigits[_activeWordIndex] = newDigits;
});
// If 3 digits filled and more words remain, auto-advance to next word!
if (newDigits.length == 3 && _activeWordIndex < _wordsCount - 1) {
Future.delayed(const Duration(milliseconds: 150), () {
if (mounted) {
setState(() {
_activeWordIndex++;
});
}
});
}
}
void _onVirtualRollCompleted(int finalNumber) {
final formatted = finalNumber.toString().padLeft(3, '0');
setState(() {
_wordsDigits[_activeWordIndex] = formatted;
});
// Advance to next word if not last
if (_activeWordIndex < _wordsCount - 1) {
Future.delayed(const Duration(milliseconds: 200), () {
if (mounted) {
setState(() {
_activeWordIndex++;
});
}
});
}
}
bool get _isSessionReady {
if (_wordsDigits.length != _wordsCount) return false;
return _wordsDigits.every((d) => d.length == 3);
}
void _openSessionResults() {
final sessionWords = <SessionWord>[];
for (int i = 0; i < _wordsCount; i++) {
final digits = _wordsDigits[i];
final num = int.tryParse(digits) ?? 0;
final wordItem = _repo.getWordBySessionNumber(num) ??
WordItem(
id: num,
word: 'Символ #$digits',
partOfSpeech: 'знак',
frequency: 0,
category: 'Символ',
description: 'Сакральный знак под номером $digits',
);
sessionWords.add(SessionWord.fromWordItem(wordItem));
}
final question = _questionController.text.trim();
final session = OracleSession(
id: DateTime.now().millisecondsSinceEpoch.toString(),
timestamp: DateTime.now(),
question: question.isNotEmpty ? question : null,
wordsCount: _wordsCount,
words: sessionWords,
);
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => SessionResultScreen(session: session),
),
);
}
void _resetSession() {
setState(() {
_activeWordIndex = 0;
_wordsDigits = List.filled(_wordsCount, '');
});
}
Future<void> _rollRandomCleromancy() async {
HapticFeedback.heavyImpact();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Row(
children: [
GradientStar(size: 20),
SizedBox(width: 10),
Expanded(
child: Text(
'Бросаю священный жребий (3 × d10)…',
style: TextStyle(fontWeight: FontWeight.w600),
),
),
],
),
duration: Duration(milliseconds: 1000),
behavior: SnackBarBehavior.floating,
),
);
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)),
),
);
} catch (_) {
final rng = math.Random();
if (!mounted) return;
setState(() {
_wordsDigits = List.generate(
_wordsCount,
(_) => List.generate(3, (_) => rng.nextInt(10)).join(),
);
_activeWordIndex = _wordsCount - 1;
});
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final currentActiveDigits =
_activeWordIndex < _wordsDigits.length ? _wordsDigits[_activeWordIndex] : '';
return Scaffold(
appBar: AppBar(
title: const Row(
children: [
GradientStar(size: 24),
SizedBox(width: 10),
Text(
'Оракул',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w900,
letterSpacing: 0.5,
),
),
],
),
actions: [
IconButton(
tooltip: 'Инструкция: Клеромантия и кубики d10',
icon: const Icon(Icons.help_outline_rounded, color: Color(0xFFFFD54F)),
onPressed: () => CleromancyGuideDialog.show(context),
),
IconButton(
tooltip: 'Очистить броски',
icon: const Icon(Icons.refresh_rounded),
onPressed: _resetSession,
),
IconButton(
tooltip: 'Бросить священный жребий (3 × d10)',
icon: const Icon(Icons.casino_rounded, color: Color(0xFF64B5F6)),
onPressed: _rollRandomCleromancy,
),
IconButton(
tooltip: _showKeypad ? 'Скрыть панель' : 'Показать панель',
icon: Icon(_showKeypad ? Icons.dialpad_outlined : Icons.dialpad),
onPressed: () {
setState(() => _showKeypad = !_showKeypad);
},
),
],
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const InterpreterPersonaPicker(),
const SizedBox(height: 12),
// Row: Question & Words count selector (1..9)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Question Input
Expanded(
child: TextField(
controller: _questionController,
decoration: InputDecoration(
labelText: 'Ваш вопрос или контекст',
hintText: 'В чем корень ситуации?',
prefixIcon: const Icon(Icons.help_outline_rounded, color: Color(0xFFFFD54F), size: 20),
suffixIcon: _questionController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear, size: 18),
onPressed: () => _questionController.clear(),
)
: null,
filled: true,
fillColor: isDark ? const Color(0xFF17192C) : theme.colorScheme.surfaceContainerHigh,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: const Color(0xFFFFD54F).withValues(alpha: 0.25),
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: const Color(0xFFFFD54F).withValues(alpha: 0.2),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(
color: Color(0xFFFFD54F),
width: 1.5,
),
),
),
),
),
const SizedBox(width: 10),
// Words Count Input Box (1..9)
Container(
width: 78,
height: 56,
decoration: BoxDecoration(
color: isDark ? const Color(0xFF17192C) : theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: const Color(0xFFFFD54F).withValues(alpha: 0.4),
),
),
child: TextField(
controller: _countController,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
maxLength: 1,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFFFFF9C4),
),
decoration: const InputDecoration(
counterText: '',
labelText: 'Слов',
labelStyle: TextStyle(fontSize: 11, color: Color(0xFFFFD54F)),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 6, horizontal: 8),
),
),
),
],
),
const SizedBox(height: 12),
// Multi-Word Slots Progress Indicator (if > 1 word)
if (_wordsCount > 1) ...[
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF151829) : theme.colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: const Color(0xFFFFD54F).withValues(alpha: 0.2),
),
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(_wordsCount, (i) {
final isSelected = i == _activeWordIndex;
final digits = i < _wordsDigits.length ? _wordsDigits[i] : '';
final isFilled = digits.length == 3;
return Padding(
padding: const EdgeInsets.only(right: 6),
child: InkWell(
onTap: () {
setState(() => _activeWordIndex = i);
HapticFeedback.selectionClick();
},
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: isSelected
? const Color(0xFF2A2D4A)
: (isFilled ? const Color(0xFF1E2138) : Colors.transparent),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected
? const Color(0xFFFFD54F)
: (isFilled ? const Color(0xFFFFD54F).withValues(alpha: 0.4) : Colors.white12),
width: isSelected ? 1.5 : 1.0,
),
),
child: Row(
children: [
Text(
'Слово ${i + 1}: ',
style: TextStyle(
fontSize: 11,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? const Color(0xFFFFF9C4) : theme.colorScheme.outline,
),
),
Text(
isFilled ? '#$digits' : (digits.isEmpty ? '• • •' : '$digits..'),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w900,
color: isFilled ? const Color(0xFFFFD54F) : Colors.white54,
),
),
],
),
),
),
);
}),
),
),
),
const SizedBox(height: 10),
],
// D10 Dice Roller Slots Widget (Purely for rolling/entering faces before opening word)
D10DiceRollerWidget(
key: _diceRollerKey,
digits: currentActiveDigits,
isRolling: false,
onRollPressed: () {},
onRollCompleted: _onVirtualRollCompleted,
),
const SizedBox(height: 14),
// Button to open the Result Screen when ready
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: _isSessionReady ? _openSessionResults : null,
icon: const Icon(Icons.visibility_rounded, size: 20),
label: Text(
_isSessionReady
? '🔮 ОТКРЫТЬ ЗНАКИ И ТРАКТОВКУ ($_wordsCount ${_wordsCount == 1 ? "знак" : (_wordsCount < 5 ? "знака" : "знаков")})'
: 'Заполните броски (Слово ${_activeWordIndex + 1} из $_wordsCount)',
style: const TextStyle(
fontWeight: FontWeight.w900,
letterSpacing: 0.6,
fontSize: 13.5,
),
),
style: FilledButton.styleFrom(
backgroundColor: const Color(0xFF2A2D4A),
foregroundColor: const Color(0xFFFFF9C4),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
side: BorderSide(
color: _isSessionReady
? const Color(0xFFFFD54F)
: const Color(0xFFFFD54F).withValues(alpha: 0.3),
width: 1.5,
),
),
),
),
),
const SizedBox(height: 14),
// Interactive Numeric Keypad (Directly inputs to active d10 word slots)
if (_showKeypad) ...[
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isDark
? const Color(0xFF17192A)
: theme.colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: const Color(0xFFFFD54F).withValues(alpha: 0.25),
),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
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'
: 'Введено: ${currentActiveDigits.length} из 3',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: currentActiveDigits.length == 3
? const Color(0xFFFFD54F)
: theme.colorScheme.outline,
),
),
],
),
const SizedBox(height: 10),
_buildKeypadRow(['1', '2', '3']),
const SizedBox(height: 8),
_buildKeypadRow(['4', '5', '6']),
const SizedBox(height: 8),
_buildKeypadRow(['7', '8', '9']),
const SizedBox(height: 8),
_buildKeypadRow(['C', '0', '⌫']),
],
),
),
const SizedBox(height: 16),
],
],
),
),
),
);
}
Widget _buildKeypadRow(List<String> keys) {
return Row(
children: keys.map((key) {
final isSpecial = key == 'C' || key == '⌫';
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Material(
color: isSpecial
? const Color(0xFF2A2D4A)
: isDark
? const Color(0xFF20233A)
: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(14),
elevation: 1,
child: InkWell(
onTap: () => _onKeypadTap(key),
borderRadius: BorderRadius.circular(14),
child: Container(
height: 46,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: isSpecial
? const Color(0xFFFFD54F).withValues(alpha: 0.4)
: Colors.white10,
),
),
child: Text(
key,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: isSpecial
? const Color(0xFFFFF9C4)
: isDark
? Colors.white
: Colors.black87,
),
),
),
),
),
),
);
}).toList(),
);
}
}