Files
999/lib/screens/home_search_screen.dart
T

623 lines
23 KiB
Dart
Raw 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 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/history_entry_model.dart';
import '../models/word_model.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/word_card_widget.dart';
class HomeSearchScreen extends StatefulWidget {
const HomeSearchScreen({super.key});
@override
State<HomeSearchScreen> createState() => _HomeSearchScreenState();
}
class _HomeSearchScreenState extends State<HomeSearchScreen> {
final _repo = WordsRepository();
final _searchController = TextEditingController();
final _questionController = TextEditingController();
final _focusNode = FocusNode();
final _diceRollerKey = GlobalKey<D10DiceRollerWidgetState>();
WordItem? _currentWord;
String? _activeHistoryId;
List<WordItem> _searchResults = [];
bool _showKeypad = true;
String _currentDigits = ''; // Tracks 0..3 digits input for d10 dice cleromancy
@override
void initState() {
super.initState();
_searchController.addListener(_onSearchChanged);
}
@override
void dispose() {
_searchController.removeListener(_onSearchChanged);
_searchController.dispose();
_questionController.dispose();
_focusNode.dispose();
super.dispose();
}
void _onSearchChanged() {
final query = _searchController.text.trim();
if (query.isEmpty) {
setState(() {
_searchResults = [];
_currentDigits = '';
});
return;
}
final isNumeric = int.tryParse(query) != null;
if (isNumeric) {
setState(() {
_currentDigits = query;
});
// In Cleromancy, exactly 3 digits (d10x3) are required to resolve a number 000..999
if (query.length >= 3) {
final num = int.tryParse(query);
if (num != null && num <= 999) {
final word = _repo.getWordBySessionNumber(num);
if (word != null) {
_setWord(word, recordHistory: true);
}
}
}
} else {
// Text search by word
final results = _repo.search(query);
setState(() {
_searchResults = results;
_currentDigits = '';
if (results.isNotEmpty) {
_setWord(results.first, recordHistory: true);
}
});
}
}
Future<void> _setWord(WordItem word, {bool updateText = false, bool recordHistory = true}) async {
final question = _questionController.text.trim();
String? newHistId;
if (recordHistory) {
newHistId = DateTime.now().millisecondsSinceEpoch.toString();
final entry = HistoryEntry(
id: newHistId,
timestamp: DateTime.now(),
word: word.word,
sessionNumber: word.sessionNumber,
partOfSpeech: word.partOfSpeech,
symbolicMeaning: word.description,
readingType: 'Клеромантия (3× d10)',
question: question.isNotEmpty ? question : null,
);
await _repo.addHistoryEntry(entry);
}
setState(() {
_currentWord = word;
_activeHistoryId = newHistId ?? _activeHistoryId;
_currentDigits = word.formattedSessionNumber;
if (updateText) {
_searchController.text = word.formattedSessionNumber;
}
});
HapticFeedback.selectionClick();
}
void _onKeypadTap(String val) {
HapticFeedback.lightImpact();
String currentText = _searchController.text;
if (val == 'C') {
_searchController.clear();
setState(() => _currentDigits = '');
return;
}
if (val == '⌫') {
if (currentText.isNotEmpty) {
_searchController.text = currentText.substring(0, currentText.length - 1);
_searchController.selection = TextSelection.fromPosition(
TextPosition(offset: _searchController.text.length),
);
}
return;
}
// In Cleromancy, append digit up to 3 digits
if (currentText.length >= 3) {
_searchController.text = val;
} else {
_searchController.text = currentText + val;
}
_searchController.selection = TextSelection.fromPosition(
TextPosition(offset: _searchController.text.length),
);
}
void _onVirtualRollCompleted(int finalNumber) {
final word = _repo.getWordBySessionNumber(finalNumber);
if (word != null) {
_setWord(word, updateText: true, recordHistory: true);
}
}
void _resetToWelcome() {
setState(() {
_currentWord = null;
_searchController.clear();
_searchResults = [];
_currentDigits = '';
});
}
void _goToNext() {
if (_currentWord == null) return;
final next = _repo.getNextWord(_currentWord!.sessionNumber);
_setWord(next, updateText: true, recordHistory: false);
}
void _goToPrevious() {
if (_currentWord == null) return;
final prev = _repo.getPreviousWord(_currentWord!.sessionNumber);
_setWord(prev, updateText: true, recordHistory: false);
}
void _reshuffleSession() {
_repo.reshuffleSessionNumbers();
_resetToWelcome();
HapticFeedback.heavyImpact();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Row(
children: [
GradientStar(size: 20),
SizedBox(width: 10),
Expanded(
child: Text(
'Случайный порядок 1000 чисел перетасован заново!',
style: TextStyle(fontWeight: FontWeight.w600),
),
),
],
),
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;
return Scaffold(
appBar: AppBar(
title: const Row(
children: [
GradientStar(size: 24),
SizedBox(width: 10),
Text(
'Оракул 999',
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),
),
if (_currentWord != null)
IconButton(
tooltip: 'Новый вопрос / Сброс',
icon: const Icon(Icons.refresh_rounded),
onPressed: _resetToWelcome,
),
IconButton(
tooltip: 'Перетасовать матрицу (#000..#999)',
icon: const Icon(Icons.shuffle_rounded),
onPressed: _reshuffleSession,
),
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: [
// Question Input Field
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(height: 12),
// D10 Dice Roller Slots Widget
D10DiceRollerWidget(
key: _diceRollerKey,
digits: _currentDigits,
isRolling: false,
onRollPressed: () {},
onRollCompleted: _onVirtualRollCompleted,
),
const SizedBox(height: 10),
// Search & Direct Number Input Bar
TextField(
controller: _searchController,
focusNode: _focusNode,
keyboardType: TextInputType.text,
decoration: InputDecoration(
labelText: 'Введите число (#000..#999) или слово',
hintText: 'Например: 777 или огонь',
prefixIcon: const Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: GradientStar(size: 18),
),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear_rounded),
onPressed: () {
_searchController.clear();
_focusNode.unfocus();
setState(() => _currentDigits = '');
},
)
: null,
filled: true,
fillColor: isDark
? const Color(0xFF1B1D30)
: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.4),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(18),
borderSide: BorderSide(
color: const Color(0xFFFFD54F).withValues(alpha: 0.3),
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(18),
borderSide: BorderSide(
color: const Color(0xFFFFD54F).withValues(alpha: 0.25),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(18),
borderSide: const BorderSide(
color: Color(0xFFFFD54F),
width: 1.8,
),
),
),
),
// Search matches dropdown list (for text search)
if (_searchResults.length > 1 && _searchController.text.isNotEmpty && int.tryParse(_searchController.text) == null) ...[
const SizedBox(height: 8),
Container(
constraints: const BoxConstraints(maxHeight: 140),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF161828) : theme.colorScheme.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: const Color(0xFFFFD54F).withValues(alpha: 0.3),
),
),
child: ListView.separated(
shrinkWrap: true,
padding: const EdgeInsets.symmetric(vertical: 4),
itemCount: _searchResults.length > 10 ? 10 : _searchResults.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, idx) {
final item = _searchResults[idx];
final isSelected = item.sessionNumber == _currentWord?.sessionNumber;
return ListTile(
dense: true,
selected: isSelected,
leading: GradientStarBadge(
text: '#${item.formattedSessionNumber}',
size: 14,
),
title: Text(
item.word.toUpperCase(),
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
item.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 11),
),
trailing: Text(
item.partOfSpeech,
style: TextStyle(
fontSize: 11,
color: theme.colorScheme.outline,
),
),
onTap: () {
_setWord(item, updateText: true, recordHistory: true);
},
);
},
),
),
],
const SizedBox(height: 14),
// Welcome Idle State OR Active Word Card
if (_currentWord == null) ...[
// Welcome Initiation Card
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
const Color(0xFF1F223B),
const Color(0xFF131526),
],
),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: const Color(0xFFFFD54F).withValues(alpha: 0.35),
width: 1.2,
),
),
child: Column(
children: [
const GradientStar(size: 38),
const SizedBox(height: 12),
const Text(
'СВЯЩЕННЫЙ ЖРЕБИЙ КЛЕРОМАНТИИ',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w900,
letterSpacing: 0.8,
color: Color(0xFFFFF9C4),
),
),
const SizedBox(height: 8),
Text(
'Сформулируйте вопрос, затем совершите 3 броска кубика d10 кнопкой выше или введите 3 выпавших числа (Сотни, Десятки, Единицы).',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
height: 1.4,
color: theme.colorScheme.onSurface.withValues(alpha: 0.85),
),
),
const SizedBox(height: 14),
OutlinedButton.icon(
onPressed: () => CleromancyGuideDialog.show(context),
icon: const Icon(Icons.menu_book_rounded, size: 16, color: Color(0xFFFFD54F)),
label: const Text(
'Инструкция по броскам d10',
style: TextStyle(
color: Color(0xFFFFD54F),
fontWeight: FontWeight.bold,
fontSize: 12.5,
),
),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xFFFFD54F), width: 1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
),
],
),
),
] else ...[
// Main Sacred Word Card Display
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, 0.04),
end: Offset.zero,
).animate(animation),
child: child,
),
);
},
child: WordCardWidget(
key: ValueKey('${_currentWord!.id}_${_currentWord!.sessionNumber}'),
word: _currentWord!,
question: _questionController.text.trim(),
historyEntryId: _activeHistoryId,
onNext: _goToNext,
onPrevious: _goToPrevious,
onRandom: () {
_diceRollerKey.currentState?.triggerRollAnimation();
},
),
),
],
const SizedBox(height: 14),
// Interactive Numeric Keypad
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: [
const Row(
children: [
Icon(Icons.casino_outlined, size: 15, color: Color(0xFFFFD54F)),
SizedBox(width: 6),
Text(
'Ввод граней d10 (3 цифры):',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: Color(0xFFFFD54F),
),
),
],
),
Text(
_currentDigits.length == 3
? 'Код: #$_currentDigits'
: 'Введено: ${_currentDigits.length} из 3',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: _currentDigits.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(),
);
}
}