296 lines
11 KiB
Dart
296 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_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 StatelessWidget {
|
|
final Book? editBook;
|
|
final Book? prefilledData;
|
|
|
|
const AddBookScreen({super.key, this.editBook, this.prefilledData});
|
|
|
|
@override
|
|
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 _AddBookScreenContent extends StatefulWidget {
|
|
const _AddBookScreenContent();
|
|
|
|
@override
|
|
State<_AddBookScreenContent> createState() => _AddBookScreenContentState();
|
|
}
|
|
|
|
class _AddBookScreenContentState extends State<_AddBookScreenContent> {
|
|
final _titleController = TextEditingController();
|
|
final _authorController = TextEditingController();
|
|
final _annotationController = TextEditingController();
|
|
|
|
@override
|
|
void dispose() {
|
|
_titleController.dispose();
|
|
_authorController.dispose();
|
|
_annotationController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
final textTheme = Theme.of(context).textTheme;
|
|
|
|
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: 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('Сохранить'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
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, onChanged: onChanged),
|
|
],
|
|
);
|
|
}
|
|
|
|
Future<void> _openScanner(BuildContext context) async {
|
|
if (!context.mounted) return;
|
|
|
|
final scannedBook = await Navigator.of(context, rootNavigator: true)
|
|
.push<Book>(
|
|
MaterialPageRoute(
|
|
builder: (_) => ScannerScreen(
|
|
geminiApiKey: ApiConfig.geminiApiKey,
|
|
openaiApiKey: ApiConfig.openaiApiKey,
|
|
openaiBaseUrl: ApiConfig.openaiBaseUrl,
|
|
),
|
|
),
|
|
);
|
|
|
|
if (scannedBook != null && context.mounted) {
|
|
context.read<AddBookBloc>().add(ApplyScannedBook(scannedBook));
|
|
}
|
|
}
|
|
}
|