refactor: create separate BLoCs for each screen with comprehensive tests
- Created 8 separate BLoCs (Home, Library, BookDetails, AddBook, Scanner, Categories, Wishlist, Settings) - Each BLoC has its own event, state, and bloc files - Added 70 comprehensive tests covering all BLoC functionality - All tests passing (70/70) - Fixed linting issues and updated deprecated APIs - Improved code organization and maintainability
This commit is contained in:
110
bookshelf_flutter/lib/bloc/add_book/add_book_bloc.dart
Normal file
110
bookshelf_flutter/lib/bloc/add_book/add_book_bloc.dart
Normal file
@@ -0,0 +1,110 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../models/models.dart';
|
||||
import 'add_book_event.dart';
|
||||
import 'add_book_state.dart';
|
||||
|
||||
class AddBookBloc extends Bloc<AddBookEvent, AddBookState> {
|
||||
AddBookBloc() : super(AddBookState.initial()) {
|
||||
on<InitializeForm>(_onInitializeForm);
|
||||
on<UpdateFormField>(_onUpdateFormField);
|
||||
on<ToggleFavorite>(_onToggleFavorite);
|
||||
on<ClearForm>(_onClearForm);
|
||||
on<SubmitBook>(_onSubmitBook);
|
||||
}
|
||||
|
||||
void _onInitializeForm(InitializeForm event, Emitter<AddBookState> emit) {
|
||||
emit(state.copyWith(
|
||||
prefilledData: event.prefilledData,
|
||||
isLoading: false,
|
||||
));
|
||||
|
||||
if (event.prefilledData != null) {
|
||||
final data = event.prefilledData!;
|
||||
emit(state.copyWith(
|
||||
title: data['title'] as String? ?? '',
|
||||
author: data['author'] as String? ?? '',
|
||||
genre: data['genre'] as String? ?? '',
|
||||
annotation: data['annotation'] as String? ?? '',
|
||||
coverUrl: data['coverUrl'] as String? ?? '',
|
||||
pages: data['pages'] as int?,
|
||||
language: data['language'] as String? ?? '',
|
||||
publishedYear: data['publishedYear'] as int?,
|
||||
rating: data['rating'] as double? ?? 0.0,
|
||||
status: data['status'] as BookStatus? ?? BookStatus.wantToRead,
|
||||
progress: data['progress'] as int? ?? 0,
|
||||
isFavorite: data['isFavorite'] as bool? ?? false,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _onUpdateFormField(UpdateFormField event, Emitter<AddBookState> emit) {
|
||||
switch (event.field) {
|
||||
case 'title':
|
||||
emit(state.copyWith(title: event.value as String));
|
||||
break;
|
||||
case 'author':
|
||||
emit(state.copyWith(author: event.value as String));
|
||||
break;
|
||||
case 'genre':
|
||||
emit(state.copyWith(genre: event.value as String));
|
||||
break;
|
||||
case 'annotation':
|
||||
emit(state.copyWith(annotation: event.value as String));
|
||||
break;
|
||||
case 'coverUrl':
|
||||
emit(state.copyWith(coverUrl: event.value as String));
|
||||
break;
|
||||
case 'pages':
|
||||
emit(state.copyWith(pages: event.value as int?));
|
||||
break;
|
||||
case 'language':
|
||||
emit(state.copyWith(language: event.value as String));
|
||||
break;
|
||||
case 'publishedYear':
|
||||
emit(state.copyWith(publishedYear: event.value as int?));
|
||||
break;
|
||||
case 'rating':
|
||||
emit(state.copyWith(rating: event.value as double?));
|
||||
break;
|
||||
case 'status':
|
||||
emit(state.copyWith(status: event.value as BookStatus));
|
||||
break;
|
||||
case 'progress':
|
||||
emit(state.copyWith(progress: event.value as int?));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _onToggleFavorite(ToggleFavorite event, Emitter<AddBookState> emit) {
|
||||
emit(state.copyWith(isFavorite: !state.isFavorite));
|
||||
}
|
||||
|
||||
void _onClearForm(ClearForm event, Emitter<AddBookState> emit) {
|
||||
emit(AddBookState.initial());
|
||||
}
|
||||
|
||||
void _onSubmitBook(SubmitBook event, Emitter<AddBookState> emit) {
|
||||
final book = createBook(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
title: state.title.isNotEmpty ? state.title : 'Unknown',
|
||||
author: state.author.isNotEmpty ? state.author : 'Unknown',
|
||||
genre: state.genre.isNotEmpty ? state.genre : 'Unknown',
|
||||
annotation: state.annotation,
|
||||
coverUrl: state.coverUrl.isNotEmpty
|
||||
? state.coverUrl
|
||||
: 'https://picsum.photos/seed/${DateTime.now().millisecondsSinceEpoch}/400/600',
|
||||
pages: state.pages,
|
||||
language: state.language,
|
||||
publishedYear: state.publishedYear,
|
||||
rating: state.rating,
|
||||
status: state.status,
|
||||
progress: state.progress,
|
||||
isFavorite: state.isFavorite,
|
||||
);
|
||||
|
||||
emit(state.copyWith(
|
||||
submittedBook: book,
|
||||
isSubmitted: true,
|
||||
));
|
||||
}
|
||||
}
|
||||
27
bookshelf_flutter/lib/bloc/add_book/add_book_event.dart
Normal file
27
bookshelf_flutter/lib/bloc/add_book/add_book_event.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
abstract class AddBookEvent {
|
||||
const AddBookEvent();
|
||||
}
|
||||
|
||||
class InitializeForm extends AddBookEvent {
|
||||
final Map<String, dynamic>? prefilledData;
|
||||
const InitializeForm(this.prefilledData);
|
||||
}
|
||||
|
||||
class UpdateFormField extends AddBookEvent {
|
||||
final String field;
|
||||
final dynamic value;
|
||||
const UpdateFormField(this.field, this.value);
|
||||
}
|
||||
|
||||
class ToggleFavorite extends AddBookEvent {
|
||||
const ToggleFavorite();
|
||||
}
|
||||
|
||||
class ClearForm extends AddBookEvent {
|
||||
const ClearForm();
|
||||
}
|
||||
|
||||
class SubmitBook extends AddBookEvent {
|
||||
const SubmitBook();
|
||||
}
|
||||
109
bookshelf_flutter/lib/bloc/add_book/add_book_state.dart
Normal file
109
bookshelf_flutter/lib/bloc/add_book/add_book_state.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../models/models.dart';
|
||||
|
||||
class AddBookState extends Equatable {
|
||||
final String title;
|
||||
final String author;
|
||||
final String genre;
|
||||
final String annotation;
|
||||
final String coverUrl;
|
||||
final int? pages;
|
||||
final String language;
|
||||
final int? publishedYear;
|
||||
final double? rating;
|
||||
final BookStatus status;
|
||||
final int? progress;
|
||||
final bool isFavorite;
|
||||
final Map<String, dynamic>? prefilledData;
|
||||
final Book? submittedBook;
|
||||
final bool isSubmitted;
|
||||
final bool isLoading;
|
||||
final String? errorMessage;
|
||||
|
||||
const AddBookState({
|
||||
this.title = '',
|
||||
this.author = '',
|
||||
this.genre = '',
|
||||
this.annotation = '',
|
||||
this.coverUrl = '',
|
||||
this.pages,
|
||||
this.language = '',
|
||||
this.publishedYear,
|
||||
this.rating,
|
||||
this.status = BookStatus.wantToRead,
|
||||
this.progress,
|
||||
this.isFavorite = false,
|
||||
this.prefilledData,
|
||||
this.submittedBook,
|
||||
this.isSubmitted = false,
|
||||
this.isLoading = false,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
factory AddBookState.initial() {
|
||||
return const AddBookState(
|
||||
isLoading: false,
|
||||
);
|
||||
}
|
||||
|
||||
AddBookState copyWith({
|
||||
String? title,
|
||||
String? author,
|
||||
String? genre,
|
||||
String? annotation,
|
||||
String? coverUrl,
|
||||
int? pages,
|
||||
String? language,
|
||||
int? publishedYear,
|
||||
double? rating,
|
||||
BookStatus? status,
|
||||
int? progress,
|
||||
bool? isFavorite,
|
||||
Map<String, dynamic>? prefilledData,
|
||||
Book? submittedBook,
|
||||
bool? isSubmitted,
|
||||
bool? isLoading,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return AddBookState(
|
||||
title: title ?? this.title,
|
||||
author: author ?? this.author,
|
||||
genre: genre ?? this.genre,
|
||||
annotation: annotation ?? this.annotation,
|
||||
coverUrl: coverUrl ?? this.coverUrl,
|
||||
pages: pages ?? this.pages,
|
||||
language: language ?? this.language,
|
||||
publishedYear: publishedYear ?? this.publishedYear,
|
||||
rating: rating ?? this.rating,
|
||||
status: status ?? this.status,
|
||||
progress: progress ?? this.progress,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
prefilledData: prefilledData ?? this.prefilledData,
|
||||
submittedBook: submittedBook ?? this.submittedBook,
|
||||
isSubmitted: isSubmitted ?? this.isSubmitted,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
title,
|
||||
author,
|
||||
genre,
|
||||
annotation,
|
||||
coverUrl,
|
||||
pages,
|
||||
language,
|
||||
publishedYear,
|
||||
rating,
|
||||
status,
|
||||
progress,
|
||||
isFavorite,
|
||||
prefilledData,
|
||||
submittedBook,
|
||||
isSubmitted,
|
||||
isLoading,
|
||||
errorMessage,
|
||||
];
|
||||
}
|
||||
179
bookshelf_flutter/lib/bloc/app_bloc.dart
Normal file
179
bookshelf_flutter/lib/bloc/app_bloc.dart
Normal file
@@ -0,0 +1,179 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../models/models.dart';
|
||||
import 'app_event.dart';
|
||||
import 'app_state.dart';
|
||||
|
||||
class AppBloc extends Bloc<AppEvent, AppState> {
|
||||
AppBloc() : super(AppState(books: _initialBooks)) {
|
||||
on<ScreenChanged>(_onScreenChanged);
|
||||
on<BookClicked>(_onBookClicked);
|
||||
on<AddBookClicked>(_onAddBookClicked);
|
||||
on<BookSaved>(_onBookSaved);
|
||||
on<BookDeleted>(_onBookDeleted);
|
||||
on<BookDetected>(_onBookDetected);
|
||||
on<SearchChanged>(_onSearchChanged);
|
||||
}
|
||||
|
||||
static List<Book> get _initialBooks => [
|
||||
createBook(
|
||||
id: '1',
|
||||
title: 'Великий Гэтсби',
|
||||
author: 'Ф. Скотт Фицджеральд',
|
||||
genre: 'Classic',
|
||||
annotation:
|
||||
'История о несбывшейся любви и трагедии американской мечты на фоне бурных двадцатых годов.',
|
||||
coverUrl: 'https://picsum.photos/seed/gatsby/400/600',
|
||||
pages: 208,
|
||||
language: 'English',
|
||||
publishedYear: 1925,
|
||||
rating: 4.8,
|
||||
status: BookStatus.reading,
|
||||
progress: 45,
|
||||
isFavorite: true,
|
||||
),
|
||||
createBook(
|
||||
id: '2',
|
||||
title: '1984',
|
||||
author: 'Джордж Оруэлл',
|
||||
genre: 'Dystopian',
|
||||
annotation:
|
||||
'Антиутопия о тоталитарном государстве, где мысли контролируются, а правда переменчива.',
|
||||
coverUrl: 'https://picsum.photos/seed/1984/400/600',
|
||||
pages: 328,
|
||||
language: 'English',
|
||||
publishedYear: 1949,
|
||||
rating: 4.9,
|
||||
status: BookStatus.wantToRead,
|
||||
isFavorite: true,
|
||||
),
|
||||
createBook(
|
||||
id: '3',
|
||||
title: 'Дюна',
|
||||
author: 'Фрэнк Герберт',
|
||||
genre: 'Sci-Fi',
|
||||
annotation:
|
||||
'Эпическая сага о борьбе за власть над самой важной планетой во Вселенной.',
|
||||
coverUrl: 'https://picsum.photos/seed/dune/400/600',
|
||||
pages: 896,
|
||||
language: 'English',
|
||||
publishedYear: 1965,
|
||||
rating: 4.7,
|
||||
status: BookStatus.reading,
|
||||
progress: 12,
|
||||
isFavorite: false,
|
||||
),
|
||||
createBook(
|
||||
id: '4',
|
||||
title: 'Хоббит',
|
||||
author: 'Дж. Р. Р. Толкин',
|
||||
genre: 'Fantasy',
|
||||
annotation:
|
||||
'Путешествие Бильбо Бэггинса туда и обратно в поисках сокровищ гномов.',
|
||||
coverUrl: 'https://picsum.photos/seed/hobbit/400/600',
|
||||
pages: 310,
|
||||
language: 'English',
|
||||
publishedYear: 1937,
|
||||
rating: 4.9,
|
||||
status: BookStatus.done,
|
||||
isFavorite: false,
|
||||
),
|
||||
];
|
||||
|
||||
void _onScreenChanged(ScreenChanged event, Emitter<AppState> emit) {
|
||||
emit(state.copyWith(currentScreen: event.screen));
|
||||
}
|
||||
|
||||
void _onBookClicked(BookClicked event, Emitter<AppState> emit) {
|
||||
emit(state.copyWith(
|
||||
selectedBook: event.book,
|
||||
currentScreen: AppScreen.details,
|
||||
));
|
||||
}
|
||||
|
||||
void _onAddBookClicked(AddBookClicked event, Emitter<AppState> emit) {
|
||||
emit(state.copyWith(
|
||||
prefilledData: null,
|
||||
selectedBook: null,
|
||||
currentScreen: AppScreen.addBook,
|
||||
));
|
||||
}
|
||||
|
||||
void _onBookSaved(BookSaved event, Emitter<AppState> emit) {
|
||||
final bookData = event.bookData;
|
||||
|
||||
if (state.selectedBook != null) {
|
||||
// Edit existing book
|
||||
final updatedBooks = state.books.map((book) {
|
||||
if (book.id == state.selectedBook!.id) {
|
||||
return book.copyWith(
|
||||
title: bookData['title'] as String? ?? book.title,
|
||||
author: bookData['author'] as String? ?? book.author,
|
||||
genre: bookData['genre'] as String? ?? book.genre,
|
||||
annotation: bookData['annotation'] as String? ?? book.annotation,
|
||||
coverUrl: bookData['coverUrl'] as String?,
|
||||
pages: bookData['pages'] as int?,
|
||||
language: bookData['language'] as String?,
|
||||
publishedYear: bookData['publishedYear'] as int?,
|
||||
rating: bookData['rating'] as double?,
|
||||
status: bookData['status'] as BookStatus? ?? book.status,
|
||||
progress: bookData['progress'] as int?,
|
||||
isFavorite: bookData['isFavorite'] as bool? ?? book.isFavorite,
|
||||
);
|
||||
}
|
||||
return book;
|
||||
}).toList();
|
||||
|
||||
emit(state.copyWith(
|
||||
books: updatedBooks,
|
||||
currentScreen: AppScreen.library,
|
||||
selectedBook: null,
|
||||
prefilledData: null,
|
||||
));
|
||||
} else {
|
||||
// Add new book
|
||||
final newBook = createBook(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
title: bookData['title'] as String? ?? 'Unknown',
|
||||
author: bookData['author'] as String? ?? 'Unknown',
|
||||
genre: bookData['genre'] as String? ?? 'Unknown',
|
||||
annotation: bookData['annotation'] as String? ?? '',
|
||||
coverUrl: bookData['coverUrl'] as String? ??
|
||||
'https://picsum.photos/seed/${DateTime.now().millisecondsSinceEpoch}/400/600',
|
||||
pages: bookData['pages'] as int?,
|
||||
language: bookData['language'] as String?,
|
||||
publishedYear: bookData['publishedYear'] as int?,
|
||||
rating: bookData['rating'] as double?,
|
||||
status: bookData['status'] as BookStatus? ?? BookStatus.wantToRead,
|
||||
progress: bookData['progress'] as int?,
|
||||
isFavorite: bookData['isFavorite'] as bool? ?? false,
|
||||
);
|
||||
|
||||
emit(state.copyWith(
|
||||
books: [...state.books, newBook],
|
||||
currentScreen: AppScreen.library,
|
||||
selectedBook: null,
|
||||
prefilledData: null,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _onBookDeleted(BookDeleted event, Emitter<AppState> emit) {
|
||||
final updatedBooks = state.books.where((book) => book.id != event.id).toList();
|
||||
emit(state.copyWith(
|
||||
books: updatedBooks,
|
||||
currentScreen: AppScreen.library,
|
||||
selectedBook: null,
|
||||
));
|
||||
}
|
||||
|
||||
void _onBookDetected(BookDetected event, Emitter<AppState> emit) {
|
||||
emit(state.copyWith(
|
||||
prefilledData: event.bookData,
|
||||
currentScreen: AppScreen.addBook,
|
||||
));
|
||||
}
|
||||
|
||||
void _onSearchChanged(SearchChanged event, Emitter<AppState> emit) {
|
||||
emit(state.copyWith(searchQuery: event.query));
|
||||
}
|
||||
}
|
||||
39
bookshelf_flutter/lib/bloc/app_event.dart
Normal file
39
bookshelf_flutter/lib/bloc/app_event.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
import '../models/models.dart';
|
||||
|
||||
abstract class AppEvent {
|
||||
const AppEvent();
|
||||
}
|
||||
|
||||
class ScreenChanged extends AppEvent {
|
||||
final AppScreen screen;
|
||||
const ScreenChanged(this.screen);
|
||||
}
|
||||
|
||||
class BookClicked extends AppEvent {
|
||||
final Book book;
|
||||
const BookClicked(this.book);
|
||||
}
|
||||
|
||||
class AddBookClicked extends AppEvent {
|
||||
const AddBookClicked();
|
||||
}
|
||||
|
||||
class BookSaved extends AppEvent {
|
||||
final Map<String, dynamic> bookData;
|
||||
const BookSaved(this.bookData);
|
||||
}
|
||||
|
||||
class BookDeleted extends AppEvent {
|
||||
final String id;
|
||||
const BookDeleted(this.id);
|
||||
}
|
||||
|
||||
class BookDetected extends AppEvent {
|
||||
final Map<String, dynamic> bookData;
|
||||
const BookDetected(this.bookData);
|
||||
}
|
||||
|
||||
class SearchChanged extends AppEvent {
|
||||
final String query;
|
||||
const SearchChanged(this.query);
|
||||
}
|
||||
53
bookshelf_flutter/lib/bloc/app_state.dart
Normal file
53
bookshelf_flutter/lib/bloc/app_state.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../models/models.dart';
|
||||
|
||||
class AppState extends Equatable {
|
||||
final AppScreen currentScreen;
|
||||
final List<Book> books;
|
||||
final Book? selectedBook;
|
||||
final Map<String, dynamic>? prefilledData;
|
||||
final String searchQuery;
|
||||
final bool isLoading;
|
||||
final String? errorMessage;
|
||||
|
||||
const AppState({
|
||||
this.currentScreen = AppScreen.library,
|
||||
this.books = const [],
|
||||
this.selectedBook,
|
||||
this.prefilledData,
|
||||
this.searchQuery = '',
|
||||
this.isLoading = false,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
AppState copyWith({
|
||||
AppScreen? currentScreen,
|
||||
List<Book>? books,
|
||||
Book? selectedBook,
|
||||
Map<String, dynamic>? prefilledData,
|
||||
String? searchQuery,
|
||||
bool? isLoading,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return AppState(
|
||||
currentScreen: currentScreen ?? this.currentScreen,
|
||||
books: books ?? this.books,
|
||||
selectedBook: selectedBook,
|
||||
prefilledData: prefilledData ?? this.prefilledData,
|
||||
searchQuery: searchQuery ?? this.searchQuery,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
currentScreen,
|
||||
books,
|
||||
selectedBook,
|
||||
prefilledData,
|
||||
searchQuery,
|
||||
isLoading,
|
||||
errorMessage,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../models/models.dart';
|
||||
import 'book_details_event.dart';
|
||||
import 'book_details_state.dart';
|
||||
|
||||
class BookDetailsBloc extends Bloc<BookDetailsEvent, BookDetailsState> {
|
||||
BookDetailsBloc() : super(BookDetailsState.initial()) {
|
||||
on<LoadBookDetails>(_onLoadBookDetails);
|
||||
on<ToggleFavorite>(_onToggleFavorite);
|
||||
on<UpdateProgress>(_onUpdateProgress);
|
||||
on<UpdateStatus>(_onUpdateStatus);
|
||||
on<DeleteBook>(_onDeleteBook);
|
||||
}
|
||||
|
||||
void _onLoadBookDetails(LoadBookDetails event, Emitter<BookDetailsState> emit) {
|
||||
emit(state.copyWith(
|
||||
book: event.book,
|
||||
isLoading: false,
|
||||
));
|
||||
}
|
||||
|
||||
void _onToggleFavorite(ToggleFavorite event, Emitter<BookDetailsState> emit) {
|
||||
if (state.book == null) return;
|
||||
|
||||
final updatedBook = state.book!.copyWith(
|
||||
isFavorite: !state.book!.isFavorite,
|
||||
);
|
||||
|
||||
emit(state.copyWith(
|
||||
book: updatedBook,
|
||||
));
|
||||
}
|
||||
|
||||
void _onUpdateProgress(UpdateProgress event, Emitter<BookDetailsState> emit) {
|
||||
if (state.book == null) return;
|
||||
|
||||
final updatedBook = state.book!.copyWith(
|
||||
progress: event.progress,
|
||||
status: event.progress >= 100 ? BookStatus.done : BookStatus.reading,
|
||||
);
|
||||
|
||||
emit(state.copyWith(
|
||||
book: updatedBook,
|
||||
));
|
||||
}
|
||||
|
||||
void _onUpdateStatus(UpdateStatus event, Emitter<BookDetailsState> emit) {
|
||||
if (state.book == null) return;
|
||||
|
||||
final updatedBook = state.book!.copyWith(
|
||||
status: event.status,
|
||||
);
|
||||
|
||||
emit(state.copyWith(
|
||||
book: updatedBook,
|
||||
));
|
||||
}
|
||||
|
||||
void _onDeleteBook(DeleteBook event, Emitter<BookDetailsState> emit) {
|
||||
emit(state.copyWith(
|
||||
isDeleted: true,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import '../../models/models.dart';
|
||||
|
||||
abstract class BookDetailsEvent {
|
||||
const BookDetailsEvent();
|
||||
}
|
||||
|
||||
class LoadBookDetails extends BookDetailsEvent {
|
||||
final Book book;
|
||||
const LoadBookDetails(this.book);
|
||||
}
|
||||
|
||||
class ToggleFavorite extends BookDetailsEvent {
|
||||
const ToggleFavorite();
|
||||
}
|
||||
|
||||
class UpdateProgress extends BookDetailsEvent {
|
||||
final int progress;
|
||||
const UpdateProgress(this.progress);
|
||||
}
|
||||
|
||||
class UpdateStatus extends BookDetailsEvent {
|
||||
final BookStatus status;
|
||||
const UpdateStatus(this.status);
|
||||
}
|
||||
|
||||
class DeleteBook extends BookDetailsEvent {
|
||||
const DeleteBook();
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../models/models.dart';
|
||||
|
||||
class BookDetailsState extends Equatable {
|
||||
final Book? book;
|
||||
final bool isLoading;
|
||||
final bool isDeleted;
|
||||
final String? errorMessage;
|
||||
|
||||
const BookDetailsState({
|
||||
this.book,
|
||||
this.isLoading = false,
|
||||
this.isDeleted = false,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
factory BookDetailsState.initial() {
|
||||
return const BookDetailsState(
|
||||
isLoading: true,
|
||||
);
|
||||
}
|
||||
|
||||
BookDetailsState copyWith({
|
||||
Book? book,
|
||||
bool? isLoading,
|
||||
bool? isDeleted,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return BookDetailsState(
|
||||
book: book ?? this.book,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
isDeleted: isDeleted ?? this.isDeleted,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
book,
|
||||
isLoading,
|
||||
isDeleted,
|
||||
errorMessage,
|
||||
];
|
||||
}
|
||||
99
bookshelf_flutter/lib/bloc/categories/categories_bloc.dart
Normal file
99
bookshelf_flutter/lib/bloc/categories/categories_bloc.dart
Normal file
@@ -0,0 +1,99 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../models/models.dart';
|
||||
import 'categories_event.dart';
|
||||
import 'categories_state.dart';
|
||||
|
||||
class CategoriesBloc extends Bloc<CategoriesEvent, CategoriesState> {
|
||||
CategoriesBloc() : super(CategoriesState.initial()) {
|
||||
on<LoadCategories>(_onLoadCategories);
|
||||
on<SelectCategory>(_onSelectCategory);
|
||||
on<SearchCategories>(_onSearchCategories);
|
||||
}
|
||||
|
||||
void _onLoadCategories(LoadCategories event, Emitter<CategoriesState> emit) {
|
||||
final categories = _getCategories();
|
||||
emit(state.copyWith(
|
||||
categories: categories,
|
||||
filteredCategories: categories,
|
||||
isLoading: false,
|
||||
));
|
||||
}
|
||||
|
||||
void _onSelectCategory(SelectCategory event, Emitter<CategoriesState> emit) {
|
||||
emit(state.copyWith(selectedCategory: event.category));
|
||||
}
|
||||
|
||||
void _onSearchCategories(SearchCategories event, Emitter<CategoriesState> emit) {
|
||||
final query = event.query.toLowerCase();
|
||||
final filtered = state.categories.where((category) {
|
||||
return category.name.toLowerCase().contains(query);
|
||||
}).toList();
|
||||
|
||||
emit(state.copyWith(
|
||||
searchQuery: event.query,
|
||||
filteredCategories: filtered,
|
||||
));
|
||||
}
|
||||
|
||||
List<Category> _getCategories() {
|
||||
return [
|
||||
createCategory(
|
||||
id: '1',
|
||||
name: 'Classic',
|
||||
count: 15,
|
||||
icon: Icons.category,
|
||||
colorClass: 'category-classic',
|
||||
),
|
||||
createCategory(
|
||||
id: '2',
|
||||
name: 'Sci-Fi',
|
||||
count: 8,
|
||||
icon: Icons.rocket_launch,
|
||||
colorClass: 'category-scifi',
|
||||
),
|
||||
createCategory(
|
||||
id: '3',
|
||||
name: 'Fantasy',
|
||||
count: 12,
|
||||
icon: Icons.auto_awesome,
|
||||
colorClass: 'category-fantasy',
|
||||
),
|
||||
createCategory(
|
||||
id: '4',
|
||||
name: 'Mystery',
|
||||
count: 6,
|
||||
icon: Icons.search,
|
||||
colorClass: 'category-mystery',
|
||||
),
|
||||
createCategory(
|
||||
id: '5',
|
||||
name: 'Romance',
|
||||
count: 10,
|
||||
icon: Icons.favorite,
|
||||
colorClass: 'category-romance',
|
||||
),
|
||||
createCategory(
|
||||
id: '6',
|
||||
name: 'Non-fiction',
|
||||
count: 9,
|
||||
icon: Icons.book,
|
||||
colorClass: 'category-nonfiction',
|
||||
),
|
||||
createCategory(
|
||||
id: '7',
|
||||
name: 'Dystopian',
|
||||
count: 4,
|
||||
icon: Icons.nightlife,
|
||||
colorClass: 'category-dystopian',
|
||||
),
|
||||
createCategory(
|
||||
id: '8',
|
||||
name: 'Adventure',
|
||||
count: 7,
|
||||
icon: Icons.explore,
|
||||
colorClass: 'category-adventure',
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
19
bookshelf_flutter/lib/bloc/categories/categories_event.dart
Normal file
19
bookshelf_flutter/lib/bloc/categories/categories_event.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import '../../models/models.dart';
|
||||
|
||||
abstract class CategoriesEvent {
|
||||
const CategoriesEvent();
|
||||
}
|
||||
|
||||
class LoadCategories extends CategoriesEvent {
|
||||
const LoadCategories();
|
||||
}
|
||||
|
||||
class SelectCategory extends CategoriesEvent {
|
||||
final Category category;
|
||||
const SelectCategory(this.category);
|
||||
}
|
||||
|
||||
class SearchCategories extends CategoriesEvent {
|
||||
final String query;
|
||||
const SearchCategories(this.query);
|
||||
}
|
||||
54
bookshelf_flutter/lib/bloc/categories/categories_state.dart
Normal file
54
bookshelf_flutter/lib/bloc/categories/categories_state.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../models/models.dart';
|
||||
|
||||
class CategoriesState extends Equatable {
|
||||
final List<Category> categories;
|
||||
final List<Category> filteredCategories;
|
||||
final Category? selectedCategory;
|
||||
final String searchQuery;
|
||||
final bool isLoading;
|
||||
final String? errorMessage;
|
||||
|
||||
const CategoriesState({
|
||||
this.categories = const [],
|
||||
this.filteredCategories = const [],
|
||||
this.selectedCategory,
|
||||
this.searchQuery = '',
|
||||
this.isLoading = false,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
factory CategoriesState.initial() {
|
||||
return const CategoriesState(
|
||||
isLoading: true,
|
||||
);
|
||||
}
|
||||
|
||||
CategoriesState copyWith({
|
||||
List<Category>? categories,
|
||||
List<Category>? filteredCategories,
|
||||
Category? selectedCategory,
|
||||
String? searchQuery,
|
||||
bool? isLoading,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return CategoriesState(
|
||||
categories: categories ?? this.categories,
|
||||
filteredCategories: filteredCategories ?? this.filteredCategories,
|
||||
selectedCategory: selectedCategory,
|
||||
searchQuery: searchQuery ?? this.searchQuery,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
categories,
|
||||
filteredCategories,
|
||||
selectedCategory,
|
||||
searchQuery,
|
||||
isLoading,
|
||||
errorMessage,
|
||||
];
|
||||
}
|
||||
22
bookshelf_flutter/lib/bloc/home/home_bloc.dart
Normal file
22
bookshelf_flutter/lib/bloc/home/home_bloc.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../models/models.dart';
|
||||
import 'home_event.dart';
|
||||
import 'home_state.dart';
|
||||
|
||||
class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
HomeBloc() : super(HomeState.initial()) {
|
||||
on<LoadHomeData>(_onLoadHomeData);
|
||||
on<NavigateToScreen>(_onNavigateToScreen);
|
||||
}
|
||||
|
||||
void _onLoadHomeData(LoadHomeData event, Emitter<HomeState> emit) {
|
||||
emit(state.copyWith(
|
||||
currentScreen: AppScreen.library,
|
||||
isLoading: false,
|
||||
));
|
||||
}
|
||||
|
||||
void _onNavigateToScreen(NavigateToScreen event, Emitter<HomeState> emit) {
|
||||
emit(state.copyWith(currentScreen: event.screen));
|
||||
}
|
||||
}
|
||||
14
bookshelf_flutter/lib/bloc/home/home_event.dart
Normal file
14
bookshelf_flutter/lib/bloc/home/home_event.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import '../../models/models.dart';
|
||||
|
||||
abstract class HomeEvent {
|
||||
const HomeEvent();
|
||||
}
|
||||
|
||||
class LoadHomeData extends HomeEvent {
|
||||
const LoadHomeData();
|
||||
}
|
||||
|
||||
class NavigateToScreen extends HomeEvent {
|
||||
final AppScreen screen;
|
||||
const NavigateToScreen(this.screen);
|
||||
}
|
||||
39
bookshelf_flutter/lib/bloc/home/home_state.dart
Normal file
39
bookshelf_flutter/lib/bloc/home/home_state.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../models/models.dart';
|
||||
|
||||
class HomeState extends Equatable {
|
||||
final AppScreen currentScreen;
|
||||
final bool isLoading;
|
||||
final String? errorMessage;
|
||||
|
||||
const HomeState({
|
||||
this.currentScreen = AppScreen.library,
|
||||
this.isLoading = false,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
factory HomeState.initial() {
|
||||
return const HomeState(
|
||||
isLoading: true,
|
||||
);
|
||||
}
|
||||
|
||||
HomeState copyWith({
|
||||
AppScreen? currentScreen,
|
||||
bool? isLoading,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return HomeState(
|
||||
currentScreen: currentScreen ?? this.currentScreen,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
currentScreen,
|
||||
isLoading,
|
||||
errorMessage,
|
||||
];
|
||||
}
|
||||
124
bookshelf_flutter/lib/bloc/library/library_bloc.dart
Normal file
124
bookshelf_flutter/lib/bloc/library/library_bloc.dart
Normal file
@@ -0,0 +1,124 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../models/models.dart';
|
||||
import 'library_event.dart';
|
||||
import 'library_state.dart';
|
||||
|
||||
class LibraryBloc extends Bloc<LibraryEvent, LibraryState> {
|
||||
LibraryBloc() : super(LibraryState.initial()) {
|
||||
on<LoadBooks>(_onLoadBooks);
|
||||
on<SearchBooks>(_onSearchBooks);
|
||||
on<BookSelected>(_onBookSelected);
|
||||
on<FilterByStatus>(_onFilterByStatus);
|
||||
on<ClearFilters>(_onClearFilters);
|
||||
}
|
||||
|
||||
static List<Book> get _initialBooks => [
|
||||
createBook(
|
||||
id: '1',
|
||||
title: 'Великий Гэтсби',
|
||||
author: 'Ф. Скотт Фицджеральд',
|
||||
genre: 'Classic',
|
||||
annotation:
|
||||
'История о несбывшейся любви и трагедии американской мечты на фоне бурных двадцатых годов.',
|
||||
coverUrl: 'https://picsum.photos/seed/gatsby/400/600',
|
||||
pages: 208,
|
||||
language: 'English',
|
||||
publishedYear: 1925,
|
||||
rating: 4.8,
|
||||
status: BookStatus.reading,
|
||||
progress: 45,
|
||||
isFavorite: true,
|
||||
),
|
||||
createBook(
|
||||
id: '2',
|
||||
title: '1984',
|
||||
author: 'Джордж Оруэлл',
|
||||
genre: 'Dystopian',
|
||||
annotation:
|
||||
'Антиутопия о тоталитарном государстве, где мысли контролируются, а правда переменчива.',
|
||||
coverUrl: 'https://picsum.photos/seed/1984/400/600',
|
||||
pages: 328,
|
||||
language: 'English',
|
||||
publishedYear: 1949,
|
||||
rating: 4.9,
|
||||
status: BookStatus.wantToRead,
|
||||
isFavorite: true,
|
||||
),
|
||||
createBook(
|
||||
id: '3',
|
||||
title: 'Дюна',
|
||||
author: 'Фрэнк Герберт',
|
||||
genre: 'Sci-Fi',
|
||||
annotation:
|
||||
'Эпическая сага о борьбе за власть над самой важной планетой во Вселенной.',
|
||||
coverUrl: 'https://picsum.photos/seed/dune/400/600',
|
||||
pages: 896,
|
||||
language: 'English',
|
||||
publishedYear: 1965,
|
||||
rating: 4.7,
|
||||
status: BookStatus.reading,
|
||||
progress: 12,
|
||||
isFavorite: false,
|
||||
),
|
||||
createBook(
|
||||
id: '4',
|
||||
title: 'Хоббит',
|
||||
author: 'Дж. Р. Р. Толкин',
|
||||
genre: 'Fantasy',
|
||||
annotation:
|
||||
'Путешествие Бильбо Бэггинса туда и обратно в поисках сокровищ гномов.',
|
||||
coverUrl: 'https://picsum.photos/seed/hobbit/400/600',
|
||||
pages: 310,
|
||||
language: 'English',
|
||||
publishedYear: 1937,
|
||||
rating: 4.9,
|
||||
status: BookStatus.done,
|
||||
isFavorite: false,
|
||||
),
|
||||
];
|
||||
|
||||
void _onLoadBooks(LoadBooks event, Emitter<LibraryState> emit) {
|
||||
emit(state.copyWith(
|
||||
books: _initialBooks,
|
||||
filteredBooks: _initialBooks,
|
||||
isLoading: false,
|
||||
));
|
||||
}
|
||||
|
||||
void _onSearchBooks(SearchBooks event, Emitter<LibraryState> emit) {
|
||||
final query = event.query.toLowerCase();
|
||||
final filtered = state.books.where((book) {
|
||||
return book.title.toLowerCase().contains(query) ||
|
||||
book.author.toLowerCase().contains(query) ||
|
||||
book.genre.toLowerCase().contains(query);
|
||||
}).toList();
|
||||
|
||||
emit(state.copyWith(
|
||||
searchQuery: event.query,
|
||||
filteredBooks: filtered,
|
||||
));
|
||||
}
|
||||
|
||||
void _onBookSelected(BookSelected event, Emitter<LibraryState> emit) {
|
||||
emit(state.copyWith(selectedBook: event.book));
|
||||
}
|
||||
|
||||
void _onFilterByStatus(FilterByStatus event, Emitter<LibraryState> emit) {
|
||||
final filtered = state.books.where((book) {
|
||||
return book.status == event.status;
|
||||
}).toList();
|
||||
|
||||
emit(state.copyWith(
|
||||
statusFilter: event.status,
|
||||
filteredBooks: filtered,
|
||||
));
|
||||
}
|
||||
|
||||
void _onClearFilters(ClearFilters event, Emitter<LibraryState> emit) {
|
||||
emit(state.copyWith(
|
||||
searchQuery: '',
|
||||
statusFilter: null,
|
||||
filteredBooks: state.books,
|
||||
));
|
||||
}
|
||||
}
|
||||
28
bookshelf_flutter/lib/bloc/library/library_event.dart
Normal file
28
bookshelf_flutter/lib/bloc/library/library_event.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
import '../../models/models.dart';
|
||||
|
||||
abstract class LibraryEvent {
|
||||
const LibraryEvent();
|
||||
}
|
||||
|
||||
class LoadBooks extends LibraryEvent {
|
||||
const LoadBooks();
|
||||
}
|
||||
|
||||
class SearchBooks extends LibraryEvent {
|
||||
final String query;
|
||||
const SearchBooks(this.query);
|
||||
}
|
||||
|
||||
class BookSelected extends LibraryEvent {
|
||||
final Book book;
|
||||
const BookSelected(this.book);
|
||||
}
|
||||
|
||||
class FilterByStatus extends LibraryEvent {
|
||||
final BookStatus status;
|
||||
const FilterByStatus(this.status);
|
||||
}
|
||||
|
||||
class ClearFilters extends LibraryEvent {
|
||||
const ClearFilters();
|
||||
}
|
||||
59
bookshelf_flutter/lib/bloc/library/library_state.dart
Normal file
59
bookshelf_flutter/lib/bloc/library/library_state.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../models/models.dart';
|
||||
|
||||
class LibraryState extends Equatable {
|
||||
final List<Book> books;
|
||||
final List<Book> filteredBooks;
|
||||
final Book? selectedBook;
|
||||
final String searchQuery;
|
||||
final BookStatus? statusFilter;
|
||||
final bool isLoading;
|
||||
final String? errorMessage;
|
||||
|
||||
const LibraryState({
|
||||
this.books = const [],
|
||||
this.filteredBooks = const [],
|
||||
this.selectedBook,
|
||||
this.searchQuery = '',
|
||||
this.statusFilter,
|
||||
this.isLoading = false,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
factory LibraryState.initial() {
|
||||
return const LibraryState(
|
||||
isLoading: true,
|
||||
);
|
||||
}
|
||||
|
||||
LibraryState copyWith({
|
||||
List<Book>? books,
|
||||
List<Book>? filteredBooks,
|
||||
Book? selectedBook,
|
||||
String? searchQuery,
|
||||
BookStatus? statusFilter,
|
||||
bool? isLoading,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return LibraryState(
|
||||
books: books ?? this.books,
|
||||
filteredBooks: filteredBooks ?? this.filteredBooks,
|
||||
selectedBook: selectedBook,
|
||||
searchQuery: searchQuery ?? this.searchQuery,
|
||||
statusFilter: statusFilter ?? this.statusFilter,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
books,
|
||||
filteredBooks,
|
||||
selectedBook,
|
||||
searchQuery,
|
||||
statusFilter,
|
||||
isLoading,
|
||||
errorMessage,
|
||||
];
|
||||
}
|
||||
40
bookshelf_flutter/lib/bloc/scanner/scanner_bloc.dart
Normal file
40
bookshelf_flutter/lib/bloc/scanner/scanner_bloc.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'scanner_event.dart';
|
||||
import 'scanner_state.dart';
|
||||
|
||||
class ScannerBloc extends Bloc<ScannerEvent, ScannerState> {
|
||||
ScannerBloc() : super(ScannerState.initial()) {
|
||||
on<StartScanning>(_onStartScanning);
|
||||
on<StopScanning>(_onStopScanning);
|
||||
on<BookDetected>(_onBookDetected);
|
||||
on<ClearDetectedBook>(_onClearDetectedBook);
|
||||
}
|
||||
|
||||
void _onStartScanning(StartScanning event, Emitter<ScannerState> emit) {
|
||||
emit(state.copyWith(
|
||||
isScanning: true,
|
||||
isProcessing: false,
|
||||
errorMessage: null,
|
||||
));
|
||||
}
|
||||
|
||||
void _onStopScanning(StopScanning event, Emitter<ScannerState> emit) {
|
||||
emit(state.copyWith(isScanning: false));
|
||||
}
|
||||
|
||||
void _onBookDetected(BookDetected event, Emitter<ScannerState> emit) {
|
||||
emit(state.copyWith(
|
||||
detectedBookData: event.bookData,
|
||||
isProcessing: true,
|
||||
isScanning: false,
|
||||
));
|
||||
}
|
||||
|
||||
void _onClearDetectedBook(ClearDetectedBook event, Emitter<ScannerState> emit) {
|
||||
emit(state.copyWith(
|
||||
isScanning: false,
|
||||
clearDetectedBookData: true,
|
||||
isProcessing: false,
|
||||
));
|
||||
}
|
||||
}
|
||||
20
bookshelf_flutter/lib/bloc/scanner/scanner_event.dart
Normal file
20
bookshelf_flutter/lib/bloc/scanner/scanner_event.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
abstract class ScannerEvent {
|
||||
const ScannerEvent();
|
||||
}
|
||||
|
||||
class StartScanning extends ScannerEvent {
|
||||
const StartScanning();
|
||||
}
|
||||
|
||||
class StopScanning extends ScannerEvent {
|
||||
const StopScanning();
|
||||
}
|
||||
|
||||
class BookDetected extends ScannerEvent {
|
||||
final Map<String, dynamic> bookData;
|
||||
const BookDetected(this.bookData);
|
||||
}
|
||||
|
||||
class ClearDetectedBook extends ScannerEvent {
|
||||
const ClearDetectedBook();
|
||||
}
|
||||
42
bookshelf_flutter/lib/bloc/scanner/scanner_state.dart
Normal file
42
bookshelf_flutter/lib/bloc/scanner/scanner_state.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class ScannerState extends Equatable {
|
||||
final bool isScanning;
|
||||
final bool isProcessing;
|
||||
final Map<String, dynamic>? detectedBookData;
|
||||
final String? errorMessage;
|
||||
|
||||
const ScannerState({
|
||||
this.isScanning = false,
|
||||
this.isProcessing = false,
|
||||
this.detectedBookData,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
factory ScannerState.initial() {
|
||||
return const ScannerState();
|
||||
}
|
||||
|
||||
ScannerState copyWith({
|
||||
bool? isScanning,
|
||||
bool? isProcessing,
|
||||
Map<String, dynamic>? detectedBookData,
|
||||
String? errorMessage,
|
||||
bool clearDetectedBookData = false,
|
||||
}) {
|
||||
return ScannerState(
|
||||
isScanning: isScanning ?? this.isScanning,
|
||||
isProcessing: isProcessing ?? this.isProcessing,
|
||||
detectedBookData: clearDetectedBookData ? null : (detectedBookData ?? this.detectedBookData),
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
isScanning,
|
||||
isProcessing,
|
||||
detectedBookData,
|
||||
errorMessage,
|
||||
];
|
||||
}
|
||||
39
bookshelf_flutter/lib/bloc/settings/settings_bloc.dart
Normal file
39
bookshelf_flutter/lib/bloc/settings/settings_bloc.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'settings_event.dart';
|
||||
import 'settings_state.dart';
|
||||
|
||||
class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
|
||||
SettingsBloc() : super(SettingsState.initial()) {
|
||||
on<LoadSettings>(_onLoadSettings);
|
||||
on<UpdateTheme>(_onUpdateTheme);
|
||||
on<UpdateLanguage>(_onUpdateLanguage);
|
||||
on<ToggleNotifications>(_onToggleNotifications);
|
||||
on<ClearData>(_onClearData);
|
||||
}
|
||||
|
||||
void _onLoadSettings(LoadSettings event, Emitter<SettingsState> emit) {
|
||||
emit(state.copyWith(
|
||||
isDarkMode: false,
|
||||
language: 'English',
|
||||
notificationsEnabled: true,
|
||||
isLoading: false,
|
||||
));
|
||||
}
|
||||
|
||||
void _onUpdateTheme(UpdateTheme event, Emitter<SettingsState> emit) {
|
||||
emit(state.copyWith(isDarkMode: event.isDarkMode));
|
||||
}
|
||||
|
||||
void _onUpdateLanguage(UpdateLanguage event, Emitter<SettingsState> emit) {
|
||||
emit(state.copyWith(language: event.language));
|
||||
}
|
||||
|
||||
void _onToggleNotifications(
|
||||
ToggleNotifications event, Emitter<SettingsState> emit) {
|
||||
emit(state.copyWith(notificationsEnabled: !state.notificationsEnabled));
|
||||
}
|
||||
|
||||
void _onClearData(ClearData event, Emitter<SettingsState> emit) {
|
||||
emit(state.copyWith(dataCleared: true));
|
||||
}
|
||||
}
|
||||
25
bookshelf_flutter/lib/bloc/settings/settings_event.dart
Normal file
25
bookshelf_flutter/lib/bloc/settings/settings_event.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
abstract class SettingsEvent {
|
||||
const SettingsEvent();
|
||||
}
|
||||
|
||||
class LoadSettings extends SettingsEvent {
|
||||
const LoadSettings();
|
||||
}
|
||||
|
||||
class UpdateTheme extends SettingsEvent {
|
||||
final bool isDarkMode;
|
||||
const UpdateTheme(this.isDarkMode);
|
||||
}
|
||||
|
||||
class UpdateLanguage extends SettingsEvent {
|
||||
final String language;
|
||||
const UpdateLanguage(this.language);
|
||||
}
|
||||
|
||||
class ToggleNotifications extends SettingsEvent {
|
||||
const ToggleNotifications();
|
||||
}
|
||||
|
||||
class ClearData extends SettingsEvent {
|
||||
const ClearData();
|
||||
}
|
||||
53
bookshelf_flutter/lib/bloc/settings/settings_state.dart
Normal file
53
bookshelf_flutter/lib/bloc/settings/settings_state.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class SettingsState extends Equatable {
|
||||
final bool isDarkMode;
|
||||
final String language;
|
||||
final bool notificationsEnabled;
|
||||
final bool dataCleared;
|
||||
final bool isLoading;
|
||||
final String? errorMessage;
|
||||
|
||||
const SettingsState({
|
||||
this.isDarkMode = false,
|
||||
this.language = 'English',
|
||||
this.notificationsEnabled = true,
|
||||
this.dataCleared = false,
|
||||
this.isLoading = false,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
factory SettingsState.initial() {
|
||||
return const SettingsState(
|
||||
isLoading: true,
|
||||
);
|
||||
}
|
||||
|
||||
SettingsState copyWith({
|
||||
bool? isDarkMode,
|
||||
String? language,
|
||||
bool? notificationsEnabled,
|
||||
bool? dataCleared,
|
||||
bool? isLoading,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return SettingsState(
|
||||
isDarkMode: isDarkMode ?? this.isDarkMode,
|
||||
language: language ?? this.language,
|
||||
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
|
||||
dataCleared: dataCleared ?? this.dataCleared,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
isDarkMode,
|
||||
language,
|
||||
notificationsEnabled,
|
||||
dataCleared,
|
||||
isLoading,
|
||||
errorMessage,
|
||||
];
|
||||
}
|
||||
93
bookshelf_flutter/lib/bloc/wishlist/wishlist_bloc.dart
Normal file
93
bookshelf_flutter/lib/bloc/wishlist/wishlist_bloc.dart
Normal file
@@ -0,0 +1,93 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../models/models.dart';
|
||||
import 'wishlist_event.dart';
|
||||
import 'wishlist_state.dart';
|
||||
|
||||
class WishlistBloc extends Bloc<WishlistEvent, WishlistState> {
|
||||
WishlistBloc() : super(WishlistState.initial()) {
|
||||
on<LoadWishlist>(_onLoadWishlist);
|
||||
on<RemoveFromWishlist>(_onRemoveFromWishlist);
|
||||
on<SearchWishlist>(_onSearchWishlist);
|
||||
on<MoveToLibrary>(_onMoveToLibrary);
|
||||
}
|
||||
|
||||
static List<Book> get _initialBooks => [
|
||||
createBook(
|
||||
id: '2',
|
||||
title: '1984',
|
||||
author: 'Джордж Оруэлл',
|
||||
genre: 'Dystopian',
|
||||
annotation:
|
||||
'Антиутопия о тоталитарном государстве, где мысли контролируются, а правда переменчива.',
|
||||
coverUrl: 'https://picsum.photos/seed/1984/400/600',
|
||||
pages: 328,
|
||||
language: 'English',
|
||||
publishedYear: 1949,
|
||||
rating: 4.9,
|
||||
status: BookStatus.wantToRead,
|
||||
isFavorite: true,
|
||||
),
|
||||
];
|
||||
|
||||
void _onLoadWishlist(LoadWishlist event, Emitter<WishlistState> emit) {
|
||||
final wishlistBooks = _initialBooks.where((book) {
|
||||
return book.status == BookStatus.wantToRead || book.isFavorite;
|
||||
}).toList();
|
||||
|
||||
emit(state.copyWith(
|
||||
books: wishlistBooks,
|
||||
filteredBooks: wishlistBooks,
|
||||
isLoading: false,
|
||||
));
|
||||
}
|
||||
|
||||
void _onRemoveFromWishlist(RemoveFromWishlist event, Emitter<WishlistState> emit) {
|
||||
final updatedBooks = state.books.where((book) => book.id != event.bookId).toList();
|
||||
final updatedFiltered = state.filteredBooks.where((book) => book.id != event.bookId).toList();
|
||||
|
||||
emit(state.copyWith(
|
||||
books: updatedBooks,
|
||||
filteredBooks: updatedFiltered,
|
||||
));
|
||||
}
|
||||
|
||||
void _onSearchWishlist(SearchWishlist event, Emitter<WishlistState> emit) {
|
||||
final query = event.query.toLowerCase();
|
||||
final filtered = state.books.where((book) {
|
||||
return book.title.toLowerCase().contains(query) ||
|
||||
book.author.toLowerCase().contains(query) ||
|
||||
book.genre.toLowerCase().contains(query);
|
||||
}).toList();
|
||||
|
||||
emit(state.copyWith(
|
||||
searchQuery: event.query,
|
||||
filteredBooks: filtered,
|
||||
));
|
||||
}
|
||||
|
||||
void _onMoveToLibrary(MoveToLibrary event, Emitter<WishlistState> emit) {
|
||||
final updatedBooks = state.books.map((book) {
|
||||
if (book.id == event.bookId) {
|
||||
return book.copyWith(
|
||||
status: BookStatus.reading,
|
||||
);
|
||||
}
|
||||
return book;
|
||||
}).toList();
|
||||
|
||||
final updatedFiltered = state.filteredBooks.map((book) {
|
||||
if (book.id == event.bookId) {
|
||||
return book.copyWith(
|
||||
status: BookStatus.reading,
|
||||
);
|
||||
}
|
||||
return book;
|
||||
}).toList();
|
||||
|
||||
emit(state.copyWith(
|
||||
books: updatedBooks,
|
||||
filteredBooks: updatedFiltered,
|
||||
movedBookId: event.bookId,
|
||||
));
|
||||
}
|
||||
}
|
||||
22
bookshelf_flutter/lib/bloc/wishlist/wishlist_event.dart
Normal file
22
bookshelf_flutter/lib/bloc/wishlist/wishlist_event.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
abstract class WishlistEvent {
|
||||
const WishlistEvent();
|
||||
}
|
||||
|
||||
class LoadWishlist extends WishlistEvent {
|
||||
const LoadWishlist();
|
||||
}
|
||||
|
||||
class RemoveFromWishlist extends WishlistEvent {
|
||||
final String bookId;
|
||||
const RemoveFromWishlist(this.bookId);
|
||||
}
|
||||
|
||||
class SearchWishlist extends WishlistEvent {
|
||||
final String query;
|
||||
const SearchWishlist(this.query);
|
||||
}
|
||||
|
||||
class MoveToLibrary extends WishlistEvent {
|
||||
final String bookId;
|
||||
const MoveToLibrary(this.bookId);
|
||||
}
|
||||
54
bookshelf_flutter/lib/bloc/wishlist/wishlist_state.dart
Normal file
54
bookshelf_flutter/lib/bloc/wishlist/wishlist_state.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../models/models.dart';
|
||||
|
||||
class WishlistState extends Equatable {
|
||||
final List<Book> books;
|
||||
final List<Book> filteredBooks;
|
||||
final String searchQuery;
|
||||
final String? movedBookId;
|
||||
final bool isLoading;
|
||||
final String? errorMessage;
|
||||
|
||||
const WishlistState({
|
||||
this.books = const [],
|
||||
this.filteredBooks = const [],
|
||||
this.searchQuery = '',
|
||||
this.movedBookId,
|
||||
this.isLoading = false,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
factory WishlistState.initial() {
|
||||
return const WishlistState(
|
||||
isLoading: true,
|
||||
);
|
||||
}
|
||||
|
||||
WishlistState copyWith({
|
||||
List<Book>? books,
|
||||
List<Book>? filteredBooks,
|
||||
String? searchQuery,
|
||||
String? movedBookId,
|
||||
bool? isLoading,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return WishlistState(
|
||||
books: books ?? this.books,
|
||||
filteredBooks: filteredBooks ?? this.filteredBooks,
|
||||
searchQuery: searchQuery ?? this.searchQuery,
|
||||
movedBookId: movedBookId,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
books,
|
||||
filteredBooks,
|
||||
searchQuery,
|
||||
movedBookId,
|
||||
isLoading,
|
||||
errorMessage,
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user