feat: complete academic symbol lexicon heal, 2-column Cooper extraction, and updated assets
This commit is contained in:
@@ -0,0 +1,755 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../models/tradition_model.dart';
|
||||
import '../screens/lenormand_extras_screen.dart';
|
||||
import '../screens/lenormand_station_screen.dart';
|
||||
import '../services/lenormand_pairs_repository.dart';
|
||||
import '../services/ogham_feda_repository.dart';
|
||||
import '../services/tarot_majors_repository.dart';
|
||||
|
||||
typedef OpenLexiconWord = void Function(int id, String word);
|
||||
|
||||
class TraditionsAtlasView extends StatelessWidget {
|
||||
final List<TraditionCanon> traditions;
|
||||
final TraditionCanon? selected;
|
||||
final String searchQuery;
|
||||
final String? focusStationId;
|
||||
final ValueChanged<TraditionCanon> onSelectTradition;
|
||||
final VoidCallback onBackToOverview;
|
||||
final OpenLexiconWord onOpenWord;
|
||||
|
||||
const TraditionsAtlasView({
|
||||
super.key,
|
||||
required this.traditions,
|
||||
required this.selected,
|
||||
required this.searchQuery,
|
||||
this.focusStationId,
|
||||
required this.onSelectTradition,
|
||||
required this.onBackToOverview,
|
||||
required this.onOpenWord,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (selected == null) {
|
||||
return _Overview(
|
||||
traditions: traditions,
|
||||
onSelect: onSelectTradition,
|
||||
);
|
||||
}
|
||||
return _TraditionStations(
|
||||
tradition: selected!,
|
||||
traditions: traditions,
|
||||
searchQuery: searchQuery,
|
||||
focusStationId: focusStationId,
|
||||
onBack: onBackToOverview,
|
||||
onOpenWord: onOpenWord,
|
||||
onSelectTradition: onSelectTradition,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Overview extends StatelessWidget {
|
||||
final List<TraditionCanon> traditions;
|
||||
final ValueChanged<TraditionCanon> onSelect;
|
||||
|
||||
const _Overview({required this.traditions, required this.onSelect});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xCC1E2238), Color(0xE6151829)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0x4DFFD54F)),
|
||||
),
|
||||
child: const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'🏛️ Сакральные каноны',
|
||||
style: TextStyle(
|
||||
color: Color(0xFFFFF9C4),
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 6),
|
||||
Text(
|
||||
'Выберите традицию, чтобы открыть её станции, первородные архетипы и созвучия базы 999 слов.',
|
||||
style: TextStyle(color: Color(0xFFCBD5E1), fontSize: 13, height: 1.45),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
'${traditions.length} великих систем',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFFFD54F),
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...traditions.map(
|
||||
(t) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: _TraditionCard(tradition: t, onTap: () => onSelect(t)),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TraditionCard extends StatelessWidget {
|
||||
final TraditionCanon tradition;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _TraditionCard({required this.tradition, required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: const Color(0xFF151829),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
onTap();
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0x1AFFFFFF)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x1AFFD54F),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: const Color(0x4DFFD54F)),
|
||||
),
|
||||
child: Text(tradition.icon, style: const TextStyle(fontSize: 26)),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
tradition.name,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFFFF9C4),
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${tradition.count} сакральных станций',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF90CDF4),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
tradition.description,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFCBD5E1),
|
||||
fontSize: 13,
|
||||
height: 1.45,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Row(
|
||||
children: [
|
||||
Text(
|
||||
'Открыть атлас традиции',
|
||||
style: TextStyle(
|
||||
color: Color(0xFFFFD54F),
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 6),
|
||||
Icon(Icons.arrow_forward_rounded, color: Color(0xFFFFD54F), size: 16),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TraditionStations extends StatefulWidget {
|
||||
final TraditionCanon tradition;
|
||||
final List<TraditionCanon> traditions;
|
||||
final String searchQuery;
|
||||
final String? focusStationId;
|
||||
final VoidCallback onBack;
|
||||
final OpenLexiconWord onOpenWord;
|
||||
final ValueChanged<TraditionCanon> onSelectTradition;
|
||||
|
||||
const _TraditionStations({
|
||||
required this.tradition,
|
||||
required this.traditions,
|
||||
required this.searchQuery,
|
||||
required this.focusStationId,
|
||||
required this.onBack,
|
||||
required this.onOpenWord,
|
||||
required this.onSelectTradition,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_TraditionStations> createState() => _TraditionStationsState();
|
||||
}
|
||||
|
||||
class _TraditionStationsState extends State<_TraditionStations> {
|
||||
final Map<String, GlobalKey> _stationKeys = {};
|
||||
String? _highlightedId;
|
||||
|
||||
TraditionCanon get tradition => widget.tradition;
|
||||
String get searchQuery => widget.searchQuery;
|
||||
VoidCallback get onBack => widget.onBack;
|
||||
OpenLexiconWord get onOpenWord => widget.onOpenWord;
|
||||
|
||||
bool get _isLenormand => tradition.id == 'lenormand';
|
||||
bool get _isTarot => tradition.id == 'tarot';
|
||||
bool get _isOgham => tradition.id == 'ogham';
|
||||
bool get _hasDeck => _isLenormand || _isTarot || _isOgham;
|
||||
|
||||
GlobalKey _keyFor(String id) => _stationKeys.putIfAbsent(id, GlobalKey.new);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scheduleFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _TraditionStations oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.focusStationId != widget.focusStationId ||
|
||||
oldWidget.tradition.id != widget.tradition.id) {
|
||||
_scheduleFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void _scheduleFocus() {
|
||||
final id = widget.focusStationId;
|
||||
if (id == null || id.isEmpty) return;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
final ctx = _stationKeys[id]?.currentContext;
|
||||
if (ctx != null) {
|
||||
Scrollable.ensureVisible(
|
||||
ctx,
|
||||
alignment: 0.25,
|
||||
duration: const Duration(milliseconds: 320),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
setState(() => _highlightedId = id);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final stations = tradition.stations
|
||||
.where((s) => s.matchesQuery(searchQuery))
|
||||
.toList();
|
||||
|
||||
final groups = <String, List<TraditionStation>>{};
|
||||
final order = <String>[];
|
||||
for (final st in stations) {
|
||||
final key = st.groupKey.isEmpty ? tradition.name : st.groupKey;
|
||||
if (!groups.containsKey(key)) {
|
||||
groups[key] = [];
|
||||
order.add(key);
|
||||
}
|
||||
groups[key]!.add(st);
|
||||
}
|
||||
final hasGroups = order.length > 1;
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xCC1E2238), Color(0xE6151829)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0x4DFFD54F)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: 'К списку слов',
|
||||
onPressed: onBack,
|
||||
icon: const Icon(Icons.arrow_back_rounded, color: Color(0xFFFFD54F)),
|
||||
),
|
||||
Text(tradition.icon, style: const TextStyle(fontSize: 26)),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
tradition.name,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFFFF9C4),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x26FFD54F),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0x66FFD54F)),
|
||||
),
|
||||
child: Text(
|
||||
'${stations.length} из ${tradition.count}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFFFD54F),
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 4, 8, 4),
|
||||
child: Text(
|
||||
tradition.description,
|
||||
style: const TextStyle(color: Color(0xFFCBD5E1), fontSize: 13, height: 1.45),
|
||||
),
|
||||
),
|
||||
if (_isLenormand)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 8, 4),
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
HapticFeedback.selectionClick();
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const LenormandExtrasScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.layers_outlined, color: Color(0xFFFFD54F), size: 18),
|
||||
label: const Text(
|
||||
'Триады, любовь, работа, карма',
|
||||
style: TextStyle(color: Color(0xFFFFD54F), fontWeight: FontWeight.w700),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: const BorderSide(color: Color(0x66FFD54F)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (stations.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 48),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Станций не найдено',
|
||||
style: TextStyle(color: Color(0xFF8C93B8), fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
for (final gKey in order) ...[
|
||||
if (hasGroups) _GroupHeader(label: gKey),
|
||||
for (final st in groups[gKey]!)
|
||||
Padding(
|
||||
key: _keyFor(st.id),
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: _StationCard(
|
||||
station: st,
|
||||
highlighted: st.id == _highlightedId,
|
||||
onOpenWord: onOpenWord,
|
||||
deckButtonLabel: _isOgham ? 'Значение феды' : (_isTarot ? 'Значение аркана и пары' : 'Описания и сочетания'),
|
||||
onOpenLenormand: _hasDeck
|
||||
? () {
|
||||
HapticFeedback.selectionClick();
|
||||
if (_isTarot) {
|
||||
final card = TarotMajorsRepository().byCanonId(st.id);
|
||||
if (card == null) return;
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => LenormandStationScreen(
|
||||
number: card.n,
|
||||
lookup: TarotMajorsRepository().byNumber,
|
||||
title: 'Таро',
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_isOgham) {
|
||||
final card = OghamFedaRepository().byCanonId(st.id);
|
||||
if (card == null) return;
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => LenormandStationScreen(
|
||||
number: card.n,
|
||||
lookup: OghamFedaRepository().byNumber,
|
||||
title: 'Огам',
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final card = LenormandPairsRepository().byStationId(st.id);
|
||||
if (card == null) return;
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => LenormandStationScreen(number: card.n),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GroupHeader extends StatelessWidget {
|
||||
final String label;
|
||||
const _GroupHeader({required this.label});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 16, 0, 10),
|
||||
child: Row(
|
||||
children: [
|
||||
const Expanded(child: Divider(color: Color(0x4DFBBF24), height: 1)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x14FBBF24),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: const Color(0x40FBBF24)),
|
||||
),
|
||||
child: Text(
|
||||
label.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFFBBF24),
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Expanded(child: Divider(color: Color(0x4DFBBF24), height: 1)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StationCard extends StatelessWidget {
|
||||
final TraditionStation station;
|
||||
final bool highlighted;
|
||||
final OpenLexiconWord onOpenWord;
|
||||
final VoidCallback? onOpenLenormand;
|
||||
final String? deckButtonLabel;
|
||||
|
||||
const _StationCard({
|
||||
required this.station,
|
||||
required this.onOpenWord,
|
||||
this.highlighted = false,
|
||||
this.onOpenLenormand,
|
||||
this.deckButtonLabel,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sub = station.arcanum?.isNotEmpty == true
|
||||
? station.arcanum!
|
||||
: station.groupSub;
|
||||
final primary = station.primary;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF151829),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: highlighted ? const Color(0xFFFFD54F) : const Color(0x1AFFFFFF),
|
||||
width: highlighted ? 1.6 : 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x1AFFD54F),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0x4DFFD54F)),
|
||||
),
|
||||
child: station.geomancyRows.isNotEmpty
|
||||
? _GeomancyGlyph(rows: station.geomancyRows)
|
||||
: Text(
|
||||
station.symbol,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
color: Color(0xFFFFD54F),
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: station.name,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFFFF9C4),
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 15,
|
||||
height: 1.25,
|
||||
),
|
||||
children: [
|
||||
if (station.hebrew != null && station.hebrew!.isNotEmpty)
|
||||
TextSpan(
|
||||
text: ' ${station.hebrew}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xD9D4A855),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 18,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
if (station.hanzi != null && station.hanzi!.isNotEmpty)
|
||||
TextSpan(
|
||||
text: ' ${station.hanzi}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xD9D4A855),
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
if (station.arabic != null && station.arabic!.isNotEmpty)
|
||||
TextSpan(
|
||||
text: ' ${station.arabic}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xD9D4A855),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 17,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (sub.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
sub,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF90CDF4),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
station.lore,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFD1D5DB),
|
||||
fontSize: 13,
|
||||
height: 1.45,
|
||||
),
|
||||
),
|
||||
if (primary != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Material(
|
||||
color: const Color(0x14FFD54F),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: InkWell(
|
||||
onTap: () => onOpenWord(primary.id, primary.word),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0x4DFFD54F)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (primary.emoji.isNotEmpty)
|
||||
Text(primary.emoji, style: const TextStyle(fontSize: 16)),
|
||||
if (primary.emoji.isNotEmpty) const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
primary.word.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 14,
|
||||
letterSpacing: 0.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right_rounded, color: Color(0xFFFFD54F), size: 18),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (station.cluster.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
for (final cw in station.cluster)
|
||||
InkWell(
|
||||
onTap: () => onOpenWord(cw.id, cw.word),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: Tooltip(
|
||||
message: 'Открыть знак «${cw.word}»',
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x0FFFFFFF),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: const Color(0x1FFFFFFF)),
|
||||
),
|
||||
child: Text(
|
||||
'${cw.emoji} ${cw.word}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFE2E8F0),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
if (onOpenLenormand != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.tonalIcon(
|
||||
onPressed: onOpenLenormand,
|
||||
icon: const Icon(Icons.hub_outlined, size: 18),
|
||||
label: Text(deckButtonLabel ?? 'Описания и сочетания'),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: const Color(0x26FFD54F),
|
||||
foregroundColor: const Color(0xFFFFD54F),
|
||||
textStyle: const TextStyle(fontWeight: FontWeight.w800),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Four-row geomantic figure: one centered dot or a centered pair per row.
|
||||
class _GeomancyGlyph extends StatelessWidget {
|
||||
final List<int> rows;
|
||||
const _GeomancyGlyph({required this.rows});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
for (final n in rows)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 1.5),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (int i = 0; i < n; i++) ...[
|
||||
if (i > 0) const SizedBox(width: 5),
|
||||
Container(
|
||||
width: 7,
|
||||
height: 7,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFFFD54F),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user