Files
999/lib/screens/session_result_screen.dart

1001 lines
37 KiB
Dart

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;
const SessionResultScreen({
super.key,
required this.session,
});
@override
State<SessionResultScreen> createState() => _SessionResultScreenState();
}
class _SessionResultScreenState extends State<SessionResultScreen> {
final _aiService = AiOracleService();
final _repo = WordsRepository();
final _chatInputController = TextEditingController();
final _scrollController = ScrollController();
late OracleSession _session;
bool _isGeneratingInitialAi = false;
bool _isChatLoading = false;
@override
void initState() {
super.initState();
_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();
}
}
@override
void dispose() {
_chatInputController.dispose();
_scrollController.dispose();
super.dispose();
}
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 labeled = '[${voice.title}] $result';
final message = SessionChatMessage(
id: 'msg_${DateTime.now().millisecondsSinceEpoch}',
sender: 'oracle',
text: labeled,
timestamp: DateTime.now(),
);
final nextMessages = replaceOpening && _session.messages.isEmpty
? [message]
: [..._session.messages, message];
final updated = _session.copyWith(
aiInterpretation: labeled,
messages: nextMessages,
);
setState(() {
_session = updated;
_isGeneratingInitialAi = false;
});
await _repo.saveSession(updated);
_scrollToBottom();
}
} catch (_) {
if (mounted) {
setState(() => _isGeneratingInitialAi = false);
}
}
}
Future<void> _sendMessage({SessionWord? newlyAddedWord}) async {
final text = _chatInputController.text.trim();
if (text.isEmpty && newlyAddedWord == null) return;
final userMessageText = text.isNotEmpty
? text
: 'Раскрой значение добавленного знака #${newlyAddedWord?.formattedSessionNumber} «${newlyAddedWord?.word.toUpperCase()}».';
_chatInputController.clear();
final userMsg = SessionChatMessage(
id: 'msg_${DateTime.now().millisecondsSinceEpoch}',
sender: 'user',
text: userMessageText,
timestamp: DateTime.now(),
attachedWord: newlyAddedWord,
);
final updatedWithUser = _session.copyWith(
messages: [..._session.messages, userMsg],
);
setState(() {
_session = updatedWithUser;
_isChatLoading = true;
});
_scrollToBottom();
try {
final reply = await _aiService.chatWithOracle(
session: updatedWithUser,
userMessage: userMessageText,
newlyAddedWord: newlyAddedWord,
);
if (mounted) {
final oracleMsg = SessionChatMessage(
id: 'msg_${DateTime.now().millisecondsSinceEpoch + 1}',
sender: 'oracle',
text: reply,
timestamp: DateTime.now(),
);
final finalSession = updatedWithUser.copyWith(
messages: [...updatedWithUser.messages, oracleMsg],
);
setState(() {
_session = finalSession;
_isChatLoading = false;
});
await _repo.saveSession(finalSession);
_scrollToBottom();
}
} catch (e) {
if (mounted) {
setState(() => _isChatLoading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Ошибка отправки: $e')),
);
}
}
}
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent + 100,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
void _showAddWordModal() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) => _AddWordBottomSheet(
onWordChosen: (WordItem word) async {
Navigator.pop(ctx);
final sessionWord = SessionWord.fromWordItem(
word,
positionTitle: 'Уточняющий знак #${_session.words.length + 1}',
);
final updated = _session.copyWith(
words: [..._session.words, sessionWord],
wordsCount: _session.words.length + 1,
);
setState(() {
_session = updated;
});
await _repo.saveSession(updated);
// Trigger chat refinement with this new word
await _sendMessage(newlyAddedWord: sessionWord);
},
),
);
}
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('✨ Оракул — Сессия Клеромантии ✨');
buf.writeln('Дата: ${_session.formattedDate}');
if (_session.question != null && _session.question!.isNotEmpty) {
buf.writeln('Вопрос: «${_session.question}»');
}
buf.writeln('Знаков в цепочке: ${_session.words.length}\n');
for (int i = 0; i < _session.words.length; i++) {
final w = _session.words[i];
final pos = w.positionTitle != null ? ' (${w.positionTitle})' : '';
buf.writeln('🔮 Знак #${w.formattedSessionNumber}: ${w.word.toUpperCase()}$pos');
buf.writeln(' ${w.symbolicMeaning}\n');
}
if (_session.aiInterpretation != null) {
buf.writeln('🤖 Толкование Оракула (G4F):');
buf.writeln(_session.aiInterpretation);
}
Clipboard.setData(ClipboardData(text: buf.toString()));
HapticFeedback.lightImpact();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Row(
children: [
GradientStar(size: 18),
SizedBox(width: 10),
Expanded(child: Text('Сессия скопирована в буфер обмена')),
],
),
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: Row(
children: [
const GradientStar(size: 22),
const SizedBox(width: 8),
Text(
'Сессия (${_session.words.length} ${_session.words.length == 1 ? "знак" : (_session.words.length < 5 ? "знака" : "знаков")})',
style: const TextStyle(fontWeight: FontWeight.w900),
),
],
),
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)),
onPressed: _showAddWordModal,
),
IconButton(
tooltip: 'Скопировать сессию',
icon: const Icon(Icons.copy_rounded),
onPressed: _copySessionToClipboard,
),
],
),
body: SafeArea(
child: Column(
children: [
// Top Collapsible / Horizontal Words Strip
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF141728) : theme.colorScheme.surfaceContainerLow,
border: const Border(
bottom: BorderSide(color: Colors.white12, width: 1),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Question line if present
if (_session.question != null && _session.question!.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Row(
children: [
const Icon(Icons.help_outline_rounded, size: 14, color: Color(0xFFFFD54F)),
const SizedBox(width: 6),
Expanded(
child: Text(
'Вопрос: «${_session.question}»',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFFFFF9C4),
fontStyle: FontStyle.italic,
),
),
),
],
),
),
],
// Words Horizontal Badges List + Add Button
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
..._session.words.asMap().entries.map((e) {
final idx = e.key;
final w = e.value;
return Padding(
padding: const EdgeInsets.only(right: 6),
child: InkWell(
onTap: () => _showWordDetails(w),
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: const Color(0xFF232742),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFFFFD54F).withValues(alpha: 0.4),
),
),
child: Row(
children: [
Text(
'${idx + 1}. ',
style: TextStyle(
fontSize: 11,
color: theme.colorScheme.outline,
fontWeight: FontWeight.bold,
),
),
Text(
'#${w.formattedSessionNumber} ${w.word.toUpperCase()}',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w900,
color: Color(0xFFFFD54F),
),
),
],
),
),
),
);
}),
// Button: Add word
OutlinedButton.icon(
onPressed: _showAddWordModal,
icon: const Icon(Icons.casino_rounded, size: 14, color: Color(0xFFFFD54F)),
label: const Text(
'+ Докинуть знак',
style: TextStyle(
fontSize: 11.5,
fontWeight: FontWeight.bold,
color: Color(0xFFFFD54F),
),
),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
side: const BorderSide(color: Color(0xFFFFD54F)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
],
),
),
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,
),
),
],
),
),
),
],
),
],
),
),
// Live Chat Area
Expanded(
child: _session.messages.isEmpty && _isGeneratingInitialAi
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFFFD54F)),
),
SizedBox(height: 16),
Text(
'Оракул синтезирует символы сессии...',
style: TextStyle(fontStyle: FontStyle.italic),
),
],
),
)
: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
itemCount: _session.messages.length + (_isChatLoading ? 1 : 0),
itemBuilder: (context, idx) {
if (idx == _session.messages.length) {
return _buildTypingIndicator();
}
final msg = _session.messages[idx];
final isUser = msg.sender == 'user';
return _buildChatBubble(msg, isUser, isDark, theme);
},
),
),
// Bottom Chat Input Bar
Container(
padding: const EdgeInsets.fromLTRB(10, 8, 10, 10),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF141728) : theme.colorScheme.surfaceContainerHigh,
border: const Border(
top: BorderSide(color: Colors.white12, width: 1),
),
),
child: Row(
children: [
IconButton(
tooltip: 'Докинуть уточняющий знак d10',
icon: const Icon(Icons.casino_rounded, color: Color(0xFFFFD54F), size: 24),
onPressed: _showAddWordModal,
),
const SizedBox(width: 4),
Expanded(
child: TextField(
controller: _chatInputController,
textInputAction: TextInputAction.send,
onSubmitted: (_) => _sendMessage(),
decoration: InputDecoration(
hintText: 'Задайте уточняющий вопрос оракулу...',
hintStyle: const TextStyle(fontSize: 13),
filled: true,
fillColor: isDark ? const Color(0xFF1F223B) : theme.colorScheme.surface,
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide.none,
),
),
),
),
const SizedBox(width: 6),
IconButton.filled(
onPressed: _isChatLoading ? null : () => _sendMessage(),
icon: const Icon(Icons.arrow_upward_rounded, size: 20),
style: IconButton.styleFrom(
backgroundColor: const Color(0xFF2A2D4A),
foregroundColor: const Color(0xFFFFD54F),
side: const BorderSide(color: Color(0xFFFFD54F), width: 1.2),
),
),
],
),
),
],
),
),
);
}
Widget _buildChatBubble(
SessionChatMessage msg, bool isUser, bool isDark, ThemeData theme) {
return Align(
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 12),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.85,
),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
gradient: isUser
? const LinearGradient(
colors: [Color(0xFF2A2D4A), Color(0xFF1E2138)],
)
: const LinearGradient(
colors: [Color(0xFF2B1F4A), Color(0xFF1A1B35)],
),
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(18),
topRight: const Radius.circular(18),
bottomLeft: Radius.circular(isUser ? 18 : 4),
bottomRight: Radius.circular(isUser ? 4 : 18),
),
border: Border.all(
color: isUser
? const Color(0xFFFFD54F).withValues(alpha: 0.3)
: const Color(0xFFBA68C8).withValues(alpha: 0.45),
width: 1.2,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header: Sender icon & name
Row(
children: [
Icon(
isUser ? Icons.person_outline_rounded : Icons.psychology_rounded,
size: 14,
color: isUser ? const Color(0xFFFFD54F) : const Color(0xFFE1BEE7),
),
const SizedBox(width: 5),
Text(
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),
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)),
],
),
),
),
],
const SizedBox(height: 6),
SelectableText(
msg.text,
style: TextStyle(
fontSize: 13.5,
height: 1.45,
color: isUser ? Colors.white : const Color(0xFFF3E5F5),
),
),
],
),
),
);
}
Widget _buildTypingIndicator() {
return Align(
alignment: Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFF241B3B),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFBA68C8).withValues(alpha: 0.3)),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 1.8,
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFE1BEE7)),
),
),
SizedBox(width: 8),
Text(
'Оракул внимает и толкует знаки...',
style: TextStyle(fontSize: 12, color: Color(0xFFE1BEE7), fontStyle: FontStyle.italic),
),
],
),
),
);
}
void _showWordDetails(SessionWord word) {
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),
),
);
}
}
/// Modal Bottom Sheet for rolling or typing an additional d10 word
class _AddWordBottomSheet extends StatefulWidget {
final ValueChanged<WordItem> onWordChosen;
const _AddWordBottomSheet({required this.onWordChosen});
@override
State<_AddWordBottomSheet> createState() => _AddWordBottomSheetState();
}
class _AddWordBottomSheetState extends State<_AddWordBottomSheet> {
final _repo = WordsRepository();
String _digits = '';
void _onRollCompleted(int finalNumber) {
final word = _repo.getWordBySessionNumber(finalNumber);
if (word != null) {
widget.onWordChosen(word);
}
}
void _onKeypadTap(String val) {
HapticFeedback.lightImpact();
if (val == 'C') {
setState(() => _digits = '');
return;
}
if (val == '⌫') {
if (_digits.isNotEmpty) {
setState(() => _digits = _digits.substring(0, _digits.length - 1));
}
return;
}
String newDigits = _digits.length >= 3 ? val : _digits + val;
setState(() => _digits = newDigits);
if (newDigits.length == 3) {
final num = int.tryParse(newDigits);
if (num != null && num <= 999) {
final word = _repo.getWordBySessionNumber(num);
if (word != null) {
widget.onWordChosen(word);
}
}
}
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF141728) : Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(28)),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.35)),
),
child: SafeArea(
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: const Color(0xFFFFD54F).withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 12),
const Row(
children: [
GradientStar(size: 20),
SizedBox(width: 8),
Text(
'Докинуть уточняющий знак (3× d10)',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900),
),
],
),
const SizedBox(height: 12),
D10DiceRollerWidget(
digits: _digits,
isRolling: false,
onRollPressed: () {},
onRollCompleted: _onRollCompleted,
),
const SizedBox(height: 12),
// Keypad for 3 digits
_buildKeypadRow(['1', '2', '3']),
const SizedBox(height: 6),
_buildKeypadRow(['4', '5', '6']),
const SizedBox(height: 6),
_buildKeypadRow(['7', '8', '9']),
const SizedBox(height: 6),
_buildKeypadRow(['C', '0', '⌫']),
],
),
),
),
);
}
Widget _buildKeypadRow(List<String> keys) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Row(
children: keys.map((key) {
final isSpecial = key == 'C' || key == '⌫';
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Material(
color: isSpecial ? const Color(0xFF2A2D4A) : (isDark ? const Color(0xFF20233A) : Colors.black12),
borderRadius: BorderRadius.circular(12),
child: InkWell(
onTap: () => _onKeypadTap(key),
borderRadius: BorderRadius.circular(12),
child: Container(
height: 42,
alignment: Alignment.center,
child: Text(
key,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isSpecial ? const Color(0xFFFFF9C4) : Colors.white,
),
),
),
),
),
),
);
}).toList(),
);
}
}