- Fixed test file import paths to point to correct Bloc file locations - Fixed Bloc file import paths for models (../../../models/models.dart) - Added explicit type annotations to resolve null safety warnings - Fixed null safety issues in wishlist_bloc_test.dart - All 70 tests now passing
320 lines
11 KiB
Dart
320 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import '../../bloc/app_bloc.dart';
|
|
import '../../bloc/app_event.dart';
|
|
import '../../../models/models.dart';
|
|
|
|
class AddBookScreen extends StatefulWidget {
|
|
final dynamic initialData;
|
|
|
|
const AddBookScreen({super.key, this.initialData});
|
|
|
|
@override
|
|
State<AddBookScreen> createState() => _AddBookScreenState();
|
|
}
|
|
|
|
class _AddBookScreenState extends State<AddBookScreen> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
late TextEditingController _titleController;
|
|
late TextEditingController _authorController;
|
|
late TextEditingController _genreController;
|
|
late TextEditingController _annotationController;
|
|
String? _coverUrl;
|
|
BookStatus _selectedStatus = BookStatus.wantToRead;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_titleController = TextEditingController();
|
|
_authorController = TextEditingController();
|
|
_genreController = TextEditingController();
|
|
_annotationController = TextEditingController();
|
|
|
|
if (widget.initialData is Book) {
|
|
final book = widget.initialData as Book;
|
|
_titleController.text = book.title;
|
|
_authorController.text = book.author;
|
|
_genreController.text = book.genre;
|
|
_annotationController.text = book.annotation;
|
|
_coverUrl = book.coverUrl;
|
|
_selectedStatus = book.status;
|
|
} else if (widget.initialData is Map) {
|
|
final data = widget.initialData as Map<String, dynamic>;
|
|
_titleController.text = data['title']?.toString() ?? '';
|
|
_authorController.text = data['author']?.toString() ?? '';
|
|
_genreController.text = data['genre']?.toString() ?? '';
|
|
_annotationController.text = data['annotation']?.toString() ?? '';
|
|
_coverUrl = data['coverUrl']?.toString();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_titleController.dispose();
|
|
_authorController.dispose();
|
|
_genreController.dispose();
|
|
_annotationController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _pickImage() async {
|
|
final picker = ImagePicker();
|
|
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
|
|
if (image != null) {
|
|
setState(() {
|
|
_coverUrl = image.path;
|
|
});
|
|
}
|
|
}
|
|
|
|
void _saveBook() {
|
|
if (_formKey.currentState!.validate()) {
|
|
context.read<AppBloc>().add(
|
|
BookSaved({
|
|
'title': _titleController.text,
|
|
'author': _authorController.text,
|
|
'genre': _genreController.text,
|
|
'annotation': _annotationController.text,
|
|
'coverUrl': _coverUrl,
|
|
'status': _selectedStatus,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFF112116),
|
|
appBar: AppBar(
|
|
backgroundColor: const Color(0xFF112116).withValues(alpha: 0.9),
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white),
|
|
onPressed: () {
|
|
context.read<AppBloc>().add(
|
|
widget.initialData is Book
|
|
? const ScreenChanged(AppScreen.details)
|
|
: const ScreenChanged(AppScreen.library),
|
|
);
|
|
},
|
|
),
|
|
title: const Text('Добавить книгу'),
|
|
centerTitle: true,
|
|
),
|
|
body: Form(
|
|
key: _formKey,
|
|
child: ListView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
|
children: [
|
|
Center(
|
|
child: GestureDetector(
|
|
onTap: _pickImage,
|
|
child: Container(
|
|
width: 160,
|
|
height: 240,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: const Color(0xFF346544),
|
|
width: 2,
|
|
),
|
|
color: const Color(0xFF1A3222),
|
|
),
|
|
child: _coverUrl != null
|
|
? ClipRRect(
|
|
borderRadius: BorderRadius.circular(10),
|
|
child: _coverUrl!.startsWith('http')
|
|
? CachedNetworkImage(
|
|
imageUrl: _coverUrl!,
|
|
fit: BoxFit.cover,
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
)
|
|
: Image.network(_coverUrl!, fit: BoxFit.cover),
|
|
)
|
|
: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF17CF54).withValues(alpha: 0.1),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(
|
|
Icons.add_a_photo,
|
|
color: Color(0xFF17CF54),
|
|
size: 28,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
const Text(
|
|
'Загрузить',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Color(0xFF93C8A5),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 32),
|
|
_buildTextField(
|
|
controller: _titleController,
|
|
label: 'Название',
|
|
validator: (value) =>
|
|
value?.isEmpty ?? true ? 'Введите название' : null,
|
|
),
|
|
const SizedBox(height: 16),
|
|
_buildTextField(
|
|
controller: _authorController,
|
|
label: 'Автор',
|
|
validator: (value) =>
|
|
value?.isEmpty ?? true ? 'Введите автора' : null,
|
|
),
|
|
const SizedBox(height: 16),
|
|
_buildTextField(
|
|
controller: _genreController,
|
|
label: 'Жанр',
|
|
validator: (value) =>
|
|
value?.isEmpty ?? true ? 'Введите жанр' : null,
|
|
),
|
|
const SizedBox(height: 16),
|
|
_buildTextField(
|
|
controller: _annotationController,
|
|
label: 'Аннотация',
|
|
maxLines: 4,
|
|
validator: (value) =>
|
|
value?.isEmpty ?? true ? 'Введите аннотацию' : null,
|
|
),
|
|
const SizedBox(height: 16),
|
|
DropdownButtonFormField<BookStatus>(
|
|
initialValue: _selectedStatus,
|
|
decoration: InputDecoration(
|
|
labelText: 'Статус',
|
|
labelStyle: const TextStyle(
|
|
color: Color(0xFFE0E0E0),
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
filled: true,
|
|
fillColor: const Color(0xFF1A3222),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: const BorderSide(color: Color(0xFF346544)),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: const BorderSide(color: Color(0xFF346544)),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: const BorderSide(color: Color(0xFF17CF54)),
|
|
),
|
|
),
|
|
dropdownColor: const Color(0xFF1A3222),
|
|
style: const TextStyle(color: Colors.white),
|
|
items: BookStatus.values.map((status) {
|
|
return DropdownMenuItem(
|
|
value: status,
|
|
child: Text(status.displayName),
|
|
);
|
|
}).toList(),
|
|
onChanged: (value) {
|
|
if (value != null) {
|
|
setState(() {
|
|
_selectedStatus = value;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
const SizedBox(height: 120),
|
|
],
|
|
),
|
|
),
|
|
bottomNavigationBar: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF112116).withValues(alpha: 0.95),
|
|
border: Border(
|
|
top: BorderSide(color: Colors.white.withValues(alpha: 0.05), width: 1),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton(
|
|
onPressed: () {
|
|
context.read<AppBloc>().add(
|
|
widget.initialData is Book
|
|
? const ScreenChanged(AppScreen.details)
|
|
: const ScreenChanged(AppScreen.library),
|
|
);
|
|
},
|
|
style: OutlinedButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
side: const BorderSide(color: Colors.transparent),
|
|
),
|
|
child: const Text('Отмена'),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
flex: 2,
|
|
child: ElevatedButton.icon(
|
|
onPressed: _saveBook,
|
|
icon: const Icon(Icons.save),
|
|
label: const Text('Сохранить'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF17CF54),
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTextField({
|
|
required TextEditingController controller,
|
|
required String label,
|
|
int maxLines = 1,
|
|
String? Function(String?)? validator,
|
|
}) {
|
|
return TextFormField(
|
|
controller: controller,
|
|
maxLines: maxLines,
|
|
validator: validator,
|
|
style: const TextStyle(color: Colors.white, fontSize: 16),
|
|
decoration: InputDecoration(
|
|
labelText: label,
|
|
labelStyle: const TextStyle(
|
|
color: Color(0xFFE0E0E0),
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
filled: true,
|
|
fillColor: const Color(0xFF1A3222),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: const BorderSide(color: Color(0xFF346544)),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: const BorderSide(color: Color(0xFF346544)),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: const BorderSide(color: Color(0xFF17CF54)),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} |