lab6 done
This commit is contained in:
parent
76dcbb954b
commit
1c46c1c0fb
20
lib/components/utils/debounce.dart
Normal file
20
lib/components/utils/debounce.dart
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
class Debounce {
|
||||||
|
factory Debounce() => _instance;
|
||||||
|
|
||||||
|
Debounce._();
|
||||||
|
|
||||||
|
static final Debounce _instance = Debounce._();
|
||||||
|
|
||||||
|
static Timer? _timer;
|
||||||
|
|
||||||
|
static void run(
|
||||||
|
VoidCallback action, {
|
||||||
|
Duration delay = const Duration(milliseconds: 1000),
|
||||||
|
}) {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = Timer(delay, action);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
|
import '../../domain/quote.dart';
|
||||||
import '/data/dtos/quotes_dto.dart';
|
import '/data/dtos/quotes_dto.dart';
|
||||||
import '/main.dart';
|
import '/presentation/home_page/home_page.dart';
|
||||||
|
|
||||||
const _imagePlaceholder =
|
const _imagePlaceholder =
|
||||||
'https://cdn-icons-png.flaticon.com/128/17818/17818874.png';
|
'https://cdn-icons-png.flaticon.com/128/17818/17818874.png';
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import '/main.dart';
|
import '../../domain/quote.dart';
|
||||||
|
import '/presentation/home_page/home_page.dart';
|
||||||
|
|
||||||
typedef OnErrorCallback = void Function(String? error);
|
typedef OnErrorCallback = void Function(String? error);
|
||||||
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import '../../domain/quote.dart';
|
||||||
import '/data/dtos/quotes_dto.dart';
|
import '/data/dtos/quotes_dto.dart';
|
||||||
import '/data/mappers/quotes_mapper.dart';
|
import '/data/mappers/quotes_mapper.dart';
|
||||||
import '/data/repositories/api_interface.dart';
|
import '/data/repositories/api_interface.dart';
|
||||||
import '/main.dart';
|
import '/presentation/home_page/home_page.dart';
|
||||||
import '/data/presentation/dialogs/show_dialog.dart';
|
import '/presentation/dialogs/show_dialog.dart';
|
||||||
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
||||||
|
|
||||||
class QuotesRepository extends ApiInterface {
|
class QuotesRepository extends ApiInterface {
|
||||||
@ -23,12 +24,12 @@ class QuotesRepository extends ApiInterface {
|
|||||||
try {
|
try {
|
||||||
final Map<String, dynamic> queryParams = {};
|
final Map<String, dynamic> queryParams = {};
|
||||||
|
|
||||||
// Добавляем фильтрацию по тексту цитаты
|
|
||||||
if (q != null && q.isNotEmpty) {
|
if (q != null && q.isNotEmpty) {
|
||||||
queryParams['filter'] = q; // Фильтруем по слову
|
queryParams['filter'] = q;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем фильтрацию по автору
|
|
||||||
if (author != null && author.isNotEmpty) {
|
if (author != null && author.isNotEmpty) {
|
||||||
queryParams['type'] = 'author';
|
queryParams['type'] = 'author';
|
||||||
queryParams['filter'] = author;
|
queryParams['filter'] = author;
|
||||||
|
12
lib/domain/quote.dart
Normal file
12
lib/domain/quote.dart
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
class Quote {
|
||||||
|
final String text;
|
||||||
|
final String author;
|
||||||
|
final String imagePath;
|
||||||
|
bool isFavorite;
|
||||||
|
|
||||||
|
Quote(this.text, this.author, this.imagePath, [this.isFavorite = false]);
|
||||||
|
|
||||||
|
void toggleFavorite() {
|
||||||
|
isFavorite = !isFavorite;
|
||||||
|
}
|
||||||
|
}
|
270
lib/main.dart
270
lib/main.dart
@ -1,5 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '/data/repositories/quotes_repository.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'data/repositories/quotes_repository.dart';
|
||||||
|
import '/presentation/home_page/bloc/bloc.dart';
|
||||||
|
import '/presentation/home_page/home_page.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
@ -16,266 +19,15 @@ class MyApp extends StatelessWidget {
|
|||||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
),
|
),
|
||||||
home: const MyHomePage(title: 'Цитаты'),
|
home: RepositoryProvider<QuotesRepository>(
|
||||||
);
|
lazy: true,
|
||||||
}
|
create: (_) => QuotesRepository(),
|
||||||
}
|
child: BlocProvider<HomeBloc>(
|
||||||
|
lazy: false,
|
||||||
class Quote {
|
create: (context) => HomeBloc(context.read<QuotesRepository>()),
|
||||||
final String text;
|
child: const MyHomePage(title: "Цитаты",),
|
||||||
final String author;
|
|
||||||
final String imagePath;
|
|
||||||
bool isFavorite;
|
|
||||||
|
|
||||||
Quote(this.text, this.author, this.imagePath, [this.isFavorite = false]);
|
|
||||||
|
|
||||||
void toggleFavorite() {
|
|
||||||
isFavorite = !isFavorite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MyHomePage extends StatefulWidget {
|
|
||||||
const MyHomePage({super.key, required this.title});
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<MyHomePage> createState() => _MyHomePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MyHomePageState extends State<MyHomePage> {
|
|
||||||
final List<Quote> _quotes = [];
|
|
||||||
final List<Quote> _filteredQuotes = [];
|
|
||||||
|
|
||||||
final QuotesRepository _quotesRepository = QuotesRepository();
|
|
||||||
|
|
||||||
final TextEditingController searchController = TextEditingController();
|
|
||||||
final TextEditingController authorController = TextEditingController();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_loadQuotes();
|
|
||||||
|
|
||||||
searchController.addListener(() {
|
|
||||||
_loadQuotes();
|
|
||||||
});
|
|
||||||
|
|
||||||
authorController.addListener(() {
|
|
||||||
_loadQuotes();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Метод для загрузки цитат с учетом обоих фильтров
|
|
||||||
Future<void> _loadQuotes() async {
|
|
||||||
final query = searchController.text;
|
|
||||||
final author = authorController.text;
|
|
||||||
|
|
||||||
final quotes = await _quotesRepository.loadData(
|
|
||||||
q: query, // Поиск по цитате
|
|
||||||
author: author, // Поиск по автору
|
|
||||||
onError: (error) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('Ошибка загрузки: $error')),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (quotes != null) {
|
|
||||||
setState(() {
|
|
||||||
_quotes.clear();
|
|
||||||
_quotes.addAll(quotes);
|
|
||||||
_filterQuotes();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
print('Загружено цитат: ${_quotes.length}');
|
|
||||||
print('Отфильтрованные цитаты: ${_filteredQuotes.length}');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Фильтрация цитат по введенным значениям
|
|
||||||
void _filterQuotes() {
|
|
||||||
setState(() {
|
|
||||||
_filteredQuotes.clear(); // Очистите список фильтрации
|
|
||||||
_filteredQuotes.addAll(_quotes); // Изначально показываем все цитаты
|
|
||||||
|
|
||||||
// Применяем фильтрацию по цитате
|
|
||||||
if (searchController.text.isNotEmpty) {
|
|
||||||
_filteredQuotes.retainWhere((quote) =>
|
|
||||||
quote.text.toLowerCase().contains(searchController.text.toLowerCase()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Применяем фильтрацию по автору
|
|
||||||
if (authorController.text.isNotEmpty) {
|
|
||||||
_filteredQuotes.retainWhere((quote) =>
|
|
||||||
quote.author.toLowerCase().contains(authorController.text.toLowerCase()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text('Цитаты'),
|
|
||||||
),
|
|
||||||
body: Column(
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: TextField(
|
|
||||||
controller: searchController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Поиск по цитате',
|
|
||||||
prefixIcon: Icon(Icons.search),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: TextField(
|
|
||||||
controller: authorController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Поиск по автору',
|
|
||||||
prefixIcon: Icon(Icons.person),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: _filteredQuotes.isEmpty
|
|
||||||
? const Center(
|
|
||||||
child: Text(
|
|
||||||
'Нет цитат для отображения.',
|
|
||||||
style: TextStyle(fontSize: 18, color: Colors.grey),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: ListView.builder(
|
|
||||||
itemCount: _filteredQuotes.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final quote = _filteredQuotes[index]; // Используем _filteredQuotes для отображения
|
|
||||||
return Card(
|
|
||||||
margin: const EdgeInsets.symmetric(
|
|
||||||
vertical: 8.0,
|
|
||||||
horizontal: 10.0,
|
|
||||||
),
|
|
||||||
child: ListTile(
|
|
||||||
contentPadding: const EdgeInsets.all(8.0),
|
|
||||||
leading: SizedBox(
|
|
||||||
width: 50.0,
|
|
||||||
child: Image.network(
|
|
||||||
quote.imagePath,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (_, __, ___) =>
|
|
||||||
const Icon(Icons.error, color: Colors.red),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
quote.text,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 18, fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
subtitle: Text('- ${quote.author}'),
|
|
||||||
trailing: IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
quote.isFavorite
|
|
||||||
? Icons.favorite
|
|
||||||
: Icons.favorite_border,
|
|
||||||
color: quote.isFavorite ? Colors.red : null,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
quote.toggleFavorite();
|
|
||||||
});
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(quote.isFavorite
|
|
||||||
? 'Цитата добавлена в избранное'
|
|
||||||
: 'Цитата удалена из избранного'),
|
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) =>
|
|
||||||
QuoteDetailScreen(quote: quote),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class QuoteDetailScreen extends StatelessWidget {
|
|
||||||
final Quote quote;
|
|
||||||
|
|
||||||
const QuoteDetailScreen({super.key, required this.quote});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text('Детали цитаты'),
|
|
||||||
),
|
|
||||||
body: Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Image.network(
|
|
||||||
quote.imagePath,
|
|
||||||
height: 150,
|
|
||||||
errorBuilder: (_, __, ___) =>
|
|
||||||
const Icon(Icons.error, color: Colors.red),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Text(
|
|
||||||
quote.text,
|
|
||||||
style:
|
|
||||||
const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Text(
|
|
||||||
'- ${quote.author}',
|
|
||||||
style: const TextStyle(fontSize: 18, color: Colors.grey),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
extension StringExtension on String {
|
|
||||||
String capitalize() {
|
|
||||||
return split(' ').map((word) {
|
|
||||||
if (word.isNotEmpty) {
|
|
||||||
return '${word[0].toUpperCase()}${word.substring(1).toLowerCase()}';
|
|
||||||
}
|
|
||||||
return word;
|
|
||||||
}).join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
String addQuotesIfMissing() {
|
|
||||||
if (startsWith('\"') && endsWith('\"')) return this;
|
|
||||||
if (startsWith('\"') && !endsWith('\"')) return '$this\"';
|
|
||||||
if (endsWith('\"') && !startsWith('\"')) return '\"$this';
|
|
||||||
return '\"$this\"';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
51
lib/presentation/home_page/bloc/bloc.dart
Normal file
51
lib/presentation/home_page/bloc/bloc.dart
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import '../../../data/repositories/quotes_repository.dart';
|
||||||
|
import '/presentation/home_page/bloc/events.dart';
|
||||||
|
import '/presentation/home_page/bloc/state.dart';
|
||||||
|
|
||||||
|
class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||||
|
final QuotesRepository repo;
|
||||||
|
|
||||||
|
HomeBloc(this.repo) : super(const HomeState()) {
|
||||||
|
on<HomeLoadDataEvent>(_onLoadData);
|
||||||
|
on<HomeRefreshEvent>(_onRefreshData);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onLoadData(HomeLoadDataEvent event, Emitter<HomeState> emit) async {
|
||||||
|
final author = event.author?.trim() ?? '';
|
||||||
|
final search = event.search?.trim() ?? '';
|
||||||
|
|
||||||
|
// Если поле автора не заполнено, загружаем данные из репозитория
|
||||||
|
if (author.isEmpty) {
|
||||||
|
emit(state.copyWith(
|
||||||
|
data: repo.loadData(q: search).then((result) => result ?? []), // Обработка null
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
// Если автор указан, фильтруем данные по тексту цитаты
|
||||||
|
try {
|
||||||
|
final currentData = await repo.loadData(q:search, author: author,
|
||||||
|
);
|
||||||
|
final filteredData = currentData?.where((quote) {
|
||||||
|
final matchesAuthor = quote.author.toLowerCase().contains(author.toLowerCase());
|
||||||
|
final matchesSearch = search.isEmpty ||
|
||||||
|
quote.text.toLowerCase().contains(search.toLowerCase());
|
||||||
|
return matchesAuthor && matchesSearch;
|
||||||
|
}).toList();
|
||||||
|
emit(state.copyWith(data: Future.value(filteredData)));
|
||||||
|
} catch (error) {
|
||||||
|
emit(state.copyWith(data: Future.error(error)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Future<void> _onRefreshData(HomeRefreshEvent event, Emitter<HomeState> emit) async {
|
||||||
|
add(HomeLoadDataEvent(search: event.search, author: event.author)); // Просто перезапускаем загрузку
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
17
lib/presentation/home_page/bloc/events.dart
Normal file
17
lib/presentation/home_page/bloc/events.dart
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
abstract class HomeEvent {
|
||||||
|
const HomeEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class HomeLoadDataEvent extends HomeEvent {
|
||||||
|
final String? search;
|
||||||
|
final String? author;
|
||||||
|
|
||||||
|
const HomeLoadDataEvent({this.search, this.author});
|
||||||
|
}
|
||||||
|
|
||||||
|
class HomeRefreshEvent extends HomeEvent {
|
||||||
|
final String? search;
|
||||||
|
final String? author;
|
||||||
|
|
||||||
|
const HomeRefreshEvent({this.search, this.author});
|
||||||
|
}
|
15
lib/presentation/home_page/bloc/state.dart
Normal file
15
lib/presentation/home_page/bloc/state.dart
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import '../../../domain/quote.dart';
|
||||||
|
import '../home_page.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class HomeState extends Equatable {
|
||||||
|
final Future<List<Quote>>? data; // Изменено
|
||||||
|
|
||||||
|
const HomeState({this.data});
|
||||||
|
|
||||||
|
HomeState copyWith({Future<List<Quote>>? data}) => HomeState(data: data ?? this.data);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [data];
|
||||||
|
}
|
98
lib/presentation/home_page/card.dart
Normal file
98
lib/presentation/home_page/card.dart
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../domain/quote.dart';
|
||||||
|
import 'home_page.dart';
|
||||||
|
|
||||||
|
class QuoteCard extends StatelessWidget {
|
||||||
|
final Quote quote;
|
||||||
|
final VoidCallback onFavoriteToggle;
|
||||||
|
|
||||||
|
const QuoteCard({
|
||||||
|
super.key,
|
||||||
|
required this.quote,
|
||||||
|
required this.onFavoriteToggle,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8.0,
|
||||||
|
horizontal: 10.0,
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
contentPadding: const EdgeInsets.all(8.0),
|
||||||
|
leading: SizedBox(
|
||||||
|
width: 50.0,
|
||||||
|
child: Image.network(
|
||||||
|
quote.imagePath,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (_, __, ___) =>
|
||||||
|
const Icon(Icons.error, color: Colors.red),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
quote.text,
|
||||||
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
subtitle: Text('- ${quote.author}'),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
quote.isFavorite ? Icons.favorite : Icons.favorite_border,
|
||||||
|
color: quote.isFavorite ? Colors.red : null,
|
||||||
|
),
|
||||||
|
onPressed: onFavoriteToggle,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => QuoteDetailScreen(quote: quote),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class QuoteDetailScreen extends StatelessWidget {
|
||||||
|
final Quote quote;
|
||||||
|
|
||||||
|
const QuoteDetailScreen({super.key, required this.quote});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Детали цитаты'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Image.network(
|
||||||
|
quote.imagePath,
|
||||||
|
height: 150,
|
||||||
|
errorBuilder: (_, __, ___) =>
|
||||||
|
const Icon(Icons.error, color: Colors.red),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
quote.text,
|
||||||
|
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
'- ${quote.author}',
|
||||||
|
style: const TextStyle(fontSize: 18, color: Colors.grey),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
138
lib/presentation/home_page/home_page.dart
Normal file
138
lib/presentation/home_page/home_page.dart
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import '../../components/utils/debounce.dart';
|
||||||
|
import '../../domain/quote.dart';
|
||||||
|
import '/data/repositories/quotes_repository.dart';
|
||||||
|
import '/presentation/home_page/bloc/bloc.dart';
|
||||||
|
import '/presentation/home_page/bloc/events.dart';
|
||||||
|
import '/presentation/home_page/bloc/state.dart';
|
||||||
|
import 'card.dart';
|
||||||
|
|
||||||
|
class MyHomePage extends StatelessWidget {
|
||||||
|
const MyHomePage({super.key, required this.title});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => HomeBloc(QuotesRepository())..add(const HomeLoadDataEvent()),
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(title),
|
||||||
|
),
|
||||||
|
body: const _HomePageBody(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomePageBody extends StatefulWidget {
|
||||||
|
const _HomePageBody();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_HomePageBody> createState() => _HomePageBodyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomePageBodyState extends State<_HomePageBody> {
|
||||||
|
final TextEditingController searchController = TextEditingController();
|
||||||
|
final TextEditingController authorController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
searchController.addListener(() {
|
||||||
|
Debounce.run(() {
|
||||||
|
context.read<HomeBloc>().add(HomeLoadDataEvent(search: searchController.text));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
authorController.addListener(() {
|
||||||
|
Debounce.run(() {
|
||||||
|
context.read<HomeBloc>().add(HomeLoadDataEvent(
|
||||||
|
search: searchController.text,
|
||||||
|
author: authorController.text,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<HomeBloc, HomeState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: TextField(
|
||||||
|
controller: searchController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Поиск по цитате',
|
||||||
|
prefixIcon: Icon(Icons.search),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: TextField(
|
||||||
|
controller: authorController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Поиск по автору',
|
||||||
|
prefixIcon: Icon(Icons.person),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: state.data == null
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: FutureBuilder<List<Quote>?>(
|
||||||
|
future: state.data,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return Center(child: Text('Ошибка: ${snapshot.error}'));
|
||||||
|
}
|
||||||
|
if (snapshot.data == null || snapshot.data!.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Text(
|
||||||
|
'Нет цитат для отображения.',
|
||||||
|
style: TextStyle(fontSize: 18, color: Colors.grey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final quotes = snapshot.data!;
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
context.read<HomeBloc>().add(HomeRefreshEvent(
|
||||||
|
search: searchController.text,
|
||||||
|
author: authorController.text,
|
||||||
|
));
|
||||||
|
},
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: quotes.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final quote = quotes[index];
|
||||||
|
return QuoteCard(
|
||||||
|
quote: quote,
|
||||||
|
onFavoriteToggle: () {
|
||||||
|
setState(() {
|
||||||
|
quote.toggleFavorite();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user