591 lines
22 KiB
Dart
591 lines
22 KiB
Dart
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/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();
|
|
|
|
WordItem? _currentWord;
|
|
String? _activeHistoryId;
|
|
List<WordItem> _searchResults = [];
|
|
bool _showKeypad = true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// Do NOT show word by default until user interacts or starts a session
|
|
_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 = [];
|
|
});
|
|
return;
|
|
}
|
|
|
|
final results = _repo.search(query);
|
|
setState(() {
|
|
_searchResults = results;
|
|
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: 'Жребий номера',
|
|
question: question.isNotEmpty ? question : null,
|
|
);
|
|
await _repo.addHistoryEntry(entry);
|
|
}
|
|
|
|
setState(() {
|
|
_currentWord = word;
|
|
_activeHistoryId = newHistId ?? _activeHistoryId;
|
|
if (updateText) {
|
|
_searchController.text = word.sessionNumber.toString();
|
|
}
|
|
});
|
|
HapticFeedback.selectionClick();
|
|
}
|
|
|
|
void _onKeypadTap(String val) {
|
|
HapticFeedback.lightImpact();
|
|
String currentText = _searchController.text;
|
|
|
|
if (val == 'C') {
|
|
_searchController.clear();
|
|
return;
|
|
}
|
|
|
|
if (val == '⌫') {
|
|
if (currentText.isNotEmpty) {
|
|
_searchController.text = currentText.substring(0, currentText.length - 1);
|
|
_searchController.selection = TextSelection.fromPosition(
|
|
TextPosition(offset: _searchController.text.length),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Append digit
|
|
if (currentText.length >= 3) {
|
|
_searchController.text = val;
|
|
} else {
|
|
final newText = currentText + val;
|
|
final num = int.tryParse(newText);
|
|
if (num != null && num <= 999) {
|
|
_searchController.text = newText;
|
|
} else {
|
|
_searchController.text = val;
|
|
}
|
|
}
|
|
|
|
_searchController.selection = TextSelection.fromPosition(
|
|
TextPosition(offset: _searchController.text.length),
|
|
);
|
|
}
|
|
|
|
void _startNewSessionDraw() {
|
|
final rnd = _repo.getRandomWord();
|
|
_setWord(rnd, updateText: true, recordHistory: true);
|
|
}
|
|
|
|
void _resetToWelcome() {
|
|
setState(() {
|
|
_currentWord = null;
|
|
_searchController.clear();
|
|
_searchResults = [];
|
|
});
|
|
}
|
|
|
|
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();
|
|
setState(() {
|
|
_currentWord = null;
|
|
_searchController.clear();
|
|
_searchResults = [];
|
|
});
|
|
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: [
|
|
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: 10),
|
|
|
|
// Search & 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();
|
|
},
|
|
)
|
|
: 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
|
|
if (_searchResults.length > 1 && _searchController.text.isNotEmpty) ...[
|
|
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: false, recordHistory: true);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
|
|
const SizedBox(height: 14),
|
|
|
|
// Welcome Idle State OR Active Word Card
|
|
if (_currentWord == null) ...[
|
|
// Welcome Initiation Card
|
|
Container(
|
|
padding: const EdgeInsets.all(22),
|
|
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,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xFFFFD54F).withValues(alpha: 0.1),
|
|
blurRadius: 16,
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
const GradientStar(size: 44),
|
|
const SizedBox(height: 14),
|
|
const Text(
|
|
'СВЯЩЕННЫЙ ЖРЕБИЙ ОРАКУЛА',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w900,
|
|
letterSpacing: 1.0,
|
|
color: Color(0xFFFFF9C4),
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
'Сформулируйте в мыслях или запишите сверху ваш вопрос, затем нажмите кнопку или наберите номер от 000 до 999 для раскрытия символического ключа.',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
height: 1.45,
|
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.85),
|
|
),
|
|
),
|
|
const SizedBox(height: 18),
|
|
FilledButton.icon(
|
|
onPressed: _startNewSessionDraw,
|
|
icon: const GradientStar(size: 20),
|
|
label: const Text(
|
|
'НАЧАТЬ СЕССИЮ / БРОСИТЬ ЖРЕБИЙ',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w900,
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: const Color(0xFF2A2D4A),
|
|
foregroundColor: const Color(0xFFFFF9C4),
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(18),
|
|
side: const BorderSide(
|
|
color: Color(0xFFFFD54F),
|
|
width: 1.2,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
] 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: _startNewSessionDraw,
|
|
),
|
|
),
|
|
],
|
|
|
|
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: [
|
|
GradientStar(size: 14),
|
|
SizedBox(width: 6),
|
|
Text(
|
|
'Случайный жребий числа:',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w700,
|
|
color: Color(0xFFFFD54F),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Text(
|
|
'000 — 999',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: 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(),
|
|
);
|
|
}
|
|
}
|