This commit is contained in:
Вера 2024-12-19 22:49:26 +04:00
parent 68d10dfbe5
commit 98b1d07670
18 changed files with 632 additions and 295 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -4,38 +4,55 @@ part 'recipes_dto.g.dart';
@JsonSerializable(createToJson: false) @JsonSerializable(createToJson: false)
class RecipesDto { class RecipesDto {
final List<RecipeDataDto>? hits; final List<RecipeDataDto>? data;
final MetaDto? meta;
const RecipesDto({this.hits}); const RecipesDto({
this.data,
this.meta,
});
factory RecipesDto.fromJson(Map<String, dynamic> json) => factory RecipesDto.fromJson(Map<String, dynamic> json) => _$RecipesDtoFromJson(json);
_$RecipesDtoFromJson(json);
} }
@JsonSerializable(createToJson: false) @JsonSerializable(createToJson: false)
class RecipeDataDto { class RecipeDataDto {
final RecipeAttributesDataDto? recipe; final String? id;
final String? type;
final RecipeAttributesDataDto? attributes;
const RecipeDataDto({this.recipe}); const RecipeDataDto({this.id, this.type, this.attributes});
factory RecipeDataDto.fromJson(Map<String, dynamic> json) => factory RecipeDataDto.fromJson(Map<String, dynamic> json) => _$RecipeDataDtoFromJson(json);
_$RecipeDataDtoFromJson(json);
} }
@JsonSerializable(createToJson: false) @JsonSerializable(createToJson: false)
class RecipeAttributesDataDto { class RecipeAttributesDataDto {
final String? label; final String? name;
final double? calories; final String? calories;
final String? url; final String? died;
final String? image; final String? image;
const RecipeAttributesDataDto({ const RecipeAttributesDataDto({this.name, this.calories, this.died, this.image});
this.label,
this.calories,
this.url,
this.image,
});
factory RecipeAttributesDataDto.fromJson(Map<String, dynamic> json) => factory RecipeAttributesDataDto.fromJson(Map<String, dynamic> json) =>
_$RecipeAttributesDataDtoFromJson(json); _$RecipeAttributesDataDtoFromJson(json);
} }
@JsonSerializable(createToJson: false)
class MetaDto {
final PaginationDto? pagination;
const MetaDto({this.pagination});
factory MetaDto.fromJson(Map<String, dynamic> json) => _$MetaDtoFromJson(json);
}
@JsonSerializable(createToJson: false)
class PaginationDto {
final int? next;
const PaginationDto({ this.next});
factory PaginationDto.fromJson(Map<String, dynamic> json) => _$PaginationDtoFromJson(json);
}

View File

@ -1,28 +1,28 @@
import 'package:leonteva_pmu/data/dtos/recipes_dto.dart'; import 'package:leonteva_pmu/data/dtos/recipes_dto.dart';
import 'package:leonteva_pmu/domain/models/card.dart'; import 'package:leonteva_pmu/domain/models/card.dart';
import 'package:leonteva_pmu/domain/models/home.dart';
const String imagePlaceholder = const _imagePlaceholder =
'https://cdn-icons-png.flaticon.com/512/4036/4036418.png'; 'https://upload.wikimedia.org/wikipedia/en/archive/b/b1/20210811082420%21Portrait_placeholder.png';
extension RecipesDtoToModel on RecipesDto {
HomeData toDomain() => HomeData(
data: data?.map((e) => e.toDomain()).toList(),
nextPage: meta?.pagination?.next,
);
}
extension RecipeDataDtoToModel on RecipeDataDto { extension RecipeDataDtoToModel on RecipeDataDto {
CardData toDomain() => CardData( CardData toDomain() => CardData(
recipe?.label ?? 'UNKNOWN', // Используем поле label из recipe. attributes?.name ?? 'UNKNOWN',
imageUrl: recipe?.image ?? imagePlaceholder, // Используем поле image. imageUrl: attributes?.image ?? _imagePlaceholder,
descriptionText: _makeDescriptionText( descriptionText: _makeDescriptionText(attributes?.calories),
recipe?.calories?.toString(), // Преобразуем double в строку. id: id,
recipe?.url, // Используем поле url.
),
); );
String _makeDescriptionText(String? calories, String? url) { String _makeDescriptionText(String? calories) {
if (calories != null && url != null) { return calories != null
return 'Calories: $calories\nURL: $url'; ? 'calories: $calories'
} else if (calories != null) { : '';
return 'Calories: $calories';
} else if (url != null) {
return 'URL: $url';
} else {
return 'No description available';
}
} }
} }

View File

@ -1,7 +1,12 @@
import 'package:leonteva_pmu/domain/models/card.dart'; import 'package:leonteva_pmu/domain/models/home.dart';
typedef OnErrorCallback = void Function(String? error); typedef OnErrorCallback = void Function(String? error);
abstract class ApiInterface { abstract class ApiInterface {
Future<List<CardData>?> loadData({OnErrorCallback? onError}); Future<HomeData?> loadData({
OnErrorCallback? onError,
String? q,
int page = 1,
int pageSize = 25,
});
} }

View File

@ -1,30 +1,38 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:leonteva_pmu/data/repositories/api_interface.dart'; import 'package:leonteva_pmu/data/repositories/api_interface.dart';
import 'package:leonteva_pmu/domain/models/card.dart'; import 'package:leonteva_pmu/domain/models/card.dart';
import 'package:leonteva_pmu/domain/models/home.dart';
class MockRepository extends ApiInterface { class MockRepository extends ApiInterface {
@override @override
Future<List<CardData>?> loadData({OnErrorCallback? onError}) async { Future<HomeData?> loadData({
return [ OnErrorCallback? onError,
String? q,
int page = 1,
int pageSize = 25,
}) async {
return HomeData(
data: [
CardData( CardData(
'dish1', 'Freeze',
descriptionText: 'hehehe', descriptionText: 'so cold..',
imageUrl: imageUrl:
'https://n1s2.hsmedia.ru/48/2d/63/482d63d02b668677a73a2ffbd791a71b/728x546_1_aaca034dfa8a8c33247bd8cb2ed26817@1700x1275_0xac120003_9749770561671744766.jpeg', 'https://www.skedaddlewildlife.com/wp-content/uploads/2018/09/depositphotos_22425309-stock-photo-a-lonely-raccoon-in-winter.jpg',
), ),
CardData( CardData(
'dish2', 'Hi',
descriptionText: 'eeee', descriptionText: 'pretty face',
icon: Icons.hail, icon: Icons.hail,
imageUrl: imageUrl:
'https://n1s2.hsmedia.ru/48/2d/63/482d63d02b668677a73a2ffbd791a71b/728x546_1_aaca034dfa8a8c33247bd8cb2ed26817@1700x1275_0xac120003_9749770561671744766.jpeg', 'https://www.thesprucepets.com/thmb/nKNaS4I586B_H7sEUw9QAXvWM_0=/2121x0/filters:no_upscale():strip_icc()/GettyImages-135630198-5ba7d225c9e77c0050cff91b.jpg',
), ),
CardData( CardData(
'dish3', 'Orange',
descriptionText: 'aaaaaa', descriptionText: 'I like autumn',
icon: Icons.warning_amber, icon: Icons.warning_amber,
imageUrl: 'https://n1s2.hsmedia.ru/48/2d/63/482d63d02b668677a73a2ffbd791a71b/728x546_1_aaca034dfa8a8c33247bd8cb2ed26817@1700x1275_0xac120003_9749770561671744766.jpeg', imageUrl: 'https://furmanagers.com/wp-content/uploads/2019/11/dreamstime_l_22075357.jpg',
), ),
]; ],
);
} }
} }

View File

@ -2,7 +2,7 @@ import 'package:dio/dio.dart';
import 'package:leonteva_pmu/data/dtos/recipes_dto.dart'; import 'package:leonteva_pmu/data/dtos/recipes_dto.dart';
import 'package:leonteva_pmu/data/mappers/recipes_mapper.dart'; import 'package:leonteva_pmu/data/mappers/recipes_mapper.dart';
import 'package:leonteva_pmu/data/repositories/api_interface.dart'; import 'package:leonteva_pmu/data/repositories/api_interface.dart';
import 'package:leonteva_pmu/domain/models/card.dart'; import 'package:leonteva_pmu/domain/models/home.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart'; import 'package:pretty_dio_logger/pretty_dio_logger.dart';
class RecipeRepository extends ApiInterface { class RecipeRepository extends ApiInterface {
@ -10,37 +10,34 @@ class RecipeRepository extends ApiInterface {
..interceptors.add(PrettyDioLogger( ..interceptors.add(PrettyDioLogger(
requestHeader: true, requestHeader: true,
requestBody: true, requestBody: true,
responseHeader: true,
responseBody: true,
error: true,
)); ));
static const String _baseUrl = 'https://api.edamam.com'; static const String _baseUrl = 'http://127.0.0.1:8000';
static const String _appId = '<d683b7dc>'; // Укажите ваш APP_ID.
static const String _appKey = '<988fcbbd552b83ca870efced716389e4>'; // Укажите ваш APP_KEY.
@override @override
Future<List<CardData>?> loadData({String? q, OnErrorCallback? onError}) async { Future<HomeData?> loadData({
OnErrorCallback? onError,
String? q,
int page = 1,
int pageSize = 5,
}) async {
try { try {
final String url = '$_baseUrl/search'; const String url = '$_baseUrl/recipes/';
final Response<dynamic> response = await _dio.get( final Response<dynamic> response = await _dio.get<Map<dynamic, dynamic>>(
url, url,
queryParameters: { queryParameters: {
'q': q ?? '', // Параметр запроса для поиска рецептов. 'fullName': q,
'd683b7dc': _appId, 'page': page,
'988fcbbd552b83ca870efced716389e4': _appKey, 'size': pageSize,
}, },
); );
final RecipesDto dto = final RecipesDto dto = RecipesDto.fromJson(response.data as Map<String, dynamic>);
RecipesDto.fromJson(response.data as Map<String, dynamic>); final HomeData data = dto.toDomain();
final List<CardData>? data =
dto.hits?.map((e) => e.toDomain()).toList(); // Преобразуем DTO в модель.
return data; return data;
} on DioException catch (e) { } on DioException catch (e) {
// Обработка ошибки и передача сообщения через onError. onError?.call(e.error?.toString());
onError?.call(e.response?.statusMessage ?? 'Unknown error');
return null; return null;
} }
} }

View File

@ -5,11 +5,13 @@ class CardData {
final String descriptionText; final String descriptionText;
final IconData icon; final IconData icon;
final String? imageUrl; final String? imageUrl;
final String? id;
CardData( CardData(
this.text, { this.text, {
required this.descriptionText, required this.descriptionText,
this.icon = Icons.ac_unit_outlined, this.icon = Icons.ac_unit_outlined,
this.imageUrl, this.imageUrl,
this.id,
}); });
} }

View File

@ -1,5 +1,15 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:leonteva_pmu/components/locale/l10n/app_locale.dart';
import 'package:leonteva_pmu/data/repositories/recipe_repository.dart';
import 'package:leonteva_pmu/presentation/home_page/bloc/bloc.dart';
import 'package:leonteva_pmu/presentation/home_page/home_page.dart'; import 'package:leonteva_pmu/presentation/home_page/home_page.dart';
import 'package:leonteva_pmu/presentation/like_bloc/like_bloc.dart';
import 'package:leonteva_pmu/presentation/locale_bloc/locale_bloc.dart';
import 'package:leonteva_pmu/presentation/locale_bloc/locale_state.dart';
void main() { void main() {
runApp(const MyApp()); runApp(const MyApp());
} }
@ -9,14 +19,37 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider<LocaleBloc>(
lazy: false,
create: (context) => LocaleBloc(const Locale("ru")),
child: BlocBuilder<LocaleBloc, LocaleState>(
builder: (context, state) {
return MaterialApp( return MaterialApp(
title: 'Flutter Demo', title: 'Flutter Demo',
locale: state.currentLocale,
localizationsDelegates: AppLocale.localizationsDelegates,
supportedLocales: AppLocale.supportedLocales,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData( theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), colorScheme: ColorScheme.fromSeed(seedColor: Colors.orangeAccent),
useMaterial3: true, useMaterial3: true,
), ),
home: const HomePage(title: 'Leonteva_V.A._PIbd-33'), home: RepositoryProvider<RecipeRepository>(
lazy: true,
create: (_) => RecipeRepository(),
child: BlocProvider<LikeBloc>(
lazy: false,
create: (context) => LikeBloc(),
child: BlocProvider<HomeBloc>(
lazy: false,
create: (context) => HomeBloc(context.read<RecipeRepository>()),
child: const HomePage(),
),
),
),
);
},
),
); );
} }
} }

View File

@ -1,33 +0,0 @@
import 'package:flutter/material.dart';
class ErrorDialog extends StatelessWidget {
final String? error;
const ErrorDialog(this.error, {super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Material(
color: Colors.transparent,
child: Container(
margin: const EdgeInsets.all(36),
padding: const EdgeInsets.all(20),
decoration: const BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error, color: Colors.white),
const SizedBox(height: 12),
Text(
error ?? 'UNKNOWN',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.white),
),
],
),
),
),
);
}
}

View File

@ -1,12 +0,0 @@
import 'package:flutter/material.dart';
import 'package:leonteva_pmu/presentation/dialogs/error_dialog.dart';
void showErrorDialog(
BuildContext context, {
required String? error,
}) {
showDialog(
context: context,
builder: (_) => ErrorDialog(error),
);
}

View File

@ -1,14 +1,16 @@
part of 'home_page.dart'; part of 'home_page.dart';
typedef OnLikeCallback = void Function(String title, bool isLiked)?; typedef OnLikeCallback = void Function(String? id, String title, bool isLiked)?;
class _Card extends StatefulWidget { class _Card extends StatelessWidget {
final String text; final String text;
final String descriptionText; final String descriptionText;
final IconData icon; final IconData icon;
final String? imageUrl; final String? imageUrl;
final OnLikeCallback onLike; final OnLikeCallback onLike;
final VoidCallback? onTap; final VoidCallback? onTap;
final String? id;
final bool isLiked;
const _Card( const _Card(
this.text, { this.text, {
@ -17,12 +19,15 @@ class _Card extends StatefulWidget {
this.imageUrl, this.imageUrl,
this.onLike, this.onLike,
this.onTap, this.onTap,
this.id,
this.isLiked = false,
}); });
factory _Card.fromData( factory _Card.fromData(
CardData data, { CardData data, {
OnLikeCallback onLike, OnLikeCallback onLike,
VoidCallback? onTap, VoidCallback? onTap,
bool isLiked = false,
}) => }) =>
_Card( _Card(
data.text, data.text,
@ -31,22 +36,17 @@ class _Card extends StatefulWidget {
imageUrl: data.imageUrl, imageUrl: data.imageUrl,
onLike: onLike, onLike: onLike,
onTap: onTap, onTap: onTap,
isLiked: isLiked,
id: data.id,
); );
@override
State<_Card> createState() => _CardState();
}
class _CardState extends State<_Card> {
bool isLiked = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: widget.onTap, onTap: onTap,
child: Container( child: Container(
margin: const EdgeInsets.all(16), margin: const EdgeInsets.all(16),
constraints: const BoxConstraints(minHeight: 140), constraints: const BoxConstraints(minHeight: 160),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white70, color: Colors.white70,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
@ -75,12 +75,11 @@ class _CardState extends State<_Card> {
children: [ children: [
Positioned.fill( Positioned.fill(
child: Image.network( child: Image.network(
widget.imageUrl ?? '', imageUrl ?? '',
fit: BoxFit.cover, fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Placeholder(), errorBuilder: (_, __, ___) => const Placeholder(),
), ),
), ),
], ],
), ),
), ),
@ -92,11 +91,11 @@ class _CardState extends State<_Card> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
widget.text, text,
style: Theme.of(context).textTheme.headlineLarge, style: Theme.of(context).textTheme.headlineSmall,
), ),
Text( Text(
widget.descriptionText, descriptionText,
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
) )
], ],
@ -106,18 +105,11 @@ class _CardState extends State<_Card> {
Align( Align(
alignment: Alignment.bottomRight, alignment: Alignment.bottomRight,
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(left: 8, right: 16, bottom: 16),
left: 8.0,
right: 16,
bottom: 16,
),
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () => onLike?.call(id, text, isLiked),
setState(() => isLiked = !isLiked);
widget.onLike?.call(widget.text, isLiked);
},
child: AnimatedSwitcher( child: AnimatedSwitcher(
duration: const Duration(microseconds: 200), duration: const Duration(milliseconds: 200),
child: isLiked child: isLiked
? const Icon( ? const Icon(
Icons.favorite, Icons.favorite,
@ -131,7 +123,7 @@ class _CardState extends State<_Card> {
), ),
), ),
), ),
) ),
], ],
), ),
), ),

View File

@ -1,16 +1,25 @@
import 'package:flutter/material.dart';
import 'package:leonteva_pmu/data/repositories/recipe_repository.dart';
import 'package:leonteva_pmu/presentation/details_page/details_page.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:leonteva_pmu/components/extensions/context_x.dart';
import 'package:leonteva_pmu/components/utils/debounce.dart';
import 'package:leonteva_pmu/domain/models/card.dart'; import 'package:leonteva_pmu/domain/models/card.dart';
import 'package:leonteva_pmu/presentation/dialogs/show_dialog.dart'; import 'package:leonteva_pmu/presentation/common/svg_objects.dart';
import 'package:leonteva_pmu/presentation/details_page/details_page.dart';
import 'package:leonteva_pmu/presentation/home_page/bloc/bloc.dart';
import 'package:leonteva_pmu/presentation/home_page/bloc/events.dart';
import 'package:leonteva_pmu/presentation/home_page/bloc/state.dart';
import 'package:leonteva_pmu/presentation/like_bloc/like_bloc.dart';
import 'package:leonteva_pmu/presentation/like_bloc/like_event.dart';
import 'package:leonteva_pmu/presentation/like_bloc/like_state.dart';
import 'package:leonteva_pmu/presentation/locale_bloc/locale_bloc.dart';
import 'package:leonteva_pmu/presentation/locale_bloc/locale_events.dart';
import 'package:leonteva_pmu/presentation/locale_bloc/locale_state.dart';
part 'card.dart'; part 'card.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
const HomePage({super.key, required this.title}); const HomePage({super.key});
final String title;
@override @override
State<HomePage> createState() => _HomePageState(); State<HomePage> createState() => _HomePageState();
@ -19,32 +28,53 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> { class _HomePageState extends State<HomePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Scaffold(body: Body()); return const Scaffold(body: _Body());
} }
} }
class Body extends StatefulWidget { class _Body extends StatefulWidget {
const Body({super.key}); // ключи const _Body();
@override @override
State<Body> createState() => _BodyState(); State<_Body> createState() => _BodyState();
} }
class _BodyState extends State<Body> { class _BodyState extends State<_Body> {
final searchController = TextEditingController(); final searchController = TextEditingController();
late Future<List<CardData>?> data; final scrollController = ScrollController();
final repo = RecipeRepository();
@override @override
void initState() { void initState() {
const String query = 'default_query'; // Укажите поисковый запрос по умолчанию SvgObjects.init();
// Загружаем данные с обработкой ошибок WidgetsBinding.instance.addPostFrameCallback((_) {
data = repo.loadData( context.read<HomeBloc>().add(const HomeLoadDataEvent());
q: query, context.read<LikeBloc>().add(const LoadLikesEvent());
onError: (e) => showErrorDialog(context, error: e), });
);
scrollController.addListener(_onNextPageListener);
super.initState();
}
void _onNextPageListener() {
if (scrollController.offset > scrollController.position.maxScrollExtent) {
// preventing multiple pagination request on multiple swipes
final bloc = context.read<HomeBloc>();
if (!bloc.state.isPaginationLoading) {
bloc.add(HomeLoadDataEvent(
search: searchController.text,
nextPage: bloc.state.data?.nextPage,
));
}
}
}
@override
void dispose() {
searchController.dispose();
scrollController.dispose();
super.dispose();
} }
@override @override
@ -53,43 +83,87 @@ class _BodyState extends State<Body> {
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
child: Column( child: Column(
children: [ children: [
Padding( Row(
children: [
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: CupertinoSearchTextField( child: CupertinoSearchTextField(
controller: searchController, controller: searchController,
placeholder: context.locale.search,
onChanged: (search) { onChanged: (search) {
setState(() { Debounce.run(
data = repo.loadData(q: search); () => context.read<HomeBloc>().add(HomeLoadDataEvent(search: search)));
});
}, },
), ),
), ),
Expanded( ),
child: Center( GestureDetector(
child: FutureBuilder<List<CardData>?>( onTap: () => context.read<LocaleBloc>().add(const ChangeLocaleEvent()),
future: data, child: SizedBox.square(
builder: (context, snapshot) => SingleChildScrollView( dimension: 50,
child: snapshot.hasData child: Padding(
? Column( padding: const EdgeInsets.only(right: 12),
mainAxisAlignment: MainAxisAlignment.center, child: BlocBuilder<LocaleBloc, LocaleState>(
children: snapshot.data?.map((data) { builder: (context, state) {
return _Card.fromData( return state.currentLocale.languageCode == 'ru'
data, ? const SvgRu()
onLike: (String title, bool isLiked) => : const SvgUk();
_showSnackBar(context, title, isLiked), },
onTap: () => _navToDetails(context, data),
);
}).toList() ??
[],
)
: const CircularProgressIndicator(),
), ),
), ),
), ),
), ),
], ],
), ),
BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) => state.error != null
? Text(
state.error ?? '',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: Colors.red),
)
: state.isLoading
? const CircularProgressIndicator()
: BlocBuilder<LikeBloc, LikeState>(
builder: (context, likeState) {
return Expanded(
child: RefreshIndicator(
onRefresh: _onRefresh,
child: ListView.builder(
controller: scrollController,
padding: EdgeInsets.zero,
itemCount: state.data?.data?.length ?? 0,
itemBuilder: (context, index) {
final data = state.data?.data?[index];
return data != null
? _Card.fromData(
data,
onLike: _onLike,
isLiked: likeState.likedIds?.contains(data.id) == true,
onTap: () => _navToDetails(context, data),
)
: const SizedBox.shrink();
},
),
),
); );
},
),
),
BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) => state.isPaginationLoading
? const CircularProgressIndicator()
: const SizedBox.shrink(),
),
],
),
);
}
Future<void> _onRefresh() {
context.read<HomeBloc>().add(HomeLoadDataEvent(search: searchController.text));
return Future.value(null);
} }
void _navToDetails(BuildContext context, CardData data) { void _navToDetails(BuildContext context, CardData data) {
@ -99,11 +173,18 @@ class _BodyState extends State<Body> {
); );
} }
void _onLike(String? id, String title, bool isLiked) {
if (id != null) {
context.read<LikeBloc>().add(ChangeLikeEvent(id));
_showSnackBar(context, title, !isLiked);
}
}
void _showSnackBar(BuildContext context, String title, bool isLiked) { void _showSnackBar(BuildContext context, String title, bool isLiked) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text( content: Text(
'O! $title ${isLiked ? 'liked!' : 'disliked :('}', '$title ${isLiked ? context.locale.liked : context.locale.disliked}',
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
), ),
backgroundColor: Colors.orangeAccent, backgroundColor: Colors.orangeAccent,

View File

@ -5,10 +5,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _fe_analyzer_shared name: _fe_analyzer_shared
sha256: "88399e291da5f7e889359681a8f64b18c5123e03576b01f32a6a276611e511c3" sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "78.0.0" version: "76.0.0"
_macros: _macros:
dependency: transitive dependency: transitive
description: dart description: dart
@ -18,10 +18,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
sha256: "62899ef43d0b962b056ed2ebac6b47ec76ffd003d5f7c4e4dc870afe63188e33" sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.1.0" version: "6.11.0"
archive:
dependency: transitive
description:
name: archive
sha256: "08064924cbf0ab88280a0c3f60db9dd24fec693927e725ecb176f16c629d1cb8"
url: "https://pub.dev"
source: hosted
version: "4.0.1"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -38,6 +46,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.11.0" version: "2.11.0"
bloc:
dependency: transitive
description:
name: bloc
sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
url: "https://pub.dev"
source: hosted
version: "8.1.4"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -126,6 +142,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.3" version: "2.0.3"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@ -158,6 +182,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
copy_with_extension:
dependency: transitive
description:
name: copy_with_extension
sha256: fbcf890b0c34aedf0894f91a11a579994b61b4e04080204656b582708b5b1125
url: "https://pub.dev"
source: hosted
version: "5.0.4"
copy_with_extension_gen:
dependency: "direct main"
description:
name: copy_with_extension_gen
sha256: "51cd11094096d40824c8da629ca7f16f3b7cea5fc44132b679617483d43346b0"
url: "https://pub.dev"
source: hosted
version: "5.0.4"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@ -178,10 +218,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dart_style name: dart_style
sha256: "64b717484993e85315d0c04081b6fca9ef8bac8c2cb794b2e15810250b335913" sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "2.3.7"
dio: dio:
dependency: "direct main" dependency: "direct main"
description: description:
@ -198,6 +238,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
equatable:
dependency: "direct main"
description:
name: equatable
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
url: "https://pub.dev"
source: hosted
version: "2.0.7"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -206,6 +254,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.1" version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
file: file:
dependency: transitive dependency: transitive
description: description:
@ -227,6 +283,22 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_bloc:
dependency: "direct main"
description:
name: flutter_bloc
sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a
url: "https://pub.dev"
source: hosted
version: "8.1.6"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
url: "https://pub.dev"
source: hosted
version: "0.13.1"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -235,11 +307,29 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0" version: "5.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_svg:
dependency: "direct main"
description:
name: flutter_svg
sha256: "8c5d68a82add3ca76d792f058b186a0599414f279f00ece4830b9b231b570338"
url: "https://pub.dev"
source: hosted
version: "2.0.7"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
frontend_server_client: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@ -264,6 +354,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.3.2"
http:
dependency: transitive
description:
name: http
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
url: "https://pub.dev"
source: hosted
version: "1.2.2"
http_multi_server: http_multi_server:
dependency: transitive dependency: transitive
description: description:
@ -280,6 +378,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.1" version: "4.1.1"
image:
dependency: transitive
description:
name: image
sha256: b50b415345578583de0f1cf4c7bd389f164de0b316d890c707b41133047dbc2a
url: "https://pub.dev"
source: hosted
version: "4.5.1"
intl:
dependency: "direct main"
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.19.0"
io: io:
dependency: transitive dependency: transitive
description: description:
@ -308,10 +422,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: json_serializable name: json_serializable
sha256: "8f52361c07497a7f2c16c13aac159f9be6fb12b1d67719eac98a21d9a205d571" sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.9.2" version: "6.9.0"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@ -392,6 +506,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
package_config: package_config:
dependency: transitive dependency: transitive
description: description:
@ -408,6 +530,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.0" version: "1.9.0"
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://pub.dev"
source: hosted
version: "6.0.2"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pool: pool:
dependency: transitive dependency: transitive
description: description:
@ -416,6 +594,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.1" version: "1.5.1"
posix:
dependency: transitive
description:
name: posix
sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a
url: "https://pub.dev"
source: hosted
version: "6.0.1"
pretty_dio_logger: pretty_dio_logger:
dependency: "direct main" dependency: "direct main"
description: description:
@ -424,6 +610,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
provider:
dependency: transitive
description:
name: provider
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
url: "https://pub.dev"
source: hosted
version: "6.1.2"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@ -440,6 +634,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
url: "https://pub.dev"
source: hosted
version: "2.2.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "02a7d8a9ef346c9af715811b01fbd8e27845ad2c41148eefd31321471b41863d"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shelf: shelf:
dependency: transitive dependency: transitive
description: description:
@ -465,10 +715,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_gen name: source_gen
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "1.5.0"
source_helper: source_helper:
dependency: transitive dependency: transitive
description: description:
@ -549,6 +799,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
vector_graphics:
dependency: transitive
description:
name: vector_graphics
sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7"
url: "https://pub.dev"
source: hosted
version: "1.1.15"
vector_graphics_codec:
dependency: transitive
description:
name: vector_graphics_codec
sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb"
url: "https://pub.dev"
source: hosted
version: "1.1.12"
vector_graphics_compiler:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad"
url: "https://pub.dev"
source: hosted
version: "1.1.16"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -597,6 +871,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.5.0"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:
@ -607,4 +897,4 @@ packages:
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=3.6.0 <4.0.0" dart: ">=3.6.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54" flutter: ">=3.24.0"

View File

@ -1,95 +1,52 @@
name: leonteva_pmu name: leonteva_pmu
description: "A new Flutter project." description: "A new Flutter project."
# The following line prevents the package from being accidentally published to publish_to: 'none'
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1 version: 1.0.0+1
environment: environment:
sdk: ^3.6.0 sdk: ^3.6.0
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies: dependencies:
dio: ^5.7.0 dio: ^5.7.0
pretty_dio_logger: ^1.4.0 pretty_dio_logger: ^1.4.0
json_annotation: ^4.9.0 json_annotation: ^4.9.0
# Виджеты
cupertino_icons: ^1.0.2
flutter_svg: 2.0.7
# BLoC
equatable: ^2.0.5
flutter_bloc: ^8.1.5
copy_with_extension_gen: ^5.0.4
# Localization
flutter_localizations:
sdk: flutter
intl: 0.19.0
shared_preferences: 2.2.3
flutter: flutter:
sdk: flutter sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
dev_dependencies: dev_dependencies:
build_runner: ^2.4.14 build_runner: ^2.4.14
json_serializable: ^6.9.2 json_serializable: ^6.7.1
flutter_test: flutter_test:
sdk: flutter sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0 flutter_lints: ^5.0.0
# Иконки
flutter_launcher_icons: 0.13.1
# For information on the generic Dart part of this file, see the flutter_icons:
# following page: https://dart.dev/tools/pub/pubspec android: "ic_launcher"
ios: false
image_path: "assets/launcher.jpeg"
min_sdk_android: 21
# The following section is specific to Flutter packages.
flutter: flutter:
generate: true
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true uses-material-design: true
# To add assets to your application, add an assets section, like this: assets:
# assets: - assets/svg/
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package