feat: initial release of Oracle 999 divination app

This commit is contained in:
Alma Alexyz
2026-08-31 12:35:13 +05:00
commit 28b85061b9
143 changed files with 16073 additions and 0 deletions
+313
View File
@@ -0,0 +1,313 @@
import 'package:flutter/material.dart';
import '../models/word_model.dart';
import '../services/words_repository.dart';
import '../widgets/gradient_star_badge.dart';
import '../widgets/word_card_widget.dart';
class CatalogScreen extends StatefulWidget {
const CatalogScreen({super.key});
@override
State<CatalogScreen> createState() => _CatalogScreenState();
}
class _CatalogScreenState extends State<CatalogScreen> {
final _repo = WordsRepository();
final _searchController = TextEditingController();
String _selectedCategory = 'Все';
String _sortBy = 'id'; // 'id', 'alpha'
List<WordItem> _filteredWords = [];
@override
void initState() {
super.initState();
_applyFilter();
_searchController.addListener(_applyFilter);
}
@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);
}
if (_selectedCategory != 'Все') {
list = list
.where((w) => w.partOfSpeech == _selectedCategory || w.category == _selectedCategory)
.toList();
}
if (_sortBy == 'alpha') {
list = List.of(list)..sort((a, b) => a.word.compareTo(b.word));
} else {
// By session number
list = List.of(list)..sort((a, b) => a.sessionNumber.compareTo(b.sessionNumber));
}
setState(() {
_filteredWords = list;
});
}
void _showWordDetails(WordItem word) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => StatefulBuilder(
builder: (context, setSheetState) {
return Container(
padding: const EdgeInsets.only(top: 12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(28)),
),
child: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outlineVariant,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 12),
WordCardWidget(
word: word,
onNext: () {
final next = _repo.getNextWord(word.sessionNumber);
Navigator.pop(context);
_showWordDetails(next);
},
onPrevious: () {
final prev = _repo.getPreviousWord(word.sessionNumber);
Navigator.pop(context);
_showWordDetails(prev);
},
onRandom: () {
final rnd = _repo.getRandomWord();
Navigator.pop(context);
_showWordDetails(rnd);
},
),
],
),
),
),
);
},
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final categories = _repo.getPartsOfSpeech();
return Scaffold(
appBar: AppBar(
title: const Row(
children: [
GradientStar(size: 22),
SizedBox(width: 8),
Text(
'Каталог 999 символов',
style: TextStyle(fontWeight: FontWeight.w900),
),
],
),
actions: [
PopupMenuButton<String>(
tooltip: 'Сортировка',
icon: const Icon(Icons.sort_rounded),
initialValue: _sortBy,
onSelected: (val) {
setState(() {
_sortBy = val;
_applyFilter();
});
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'id',
child: Row(
children: [
Icon(Icons.format_list_numbered, size: 20),
SizedBox(width: 8),
Text('По номеру сессии (#000..#999)'),
],
),
),
const PopupMenuItem(
value: 'alpha',
child: Row(
children: [
Icon(Icons.sort_by_alpha, size: 20),
SizedBox(width: 8),
Text('По алфавиту (А..Я)'),
],
),
),
],
),
],
),
body: Column(
children: [
// Search input
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Поиск по символу, номеру или смыслу...',
prefixIcon: const Padding(
padding: EdgeInsets.symmetric(horizontal: 10),
child: GradientStar(size: 16),
),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () => _searchController.clear(),
)
: null,
filled: true,
fillColor: isDark
? const Color(0xFF1B1D30)
: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.4),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: const Color(0xFFFFD54F).withValues(alpha: 0.3),
),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
),
),
// Categories Filter Chips
SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Row(
children: categories.map((cat) {
final isSelected = _selectedCategory == cat;
return Padding(
padding: const EdgeInsets.only(right: 6),
child: FilterChip(
label: Text(cat),
selected: isSelected,
onSelected: (selected) {
setState(() {
_selectedCategory = cat;
_applyFilter();
});
},
selectedColor: const Color(0xFF2A2D4A),
checkmarkColor: const Color(0xFFFFD54F),
labelStyle: TextStyle(
color: isSelected ? const Color(0xFFFFF9C4) : null,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
);
}).toList(),
),
),
// Stats Bar
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Найдено: ${_filteredWords.length}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: theme.colorScheme.outline,
),
),
Text(
'Всего знаков: ${_repo.totalWords}',
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.outline,
),
),
],
),
),
const Divider(height: 1),
// Words List
Expanded(
child: _filteredWords.isEmpty
? const Center(
child: Text('Ничего не найдено'),
)
: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
itemCount: _filteredWords.length,
separatorBuilder: (_, _) => const Divider(height: 1, indent: 64),
itemBuilder: (context, index) {
final item = _filteredWords[index];
return ListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
leading: GradientStarBadge(
text: '#${item.formattedSessionNumber}',
size: 14,
),
title: Text(
item.word.toUpperCase(),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
letterSpacing: 0.5,
),
),
subtitle: Text(
item.description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.onSurfaceVariant,
),
),
trailing: const Icon(Icons.chevron_right_rounded, size: 20),
onTap: () => _showWordDetails(item),
);
},
),
),
],
),
);
}
}