417 lines
18 KiB
Dart
417 lines
18 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import '../models/external_symbol_model.dart';
|
|
import '../services/external_symbols_repository.dart';
|
|
|
|
class ExternalSymbolsBrowser extends StatefulWidget {
|
|
final String searchQuery;
|
|
|
|
const ExternalSymbolsBrowser({
|
|
super.key,
|
|
this.searchQuery = '',
|
|
});
|
|
|
|
@override
|
|
State<ExternalSymbolsBrowser> createState() => _ExternalSymbolsBrowserState();
|
|
}
|
|
|
|
class _ExternalSymbolsBrowserState extends State<ExternalSymbolsBrowser> {
|
|
final _repo = ExternalSymbolsRepository();
|
|
bool _onlyWithImages = false;
|
|
String? _selectedAuthor;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_repo.init().then((_) {
|
|
if (mounted) setState(() {});
|
|
});
|
|
}
|
|
|
|
void _showDetailModal(ExternalSymbol symbol) {
|
|
HapticFeedback.selectionClick();
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
backgroundColor: const Color(0xFF121424),
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
|
),
|
|
builder: (ctx) => DraggableScrollableSheet(
|
|
initialChildSize: 0.85,
|
|
minChildSize: 0.5,
|
|
maxChildSize: 0.95,
|
|
expand: false,
|
|
builder: (context, scrollController) => Padding(
|
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
|
|
child: ListView(
|
|
controller: scrollController,
|
|
children: [
|
|
Center(
|
|
child: Container(
|
|
width: 40,
|
|
height: 4,
|
|
margin: const EdgeInsets.only(bottom: 16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white24,
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
),
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
symbol.term.toUpperCase(),
|
|
style: const TextStyle(
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.w900,
|
|
color: Color(0xFFFFD54F),
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'Внешний пантеон • ${symbol.sources.length} источн.',
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
color: Color(0xFF8E90A4),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
IconButton(
|
|
tooltip: 'Поделиться символом',
|
|
icon: const Icon(Icons.share_rounded, color: Color(0xFFFFD54F)),
|
|
onPressed: () {
|
|
final buf = StringBuffer();
|
|
buf.writeln('📚 ОРАКУЛ • АКАДЕМИЧЕСКИЙ СИМВОЛ: «${symbol.term.toUpperCase()}»');
|
|
buf.writeln('');
|
|
for (final s in symbol.sources) {
|
|
buf.writeln('🏛 [${s.author} · ${s.work}]:');
|
|
buf.writeln(s.fullText.isNotEmpty ? s.fullText : s.excerpt);
|
|
buf.writeln('');
|
|
}
|
|
buf.writeln('— Академический корпус системы «Оракул»');
|
|
Clipboard.setData(ClipboardData(text: buf.toString()));
|
|
HapticFeedback.lightImpact();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Row(
|
|
children: [
|
|
const Icon(Icons.share_rounded, color: Color(0xFFFFD54F), size: 18),
|
|
const SizedBox(width: 8),
|
|
Expanded(child: Text('Символ «${symbol.term}» скопирован для отправки')),
|
|
],
|
|
),
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.close_rounded, color: Colors.white70),
|
|
onPressed: () => Navigator.pop(ctx),
|
|
),
|
|
],
|
|
),
|
|
const Divider(color: Color(0xFF24273E), height: 24),
|
|
|
|
// Иллюстрации (если есть)
|
|
if (symbol.hasIllustrations) ...[
|
|
const Text(
|
|
'ГРАВЮРЫ И ИЛЛЮСТРАЦИИ',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
letterSpacing: 1.2,
|
|
fontWeight: FontWeight.w800,
|
|
color: Color(0xFFFFD54F),
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
for (final ill in symbol.illustrations) ...[
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(14),
|
|
child: Container(
|
|
color: const Color(0xFF161828),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Image.asset(
|
|
ill['file'] as String? ?? '',
|
|
fit: BoxFit.contain,
|
|
errorBuilder: (_, __, ___) => Container(
|
|
height: 140,
|
|
color: const Color(0xFF161828),
|
|
child: Center(
|
|
child: Text('Илл.: ${ill['file']}', style: const TextStyle(fontSize: 12, color: Colors.white54)),
|
|
),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.all(10),
|
|
child: Text(
|
|
ill['caption'] as String? ?? '',
|
|
style: const TextStyle(fontSize: 12, color: Color(0xFFFFD54F), fontWeight: FontWeight.w600),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
],
|
|
const SizedBox(height: 12),
|
|
],
|
|
|
|
// Академические источники
|
|
const Text(
|
|
'АКАДЕМИЧЕСКИЕ СТАТЬИ И ПЕРВОИСТОЧНИКИ',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
letterSpacing: 1.2,
|
|
fontWeight: FontWeight.w800,
|
|
color: Color(0xFFFFD54F),
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
for (final src in symbol.sources) ...[
|
|
Container(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF161828),
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.15)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.menu_book_rounded, size: 16, color: Color(0xFFFFD54F)),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
'${src.author} • ${src.work}',
|
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.white),
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.copy_rounded, size: 16, color: Color(0xFFFFD54F)),
|
|
tooltip: 'Скопировать цитату',
|
|
onPressed: () {
|
|
Clipboard.setData(ClipboardData(text: '[${src.author} · ${src.work}]: «${src.fullText}»'));
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Цитата скопирована в буфер')),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
src.fullText.isNotEmpty ? src.fullText : src.excerpt,
|
|
style: const TextStyle(fontSize: 13, height: 1.5, color: Color(0xFFE0E0E0)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (!_repo.isLoaded) {
|
|
return const Center(child: CircularProgressIndicator(color: Color(0xFFFFD54F)));
|
|
}
|
|
|
|
final query = widget.searchQuery;
|
|
var list = _repo.search(query, onlyWithImages: _onlyWithImages);
|
|
|
|
if (_selectedAuthor != null) {
|
|
list = list.where((s) => s.sources.any((src) => src.author == _selectedAuthor)).toList();
|
|
}
|
|
|
|
// Собираем уникальных авторов для быстрого фильтра
|
|
final authors = <String>{};
|
|
for (final s in _repo.allSymbols) {
|
|
for (final src in s.sources) {
|
|
if (src.author.isNotEmpty) authors.add(src.author);
|
|
}
|
|
}
|
|
|
|
return Column(
|
|
children: [
|
|
// Плашка фильтров внешнего пантеона
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
color: const Color(0xFF101222),
|
|
child: Row(
|
|
children: [
|
|
FilterChip(
|
|
label: Text('🖼️ С иллюстрациями (${_repo.search("", onlyWithImages: true).length})'),
|
|
selected: _onlyWithImages,
|
|
selectedColor: const Color(0xFFFFD54F),
|
|
labelStyle: TextStyle(
|
|
color: _onlyWithImages ? Colors.black : const Color(0xFFD1D5DB),
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
backgroundColor: const Color(0xFF161828),
|
|
side: BorderSide(color: _onlyWithImages ? const Color(0xFFFFD54F) : const Color(0xFF2A2D4A)),
|
|
onSelected: (val) => setState(() => _onlyWithImages = val),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Row(
|
|
children: [
|
|
for (final author in authors) ...[
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 6),
|
|
child: ChoiceChip(
|
|
label: Text(author, style: const TextStyle(fontSize: 11)),
|
|
selected: _selectedAuthor == author,
|
|
selectedColor: const Color(0xFFFFD54F).withValues(alpha: 0.3),
|
|
labelStyle: TextStyle(
|
|
color: _selectedAuthor == author ? const Color(0xFFFFD54F) : Colors.white70,
|
|
fontWeight: _selectedAuthor == author ? FontWeight.bold : FontWeight.normal,
|
|
),
|
|
backgroundColor: const Color(0xFF161828),
|
|
onSelected: (val) {
|
|
setState(() {
|
|
_selectedAuthor = val ? author : null;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Счетчик
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
|
|
child: Row(
|
|
children: [
|
|
Text(
|
|
'Внешний реестр: ${list.length} понятий (из ${_repo.totalCount})',
|
|
style: const TextStyle(fontSize: 12, color: Color(0xFF8E90A4), fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Список символов
|
|
Expanded(
|
|
child: list.isEmpty
|
|
? const Center(
|
|
child: Text(
|
|
'Понятия не найдены',
|
|
style: TextStyle(color: Color(0xFF8E90A4)),
|
|
),
|
|
)
|
|
: ListView.separated(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
itemCount: list.length,
|
|
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
|
itemBuilder: (context, idx) {
|
|
final item = list[idx];
|
|
return InkWell(
|
|
onTap: () => _showDetailModal(item),
|
|
borderRadius: BorderRadius.circular(14),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF161828),
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(
|
|
color: item.hasIllustrations
|
|
? const Color(0xFFFFD54F).withValues(alpha: 0.3)
|
|
: const Color(0xFF2A2D4A),
|
|
),
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (item.hasIllustrations) ...[
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Image.asset(
|
|
item.illustrations.first['file'] as String? ?? '',
|
|
width: 48,
|
|
height: 48,
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (_, __, ___) => const Icon(Icons.image, size: 36, color: Color(0xFFFFD54F)),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
],
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Text(
|
|
item.term.toUpperCase(),
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.w900,
|
|
fontSize: 15,
|
|
color: Color(0xFFFFD54F),
|
|
),
|
|
),
|
|
const Spacer(),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF24273E),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Text(
|
|
item.primaryAuthor,
|
|
style: const TextStyle(fontSize: 10, color: Colors.white70),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
item.primaryExcerpt,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(fontSize: 12, color: Color(0xFFC0C4D6), height: 1.4),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|