Files
999/lib/screens/history_screen.dart
T

693 lines
28 KiB
Dart

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});
@override
State<HistoryScreen> createState() => _HistoryScreenState();
}
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('Очистить историю сессий?', style: TextStyle(color: Color(0xFFFFF9C4))),
],
),
content: const Text(
'Все сохранённые сессии гаданий и AI-трактовки будут удалены. (Хроника Слов Дня в календаре сохранится).',
style: TextStyle(color: Color(0xFF8C93B8)),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Отмена'),
),
FilledButton(
onPressed: () async {
Navigator.pop(ctx);
await _repo.clearHistory();
setState(() {});
HapticFeedback.mediumImpact();
},
style: FilledButton.styleFrom(backgroundColor: Colors.red.shade800),
child: const Text('Очистить'),
),
],
),
);
}
void _copySession(OracleSession session) {
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];
buf.writeln('🔮 Знак #${w.formattedSessionNumber}: ${w.word.toUpperCase()}');
buf.writeln(' ${w.symbolicMeaning}\n');
}
if (session.aiInterpretation != null && session.aiInterpretation!.isNotEmpty) {
buf.writeln('🤖 AI Трактовка:');
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)),
),
);
}
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 sessionsList = _repo.sessions;
final dayRecords = _wordOfDayService.getAllRecords();
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: [
// 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)),
),
),
],
),
);
}
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),
),
),
color: const Color(0xFF1B1D30),
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => SessionResultScreen(
session: session,
),
),
).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),
),
],
),
)
else
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: dayRecords.length,
separatorBuilder: (_, _) => const SizedBox(height: 10),
itemBuilder: (context, 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 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),
),
),
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => WordOfTheDayScreen(initialDate: date),
),
).then((_) => setState(() {}));
},
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
isTriptych
? '⏳'
: (singleWord.emoji.isNotEmpty ? singleWord.emoji : '☀️'),
style: const TextStyle(fontSize: 22),
),
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,
),
),
Text(
'${_formatDateString(rec.dateKey)}${isTriptych ? " • Триптих" : ""}',
style: const TextStyle(
color: Color(0xFF8C93B8),
fontSize: 11,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isDone
? const Color(0xFF1B3D23)
: const Color(0xFF262A44),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isDone
? const Color(0xFF81C784)
: const Color(0xFF8C93B8).withValues(alpha: 0.3),
),
),
child: Text(
isDone
? '✓ Выполнено'
: (isTriptych
? (rec.areAllTriptychRevealed ? 'Открыто 3/3' : 'Триптих')
: 'Открыто'),
style: TextStyle(
color: isDone
? const Color(0xFF81C784)
: const Color(0xFFFFD54F),
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
],
),
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 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,
),
),
),
],
),
),
],
],
),
),
),
);
},
),
],
),
);
}
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,
),
),
],
);
}
}