253 lines
9.9 KiB
Dart
253 lines
9.9 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
import '../bloc/book/book_bloc.dart';
|
||
import '../bloc/book/book_state.dart';
|
||
import '../bloc/library/library_bloc.dart';
|
||
import '../bloc/library/library_event.dart';
|
||
import '../bloc/library/library_state.dart';
|
||
import '../widgets/book_card.dart';
|
||
import '../theme/app_spacing.dart';
|
||
import 'book_details_screen.dart';
|
||
import 'add_book_screen.dart';
|
||
|
||
class LibraryScreen extends StatelessWidget {
|
||
const LibraryScreen({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return BlocProvider(
|
||
create: (context) => LibraryBloc(),
|
||
child: const _LibraryScreenContent(),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _LibraryScreenContent extends StatelessWidget {
|
||
const _LibraryScreenContent();
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final colorScheme = Theme.of(context).colorScheme;
|
||
final textTheme = Theme.of(context).textTheme;
|
||
|
||
return BlocBuilder<LibraryBloc, LibraryState>(
|
||
builder: (context, libraryState) {
|
||
return BlocBuilder<BookBloc, BookState>(
|
||
builder: (context, bookState) {
|
||
final filtered = bookState.books.where((b) {
|
||
final q = libraryState.searchQuery.toLowerCase();
|
||
return b.title.toLowerCase().contains(q) ||
|
||
b.author.toLowerCase().contains(q);
|
||
}).toList();
|
||
|
||
return Stack(
|
||
children: [
|
||
SafeArea(
|
||
child: Column(
|
||
children: [
|
||
// Header
|
||
Padding(
|
||
padding: const EdgeInsets.fromLTRB(
|
||
AppSpacing.lg,
|
||
AppSpacing.md,
|
||
AppSpacing.lg,
|
||
0,
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
'Книжная полка',
|
||
style: textTheme.displayMedium,
|
||
),
|
||
IconButton(
|
||
icon: const Icon(Icons.notifications_outlined),
|
||
onPressed: () {},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
// Search
|
||
Padding(
|
||
padding: const EdgeInsets.fromLTRB(
|
||
AppSpacing.lg,
|
||
AppSpacing.md,
|
||
AppSpacing.lg,
|
||
0,
|
||
),
|
||
child: TextField(
|
||
onChanged: (v) {
|
||
context.read<LibraryBloc>().add(
|
||
UpdateSearchQuery(v),
|
||
);
|
||
},
|
||
decoration: InputDecoration(
|
||
hintText: 'Поиск книг...',
|
||
prefixIcon: Icon(
|
||
Icons.search,
|
||
color: colorScheme.primary,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
// Tabs
|
||
Padding(
|
||
padding: const EdgeInsets.fromLTRB(
|
||
AppSpacing.lg,
|
||
AppSpacing.md,
|
||
AppSpacing.lg,
|
||
0,
|
||
),
|
||
child: Row(
|
||
children: [
|
||
_tab(
|
||
context,
|
||
'Все книги',
|
||
0,
|
||
libraryState.tabIndex,
|
||
),
|
||
const SizedBox(width: AppSpacing.sm),
|
||
_tab(
|
||
context,
|
||
'Категории',
|
||
1,
|
||
libraryState.tabIndex,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: AppSpacing.md),
|
||
// Grid
|
||
Expanded(
|
||
child: libraryState.tabIndex == 0
|
||
? GridView.builder(
|
||
padding: const EdgeInsets.fromLTRB(
|
||
AppSpacing.lg,
|
||
0,
|
||
AppSpacing.lg,
|
||
100,
|
||
),
|
||
gridDelegate:
|
||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||
crossAxisCount: 2,
|
||
childAspectRatio: 0.55,
|
||
crossAxisSpacing: AppSpacing.md,
|
||
mainAxisSpacing: AppSpacing.md,
|
||
),
|
||
itemCount: filtered.length,
|
||
itemBuilder: (context, i) => BookCard(
|
||
book: filtered[i],
|
||
onTap: () {
|
||
Navigator.of(context).push(
|
||
MaterialPageRoute(
|
||
builder: (_) => BookDetailsScreen(
|
||
book: filtered[i],
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
)
|
||
: ListView(
|
||
padding: const EdgeInsets.fromLTRB(
|
||
AppSpacing.lg,
|
||
0,
|
||
AppSpacing.lg,
|
||
100,
|
||
),
|
||
children: [
|
||
for (final genre
|
||
in filtered.map((b) => b.genre).toSet())
|
||
Container(
|
||
margin: const EdgeInsets.only(
|
||
bottom: AppSpacing.sm,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: colorScheme.surface,
|
||
border: Border.all(
|
||
color: colorScheme.outline,
|
||
),
|
||
borderRadius: BorderRadius.circular(
|
||
AppSpacing.radiusMedium,
|
||
),
|
||
),
|
||
child: ListTile(
|
||
title: Text(
|
||
genre,
|
||
style: textTheme.titleMedium,
|
||
),
|
||
trailing: Text(
|
||
'${filtered.where((b) => b.genre == genre).length}',
|
||
style: textTheme.bodyMedium?.copyWith(
|
||
color: colorScheme.onSurface
|
||
.withValues(alpha: 0.6),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Positioned(
|
||
bottom: 16,
|
||
right: 20,
|
||
child: FloatingActionButton(
|
||
onPressed: () {
|
||
Navigator.of(context, rootNavigator: true).push(
|
||
MaterialPageRoute(
|
||
builder: (_) => const AddBookScreen(),
|
||
),
|
||
);
|
||
},
|
||
child: const Icon(Icons.add),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget _tab(BuildContext context, String label, int index, int currentIndex) {
|
||
final selected = currentIndex == index;
|
||
final colorScheme = Theme.of(context).colorScheme;
|
||
final textTheme = Theme.of(context).textTheme;
|
||
final disableAnimations = MediaQuery.of(context).disableAnimations;
|
||
|
||
return GestureDetector(
|
||
onTap: () => context.read<LibraryBloc>().add(ChangeTab(index)),
|
||
child: AnimatedContainer(
|
||
duration: disableAnimations
|
||
? Duration.zero
|
||
: const Duration(milliseconds: 200),
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.md,
|
||
vertical: AppSpacing.sm,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: selected
|
||
? colorScheme.primary
|
||
: colorScheme.surfaceContainerHighest,
|
||
border: Border.all(
|
||
color: selected ? colorScheme.primary : colorScheme.outline,
|
||
),
|
||
borderRadius: BorderRadius.circular(AppSpacing.radiusPill),
|
||
),
|
||
child: Text(
|
||
label,
|
||
style: textTheme.labelMedium?.copyWith(
|
||
color: selected
|
||
? Colors.white
|
||
: colorScheme.onSurface.withValues(alpha: 0.7),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|