Причесал код

This commit is contained in:
Вячеслав Иванов 2024-09-25 12:55:28 +04:00
parent 16f0f53745
commit decf3d296d
29 changed files with 178 additions and 151 deletions

View File

@ -2,8 +2,8 @@
"@@locale": "en",
"search": "Search",
"liked": "liked!",
"disliked": "disliked :(",
"liked": "liked",
"disliked": "disliked",
"arbEnding": "Чтобы не забыть про отсутствие запятой :)"
}

View File

@ -2,8 +2,8 @@
"@@locale": "ru",
"search": "Поиск",
"liked": "понравился!",
"disliked": "разонравился :(",
"liked": "поставлен лайк",
"disliked": "убран лайк",
"arbEnding": "Чтобы не забыть про отсутствие запятой :)"
}

View File

@ -3,4 +3,4 @@ import '../locale/l10n/app_locale.dart';
extension LocalContextX on BuildContext {
AppLocale get locale => AppLocale.of(this)!;
}
}

View File

@ -104,13 +104,13 @@ abstract class AppLocale {
/// No description provided for @liked.
///
/// In ru, this message translates to:
/// **'понравился!'**
/// **'поставлен лайк'**
String get liked;
/// No description provided for @disliked.
///
/// In ru, this message translates to:
/// **'разонравился :('**
/// **'убран лайк'**
String get disliked;
/// No description provided for @arbEnding.

View File

@ -10,10 +10,10 @@ class AppLocaleEn extends AppLocale {
String get search => 'Search';
@override
String get liked => 'liked!';
String get liked => 'liked';
@override
String get disliked => 'disliked :(';
String get disliked => 'disliked';
@override
String get arbEnding => 'Чтобы не забыть про отсутствие запятой :)';

View File

@ -10,10 +10,10 @@ class AppLocaleRu extends AppLocale {
String get search => 'Поиск';
@override
String get liked => 'понравился!';
String get liked => 'поставлен лайк';
@override
String get disliked => 'разонравился :(';
String get disliked => 'убран лайк';
@override
String get arbEnding => 'Чтобы не забыть про отсутствие запятой :)';

View File

@ -7,4 +7,4 @@ class R {
static const String ASSETS_SVG_RU_SVG = 'assets/svg/ru.svg';
static const String ASSETS_SVG_UK_SVG = 'assets/svg/uk.svg';
}
}

View File

@ -11,10 +11,10 @@ class 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);
}
}
}

View File

@ -12,7 +12,8 @@ class CharactersDto {
this.meta,
});
factory CharactersDto.fromJson(Map<String, dynamic> json) => _$CharactersDtoFromJson(json);
factory CharactersDto.fromJson(Map<String, dynamic> json) =>
_$CharactersDtoFromJson(json);
}
@JsonSerializable(createToJson: false)
@ -23,7 +24,8 @@ class CharacterDataDto {
const CharacterDataDto({this.id, this.type, this.attributes});
factory CharacterDataDto.fromJson(Map<String, dynamic> json) => _$CharacterDataDtoFromJson(json);
factory CharacterDataDto.fromJson(Map<String, dynamic> json) =>
_$CharacterDataDtoFromJson(json);
}
@JsonSerializable(createToJson: false)
@ -33,7 +35,8 @@ class CharacterAttributesDataDto {
final String? species;
final String? image;
const CharacterAttributesDataDto({this.name, this.nationality, this.species, this.image});
const CharacterAttributesDataDto(
{this.name, this.nationality, this.species, this.image});
factory CharacterAttributesDataDto.fromJson(Map<String, dynamic> json) =>
_$CharacterAttributesDataDtoFromJson(json);
@ -45,7 +48,8 @@ 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)
@ -56,5 +60,6 @@ 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);
}

View File

@ -4,27 +4,28 @@ import '../dtos/characters_dto.dart';
extension CharactersDtoToModel on CharactersDto {
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,
);
}
extension CharacterDataDtoToModel on CharacterDataDto {
CardData toDomain() => CardData(
attributes?.name ?? 'UNKNOWN',
image: attributes?.image ??
'https://upload.wikimedia.org/wikipedia/en/archive/b/b1/20210811082420%21Portrait_placeholder.png',
descriptionText: _makeDescriptionText(attributes?.nationality, attributes?.species),
id: id,
);
attributes?.name ?? 'UNKNOWN',
image: attributes?.image ??
'https://upload.wikimedia.org/wikipedia/en/archive/b/b1/20210811082420%21Portrait_placeholder.png',
descriptionText:
_makeDescriptionText(attributes?.nationality, attributes?.species),
id: id,
);
String _makeDescriptionText(String? nationality, String? species) {
return nationality != null && species != null
? 'Nationality - $nationality \nSpecies - $species'
: nationality != null
? 'Nationality - $nationality'
: species != null
? 'Species - $species'
: '';
? 'Nationality - $nationality'
: species != null
? 'Species - $species'
: '';
}
}
}

View File

@ -4,4 +4,4 @@ typedef OnErrorCallback = void Function(String? error);
abstract class ApiInterface {
Future<HomeData?> loadData({OnErrorCallback? onError});
}
}

View File

@ -12,22 +12,23 @@ class MockRepository extends ApiInterface {
'Freeze',
descriptionText: 'so cold..',
image:
'https://www.skedaddlewildlife.com/wp-content/uploads/2018/09/depositphotos_22425309-stock-photo-a-lonely-raccoon-in-winter.jpg',
'https://www.skedaddlewildlife.com/wp-content/uploads/2018/09/depositphotos_22425309-stock-photo-a-lonely-raccoon-in-winter.jpg',
),
CardData(
'Hi',
descriptionText: 'pretty face',
icon: Icons.hail,
image:
'https://www.thesprucepets.com/thmb/nKNaS4I586B_H7sEUw9QAXvWM_0=/2121x0/filters:no_upscale():strip_icc()/GettyImages-135630198-5ba7d225c9e77c0050cff91b.jpg',
'https://www.thesprucepets.com/thmb/nKNaS4I586B_H7sEUw9QAXvWM_0=/2121x0/filters:no_upscale():strip_icc()/GettyImages-135630198-5ba7d225c9e77c0050cff91b.jpg',
),
CardData(
'Orange',
descriptionText: 'I like autumn',
icon: Icons.warning_amber,
image: 'https://furmanagers.com/wp-content/uploads/2019/11/dreamstime_l_22075357.jpg',
image:
'https://furmanagers.com/wp-content/uploads/2019/11/dreamstime_l_22075357.jpg',
),
],
);
}
}
}

View File

@ -33,7 +33,8 @@ class PotterRepository extends ApiInterface {
},
);
final CharactersDto dto = CharactersDto.fromJson(response.data as Map<String, dynamic>);
final CharactersDto dto =
CharactersDto.fromJson(response.data as Map<String, dynamic>);
final HomeData data = dto.toDomain();
return data;
} on DioException catch (e) {
@ -41,4 +42,4 @@ class PotterRepository extends ApiInterface {
return null;
}
}
}
}

View File

@ -8,10 +8,10 @@ class CardData {
final String? id;
CardData(
this.text, {
required this.descriptionText,
this.icon = Icons.ac_unit_outlined,
this.image,
this.id,
});
}
this.text, {
required this.descriptionText,
this.icon = Icons.ac_unit_outlined,
this.image,
this.id,
});
}

View File

@ -5,4 +5,4 @@ class HomeData {
final int? nextPage;
HomeData({this.data, this.nextPage});
}
}

View File

@ -215,7 +215,8 @@ class MyApp extends StatelessWidget {
create: (context) => LikeBloc(),
child: BlocProvider<HomeBloc>(
lazy: false,
create: (context) => HomeBloc(context.read<PotterRepository>()),
create: (context) =>
HomeBloc(context.read<PotterRepository>()),
child: const HomePage(),
),
),
@ -225,4 +226,4 @@ class MyApp extends StatelessWidget {
),
);
}
}
}

View File

@ -10,7 +10,8 @@ abstract class SvgObjects {
];
for (final String p in pics) {
final loader = SvgAssetLoader(p);
svg.cache.putIfAbsent(loader.cacheKey(null), () => loader.loadBytes(null));
svg.cache
.putIfAbsent(loader.cacheKey(null), () => loader.loadBytes(null));
}
}
}
@ -31,4 +32,4 @@ class SvgUk extends StatelessWidget {
Widget build(BuildContext context) {
return SvgPicture.asset(R.ASSETS_SVG_UK_SVG);
}
}
}

View File

@ -23,14 +23,14 @@ class DetailsPage extends StatelessWidget {
const SizedBox(height: 20),
Text(data.text,
style:
const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
Text(data.descriptionText,
style: const TextStyle(fontSize: 22, color: Colors.black87)),
style: const TextStyle(fontSize: 22, color: Colors.black87)),
// Add more details here as needed
],
),
),
);
}
}
}

View File

@ -10,7 +10,8 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
on<HomeLoadDataEvent>(_onLoadData);
}
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 {
@ -36,4 +37,4 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
error: error,
));
}
}
}

View File

@ -7,4 +7,4 @@ class HomeLoadDataEvent extends HomeEvent {
final int? nextPage;
const HomeLoadDataEvent({this.search, this.nextPage});
}
}

View File

@ -20,9 +20,9 @@ class HomeState extends Equatable {
@override
List<Object?> get props => [
data,
isLoading,
isPaginationLoading,
error,
];
}
data,
isLoading,
isPaginationLoading,
error,
];
}

View File

@ -13,22 +13,22 @@ class _Card extends StatelessWidget {
final bool isLiked;
const _Card(
this.text, {
this.icon = Icons.ac_unit_outlined,
required this.descriptionText,
this.image,
this.onLike,
this.onTap,
this.id,
this.isLiked = false,
});
this.text, {
this.icon = Icons.ac_unit_outlined,
required this.descriptionText,
this.image,
this.onLike,
this.onTap,
this.id,
this.isLiked = false,
});
factory _Card.fromData(
CardData data, {
OnLikeCallback onLike,
VoidCallback? onTap,
bool isLiked = false,
}) =>
CardData data, {
OnLikeCallback onLike,
VoidCallback? onTap,
bool isLiked = false,
}) =>
_Card(
data.text,
descriptionText: data.descriptionText,
@ -48,7 +48,7 @@ class _Card extends StatelessWidget {
margin: const EdgeInsets.all(16),
constraints: const BoxConstraints(minHeight: 240),
decoration: BoxDecoration(
color: Colors.white30,
color: Colors.grey,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
@ -61,6 +61,29 @@ class _Card extends StatelessWidget {
),
child: Column(
children: [
Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.only(
left: 8, right: 16, bottom: 16, top: 10),
child: GestureDetector(
onTap: () => onLike?.call(id, text, isLiked),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: isLiked
? const Icon(
Icons.favorite,
color: Colors.redAccent,
key: ValueKey<int>(0),
)
: const Icon(
Icons.favorite_border,
key: ValueKey<int>(1),
),
),
),
),
),
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
@ -83,7 +106,8 @@ class _Card extends StatelessWidget {
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
padding:
const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@ -100,31 +124,9 @@ class _Card extends StatelessWidget {
],
),
),
Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.only(left: 8, right: 16, bottom: 16),
child: GestureDetector(
onTap: () => onLike?.call(id, text, isLiked),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: isLiked
? const Icon(
Icons.favorite,
color: Colors.redAccent,
key: ValueKey<int>(0),
)
: const Icon(
Icons.favorite_border,
key: ValueKey<int>(1),
),
),
),
),
),
],
),
),
);
}
}
}

View File

@ -59,7 +59,7 @@ class _BodyState extends State<_Body> {
void _onNextPageListener() {
if (scrollController.offset > scrollController.position.maxScrollExtent) {
// preventing multiple pagination request on multiple swipes
// preventing multiple pagination request on multiple swipes
final bloc = context.read<HomeBloc>();
if (!bloc.state.isPaginationLoading) {
bloc.add(HomeLoadDataEvent(
@ -93,14 +93,16 @@ class _BodyState extends State<_Body> {
controller: searchController,
placeholder: context.locale.search,
onChanged: (search) {
Debounce.run(
() => context.read<HomeBloc>().add(HomeLoadDataEvent(search: search)));
Debounce.run(() => context
.read<HomeBloc>()
.add(HomeLoadDataEvent(search: search)));
},
),
),
),
GestureDetector(
onTap: () => context.read<LocaleBloc>().add(const ChangeLocaleEvent()),
onTap: () =>
context.read<LocaleBloc>().add(const ChangeLocaleEvent()),
child: SizedBox.square(
dimension: 50,
child: Padding(
@ -120,36 +122,42 @@ class _BodyState extends State<_Body> {
BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) => state.error != null
? Text(
state.error ?? '',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: Colors.red),
)
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();
},
),
),
);
},
),
? 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
@ -162,7 +170,9 @@ class _BodyState extends State<_Body> {
}
Future<void> _onRefresh() {
context.read<HomeBloc>().add(HomeLoadDataEvent(search: searchController.text));
context
.read<HomeBloc>()
.add(HomeLoadDataEvent(search: searchController.text));
return Future.value(null);
}
@ -192,4 +202,4 @@ class _BodyState extends State<_Body> {
));
});
}
}
}

View File

@ -11,14 +11,16 @@ class LikeBloc extends Bloc<LikeEvent, LikeState> {
on<LoadLikesEvent>(_onLoadLikes);
}
Future<void> _onLoadLikes(LoadLikesEvent event, Emitter<LikeState> emit) async {
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 {
Future<void> _onChangeLike(
ChangeLikeEvent event, Emitter<LikeState> emit) async {
final updatedList = List<String>.from(state.likedIds ?? []);
if (updatedList.contains(event.id)) {
@ -32,4 +34,4 @@ class LikeBloc extends Bloc<LikeEvent, LikeState> {
emit(state.copyWith(likedIds: updatedList));
}
}
}

View File

@ -10,4 +10,4 @@ class ChangeLikeEvent extends LikeEvent {
final String id;
const ChangeLikeEvent(this.id);
}
}

View File

@ -11,4 +11,4 @@ class LikeState extends Equatable {
@override
List<Object?> get props => [likedIds];
}
}

View File

@ -5,13 +5,15 @@ import 'package:flutter_app/presentation/locale_bloc/locale_state.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class LocaleBloc extends Bloc<LocaleEvent, LocaleState> {
LocaleBloc(Locale defaultLocale) : super(LocaleState(currentLocale: defaultLocale)) {
LocaleBloc(Locale defaultLocale)
: super(LocaleState(currentLocale: defaultLocale)) {
on<ChangeLocaleEvent>(_onChangeLocale);
}
Future<void> _onChangeLocale(ChangeLocaleEvent event, Emitter<LocaleState> emit) async {
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));
}
}
}

View File

@ -4,4 +4,4 @@ abstract class LocaleEvent {
class ChangeLocaleEvent extends LocaleEvent {
const ChangeLocaleEvent();
}
}

View File

@ -12,4 +12,4 @@ class LocaleState extends Equatable {
@override
List<Object?> get props => [currentLocale];
}
}