openai service
This commit is contained in:
@@ -1,174 +1,226 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../bloc/book_bloc.dart';
|
||||
import '../bloc/navigation_bloc.dart';
|
||||
import '../models/models.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 StatefulWidget {
|
||||
class LibraryScreen extends StatelessWidget {
|
||||
const LibraryScreen({super.key});
|
||||
|
||||
@override
|
||||
State<LibraryScreen> createState() => _LibraryScreenState();
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => LibraryBloc(),
|
||||
child: const _LibraryScreenContent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LibraryScreenState extends State<LibraryScreen> {
|
||||
String _search = '';
|
||||
int _tabIndex = 0;
|
||||
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<BookBloc, BookState>(
|
||||
builder: (context, state) {
|
||||
final filtered = state.books.where((b) {
|
||||
final q = _search.toLowerCase();
|
||||
return b.title.toLowerCase().contains(q) ||
|
||||
b.author.toLowerCase().contains(q);
|
||||
}).toList();
|
||||
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 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) => setState(() => _search = 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('Все книги', 0),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
_tab('Категории', 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
// Grid
|
||||
Expanded(
|
||||
child: _tabIndex == 0
|
||||
? GridView.builder(
|
||||
return Stack(
|
||||
children: [
|
||||
SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// Header
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppSpacing.lg,
|
||||
0,
|
||||
AppSpacing.md,
|
||||
AppSpacing.lg,
|
||||
100,
|
||||
0,
|
||||
),
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 0.55,
|
||||
crossAxisSpacing: AppSpacing.md,
|
||||
mainAxisSpacing: AppSpacing.md,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Книжная полка',
|
||||
style: textTheme.displayMedium,
|
||||
),
|
||||
itemCount: filtered.length,
|
||||
itemBuilder: (context, i) => BookCard(
|
||||
book: filtered[i],
|
||||
onTap: () {
|
||||
context.read<NavigationBloc>().add(
|
||||
NavigateTo(
|
||||
AppScreen.details,
|
||||
selectedBook: filtered[i],
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView(
|
||||
),
|
||||
// Tabs
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppSpacing.lg,
|
||||
0,
|
||||
AppSpacing.md,
|
||||
AppSpacing.lg,
|
||||
100,
|
||||
0,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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(String label, int index) {
|
||||
final selected = _tabIndex == index;
|
||||
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: () => setState(() => _tabIndex = index),
|
||||
onTap: () => context.read<LibraryBloc>().add(ChangeTab(index)),
|
||||
child: AnimatedContainer(
|
||||
duration: disableAnimations
|
||||
? Duration.zero
|
||||
|
||||
Reference in New Issue
Block a user