clean: remove catalog tab, keeping 2 pure tabs (Oracle and History)
This commit is contained in:
+4
-11
@@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'screens/catalog_screen.dart';
|
|
||||||
import 'screens/history_screen.dart';
|
import 'screens/history_screen.dart';
|
||||||
import 'screens/home_search_screen.dart';
|
import 'screens/home_search_screen.dart';
|
||||||
import 'services/words_repository.dart';
|
import 'services/words_repository.dart';
|
||||||
@@ -57,19 +56,19 @@ class _Words999AppState extends State<Words999App> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
navigationBarTheme: NavigationBarThemeData(
|
navigationBarTheme: NavigationBarThemeData(
|
||||||
backgroundColor: const Color(0xFF101322),
|
backgroundColor: const Color(0xFF101222),
|
||||||
indicatorColor: const Color(0xFF2A2D4A),
|
indicatorColor: const Color(0xFF2A2D4A),
|
||||||
labelTextStyle: WidgetStateProperty.resolveWith((states) {
|
labelTextStyle: WidgetStateProperty.resolveWith((states) {
|
||||||
if (states.contains(WidgetState.selected)) {
|
if (states.contains(WidgetState.selected)) {
|
||||||
return const TextStyle(
|
return const TextStyle(
|
||||||
color: Color(0xFFFFD54F),
|
color: Color(0xFFFFF9C4),
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return const TextStyle(
|
return const TextStyle(
|
||||||
color: Color(0xFF9FA8DA),
|
color: Color(0xFF8C93B8),
|
||||||
fontSize: 11,
|
fontSize: 12,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -91,7 +90,6 @@ class _MainNavigationContainerState extends State<MainNavigationContainer> {
|
|||||||
|
|
||||||
final List<Widget> _screens = const [
|
final List<Widget> _screens = const [
|
||||||
HomeSearchScreen(),
|
HomeSearchScreen(),
|
||||||
CatalogScreen(),
|
|
||||||
HistoryScreen(),
|
HistoryScreen(),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -114,11 +112,6 @@ class _MainNavigationContainerState extends State<MainNavigationContainer> {
|
|||||||
selectedIcon: GradientStar(size: 22),
|
selectedIcon: GradientStar(size: 22),
|
||||||
label: 'Оракул',
|
label: 'Оракул',
|
||||||
),
|
),
|
||||||
NavigationDestination(
|
|
||||||
icon: Icon(Icons.menu_book_outlined),
|
|
||||||
selectedIcon: GradientStar(size: 22),
|
|
||||||
label: 'Каталог 999',
|
|
||||||
),
|
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
icon: Icon(Icons.history_rounded),
|
icon: Icon(Icons.history_rounded),
|
||||||
selectedIcon: GradientStar(size: 22),
|
selectedIcon: GradientStar(size: 22),
|
||||||
|
|||||||
@@ -1,313 +0,0 @@
|
|||||||
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),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,10 +11,9 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(const Duration(milliseconds: 300));
|
await tester.pump(const Duration(milliseconds: 300));
|
||||||
|
|
||||||
// Verify app title and 3 unified destinations
|
// Verify app title and 2 destinations
|
||||||
expect(find.text('Оракул 999'), findsOneWidget);
|
expect(find.text('Оракул 999'), findsOneWidget);
|
||||||
expect(find.text('Оракул'), findsOneWidget);
|
expect(find.text('Оракул'), findsOneWidget);
|
||||||
expect(find.text('Каталог 999'), findsOneWidget);
|
|
||||||
expect(find.text('История'), findsOneWidget);
|
expect(find.text('История'), findsOneWidget);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user