import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../models/word_model.dart'; import '../models/word_passport_model.dart'; import '../services/passport_schema_dart.dart'; import '../services/words_repository.dart'; import '../widgets/gradient_star_badge.dart'; /// Ручной обогатитель единого паспорта знака «Оракула 999» /// Полноценная система окошек и предпромптов по всем 6 разделам канона (78 полей). class ManualDiveScreen extends StatefulWidget { final WordItem? initialWord; const ManualDiveScreen({super.key, this.initialWord}); @override State createState() => _ManualDiveScreenState(); } class _ManualDiveScreenState extends State with SingleTickerProviderStateMixin { final _repo = WordsRepository(); final _searchController = TextEditingController(); late final TabController _tabController; // 78 контроллеров по ключам полей final Map _controllers = {}; // Контроллер для умной вставки сырого текста final _pasteRawController = TextEditingController(); WordItem? _selectedWord; bool _isSaving = false; final List _tabTitles = const [ 'I. Метаданные (4)', 'II. Мантика (17)', 'III. Языки (17)', 'IV. Матрица (3)', 'V. Культуры (33)', 'VI. Синтез (4)', '📄 Паспорт', ]; @override void initState() { super.initState(); _tabController = TabController(length: _tabTitles.length, vsync: this); // Инициализируем контроллеры для всех 78 полей for (final field in kPassportFields78) { _controllers[field.key] = TextEditingController(); _controllers[field.key]!.addListener(_onFieldChanged); } // Если передан начальный знак, выбираем его if (widget.initialWord != null) { _selectWord(widget.initialWord!); } else if (_repo.allWords.isNotEmpty) { _selectWord(_repo.allWords.first); } } void _onFieldChanged() { if (mounted) setState(() {}); } @override void dispose() { _tabController.dispose(); _searchController.dispose(); _pasteRawController.dispose(); for (final c in _controllers.values) { c.removeListener(_onFieldChanged); c.dispose(); } super.dispose(); } void _selectWord(WordItem word) { final fresh = _repo.getWordByName(word.word) ?? word; final passport = _repo.getPassport(fresh.id) ?? WordPassport( id: fresh.id, word: fresh.word, pos: fresh.partOfSpeech, category: fresh.category, emoji: fresh.emoji, ); setState(() { _selectedWord = fresh; _searchController.text = fresh.word; // Заполняем контроллеры из паспорта for (final field in kPassportFields78) { final val = passport.getField(field.key); _controllers[field.key]!.text = val; } }); } int get _filledCount { var count = 0; for (final c in _controllers.values) { if (c.text.trim().isNotEmpty) count++; } return count; } int _countFilledInSection(String secNum) { var count = 0; for (final field in kPassportFields78) { if (field.sectionNum == secNum) { if (_controllers[field.key]!.text.trim().isNotEmpty) { count++; } } } return count; } int _totalInSection(String secNum) { return kPassportFields78.where((f) => f.sectionNum == secNum).length; } Future _savePassport() async { final word = _selectedWord; if (word == null) return; setState(() => _isSaving = true); final fieldsMap = {}; for (final entry in _controllers.entries) { fieldsMap[entry.key] = entry.value.text.trim(); } final existingPassport = _repo.getPassport(word.id); final updated = WordPassport( id: word.id, word: word.word, pos: word.partOfSpeech, category: word.category, emoji: word.emoji, fields: fieldsMap, filledCount: _filledCount, isComplete: _filledCount >= 78, updatedAt: DateTime.now().toUtc().toIso8601String(), illustrations: existingPassport?.illustrations ?? const [], ); await _repo.savePassport(updated); if (!mounted) return; setState(() { _isSaving = false; }); HapticFeedback.mediumImpact(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: const Color(0xFF2E7D32), content: Text('Паспорт знака «${word.word.toUpperCase()}» сохранен! (Заполнено $_filledCount/78)'), ), ); } void _copyToClipboard(String text, String label) { Clipboard.setData(ClipboardData(text: text)); HapticFeedback.selectionClick(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( duration: const Duration(seconds: 2), content: Text('✓ $label скопирован в буфер обмена'), ), ); } Future _pasteFromClipboard(TextEditingController controller) async { final data = await Clipboard.getData(Clipboard.kTextPlain); if (data?.text != null && data!.text!.isNotEmpty) { controller.text = data.text!.trim(); HapticFeedback.selectionClick(); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( duration: Duration(milliseconds: 1500), content: Text('✓ Вставлено из буфера'), ), ); } } void _applyParsedFields(Map parsed) { if (parsed.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Не удалось распознать поля шаблона в тексте.')), ); return; } var applied = 0; setState(() { for (final entry in parsed.entries) { if (_controllers.containsKey(entry.key) && entry.value.isNotEmpty) { _controllers[entry.key]!.text = entry.value; applied++; } } }); HapticFeedback.heavyImpact(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: const Color(0xFF1565C0), content: Text('Успешно распознано и заполнено $applied полей! Проверьте и сохраните.'), ), ); } void _showSmartParserDialog() { _pasteRawController.clear(); showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: const Color(0xFF1E2138), title: const Row( children: [ Icon(Icons.auto_awesome, color: Color(0xFFFFD54F)), SizedBox(width: 8), Text('Умный разбор Markdown'), ], ), content: SizedBox( width: 500, child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text( 'Вставьте сюда любой скопированный ответ нейросети (карточку целиком или отдельный раздел). Система автоматически распознает строки и заполнит соответствующие окошки.', style: TextStyle(color: Color(0xFFB7B9C8), fontSize: 13), ), const SizedBox(height: 12), TextField( controller: _pasteRawController, minLines: 6, maxLines: 12, decoration: InputDecoration( hintText: 'Вставьте ответ модели сюда...', filled: true, fillColor: const Color(0xFF14172B), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Отмена'), ), FilledButton.icon( style: FilledButton.styleFrom(backgroundColor: const Color(0xFFFFD54F), foregroundColor: Colors.black), onPressed: () { final raw = _pasteRawController.text.trim(); Navigator.pop(ctx); if (raw.isNotEmpty) { final parsed = parsePassportMarkdownToFields(raw); _applyParsedFields(parsed); } }, icon: const Icon(Icons.flash_on_rounded), label: const Text('Разобрать и заполнить'), ), ], ), ); } void _sharePassport(WordItem word) { final buffer = StringBuffer(); buffer.writeln('🔮 ОРАКУЛ • ПАСПОРТ СИМВОЛА #${word.formattedSessionNumber}'); buffer.writeln('«${word.word.toUpperCase()}» ${word.emoji}'); if (word.category.isNotEmpty) { buffer.writeln('Категория: ${word.category}'); } buffer.writeln(''); if (word.core.isNotEmpty) { buffer.writeln('📖 Суть:'); buffer.writeln(word.core); buffer.writeln(''); } buffer.writeln('🔮 Базовое толкование:'); buffer.writeln(word.description); buffer.writeln(''); final passport = _repo.getPassport(word.id); if (passport != null) { final f = passport.fields; final sm = f['sacred_meaning']?.toString() ?? ''; if (sm.isNotEmpty) { buffer.writeln('✨ Сакральный смысл:'); buffer.writeln(sm); buffer.writeln(''); } final arch = f['archetype']?.toString() ?? ''; if (arch.isNotEmpty) { buffer.writeln('🏛 Архетип:'); buffer.writeln(arch); buffer.writeln(''); } } buffer.writeln('— Система сакрального жребия «Оракул»'); Clipboard.setData(ClipboardData(text: buffer.toString())); HapticFeedback.lightImpact(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ const Icon(Icons.share_rounded, color: Color(0xFFFFD54F), size: 18), const SizedBox(width: 10), Expanded(child: Text('Паспорт «${word.word}» скопирован для отправки!')), ], ), duration: const Duration(seconds: 2), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ); } @override Widget build(BuildContext context) { final word = _selectedWord; final query = _searchController.text.trim().toLowerCase(); final matches = query.isEmpty ? const [] : _repo.allWords .where((w) => w.word.toLowerCase().contains(query) || w.formattedSessionNumber.contains(query)) .take(8) .toList(); final filled = _filledCount; final progress = filled / 78.0; return Scaffold( backgroundColor: const Color(0xFF0F111E), appBar: AppBar( backgroundColor: const Color(0xFF161828), title: const Row( children: [ GradientStar(size: 22), SizedBox(width: 10), Text('Паспорт знака', style: TextStyle(fontWeight: FontWeight.w900)), ], ), actions: [ IconButton( tooltip: 'Поделиться паспортом символа', onPressed: word == null ? null : () => _sharePassport(word), icon: const Icon(Icons.share_rounded, color: Color(0xFFFFD54F)), ), IconButton( tooltip: 'Умная вставка Markdown', onPressed: word == null ? null : _showSmartParserDialog, icon: const Icon(Icons.auto_awesome, color: Color(0xFFFFD54F)), ), PopupMenuButton( tooltip: 'Пакеты промптов', icon: const Icon(Icons.more_vert), onSelected: (action) { if (word == null) return; if (action == 'all_prompts') { _copyToClipboard(buildAllPassportPrompts(word), 'Пакет из 78 промптов'); } else if (action == 'smart_paste') { _showSmartParserDialog(); } else if (action == 'export_md') { _copyToClipboard(_generateCurrentMarkdown(), 'Канонический Markdown карточки'); } }, itemBuilder: (_) => const [ PopupMenuItem( value: 'all_prompts', child: Row( children: [ Icon(Icons.copy_all_rounded, size: 18), SizedBox(width: 8), Text('Скопировать все 78 промптов'), ], ), ), PopupMenuItem( value: 'smart_paste', child: Row( children: [ Icon(Icons.auto_awesome, size: 18, color: Color(0xFFFFD54F)), SizedBox(width: 8), Text('Умная вставка ответа'), ], ), ), PopupMenuItem( value: 'export_md', child: Row( children: [ Icon(Icons.description_outlined, size: 18), SizedBox(width: 8), Text('Скопировать готовую карточку'), ], ), ), ], ), Padding( padding: const EdgeInsets.only(right: 8), child: FilledButton.icon( style: FilledButton.styleFrom( backgroundColor: const Color(0xFFFFD54F), foregroundColor: Colors.black, ), onPressed: word == null || _isSaving ? null : _savePassport, icon: Icon(_isSaving ? Icons.hourglass_top : Icons.save_rounded, size: 18), label: Text(_isSaving ? 'Сохранение...' : 'Записать знак'), ), ), ], bottom: word == null ? null : TabBar( controller: _tabController, isScrollable: true, labelColor: const Color(0xFFFFD54F), unselectedLabelColor: const Color(0xFF8E90A4), indicatorColor: const Color(0xFFFFD54F), indicatorWeight: 3, tabs: [ for (int i = 0; i < _tabTitles.length; i++) Tab( child: Row( children: [ Text(_tabTitles[i]), if (i < 6) ...[ const SizedBox(width: 6), _badge( '${_countFilledInSection(_sectionNumForTab(i))}/${_totalInSection(_sectionNumForTab(i))}', isComplete: _countFilledInSection(_sectionNumForTab(i)) == _totalInSection(_sectionNumForTab(i)), ), ], ], ), ), ], ), ), body: Column( children: [ // Поле поиска и быстрый выбор Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 6), child: TextField( controller: _searchController, onChanged: (_) => setState(() {}), decoration: InputDecoration( labelText: 'Выбор знака из 1000 (номер или слово)', hintText: '000 или огонь...', filled: true, fillColor: const Color(0xFF161828), prefixIcon: const Icon(Icons.search, color: Color(0xFFFFD54F)), suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear, size: 18), onPressed: () { _searchController.clear(); setState(() {}); }, ) : null, border: OutlineInputBorder(borderRadius: BorderRadius.circular(14)), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(14), borderSide: BorderSide(color: const Color(0xFFFFD54F).withValues(alpha: 0.2)), ), ), ), ), // Выпадающий список совпадений if (matches.isNotEmpty && (word == null || word.word.toLowerCase() != query)) Container( margin: const EdgeInsets.symmetric(horizontal: 16), height: 120, decoration: BoxDecoration( color: const Color(0xFF161828), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.3)), ), child: ListView.separated( itemCount: matches.length, separatorBuilder: (_, _) => const Divider(height: 1, color: Color(0xFF24273E)), itemBuilder: (context, idx) { final m = matches[idx]; return ListTile( dense: true, title: Text('#${m.formattedSessionNumber} ${m.word.toUpperCase()} ${m.emoji}', style: const TextStyle(fontWeight: FontWeight.bold)), subtitle: Text('${m.partOfSpeech} • ${m.category}', maxLines: 1), onTap: () { _selectWord(m); FocusScope.of(context).unfocus(); }, ); }, ), ), // Шапка выбранного знака и шкала заполнения if (word != null) Container( margin: const EdgeInsets.fromLTRB(16, 4, 16, 8), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: const Color(0xFF161828), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.15)), ), child: Column( children: [ Row( children: [ Text( '#${word.formattedSessionNumber} ${word.word.toUpperCase()}', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w900, color: Color(0xFFFFD54F)), ), const SizedBox(width: 8), Text(word.emoji, style: const TextStyle(fontSize: 18)), const Spacer(), Text( '$filled / 78 заполнено (${(progress * 100).toInt()}%)', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: progress >= 1.0 ? const Color(0xFF66BB6A) : const Color(0xFFE0E0E0), ), ), ], ), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: progress, minHeight: 6, backgroundColor: const Color(0xFF24273E), valueColor: AlwaysStoppedAnimation( progress >= 1.0 ? const Color(0xFF66BB6A) : (progress > 0.5 ? const Color(0xFFFFD54F) : const Color(0xFFFFA726)), ), ), ), if ((_repo.getPassport(word.id)?.illustrations ?? const []).isNotEmpty) ...[ const SizedBox(height: 8), _buildIllustrationsBar(_repo.getPassport(word.id)!.illustrations), ], ], ), ), // Контент: Вкладки с окошками Expanded( child: word == null ? const Center(child: Text('Выберите знак для обогащения')) : TabBarView( controller: _tabController, children: [ _buildSectionTab('I', 'Заголовочный паспорт (Метаданные)'), _buildSectionTab('II', 'Кросс-мантическая сеть и станции'), _buildSectionTab('III', 'Мультилингвальный и графический паспорт'), _buildSectionTab('IV', 'Трехполярная матрица состояний'), _buildSectionTab('V', 'Культурологическая матрица и Мета-синтез'), _buildSectionTab('VI', 'Мантический & Обучающий синтез'), _buildPassportPreviewTab(), ], ), ), ], ), ); } String _sectionNumForTab(int tabIndex) { switch (tabIndex) { case 0: return 'I'; case 1: return 'II'; case 2: return 'III'; case 3: return 'IV'; case 4: return 'V'; case 5: return 'VI'; default: return 'I'; } } Widget _badge(String text, {required bool isComplete}) { return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: isComplete ? const Color(0xFF2E7D32) : const Color(0xFF24273E), borderRadius: BorderRadius.circular(10), ), child: Text( text, style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, color: isComplete ? Colors.white : const Color(0xFFB7B9C8), ), ), ); } Widget _buildSectionTab(String sectionNum, String sectionTitle) { final fields = kPassportFields78.where((f) => f.sectionNum == sectionNum).toList(); final word = _selectedWord!; return ListView( padding: const EdgeInsets.fromLTRB(16, 10, 16, 28), children: [ // Панель управления разделом Container( margin: const EdgeInsets.only(bottom: 14), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFF161828), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.15)), ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Раздел $sectionNum: $sectionTitle', style: const TextStyle(fontWeight: FontWeight.w800, fontSize: 13, color: Color(0xFFFFD54F)), ), const SizedBox(height: 2), Text( '${_countFilledInSection(sectionNum)} из ${fields.length} полей заполнено', style: const TextStyle(fontSize: 12, color: Color(0xFFB7B9C8)), ), ], ), ), OutlinedButton.icon( style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFFFFD54F), side: const BorderSide(color: Color(0xFFFFD54F)), ), onPressed: () => _copyToClipboard( buildSectionPrePrompt(sectionNum, word), 'Промпты раздела $sectionNum', ), icon: const Icon(Icons.copy_rounded, size: 15), label: const Text('Скопировать раздел', style: TextStyle(fontSize: 12)), ), ], ), ), // Список окошек для каждого поля for (final field in fields) _buildFieldCard(field, word), ], ); } Widget _buildFieldCard(PassportFieldDef field, WordItem word) { final controller = _controllers[field.key]!; final isFilled = controller.text.trim().isNotEmpty; return Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFF161828), borderRadius: BorderRadius.circular(14), border: Border.all( color: isFilled ? const Color(0xFF66BB6A).withValues(alpha: 0.4) : const Color(0xFFFFD54F).withValues(alpha: 0.15), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Шапка поля Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), decoration: BoxDecoration( color: isFilled ? const Color(0xFF1B5E20) : const Color(0xFF24273E), borderRadius: BorderRadius.circular(6), ), child: Text( '#${field.index.toString().padLeft(2, '0')}', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w800, color: isFilled ? const Color(0xFF81C784) : const Color(0xFFB7B9C8), ), ), ), const SizedBox(width: 8), Expanded( child: Text( field.title, style: const TextStyle(fontWeight: FontWeight.w800, fontSize: 14, color: Color(0xFFFFF9C4)), ), ), if (isFilled) const Icon(Icons.check_circle_rounded, color: Color(0xFF66BB6A), size: 18) else const Icon(Icons.radio_button_unchecked_rounded, color: Color(0xFF757575), size: 18), ], ), const SizedBox(height: 8), // Кнопки предпромпта и вставки Wrap( spacing: 8, runSpacing: 6, children: [ OutlinedButton.icon( style: OutlinedButton.styleFrom( visualDensity: VisualDensity.compact, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), side: BorderSide(color: const Color(0xFFFFD54F).withValues(alpha: 0.5)), foregroundColor: const Color(0xFFFFD54F), ), onPressed: () => _copyToClipboard( field.buildPrompt(word), 'Предпромпт «${field.title}»', ), icon: const Icon(Icons.copy_rounded, size: 14), label: const Text('Скопировать предпромпт', style: TextStyle(fontSize: 12)), ), OutlinedButton.icon( style: OutlinedButton.styleFrom( visualDensity: VisualDensity.compact, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), foregroundColor: const Color(0xFFB7B9C8), ), onPressed: () => _pasteFromClipboard(controller), icon: const Icon(Icons.paste_rounded, size: 14), label: const Text('Вставить', style: TextStyle(fontSize: 12)), ), if (isFilled) IconButton( visualDensity: VisualDensity.compact, tooltip: 'Очистить поле', onPressed: () { controller.clear(); HapticFeedback.selectionClick(); }, icon: const Icon(Icons.delete_outline, size: 16, color: Color(0xFFEF5350)), ), ], ), const SizedBox(height: 8), // Текстовое окошко TextField( controller: controller, minLines: field.sectionNum == 'IV' || field.sectionNum == 'VI' ? 3 : 2, maxLines: 8, style: const TextStyle(fontSize: 13, height: 1.35), decoration: InputDecoration( hintText: 'Ожидаемый формат: ${field.formatTemplate}', hintStyle: const TextStyle(color: Color(0xFF555870), fontSize: 12), filled: true, fillColor: const Color(0xFF0F111E), contentPadding: const EdgeInsets.all(12), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: const Color(0xFF24273E)), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFFFFD54F)), ), ), ), ], ), ); } String _generateCurrentMarkdown() { final word = _selectedWord; if (word == null) return ''; final fieldsMap = {}; for (final entry in _controllers.entries) { fieldsMap[entry.key] = entry.value.text.trim(); } final tempPassport = WordPassport( id: word.id, word: word.word, pos: word.partOfSpeech, category: word.category, emoji: word.emoji, fields: fieldsMap, ); return tempPassport.toMarkdown(); } Widget _buildPassportPreviewTab() { final md = _generateCurrentMarkdown(); return ListView( padding: const EdgeInsets.fromLTRB(16, 12, 16, 32), children: [ Row( children: [ Expanded( child: Text( 'Каноническая карточка знака ($_filledCount/78 заполнено)', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: Color(0xFFFFD54F)), ), ), FilledButton.icon( style: FilledButton.styleFrom( backgroundColor: const Color(0xFFFFD54F), foregroundColor: Colors.black, ), onPressed: () => _copyToClipboard(md, 'Каноническая карточка знака'), icon: const Icon(Icons.copy_rounded, size: 16), label: const Text('Скопировать карточку'), ), ], ), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF161828), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.2)), ), child: SelectableText( md, style: const TextStyle( fontFamily: 'monospace', fontSize: 12.5, height: 1.45, color: Color(0xFFE2E4F0), ), ), ), ], ); } Widget _buildIllustrationsBar(List> illustrations) { return InkWell( onTap: () => _showIllustrationsDialog(illustrations), borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: const Color(0xFF24273E).withValues(alpha: 0.6), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.3)), ), child: Row( children: [ const Icon(Icons.photo_library_outlined, size: 16, color: Color(0xFFFFD54F)), const SizedBox(width: 8), Expanded( child: Text( 'Академические гравюры: ${illustrations.length} илл. («${illustrations.first['caption']}»)', style: const TextStyle(fontSize: 12, color: Color(0xFFFFD54F), fontWeight: FontWeight.w600), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), const Icon(Icons.arrow_forward_ios_rounded, size: 12, color: Color(0xFFFFD54F)), ], ), ), ); } void _showIllustrationsDialog(List> illustrations) { showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: const Color(0xFF161828), title: Row( children: [ const Icon(Icons.image_search_rounded, color: Color(0xFFFFD54F)), const SizedBox(width: 8), Text('Гравюры и схемы (${illustrations.length})', style: const TextStyle(fontSize: 18, color: Colors.white)), ], ), content: SizedBox( width: 500, child: ListView.separated( shrinkWrap: true, itemCount: illustrations.length, separatorBuilder: (_, __) => const Divider(color: Color(0xFF24273E)), itemBuilder: (context, i) { final ill = illustrations[i]; final filePath = ill['file'] as String? ?? ''; final caption = ill['caption'] as String? ?? ''; final source = ill['source'] as String? ?? ''; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.asset( filePath, fit: BoxFit.contain, errorBuilder: (_, __, ___) => Container( height: 120, color: const Color(0xFF0F111E), child: Center(child: Text('Изображение: $filePath', style: const TextStyle(fontSize: 12))), ), ), ), const SizedBox(height: 8), Text(caption, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Color(0xFFFFD54F))), const SizedBox(height: 2), Text(source, style: const TextStyle(fontSize: 11, color: Colors.grey)), ], ); }, ), ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('Закрыть', style: TextStyle(color: Color(0xFFFFD54F))), ), ], ), ); } }