Files
999/lib/screens/catalog_screen.dart
T

1028 lines
38 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import '../widgets/external_symbols_browser.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/word_model.dart';
import '../services/traditions_repository.dart';
import '../services/words_repository.dart';
import '../widgets/gradient_star_badge.dart';
import '../widgets/traditions_atlas_view.dart';
import 'symbol_detail_screen.dart';
class CatalogScreen extends StatefulWidget {
final String? initialAtlasMode;
final String? initialTraditionId;
const CatalogScreen({
super.key,
this.initialAtlasMode,
this.initialTraditionId,
});
@override
State<CatalogScreen> createState() => _CatalogScreenState();
}
class _CatalogScreenState extends State<CatalogScreen> {
final _repo = WordsRepository();
final _traditions = TraditionsRepository();
final _searchController = TextEditingController();
String _selectedFilter = 'Все';
String _sortBy = 'id'; // 'id', 'alpha', 'rank', 'layers', 'system'
String _atlasMode = 'words'; // 'words' | 'canon'
String? _selectedTraditionId;
String? _focusStationId;
List<WordItem> _filteredWords = [];
final List<String> _filters = [
'Все',
'🌐 Другие символы',
'✨ Все системы',
'ᚱ Руны',
'᚛ Огам',
'✡️ Каббала',
'⚗️ Алхимия',
'🎴 Таро',
'🪶 Ленорман',
'☯️ И Цзин',
'⌛ Геомантия',
'♈ Гороскоп',
'🐉 Китайский зодиак',
'🌌 Космология',
'📅 Дни недели',
'🗓️ Месяцы года',
'🔢 Нумерология',
'Мои правки',
'Со слоями',
'Архетипы',
'Действия',
];
int _getSystemOrder(String desc) {
if (desc.contains('[Руны:')) return 1;
if (desc.contains('[Огам:')) return 2;
if (desc.contains('[Каббала:')) return 3;
if (desc.contains('[Алхимия:')) return 4;
if (desc.contains('[Таро:')) return 5;
if (desc.contains('[Ленорман:')) return 6;
if (desc.contains('[И Цзин:')) return 7;
if (desc.contains('[Геомантия:')) return 8;
if (desc.contains('[Додекада Созвездий:')) return 9;
if (desc.contains('[Китайский гороскоп:')) return 10;
if (desc.contains('[Космология:')) return 11;
if (desc.contains('[Септенер:')) return 12;
if (desc.contains('[Додекада:')) return 13;
if (desc.contains('[Нумерология:') || desc.contains('[Матрица:')) return 14;
if (desc.contains('[')) return 15;
return 999;
}
@override
void initState() {
super.initState();
if (widget.initialAtlasMode != null) {
_atlasMode = widget.initialAtlasMode!;
}
if (widget.initialTraditionId != null) {
_selectedTraditionId = widget.initialTraditionId;
}
_applyFilter();
_searchController.addListener(_applyFilter);
_traditions.init().then((_) {
if (mounted) setState(() {});
});
}
@override
void dispose() {
_searchController.removeListener(_applyFilter);
_searchController.dispose();
super.dispose();
}
void _applyFilter() {
final query = _searchController.text.trim();
List<WordItem> list;
if (query.isEmpty) {
list = _repo.allWords;
} else {
list = _repo.search(query);
}
// Apply category / tag filter
if (_selectedFilter != 'Все') {
switch (_selectedFilter) {
case '✨ Все системы':
list = list.where((w) => _getSystemOrder(w.description) < 999).toList();
break;
case 'ᚱ Руны':
list = list.where((w) => w.description.contains('[Руны:')).toList();
break;
case '᚛ Огам':
list = list.where((w) => w.description.contains('[Огам:')).toList();
break;
case '✡️ Каббала':
list = list.where((w) => w.description.contains('[Каббала:')).toList();
break;
case '⚗️ Алхимия':
list = list.where((w) => w.description.contains('[Алхимия:')).toList();
break;
case '🎴 Таро':
list = list.where((w) => w.description.contains('[Таро:')).toList();
break;
case '🪶 Ленорман':
list = list.where((w) => w.description.contains('[Ленорман:')).toList();
break;
case '☯️ И Цзин':
list = list.where((w) => w.description.contains('[И Цзин:')).toList();
break;
case '⌛ Геомантия':
list = list.where((w) => w.description.contains('[Геомантия:')).toList();
break;
case '♈ Гороскоп':
list = list.where((w) => w.description.contains('[Додекада Созвездий:')).toList();
break;
case '🐉 Китайский зодиак':
list = list.where((w) => w.description.contains('[Китайский гороскоп:')).toList();
break;
case '🌌 Космология':
list = list.where((w) => w.description.contains('[Космология:')).toList();
break;
case '📅 Дни недели':
list = list.where((w) => w.description.contains('[Септенер:')).toList();
break;
case '🗓️ Месяцы года':
list = list.where((w) => w.description.contains('[Додекада:')).toList();
break;
case '🔢 Нумерология':
list = list.where((w) => w.description.contains('[Нумерология:') || w.description.contains('[Матрица:')).toList();
break;
case 'Мои правки':
list = list.where((w) => w.isUserEdited).toList();
break;
case 'Со слоями':
list = list.where((w) => w.layers.isNotEmpty).toList();
break;
case 'Архетипы':
list = list
.where((w) =>
w.category.toLowerCase().contains('архетип') ||
w.family.toLowerCase().contains('totem') ||
w.operator == 'substance')
.toList();
break;
case 'Действия':
list = list
.where((w) =>
w.operator == 'shifter' ||
w.category.toLowerCase().contains('действие'))
.toList();
break;
}
}
// Apply Sorting
switch (_sortBy) {
case 'system':
list = List.of(list)..sort((a, b) {
final sysA = _getSystemOrder(a.description);
final sysB = _getSystemOrder(b.description);
if (sysA != sysB) return sysA.compareTo(sysB);
return a.word.compareTo(b.word);
});
break;
case 'alpha':
list = List.of(list)..sort((a, b) => a.word.compareTo(b.word));
break;
case 'rank':
list = List.of(list)..sort((a, b) {
final r = a.lookup.compareTo(b.lookup);
if (r != 0) return r;
return a.sessionNumber.compareTo(b.sessionNumber);
});
break;
case 'layers':
list = List.of(list)..sort((a, b) {
final diff = b.layers.length.compareTo(a.layers.length);
if (diff != 0) return diff;
return a.sessionNumber.compareTo(b.sessionNumber);
});
break;
case 'id':
default:
list = List.of(list)..sort((a, b) => a.sessionNumber.compareTo(b.sessionNumber));
break;
}
setState(() {
_filteredWords = list;
});
}
Future<void> _openDetail(WordItem word) async {
HapticFeedback.selectionClick();
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SymbolDetailScreen(word: word),
),
);
if (mounted) {
setState(() {
_applyFilter();
});
}
}
static const _tagToCanonId = {
'Руны': 'runes',
'Огам': 'ogham',
'Каббала': 'kabbalah',
'Алхимия': 'alchemy',
'Таро': 'tarot',
'Ленорман': 'lenormand',
'И Цзин': 'iching',
'Геомантия': 'geomancy',
'Додекада Созвездий': 'western_zodiac',
'Китайский гороскоп': 'zodiac',
'Космология': 'cosmology',
'Септенер': 'days_of_week',
'Дни недели': 'days_of_week',
'Планетарный Септенер': 'days_of_week',
'Додекада': 'months_of_year',
'Месяцы года': 'months_of_year',
'Зодиак': 'western_zodiac',
'Нумерология': 'numerology',
'Матрица': 'numerology',
};
void _openTaggedStation(String sys, String detail) {
final canonId = _tagToCanonId[sys];
if (canonId == null) return;
final trad = _traditions.byId(canonId);
if (trad == null) return;
final hay = detail.toLowerCase();
var bestScore = 0;
String? bestId;
for (final st in trad.stations) {
var score = 0;
final name = st.name.toLowerCase();
if (name.isNotEmpty && (hay.contains(name) || name.contains(hay))) {
score += 12;
}
for (final tok in name.split(RegExp(r'[^0-9a-zа-яё]+'))) {
if (tok.length > 2 && hay.contains(tok)) score += 2;
}
final sid = st.id.replaceAll('_', ' ').toLowerCase();
if (sid.isNotEmpty && hay.contains(sid)) score += 4;
final prim = st.primary?.word.toLowerCase();
if (prim != null && prim.isNotEmpty && hay.contains(prim)) score += 3;
if (score > bestScore) {
bestScore = score;
bestId = st.id;
}
}
final filter = TraditionsRepository.filterToCanonId.entries
.where((e) => e.value == canonId);
_searchController.removeListener(_applyFilter);
_searchController.clear();
_searchController.addListener(_applyFilter);
setState(() {
_atlasMode = 'canon';
_selectedTraditionId = canonId;
_focusStationId = bestScore >= 2 ? bestId : trad.stations.first.id;
if (filter.isNotEmpty) _selectedFilter = filter.first.key;
});
}
Future<void> _openWordById(int id, [String name = '']) async {
final named = name.trim().isEmpty ? null : _repo.getWordByName(name.trim());
if (named != null) {
await _openDetail(named);
return;
}
if (name.trim().isNotEmpty) return;
final word = _repo.getWordById(id) ?? _repo.getWordBySessionNumber(id);
if (word == null) return;
await _openDetail(word);
}
void _showSyncDialog() {
final count = _repo.customEditsCount;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF1E2238),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: const Row(
children: [
Icon(Icons.sync_alt_rounded, color: Color(0xFFFFD54F)),
SizedBox(width: 8),
Text('Синхронизация правок', style: TextStyle(color: Color(0xFFFFF9C4), fontSize: 18, fontWeight: FontWeight.bold)),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ваших правок на телефоне: $count',
style: const TextStyle(color: Color(0xFFE8EAF6), fontWeight: FontWeight.bold, fontSize: 14),
),
const SizedBox(height: 8),
const Text(
'Вы можете выгрузить все свои исправления в виде JSON и перенести на ПК в релиз, либо загрузить ранее сохраненные правки.',
style: TextStyle(color: Color(0xFFB0BEC5), fontSize: 13, height: 1.4),
),
const SizedBox(height: 16),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2A2D4A),
foregroundColor: const Color(0xFFFFD54F),
minimumSize: const Size.fromHeight(42),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
icon: const Icon(Icons.copy_rounded, size: 18),
label: const Text('Скопировать JSON правок'),
onPressed: () {
final jsonStr = _repo.exportCustomEditsJson();
Clipboard.setData(ClipboardData(text: jsonStr));
HapticFeedback.mediumImpact();
Navigator.pop(ctx);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('JSON с правками ($count шт.) скопирован в буфер! Передайте его на ПК.'),
backgroundColor: const Color(0xFF2E7D32),
behavior: SnackBarBehavior.floating,
),
);
},
),
const SizedBox(height: 8),
OutlinedButton.icon(
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF81C784),
side: const BorderSide(color: Color(0xFF81C784)),
minimumSize: const Size.fromHeight(42),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
icon: const Icon(Icons.file_download_outlined, size: 18),
label: const Text('Импортировать JSON правок'),
onPressed: () {
Navigator.pop(ctx);
_showImportDialog();
},
),
if (count > 0) ...[
const SizedBox(height: 8),
TextButton.icon(
style: TextButton.styleFrom(
foregroundColor: const Color(0xFFE57373),
minimumSize: const Size.fromHeight(36),
),
icon: const Icon(Icons.delete_outline_rounded, size: 16),
label: const Text('Сбросить все мои правки'),
onPressed: () async {
Navigator.pop(ctx);
_confirmClearAll();
},
),
],
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Закрыть', style: TextStyle(color: Color(0xFF9FA8DA))),
),
],
),
);
}
void _showImportDialog() {
final controller = TextEditingController();
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF1E2238),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
title: const Text('Импорт JSON правок', style: TextStyle(color: Color(0xFFFFF9C4), fontSize: 17, fontWeight: FontWeight.bold)),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Вставьте текст экспортированного JSON:', style: TextStyle(color: Color(0xFFB0BEC5), fontSize: 13)),
const SizedBox(height: 8),
TextField(
controller: controller,
maxLines: 6,
style: const TextStyle(color: Colors.white, fontSize: 12, fontFamily: 'monospace'),
decoration: InputDecoration(
hintText: '{\n "знак": { "core": "...", ... }\n}',
hintStyle: const TextStyle(color: Color(0xFF546E7A)),
filled: true,
fillColor: const Color(0xFF151829),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Отмена', style: TextStyle(color: Color(0xFF9FA8DA))),
),
FilledButton(
style: FilledButton.styleFrom(backgroundColor: const Color(0xFF2E7D32)),
onPressed: () async {
final text = controller.text.trim();
if (text.isEmpty) return;
final messenger = ScaffoldMessenger.of(context);
final nav = Navigator.of(ctx);
final count = await _repo.importCustomEditsJson(text);
if (!mounted) return;
nav.pop();
setState(() => _applyFilter());
messenger.showSnackBar(
SnackBar(
content: Text('Успешно импортировано правок: $count'),
backgroundColor: const Color(0xFF2E7D32),
behavior: SnackBarBehavior.floating,
),
);
},
child: const Text('Применить'),
),
],
),
);
}
void _confirmClearAll() {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF1E2238),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: const Text('Удалить ВСЕ правки?', style: TextStyle(color: Color(0xFFE57373), fontWeight: FontWeight.bold)),
content: const Text(
'Вы действительно хотите сбросить все сделанные вами редакции знаков? База вернется к состоянию из релиза.',
style: TextStyle(color: Color(0xFFE0E0E0)),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Отмена', style: TextStyle(color: Color(0xFF9FA8DA))),
),
FilledButton(
style: FilledButton.styleFrom(backgroundColor: const Color(0xFFE57373)),
onPressed: () async {
Navigator.pop(ctx);
await _repo.clearAllCustomEdits();
if (mounted) {
setState(() => _applyFilter());
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Все пользовательские правки удалены'),
behavior: SnackBarBehavior.floating,
),
);
}
},
child: const Text('Удалить всё'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final count = _filteredWords.length;
return Scaffold(
backgroundColor: const Color(0xFF0C0E1A),
appBar: AppBar(
title: const Row(
children: [
GradientStar(size: 22),
SizedBox(width: 8),
Text(
'Справочник 999',
style: TextStyle(fontWeight: FontWeight.w900),
),
],
),
actions: [
IconButton(
tooltip: 'Синхронизация и экспорт правок',
icon: Badge(
isLabelVisible: _repo.customEditsCount > 0,
label: Text('${_repo.customEditsCount}'),
backgroundColor: const Color(0xFF81C784),
child: const Icon(Icons.sync_alt_rounded, color: Color(0xFFFFD54F)),
),
onPressed: _showSyncDialog,
),
if (_atlasMode == 'words')
PopupMenuButton<String>(
tooltip: 'Сортировка',
icon: const Icon(Icons.sort_rounded, color: Color(0xFFFFD54F)),
initialValue: _sortBy,
onSelected: (val) {
setState(() {
_sortBy = val;
_applyFilter();
});
},
itemBuilder: (context) => const [
PopupMenuItem(
value: 'system',
child: Row(
children: [
Icon(Icons.auto_awesome_rounded, size: 18, color: Color(0xFFFFD54F)),
SizedBox(width: 8),
Text('По сакральным системам'),
],
),
),
PopupMenuItem(
value: 'id',
child: Row(
children: [
Icon(Icons.format_list_numbered, size: 18),
SizedBox(width: 8),
Text('По номеру (#000..#999)'),
],
),
),
PopupMenuItem(
value: 'alpha',
child: Row(
children: [
Icon(Icons.sort_by_alpha, size: 18),
SizedBox(width: 8),
Text('По алфавиту (А..Я)'),
],
),
),
PopupMenuItem(
value: 'layers',
child: Row(
children: [
Icon(Icons.layers_rounded, size: 18),
SizedBox(width: 8),
Text('Сначала со слоями'),
],
),
),
PopupMenuItem(
value: 'rank',
child: Row(
children: [
Icon(Icons.military_tech_rounded, size: 18),
SizedBox(width: 8),
Text('По рангу (A → B → C)'),
],
),
),
],
),
],
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: _atlasMode == 'canon'
? 'Поиск по станции, лору или слову...'
: 'Поиск по слову, номеру #000, закону...',
hintStyle: const TextStyle(color: Color(0xFF8C93B8), fontSize: 14),
prefixIcon: const Icon(Icons.search_rounded, color: Color(0xFFFFD54F)),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear_rounded, size: 18),
onPressed: () {
_searchController.clear();
},
)
: null,
filled: true,
fillColor: const Color(0xFF151829),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFF2A2D4A)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFF2A2D4A)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFFFD54F), width: 1.5),
),
),
),
),
// Filters row
SizedBox(
height: 38,
child: SingleChildScrollView(
key: const Key('catalog-filters'),
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
for (final filter in _filters) ...[
if (filter != _filters.first) const SizedBox(width: 8),
ChoiceChip(
label: Text(filter),
labelStyle: TextStyle(
color: filter == _selectedFilter ? const Color(0xFF1B1D30) : const Color(0xFFD1D5DB),
fontSize: 12,
fontWeight: filter == _selectedFilter ? FontWeight.bold : FontWeight.normal,
),
selected: filter == _selectedFilter,
selectedColor: const Color(0xFFFFD54F),
backgroundColor: const Color(0xFF151829),
side: BorderSide(
color: filter == _selectedFilter ? const Color(0xFFFFD54F) : const Color(0xFF2A2D4A),
),
onSelected: (selected) {
if (selected) {
setState(() {
_selectedFilter = filter;
final canonId = TraditionsRepository.filterToCanonId[filter];
if (canonId != null) {
_atlasMode = 'canon';
_selectedTraditionId = canonId;
} else {
_atlasMode = 'words';
_selectedTraditionId = null;
}
_applyFilter();
});
}
},
),
],
],
),
),
),
if (_atlasMode == 'words' && _selectedFilter != '🌐 Другие символы')
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Row(
children: [
Flexible(
child: Text(
'Найдено знаков: $count',
style: const TextStyle(
color: Color(0xFF8C93B8),
fontSize: 12,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
const Flexible(
child: Text(
'Нажмите для полного досье',
style: TextStyle(
color: Color(0xFF5C6280),
fontSize: 11,
fontStyle: FontStyle.italic,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
Expanded(
child: _selectedFilter == '🌐 Другие символы'
? ExternalSymbolsBrowser(searchQuery: _searchController.text.trim())
: _atlasMode == 'canon' &&
_selectedTraditionId != null &&
_traditions.byId(_selectedTraditionId!) != null
? TraditionsAtlasView(
traditions: _traditions.traditions,
selected: _traditions.byId(_selectedTraditionId!),
searchQuery: _searchController.text.trim(),
focusStationId: _focusStationId,
onSelectTradition: (t) {
setState(() {
_selectedTraditionId = t.id;
_focusStationId = null;
final match = TraditionsRepository.filterToCanonId.entries
.where((e) => e.value == t.id);
if (match.isNotEmpty) {
_selectedFilter = match.first.key;
}
});
},
onBackToOverview: () {
setState(() {
_selectedTraditionId = null;
_selectedFilter = 'Все';
_atlasMode = 'words';
});
},
onOpenWord: _openWordById,
)
: _filteredWords.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GradientStar(size: 40),
SizedBox(height: 16),
Text(
'Символ не найден',
style: TextStyle(
color: Color(0xFF8C93B8),
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
)
: ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
itemCount: _filteredWords.length,
separatorBuilder: (context, index) => const SizedBox(height: 10),
itemBuilder: (context, index) {
final item = _filteredWords[index];
return _buildSymbolCard(item);
},
),
),
],
),
);
}
List<Widget> _buildSystemBadges(String desc) {
final List<Widget> badges = [];
final reg = RegExp(r'\[([А-Яа-яЁёA-Za-z\s]+):([^\]]+)\]');
for (final m in reg.allMatches(desc)) {
final sys = m.group(1)?.trim() ?? '';
final detail = m.group(2)?.trim() ?? '';
Color color;
String icon;
switch (sys) {
case 'Руны':
color = const Color(0xFF90CAF9);
icon = 'ᚱ';
break;
case 'Огам':
color = const Color(0xFFA5D6A7);
icon = '᚛';
break;
case 'Каббала':
color = const Color(0xFFCE93D8);
icon = '✡️';
break;
case 'Алхимия':
color = const Color(0xFFFFCC80);
icon = '⚗️';
break;
case 'Таро':
color = const Color(0xFFEF9A9A);
icon = '🎴';
break;
case 'Ленорман':
color = const Color(0xFF80CBC4);
icon = '🪶';
break;
case 'И Цзин':
color = const Color(0xFFECEFF1);
icon = '☯️';
break;
case 'Геомантия':
color = const Color(0xFFFFAB91);
icon = '⌛';
break;
case 'Додекада Созвездий':
color = const Color(0xFF80DEEA);
icon = '♈';
break;
case 'Китайский гороскоп':
color = const Color(0xFFFFE082);
icon = '🐉';
break;
case 'Космология':
color = const Color(0xFFB39DDB);
icon = '🌌';
break;
default:
color = const Color(0xFFFFD54F);
icon = '✨';
break;
}
badges.add(
GestureDetector(
onTap: () => _openTaggedStation(sys, detail),
child: Container(
margin: const EdgeInsets.only(right: 6, bottom: 4),
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: color.withValues(alpha: 0.35)),
),
child: Text(
'$icon ${WordItemLocalization.localizeSystem(sys)}: $detail',
style: TextStyle(
color: color,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
),
),
);
}
return badges;
}
Widget _buildSymbolCard(WordItem item) {
final previewText = item.displayMeaning;
final systemBadges = _buildSystemBadges(item.description);
return InkWell(
onTap: () => _openDetail(item),
borderRadius: BorderRadius.circular(18),
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF151829),
borderRadius: BorderRadius.circular(18),
border: Border.all(
color: item.layers.isNotEmpty
? const Color(0xFFFFD54F).withValues(alpha: 0.35)
: const Color(0xFF2A2D4A),
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top Row: Number, Word, Pills
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF2A2D4A),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.4)),
),
child: Text(
'#${item.formattedSessionNumber}',
style: const TextStyle(
color: Color(0xFFFFD54F),
fontWeight: FontWeight.w900,
fontSize: 13,
),
),
),
const SizedBox(width: 10),
Expanded(
child: Text(
item.word.toUpperCase(),
style: const TextStyle(
color: Color(0xFFFFF9C4),
fontWeight: FontWeight.w900,
fontSize: 17,
letterSpacing: 0.8,
),
),
),
if (item.isUserEdited)
Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
margin: const EdgeInsets.only(right: 6),
decoration: BoxDecoration(
color: const Color(0xFF2E7D32).withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFF81C784)),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.edit_note_rounded, color: Color(0xFFA5D6A7), size: 12),
SizedBox(width: 3),
Text('Правка', style: TextStyle(color: Color(0xFFA5D6A7), fontWeight: FontWeight.bold, fontSize: 11)),
],
),
),
if (item.layers.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
margin: const EdgeInsets.only(right: 6),
decoration: BoxDecoration(
color: const Color(0xFF1E2238),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.5)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.layers_rounded, color: Color(0xFFFFD54F), size: 12),
const SizedBox(width: 4),
Text(
'${item.layers.length}',
style: const TextStyle(
color: Color(0xFFFFD54F),
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
],
),
),
const Icon(Icons.chevron_right_rounded, color: Color(0xFF8C93B8), size: 20),
],
),
const SizedBox(height: 8),
// Law / Core snippet
Text(
previewText,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Color(0xFFC5CAE9),
fontSize: 13,
height: 1.4,
),
),
const SizedBox(height: 10),
// System Badges
if (systemBadges.isNotEmpty) ...[
const SizedBox(height: 8),
Wrap(
children: systemBadges,
),
],
// Category & POS Badges
const SizedBox(height: 8),
Wrap(
spacing: 6,
children: [
if (item.category.isNotEmpty)
_miniBadge(item.category, const Color(0xFFCE93D8)),
_miniBadge(item.partOfSpeech, const Color(0xFF80D8FF)),
],
),
],
),
),
);
}
Widget _miniBadge(String text, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: color.withValues(alpha: 0.3)),
),
child: Text(
text,
style: TextStyle(
color: color,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
);
}
}