lab7 вам чай с сахаром? или с моими слезами?
This commit is contained in:
parent
f3a964e4d6
commit
c82d240d4c
6
l10n.yaml
Normal file
6
l10n.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
arb-dir: l10n
|
||||
template-arb-file: app_ru.arb
|
||||
output-localization-file: app_locale.dart
|
||||
output-dir: lib/components/locale/l10n
|
||||
output-class: AppLocale
|
||||
synthetic-package: false
|
7
l10n/app_en.arb
Normal file
7
l10n/app_en.arb
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"@@locale": "en",
|
||||
"search": "Search",
|
||||
"liked": "liked!",
|
||||
"disliked": "disliked :(",
|
||||
"arbEnding": "Чтобы не забыть про отсутствие запятой :)"
|
||||
}
|
7
l10n/app_ru.arb
Normal file
7
l10n/app_ru.arb
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"@@locale": "ru",
|
||||
"search": "Поиск",
|
||||
"liked": "понравился!",
|
||||
"disliked": "разонравился :(",
|
||||
"arbEnding": "Чтобы не забыть про отсутствие запятой :)"
|
||||
}
|
6
lib/components/extensions/context_x.dart
Normal file
6
lib/components/extensions/context_x.dart
Normal file
@ -0,0 +1,6 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:pmu/components/locale/l10n/app_locale.dart';
|
||||
|
||||
extension LocalContextX on BuildContext {
|
||||
AppLocale get locale => AppLocale.of(this)!;
|
||||
}
|
@ -1,16 +1,16 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
class Debounce{
|
||||
class Debounce {
|
||||
factory Debounce() => _instance;
|
||||
Debounce._();
|
||||
static final Debounce _instance = Debounce._();
|
||||
static Timer? _timer;
|
||||
static void run(
|
||||
VoidCallback action,{
|
||||
Duration delay = const Duration(milliseconds: 500),
|
||||
VoidCallback action, {
|
||||
Duration delay = const Duration(milliseconds: 500),
|
||||
}) {
|
||||
_timer?.cancel();
|
||||
_timer = Timer(delay, action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,8 +13,7 @@ class NewsDto {
|
||||
this.meta,
|
||||
});
|
||||
|
||||
factory NewsDto.fromJson(Map<String, dynamic> json) =>
|
||||
_$NewsDtoFromJson(json);
|
||||
factory NewsDto.fromJson(Map<String, dynamic> json) => _$NewsDtoFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
@ -36,8 +35,7 @@ class MetaDto {
|
||||
|
||||
const MetaDto({this.pagination});
|
||||
|
||||
factory MetaDto.fromJson(Map<String, dynamic> json) =>
|
||||
_$MetaDtoFromJson(json);
|
||||
factory MetaDto.fromJson(Map<String, dynamic> json) => _$MetaDtoFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
@ -48,6 +46,5 @@ class PaginationDto {
|
||||
|
||||
const PaginationDto({this.current, this.next, this.last});
|
||||
|
||||
factory PaginationDto.fromJson(Map<String, dynamic> json) =>
|
||||
_$PaginationDtoFromJson(json);
|
||||
factory PaginationDto.fromJson(Map<String, dynamic> json) => _$PaginationDtoFromJson(json);
|
||||
}
|
||||
|
@ -2,17 +2,18 @@ import 'package:pmu/data/dtos/news_dto.dart';
|
||||
import 'package:pmu/domain/models/card.dart';
|
||||
import 'package:pmu/domain/models/home.dart';
|
||||
|
||||
extension NewDataDtoToModel on NewAttributesDataDto{
|
||||
extension NewDataDtoToModel on NewAttributesDataDto {
|
||||
CardData toDomain() => CardData(
|
||||
text: title ?? 'UNKNOWN',
|
||||
imageUrl: imagelink,
|
||||
descText: description ?? 'NOTHING',
|
||||
);
|
||||
text: title ?? 'UNKNOWN',
|
||||
imageUrl: imagelink,
|
||||
descText: description ?? 'NOTHING',
|
||||
id: id,
|
||||
);
|
||||
}
|
||||
|
||||
extension NewsDtoToModel on NewsDto {
|
||||
HomeData toDomain() => HomeData(
|
||||
data: data?.map((e) => e.toDomain()).toList(),
|
||||
nextPage: meta?.pagination?.next,
|
||||
);
|
||||
}
|
||||
data: data?.map((e) => e.toDomain()).toList(),
|
||||
nextPage: meta?.pagination?.next,
|
||||
);
|
||||
}
|
||||
|
@ -22,26 +22,24 @@ class BbcRepository extends ApiInterface {
|
||||
int pageSize = 10,
|
||||
}) async {
|
||||
try {
|
||||
// final String url = '$_baseUrl/everything?q=$q&page=$page&pageSize=$pageSize$_apiKey';
|
||||
// final String url = '$_baseUrl/everything?q=$q&page=$page&pageSize=$pageSize$_apiKey';
|
||||
final String url = '$_baseUrl/everything';
|
||||
|
||||
final Response<dynamic> response = await _dio.get<Map<dynamic, dynamic>>(
|
||||
url,
|
||||
queryParameters: ( (q != null && !q.isEmpty) ? {
|
||||
'q': q,
|
||||
'page': page,
|
||||
'pageSize': pageSize,
|
||||
'apikey': 'b9848c2aa43e4a0ba12dfe925db8513c'
|
||||
} : {
|
||||
'q': 'news',
|
||||
'page': page,
|
||||
'pageSize': pageSize,
|
||||
'apikey': 'b9848c2aa43e4a0ba12dfe925db8513c'
|
||||
}
|
||||
)
|
||||
);
|
||||
final NewsDto dto =
|
||||
NewsDto.fromJson(response.data as Map<String, dynamic>);
|
||||
final Response<dynamic> response = await _dio.get<Map<dynamic, dynamic>>(url,
|
||||
queryParameters: ((q != null && !q.isEmpty)
|
||||
? {
|
||||
'q': q,
|
||||
'page': page,
|
||||
'pageSize': pageSize,
|
||||
'apikey': 'b9848c2aa43e4a0ba12dfe925db8513c'
|
||||
}
|
||||
: {
|
||||
'q': 'news',
|
||||
'page': page,
|
||||
'pageSize': pageSize,
|
||||
'apikey': 'b9848c2aa43e4a0ba12dfe925db8513c'
|
||||
}));
|
||||
final NewsDto dto = NewsDto.fromJson(response.data as Map<String, dynamic>);
|
||||
final HomeData data = dto.toDomain();
|
||||
return data;
|
||||
} on DioException catch (e) {
|
||||
|
@ -34,8 +34,7 @@ class MockRepository extends ApiInterface {
|
||||
descText:
|
||||
'"Запущена официальная процедура смены фамилии", — прокомментировали РИА Новости в пресс-службе объединенной компании Wildberries и Russ (РВБ).',
|
||||
icon: Icons.add_call,
|
||||
imageUrl:
|
||||
'https://i.pinimg.com/736x/df/91/dc/df91dc3de2580cffc66d01000c0c6d82.jpg',
|
||||
imageUrl: 'https://i.pinimg.com/736x/df/91/dc/df91dc3de2580cffc66d01000c0c6d82.jpg',
|
||||
),
|
||||
CardData(
|
||||
text: 'В этом мире?',
|
||||
|
@ -5,11 +5,14 @@ class CardData {
|
||||
final IconData icon;
|
||||
final String descText;
|
||||
final String? imageUrl;
|
||||
final String? id;
|
||||
|
||||
|
||||
CardData({
|
||||
required this.text,
|
||||
this.icon = Icons.accessibility_new,
|
||||
required this.descText,
|
||||
this.imageUrl,
|
||||
this.id,
|
||||
});
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import 'package:pmu/domain/models/card.dart';
|
||||
|
||||
class HomeData{
|
||||
class HomeData {
|
||||
final List<CardData>? data;
|
||||
final int? nextPage;
|
||||
|
||||
HomeData({this.data,this.nextPage});
|
||||
}
|
||||
HomeData({this.data, this.nextPage});
|
||||
}
|
||||
|
@ -1,9 +1,14 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:pmu/components/locale/l10n/app_locale.dart';
|
||||
import 'package:pmu/data/repositories/bbc_repository.dart';
|
||||
import 'package:pmu/presentation/home_page/bloc/bloc.dart';
|
||||
import 'package:pmu/presentation/home_page/home_page.dart';
|
||||
|
||||
import 'package:pmu/presentation/likes_bloc/likes_bloc.dart';
|
||||
import 'package:pmu/presentation/locale_bloc/locale_bloc.dart';
|
||||
import 'package:pmu/presentation/locale_bloc/locale_state.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
@ -14,26 +19,39 @@ class MyApp extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.lightBlueAccent),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: RepositoryProvider<BbcRepository>(// чтобы ниже по дереву(в блок провайдере) был доступ к нашему репозиторию
|
||||
lazy: true,
|
||||
create: (_) => BbcRepository(),
|
||||
child: BlocProvider<HomeBloc>(// обернули чтобы сделать блок доступным внутри нашей страницы
|
||||
lazy: false,
|
||||
create: (context) => HomeBloc(context.read<BbcRepository>()),
|
||||
child: const MyHomePage()),
|
||||
return BlocProvider<LocaleBloc>(
|
||||
lazy: false,
|
||||
create: (context) => LocaleBloc(Locale(Platform.localeName)),
|
||||
child: BlocBuilder<LocaleBloc, LocaleState>(
|
||||
builder: (context, state) {
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
locale: state.currentLocale,
|
||||
localizationsDelegates: AppLocale.localizationsDelegates,
|
||||
supportedLocales: AppLocale.supportedLocales,
|
||||
theme: ThemeData(
|
||||
colorScheme:
|
||||
ColorScheme.fromSeed(seedColor: Colors.lightBlueAccent),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: RepositoryProvider<BbcRepository>(
|
||||
// чтобы ниже по дереву(в блок провайдере) был доступ к нашему репозиторию
|
||||
lazy: true,
|
||||
create: (_) => BbcRepository(),
|
||||
child: BlocProvider<LikeBloc>(
|
||||
// обернули чтобы сделать блок доступным внутри нашей страницы
|
||||
lazy: false,
|
||||
create: (context) => LikeBloc(),
|
||||
child: BlocProvider<HomeBloc>(
|
||||
lazy: false,
|
||||
create: (context) => HomeBloc(context.read<BbcRepository>()),
|
||||
child: const MyHomePage(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
34
lib/presentation/common/svg_objects.dart
Normal file
34
lib/presentation/common/svg_objects.dart
Normal file
@ -0,0 +1,34 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:pmu/components/resources/resources.g.dart';
|
||||
|
||||
abstract class SvgObjects{
|
||||
static void init(){
|
||||
final pics = <String>[
|
||||
R.ASSETS_SVG_RU_SVG,
|
||||
R.ASSETS_SVG_UK_SVG
|
||||
];
|
||||
for (final String p in pics){
|
||||
final loader = SvgAssetLoader(p);
|
||||
svg.cache.putIfAbsent(loader.cacheKey(null), () => loader.loadBytes(null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SvgRu extends StatelessWidget{
|
||||
const SvgRu({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context){
|
||||
return SvgPicture.asset(R.ASSETS_SVG_RU_SVG);
|
||||
}
|
||||
}
|
||||
|
||||
class SvgUk extends StatelessWidget{
|
||||
const SvgUk({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context){
|
||||
return SvgPicture.asset(R.ASSETS_SVG_UK_SVG);
|
||||
}
|
||||
}
|
@ -9,26 +9,21 @@ class DetailsPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.lightBlue, foregroundColor: Colors.white),
|
||||
appBar: AppBar(backgroundColor: Colors.lightBlue, foregroundColor: Colors.white),
|
||||
body: SingleChildScrollView(
|
||||
child:Column(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 4.0, top: 8.0, left: 8.0, right: 8.0),
|
||||
padding: const EdgeInsets.only(bottom: 4.0, top: 8.0, left: 8.0, right: 8.0),
|
||||
child: Text(
|
||||
data.text,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 26),
|
||||
style:
|
||||
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500, fontSize: 26),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 16.0, top: 8.0, left: 8.0, right: 8.0),
|
||||
padding: const EdgeInsets.only(bottom: 16.0, top: 8.0, left: 8.0, right: 8.0),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ClipRRect(
|
||||
@ -39,8 +34,7 @@ class DetailsPage extends StatelessWidget {
|
||||
)),
|
||||
)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 4.0, top: 8.0, left: 8.0, right: 8.0),
|
||||
padding: const EdgeInsets.only(bottom: 4.0, top: 8.0, left: 8.0, right: 8.0),
|
||||
child: Text(
|
||||
data.descText,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
|
@ -14,22 +14,22 @@ class ErrorDialog extends StatelessWidget {
|
||||
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),
|
||||
),
|
||||
],
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:pmu/presentation/dialogs/error_dialog.dart';
|
||||
|
||||
void showErrorDialog(
|
||||
BuildContext context, {
|
||||
required String? error,
|
||||
}) {
|
||||
BuildContext context, {
|
||||
required String? error,
|
||||
}) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => ErrorDialog(error),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -11,8 +11,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
}
|
||||
|
||||
// мы должны изменить наше состояние, передаем внутрь новое состояние
|
||||
Future<void> _onLoadData(
|
||||
HomeLoadDataEvent event, Emitter<HomeState> emit) async {
|
||||
Future<void> _onLoadData(HomeLoadDataEvent event, Emitter<HomeState> emit) async {
|
||||
if (event.nextPage == null) {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
} else {
|
||||
|
@ -1,8 +1,9 @@
|
||||
abstract class HomeEvent{
|
||||
abstract class HomeEvent {
|
||||
const HomeEvent();
|
||||
}
|
||||
class HomeLoadDataEvent extends HomeEvent{
|
||||
|
||||
class HomeLoadDataEvent extends HomeEvent {
|
||||
final String? search;
|
||||
final int? nextPage;
|
||||
const HomeLoadDataEvent({this.search, this.nextPage});
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +1,33 @@
|
||||
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 IconData icon;
|
||||
final String descText;
|
||||
final String? imageUrl;
|
||||
final OnLikeCallBack onLike;
|
||||
final VoidCallback? onTap;
|
||||
final String? id;
|
||||
final bool isLiked;
|
||||
|
||||
const _Card(this.text,
|
||||
{this.icon = Icons.ac_unit_sharp,
|
||||
required this.descText,
|
||||
this.imageUrl,
|
||||
this.onLike,
|
||||
this.onTap});
|
||||
const _Card(
|
||||
this.text, {
|
||||
this.icon = Icons.ac_unit_sharp,
|
||||
required this.descText,
|
||||
this.imageUrl,
|
||||
this.onLike,
|
||||
this.onTap,
|
||||
this.id,
|
||||
this.isLiked = false,
|
||||
});
|
||||
|
||||
factory _Card.fromData(
|
||||
CardData data, {
|
||||
OnLikeCallBack onLike,
|
||||
VoidCallback? onTap,
|
||||
bool isLiked = false,
|
||||
}) =>
|
||||
_Card(
|
||||
data.text,
|
||||
@ -29,21 +36,16 @@ class _Card extends StatefulWidget {
|
||||
imageUrl: data.imageUrl,
|
||||
onLike: onLike,
|
||||
onTap: onTap,
|
||||
isLiked: isLiked,
|
||||
id: data.id,
|
||||
);
|
||||
|
||||
@override
|
||||
State<_Card> createState() => _CardState();
|
||||
}
|
||||
|
||||
class _CardState extends State<_Card> {
|
||||
bool isLiked = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
|
||||
child: GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
onTap: onTap,
|
||||
child: SizedBox(
|
||||
height: 150,
|
||||
child: Container(
|
||||
@ -68,7 +70,7 @@ class _CardState extends State<_Card> {
|
||||
height: double.infinity,
|
||||
width: 120,
|
||||
child: Image.network(
|
||||
widget.imageUrl ?? '',
|
||||
imageUrl ?? '',
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => const Placeholder(),
|
||||
),
|
||||
@ -82,7 +84,7 @@ class _CardState extends State<_Card> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.text,
|
||||
text,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
@ -91,7 +93,7 @@ class _CardState extends State<_Card> {
|
||||
fontSize: 26),
|
||||
),
|
||||
Text(
|
||||
widget.descText,
|
||||
descText,
|
||||
maxLines: 5,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
@ -106,12 +108,7 @@ class _CardState extends State<_Card> {
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8.0, top: 4.0, right: 8.0, bottom: 4.0),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
isLiked = !isLiked;
|
||||
});
|
||||
widget.onLike?.call(widget.text, isLiked);
|
||||
},
|
||||
onTap: () => onLike?.call(id, text, isLiked),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: isLiked
|
||||
|
@ -1,15 +1,23 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:pmu/components/extensions/context_x.dart';
|
||||
import 'package:pmu/components/utils/debounce.dart';
|
||||
import 'package:pmu/data/repositories/bbc_repository.dart';
|
||||
import 'package:pmu/data/repositories/mock_repository.dart';
|
||||
import 'package:pmu/domain/models/card.dart';
|
||||
import 'package:pmu/presentation/common/svg_objects.dart';
|
||||
import 'package:pmu/presentation/details_page/details_page.dart';
|
||||
import 'package:pmu/presentation/dialogs/show_dialog.dart';
|
||||
import 'package:pmu/presentation/home_page/bloc/bloc.dart';
|
||||
import 'package:pmu/presentation/home_page/bloc/events.dart';
|
||||
import 'package:pmu/presentation/home_page/bloc/state.dart';
|
||||
import 'package:pmu/presentation/likes_bloc/likes_bloc.dart';
|
||||
import 'package:pmu/presentation/likes_bloc/likes_events.dart';
|
||||
import 'package:pmu/presentation/likes_bloc/likes_state.dart';
|
||||
import 'package:pmu/presentation/locale_bloc/locale_bloc.dart';
|
||||
import 'package:pmu/presentation/locale_bloc/locale_events.dart';
|
||||
import 'package:pmu/presentation/locale_bloc/locale_state.dart';
|
||||
|
||||
part 'card.dart';
|
||||
|
||||
@ -47,8 +55,10 @@ class _BodyState extends State<_Body> {
|
||||
// final repo = BbcRepository();
|
||||
@override
|
||||
void initState() {
|
||||
SvgObjects.init();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<HomeBloc>().add(const HomeLoadDataEvent());
|
||||
context.read<LikeBloc>().add(const LoadLikesEvent());
|
||||
});
|
||||
//data = repo.loadData(onError: (e) => showErrorDialog(context, error: e));
|
||||
scrollController.addListener(_onNextPageListener);
|
||||
@ -63,7 +73,6 @@ class _BodyState extends State<_Body> {
|
||||
bloc.add(HomeLoadDataEvent(
|
||||
search: searchController.text,
|
||||
nextPage: 2,
|
||||
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -81,16 +90,41 @@ class _BodyState extends State<_Body> {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
|
||||
child: Column(children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: CupertinoSearchTextField(
|
||||
controller: searchController,
|
||||
onChanged: (search) {
|
||||
Debounce.run(() => context
|
||||
.read<HomeBloc>()
|
||||
.add(HomeLoadDataEvent(search: search)));
|
||||
},
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: CupertinoSearchTextField(
|
||||
controller: searchController,
|
||||
placeholder: context.locale.search,
|
||||
onChanged: (search) {
|
||||
Debounce.run(() => context
|
||||
.read<HomeBloc>()
|
||||
.add(HomeLoadDataEvent(search: search)));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () =>
|
||||
context.read<LocaleBloc>().add(const ChangeLocaleEvent()),
|
||||
child: SizedBox.square(
|
||||
dimension: 50,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: BlocBuilder<LocaleBloc, LocaleState>(
|
||||
builder: (context, state) {
|
||||
return state.currentLocale.languageCode == 'ru'
|
||||
? const SvgRu()
|
||||
: const SvgUk();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
BlocBuilder<HomeBloc, HomeState>(
|
||||
builder: (context, state) => state.error != null
|
||||
@ -103,26 +137,33 @@ class _BodyState extends State<_Body> {
|
||||
)
|
||||
: state.isLoading
|
||||
? const CircularProgressIndicator()
|
||||
: 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: (title, isLiked) =>
|
||||
_showSnackBar(context, title, isLiked),
|
||||
onTap: () => _navToDetails(context, data),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
: 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>(
|
||||
@ -138,7 +179,7 @@ class _BodyState extends State<_Body> {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
'The New $title ${isLiked ? 'liked!' : 'disliked'}',
|
||||
'The New ${isLiked ? context.locale.liked : context.locale.disliked}',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
backgroundColor: Colors.lightBlue,
|
||||
@ -154,6 +195,14 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> _onRefresh() {
|
||||
context
|
||||
.read<HomeBloc>()
|
||||
|
28
lib/presentation/likes_bloc/likes_bloc.dart
Normal file
28
lib/presentation/likes_bloc/likes_bloc.dart
Normal file
@ -0,0 +1,28 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:pmu/presentation/likes_bloc/likes_events.dart';
|
||||
import 'package:pmu/presentation/likes_bloc/likes_state.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
const String _likedPrefsKey = 'liked';
|
||||
class LikeBloc extends Bloc<LikeEvent, LikeState> {
|
||||
LikeBloc() : super(const LikeState(likedIds: [])) {
|
||||
on<ChangeLikeEvent>(_onChangeLike);
|
||||
on<LoadLikesEvent>(_onLoadLikes);
|
||||
}
|
||||
Future<void> _onLoadLikes(LoadLikesEvent event, Emitter<LikeState> emit) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final data = prefs.getStringList(_likedPrefsKey);
|
||||
emit(state.copyWith(likedIds: data));
|
||||
}
|
||||
Future<void> _onChangeLike(ChangeLikeEvent event, Emitter<LikeState> emit) async {
|
||||
final updatedList = List<String>.from(state.likedIds ?? []);
|
||||
if (updatedList.contains(event.id)) {
|
||||
updatedList.remove(event.id);
|
||||
} else {
|
||||
updatedList.add(event.id);
|
||||
}
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
prefs.setStringList(_likedPrefsKey, updatedList);
|
||||
emit(state.copyWith(likedIds: updatedList));
|
||||
}
|
||||
}
|
10
lib/presentation/likes_bloc/likes_events.dart
Normal file
10
lib/presentation/likes_bloc/likes_events.dart
Normal file
@ -0,0 +1,10 @@
|
||||
abstract class LikeEvent {
|
||||
const LikeEvent();
|
||||
}
|
||||
class LoadLikesEvent extends LikeEvent {
|
||||
const LoadLikesEvent();
|
||||
}
|
||||
class ChangeLikeEvent extends LikeEvent {
|
||||
final String id;
|
||||
const ChangeLikeEvent(this.id);
|
||||
}
|
12
lib/presentation/likes_bloc/likes_state.dart
Normal file
12
lib/presentation/likes_bloc/likes_state.dart
Normal file
@ -0,0 +1,12 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||
|
||||
part 'likes_state.g.dart';
|
||||
|
||||
@CopyWith()
|
||||
class LikeState extends Equatable {
|
||||
final List<String>? likedIds;
|
||||
const LikeState({required this.likedIds});
|
||||
@override
|
||||
List<Object?> get props => [likedIds];
|
||||
}
|
17
lib/presentation/locale_bloc/locale_bloc.dart
Normal file
17
lib/presentation/locale_bloc/locale_bloc.dart
Normal file
@ -0,0 +1,17 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:pmu/components/locale/l10n/app_locale.dart';
|
||||
import 'package:pmu/presentation/locale_bloc/locale_events.dart';
|
||||
import 'package:pmu/presentation/locale_bloc/locale_state.dart';
|
||||
|
||||
class LocaleBloc extends Bloc<LocaleEvent, LocaleState> {
|
||||
LocaleBloc(Locale defaultLocale) : super(LocaleState(currentLocale: defaultLocale)) {
|
||||
on<ChangeLocaleEvent>(_onChangeLocale);
|
||||
}
|
||||
Future<void> _onChangeLocale(ChangeLocaleEvent event, Emitter<LocaleState> emit) async {
|
||||
final toChange = AppLocale.supportedLocales
|
||||
.firstWhere((e) => e.languageCode != state.currentLocale.languageCode);
|
||||
emit(state.copyWith(currentLocale: toChange));
|
||||
}
|
||||
}
|
6
lib/presentation/locale_bloc/locale_events.dart
Normal file
6
lib/presentation/locale_bloc/locale_events.dart
Normal file
@ -0,0 +1,6 @@
|
||||
abstract class LocaleEvent {
|
||||
const LocaleEvent();
|
||||
}
|
||||
class ChangeLocaleEvent extends LocaleEvent {
|
||||
const ChangeLocaleEvent();
|
||||
}
|
11
lib/presentation/locale_bloc/locale_state.dart
Normal file
11
lib/presentation/locale_bloc/locale_state.dart
Normal file
@ -0,0 +1,11 @@
|
||||
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
part 'locale_state.g.dart';
|
||||
@CopyWith()
|
||||
class LocaleState extends Equatable {
|
||||
final Locale currentLocale;
|
||||
const LocaleState({required this.currentLocale});
|
||||
@override
|
||||
List<Object?> get props => [currentLocale];
|
||||
}
|
@ -5,6 +5,8 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import shared_preferences_foundation
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
}
|
||||
|
9
makefile
9
makefile
@ -3,4 +3,11 @@ gen:
|
||||
icon:
|
||||
flutter pub run flutter_launcher_icons:main
|
||||
init_res:
|
||||
dart pub global activate flutter_asset_generator
|
||||
dart pub global activate flutter_asset_generator
|
||||
format:
|
||||
dart format . --line-length 100
|
||||
res:
|
||||
fgen --output lib/components/resources/resources.g.dart --no-watch --no-preview
|
||||
loc:
|
||||
flutter gen-l10n; \
|
||||
make format
|
196
pubspec.lock
196
pubspec.lock
@ -238,6 +238,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -275,11 +283,29 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.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:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -304,6 +330,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -320,6 +354,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -456,6 +498,62 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_parsing
|
||||
sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
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: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.5"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -496,6 +594,62 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.3"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.3"
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -605,6 +759,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
vector_graphics:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics
|
||||
sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.11+1"
|
||||
vector_graphics_codec:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_codec
|
||||
sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.11+1"
|
||||
vector_graphics_compiler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_compiler
|
||||
sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.11+1"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -653,6 +831,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -663,4 +857,4 @@ packages:
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.5.2 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
flutter: ">=3.24.0"
|
||||
|
12
pubspec.yaml
12
pubspec.yaml
@ -34,6 +34,7 @@ dependencies:
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
flutter_svg: 2.0.7
|
||||
cupertino_icons: ^1.0.8
|
||||
json_annotation: ^4.8.1
|
||||
dio: ^5.4.2+1
|
||||
@ -42,6 +43,13 @@ dependencies:
|
||||
flutter_bloc: ^8.1.5
|
||||
copy_with_extension_gen: ^5.0.4
|
||||
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
intl: 0.19.0
|
||||
|
||||
shared_preferences: 2.2.3
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
@ -66,7 +74,7 @@ flutter_icons:
|
||||
mid_sdk_android: 21
|
||||
# The following section is specific to Flutter packages.
|
||||
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.
|
||||
@ -102,3 +110,5 @@ flutter:
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/to/font-from-package
|
||||
assets:
|
||||
- assets/svg/
|
||||
|
Loading…
Reference in New Issue
Block a user