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,262 +1,250 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/book_bloc.dart';
import '../bloc/navigation_bloc.dart';
import '../bloc/book/book_bloc.dart';
import '../bloc/book/book_event.dart';
import '../models/models.dart';
import '../theme/app_theme.dart';
import '../theme/app_spacing.dart';
import 'add_book_screen.dart';
class BookDetailsScreen extends StatelessWidget {
const BookDetailsScreen({super.key});
final Book book;
const BookDetailsScreen({super.key, required this.book});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return BlocBuilder<NavigationBloc, NavigationState>(
builder: (context, navState) {
final book = navState.selectedBook;
if (book == null) return const SizedBox.shrink();
final statusLabel = switch (book.status) {
'reading' => 'Читаю',
'done' => 'Прочитано',
'want_to_read' => 'Хочу прочитать',
_ => book.status,
};
final statusLabel = switch (book.status) {
'reading' => 'Читаю',
'done' => 'Прочитано',
'want_to_read' => 'Хочу прочитать',
_ => book.status,
};
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Hero section
Stack(
children: [
if (book.coverUrl != null)
SizedBox(
height: 300,
width: double.infinity,
child: ShaderMask(
shaderCallback: (rect) => LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [colorScheme.surface, Colors.transparent],
).createShader(rect),
blendMode: BlendMode.dstIn,
child: Image.network(book.coverUrl!, fit: BoxFit.cover),
),
),
SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(
AppSpacing.sm,
AppSpacing.sm,
AppSpacing.sm,
0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.read<NavigationBloc>().add(
NavigateTo(AppScreen.library),
),
),
IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {},
),
],
),
return SingleChildScrollView(
child: Material(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Hero section
Stack(
children: [
if (book.coverUrl != null)
SizedBox(
height: 300,
width: double.infinity,
child: ShaderMask(
shaderCallback: (rect) => LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [colorScheme.surface, Colors.transparent],
).createShader(rect),
blendMode: BlendMode.dstIn,
child: Image.network(book.coverUrl!, fit: BoxFit.cover),
),
),
Positioned.fill(
child: Align(
alignment: Alignment.bottomCenter,
child: Hero(
tag: 'book-cover-${book.id}',
child: Container(
width: 140,
height: 210,
margin: const EdgeInsets.only(bottom: 0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
AppSpacing.radiusLarge,
),
boxShadow: AppTheme.shadowXl,
SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(
AppSpacing.sm,
AppSpacing.sm,
AppSpacing.sm,
0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {},
),
],
),
),
),
Positioned.fill(
child: Align(
alignment: Alignment.bottomCenter,
child: Hero(
tag: 'book-cover-${book.id}',
child: Container(
width: 140,
height: 210,
margin: const EdgeInsets.only(bottom: 0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
AppSpacing.radiusLarge,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
AppSpacing.radiusLarge,
),
child: book.coverUrl != null
? Image.network(
book.coverUrl!,
fit: BoxFit.cover,
)
: Container(
color: colorScheme.surfaceContainerHighest,
child: Center(
child: Icon(
Icons.book,
color: colorScheme.primary.withValues(
alpha: 0.3,
),
size: 48,
boxShadow: AppTheme.shadowXl,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
AppSpacing.radiusLarge,
),
child: book.coverUrl != null
? Image.network(book.coverUrl!, fit: BoxFit.cover)
: Container(
color: colorScheme.surfaceContainerHighest,
child: Center(
child: Icon(
Icons.book,
color: colorScheme.primary.withValues(
alpha: 0.3,
),
size: 48,
),
),
),
),
),
),
),
),
),
],
),
const SizedBox(height: AppSpacing.md),
// Status badge
Center(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
decoration: BoxDecoration(
color: colorScheme.primaryContainer,
border: Border.all(color: colorScheme.primary),
borderRadius: BorderRadius.circular(AppSpacing.radiusPill),
),
child: Text(
statusLabel,
style: textTheme.labelMedium?.copyWith(
color: colorScheme.primary,
),
),
),
),
const SizedBox(height: AppSpacing.md),
// Title & Author
Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(book.title, style: textTheme.displayMedium),
const SizedBox(height: AppSpacing.xs),
Text(
book.author,
style: textTheme.titleLarge?.copyWith(
color: colorScheme.onSurface.withValues(alpha: 0.7),
),
),
const SizedBox(height: AppSpacing.md),
// Genre tag
Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
border: Border.all(color: colorScheme.outline),
borderRadius: BorderRadius.circular(AppSpacing.radiusSmall),
),
child: Text(book.genre, style: textTheme.labelMedium),
),
const SizedBox(height: AppSpacing.lg),
// Action buttons
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => AddBookScreen(editBook: book),
),
);
},
icon: const Icon(Icons.edit, size: 18),
label: const Text('Изменить'),
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: OutlinedButton.icon(
onPressed: () {
context.read<BookBloc>().add(DeleteBook(book.id));
Navigator.pop(context);
},
icon: const Icon(Icons.delete_outline, size: 18),
label: const Text('Удалить'),
style: OutlinedButton.styleFrom(
foregroundColor: colorScheme.error,
side: BorderSide(color: colorScheme.error),
),
),
),
],
),
const SizedBox(height: AppSpacing.lg),
// About
Text('О книге', style: textTheme.headlineMedium),
const SizedBox(height: AppSpacing.sm),
Text(book.annotation, style: textTheme.bodyLarge),
const SizedBox(height: AppSpacing.lg),
// Info grid
GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
mainAxisSpacing: AppSpacing.md,
crossAxisSpacing: AppSpacing.md,
childAspectRatio: 2.5,
children: [
_infoTile(
context,
Icons.menu_book,
Colors.blue,
'Страницы',
'${book.pages ?? ""}',
),
_infoTile(
context,
Icons.language,
Colors.purple,
'Язык',
book.language ?? '',
),
_infoTile(
context,
Icons.calendar_month,
Colors.orange,
'Год',
'${book.publishedYear ?? ""}',
),
_infoTile(
context,
Icons.star,
Colors.amber,
'Рейтинг',
'${book.rating ?? ""}',
),
],
),
const SizedBox(height: 40),
],
),
const SizedBox(height: AppSpacing.md),
// Status badge
Center(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
decoration: BoxDecoration(
color: colorScheme.primaryContainer,
border: Border.all(color: colorScheme.primary),
borderRadius: BorderRadius.circular(AppSpacing.radiusPill),
),
child: Text(
statusLabel,
style: textTheme.labelMedium?.copyWith(
color: colorScheme.primary,
),
),
),
),
const SizedBox(height: AppSpacing.md),
// Title & Author
Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(book.title, style: textTheme.displayMedium),
const SizedBox(height: AppSpacing.xs),
Text(
book.author,
style: textTheme.titleLarge?.copyWith(
color: colorScheme.onSurface.withValues(alpha: 0.7),
),
),
const SizedBox(height: AppSpacing.md),
// Genre tag
Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
border: Border.all(color: colorScheme.outline),
borderRadius: BorderRadius.circular(
AppSpacing.radiusSmall,
),
),
child: Text(book.genre, style: textTheme.labelMedium),
),
const SizedBox(height: AppSpacing.lg),
// Action buttons
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () {
context.read<NavigationBloc>().add(
NavigateTo(
AppScreen.addBook,
selectedBook: book,
),
);
},
icon: const Icon(Icons.edit, size: 18),
label: const Text('Изменить'),
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: OutlinedButton.icon(
onPressed: () {
context.read<BookBloc>().add(DeleteBook(book.id));
context.read<NavigationBloc>().add(
NavigateTo(AppScreen.library),
);
},
icon: const Icon(Icons.delete_outline, size: 18),
label: const Text('Удалить'),
style: OutlinedButton.styleFrom(
foregroundColor: colorScheme.error,
side: BorderSide(color: colorScheme.error),
),
),
),
],
),
const SizedBox(height: AppSpacing.lg),
// About
Text('О книге', style: textTheme.headlineMedium),
const SizedBox(height: AppSpacing.sm),
Text(book.annotation, style: textTheme.bodyLarge),
const SizedBox(height: AppSpacing.lg),
// Info grid
GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
mainAxisSpacing: AppSpacing.md,
crossAxisSpacing: AppSpacing.md,
childAspectRatio: 2.5,
children: [
_infoTile(
context,
Icons.menu_book,
Colors.blue,
'Страницы',
'${book.pages ?? ""}',
),
_infoTile(
context,
Icons.language,
Colors.purple,
'Язык',
book.language ?? '',
),
_infoTile(
context,
Icons.calendar_month,
Colors.orange,
'Год',
'${book.publishedYear ?? ""}',
),
_infoTile(
context,
Icons.star,
Colors.amber,
'Рейтинг',
'${book.rating ?? ""}',
),
],
),
const SizedBox(height: 40),
],
),
),
],
),
);
},
),
],
),
),
);
}