openai service

This commit is contained in:
2026-02-08 12:04:45 +06:00
parent 5c7b65a0d3
commit d7722ad81d
19 changed files with 1372 additions and 1008 deletions

View File

@@ -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