openai service
This commit is contained in:
@@ -1,42 +1,44 @@
|
||||
import 'dart:math';
|
||||
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 '../bloc/add_book/add_book_bloc.dart';
|
||||
import '../bloc/add_book/add_book_event.dart';
|
||||
import '../bloc/add_book/add_book_state.dart';
|
||||
import '../config/api_config.dart';
|
||||
import '../models/models.dart';
|
||||
import '../theme/app_spacing.dart';
|
||||
import 'scanner_screen.dart';
|
||||
|
||||
class AddBookScreen extends StatefulWidget {
|
||||
const AddBookScreen({super.key});
|
||||
class AddBookScreen extends StatelessWidget {
|
||||
final Book? editBook;
|
||||
final Book? prefilledData;
|
||||
|
||||
const AddBookScreen({super.key, this.editBook, this.prefilledData});
|
||||
|
||||
@override
|
||||
State<AddBookScreen> createState() => _AddBookScreenState();
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => AddBookBloc(
|
||||
onAddBook: (book) => context.read<BookBloc>().add(AddBook(book)),
|
||||
onUpdateBook: (book) => context.read<BookBloc>().add(UpdateBook(book)),
|
||||
)..add(InitializeForm(editBook: editBook, prefilledData: prefilledData)),
|
||||
child: const _AddBookScreenContent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AddBookScreenState extends State<AddBookScreen> {
|
||||
class _AddBookScreenContent extends StatefulWidget {
|
||||
const _AddBookScreenContent();
|
||||
|
||||
@override
|
||||
State<_AddBookScreenContent> createState() => _AddBookScreenContentState();
|
||||
}
|
||||
|
||||
class _AddBookScreenContentState extends State<_AddBookScreenContent> {
|
||||
final _titleController = TextEditingController();
|
||||
final _authorController = TextEditingController();
|
||||
final _annotationController = TextEditingController();
|
||||
String _genre = 'fiction';
|
||||
bool _initialized = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (!_initialized) {
|
||||
_initialized = true;
|
||||
final navState = context.read<NavigationBloc>().state;
|
||||
final book = navState.selectedBook;
|
||||
final prefilled = navState.prefilledData;
|
||||
final source = book ?? prefilled;
|
||||
if (source != null) {
|
||||
_titleController.text = source.title;
|
||||
_authorController.text = source.author;
|
||||
_annotationController.text = source.annotation;
|
||||
_genre = source.genre.isNotEmpty ? source.genre : 'fiction';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -50,206 +52,244 @@ class _AddBookScreenState extends State<AddBookScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final navState = context.read<NavigationBloc>().state;
|
||||
final isEditing =
|
||||
navState.selectedBook != null && navState.prefilledData == null;
|
||||
final title = isEditing ? 'Редактировать' : 'Добавить книгу';
|
||||
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// Header
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppSpacing.sm,
|
||||
AppSpacing.sm,
|
||||
AppSpacing.sm,
|
||||
0,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => context.read<NavigationBloc>().add(
|
||||
isEditing
|
||||
? NavigateTo(AppScreen.details)
|
||||
: NavigateTo(AppScreen.library),
|
||||
),
|
||||
),
|
||||
Text(title, style: textTheme.headlineMedium),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
children: [
|
||||
// Cover placeholder / scanner trigger
|
||||
GestureDetector(
|
||||
onTap: () => context.read<NavigationBloc>().add(
|
||||
NavigateTo(AppScreen.scanner),
|
||||
),
|
||||
child: Container(
|
||||
height: 160,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: colorScheme.outline),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppSpacing.radiusMedium,
|
||||
),
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
return BlocListener<AddBookBloc, AddBookState>(
|
||||
listener: (context, state) {
|
||||
// Update controllers when state changes (e.g., from scanned book)
|
||||
if (_titleController.text != state.title) {
|
||||
_titleController.text = state.title;
|
||||
}
|
||||
if (_authorController.text != state.author) {
|
||||
_authorController.text = state.author;
|
||||
}
|
||||
if (_annotationController.text != state.annotation) {
|
||||
_annotationController.text = state.annotation;
|
||||
}
|
||||
|
||||
// Navigate back when saved
|
||||
if (state.isSaved) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: BlocBuilder<AddBookBloc, AddBookState>(
|
||||
builder: (context, state) {
|
||||
final title = state.isEditing ? 'Редактировать' : 'Добавить книгу';
|
||||
|
||||
return Material(
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// Header
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppSpacing.sm,
|
||||
AppSpacing.sm,
|
||||
AppSpacing.sm,
|
||||
0,
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.camera_alt,
|
||||
size: 40,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
'Загрузить или отсканировать',
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(
|
||||
alpha: 0.6,
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
Text(title, style: textTheme.headlineMedium),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
children: [
|
||||
// Cover placeholder / scanner trigger
|
||||
GestureDetector(
|
||||
onTap: () => _openScanner(context),
|
||||
child: Container(
|
||||
height: 160,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: colorScheme.outline),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppSpacing.radiusMedium,
|
||||
),
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.camera_alt,
|
||||
size: 40,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
'Загрузить или отсканировать',
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(
|
||||
alpha: 0.6,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_field(
|
||||
context,
|
||||
'Название',
|
||||
_titleController,
|
||||
textTheme,
|
||||
(value) =>
|
||||
context.read<AddBookBloc>().add(UpdateTitle(value)),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_field(
|
||||
context,
|
||||
'Автор',
|
||||
_authorController,
|
||||
textTheme,
|
||||
(value) => context.read<AddBookBloc>().add(
|
||||
UpdateAuthor(value),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
// Genre dropdown
|
||||
Text('Жанр', style: textTheme.labelMedium),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
DropdownButtonFormField<String>(
|
||||
value: state.genre,
|
||||
dropdownColor: colorScheme.surface,
|
||||
decoration: const InputDecoration(),
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: 'fiction',
|
||||
child: Text('Фантастика'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'fantasy',
|
||||
child: Text('Фэнтези'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'science',
|
||||
child: Text('Научпоп'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'biography',
|
||||
child: Text('Биография'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'detective',
|
||||
child: Text('Детектив'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'other',
|
||||
child: Text('Другое'),
|
||||
),
|
||||
],
|
||||
onChanged: (v) {
|
||||
if (v != null) {
|
||||
context.read<AddBookBloc>().add(UpdateGenre(v));
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Text('Аннотация', style: textTheme.labelMedium),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
TextField(
|
||||
controller: _annotationController,
|
||||
maxLines: 4,
|
||||
onChanged: (value) => context.read<AddBookBloc>().add(
|
||||
UpdateAnnotation(value),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 100),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Bottom actions
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.screenPadding,
|
||||
vertical: AppSpacing.md,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.shadow,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ElevatedButton(
|
||||
onPressed: () =>
|
||||
context.read<AddBookBloc>().add(SaveBook()),
|
||||
child: const Text('Сохранить'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_field('Название', _titleController, textTheme),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_field('Автор', _authorController, textTheme),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
// Genre dropdown
|
||||
Text('Жанр', style: textTheme.labelMedium),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: _genre,
|
||||
dropdownColor: colorScheme.surface,
|
||||
decoration: const InputDecoration(),
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: 'fiction',
|
||||
child: Text('Фантастика'),
|
||||
),
|
||||
DropdownMenuItem(value: 'fantasy', child: Text('Фэнтези')),
|
||||
DropdownMenuItem(value: 'science', child: Text('Научпоп')),
|
||||
DropdownMenuItem(
|
||||
value: 'biography',
|
||||
child: Text('Биография'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'detective',
|
||||
child: Text('Детектив'),
|
||||
),
|
||||
DropdownMenuItem(value: 'other', child: Text('Другое')),
|
||||
],
|
||||
onChanged: (v) => setState(() => _genre = v ?? _genre),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Text('Аннотация', style: textTheme.labelMedium),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
TextField(controller: _annotationController, maxLines: 4),
|
||||
const SizedBox(height: 100),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Bottom actions
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.screenPadding,
|
||||
vertical: AppSpacing.md,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(color: colorScheme.outlineVariant),
|
||||
],
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.shadow,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => context.read<NavigationBloc>().add(
|
||||
isEditing
|
||||
? NavigateTo(AppScreen.details)
|
||||
: NavigateTo(AppScreen.library),
|
||||
),
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ElevatedButton(
|
||||
onPressed: _save,
|
||||
child: const Text('Сохранить'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _field(
|
||||
BuildContext context,
|
||||
String label,
|
||||
TextEditingController controller,
|
||||
TextTheme textTheme,
|
||||
void Function(String) onChanged,
|
||||
) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: textTheme.labelMedium),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
TextField(controller: controller),
|
||||
TextField(controller: controller, onChanged: onChanged),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _save() {
|
||||
final navState = context.read<NavigationBloc>().state;
|
||||
final existing = navState.selectedBook;
|
||||
final isEditing = existing != null && navState.prefilledData == null;
|
||||
Future<void> _openScanner(BuildContext context) async {
|
||||
if (!context.mounted) return;
|
||||
|
||||
final Book book = (
|
||||
id: isEditing ? existing.id : '${Random().nextInt(100000)}',
|
||||
title: _titleController.text,
|
||||
author: _authorController.text,
|
||||
genre: _genre,
|
||||
annotation: _annotationController.text,
|
||||
coverUrl: isEditing
|
||||
? existing.coverUrl
|
||||
: 'https://picsum.photos/seed/newbook/400/600',
|
||||
pages: isEditing ? existing.pages : 0,
|
||||
language: isEditing ? existing.language : 'Russian',
|
||||
publishedYear: isEditing ? existing.publishedYear : DateTime.now().year,
|
||||
rating: isEditing ? existing.rating : 5.0,
|
||||
status: isEditing ? existing.status : 'want_to_read',
|
||||
progress: isEditing ? existing.progress : null,
|
||||
isFavorite: isEditing ? existing.isFavorite : false,
|
||||
);
|
||||
final scannedBook = await Navigator.of(context, rootNavigator: true)
|
||||
.push<Book>(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ScannerScreen(
|
||||
geminiApiKey: ApiConfig.geminiApiKey,
|
||||
openaiApiKey: ApiConfig.openaiApiKey,
|
||||
openaiBaseUrl: ApiConfig.openaiBaseUrl,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (isEditing) {
|
||||
context.read<BookBloc>().add(UpdateBook(book));
|
||||
} else {
|
||||
context.read<BookBloc>().add(AddBook(book));
|
||||
if (scannedBook != null && context.mounted) {
|
||||
context.read<AddBookBloc>().add(ApplyScannedBook(scannedBook));
|
||||
}
|
||||
context.read<NavigationBloc>().add(NavigateTo(AppScreen.library));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user