Седьмая сделано

This commit is contained in:
Marselchi 2024-11-12 15:20:12 +04:00
parent 0f8228076c
commit 763121d9a4
37 changed files with 771 additions and 242 deletions

View File

@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="untitled"
android:label="Fortnight"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 15 KiB

BIN
assets/icon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

19
assets/svg/ru.svg Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 -4 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_503_2726)">
<rect x="0.25" y="0.25" width="27.5" height="19.5" rx="1.75" fill="white" stroke="#F5F5F5" stroke-width="0.5"/>
<mask id="mask0_503_2726" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="28" height="20">
<rect x="0.25" y="0.25" width="27.5" height="19.5" rx="1.75" fill="white" stroke="white" stroke-width="0.5"/>
</mask>
<g mask="url(#mask0_503_2726)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 13.3333H28V6.66667H0V13.3333Z" fill="#0C47B7"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 20H28V13.3333H0V20Z" fill="#E53B35"/>
</g>
</g>
<defs>
<clipPath id="clip0_503_2726">
<rect width="28" height="20" rx="2" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 968 B

34
assets/svg/us.svg Normal file
View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 -4 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_503_3486)">
<rect width="28" height="20" rx="2" fill="white"/>
<mask id="mask0_503_3486" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="28" height="20">
<rect width="28" height="20" rx="2" fill="white"/>
</mask>
<g mask="url(#mask0_503_3486)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M28 0H0V1.33333H28V0ZM28 2.66667H0V4H28V2.66667ZM0 5.33333H28V6.66667H0V5.33333ZM28 8H0V9.33333H28V8ZM0 10.6667H28V12H0V10.6667ZM28 13.3333H0V14.6667H28V13.3333ZM0 16H28V17.3333H0V16ZM28 18.6667H0V20H28V18.6667Z" fill="#D02F44"/>
<rect width="12" height="9.33333" fill="#46467F"/>
<g filter="url(#filter0_d_503_3486)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.66665 1.99999C2.66665 2.36818 2.36817 2.66666 1.99998 2.66666C1.63179 2.66666 1.33331 2.36818 1.33331 1.99999C1.33331 1.63181 1.63179 1.33333 1.99998 1.33333C2.36817 1.33333 2.66665 1.63181 2.66665 1.99999ZM5.33331 1.99999C5.33331 2.36818 5.03484 2.66666 4.66665 2.66666C4.29846 2.66666 3.99998 2.36818 3.99998 1.99999C3.99998 1.63181 4.29846 1.33333 4.66665 1.33333C5.03484 1.33333 5.33331 1.63181 5.33331 1.99999ZM7.33331 2.66666C7.7015 2.66666 7.99998 2.36818 7.99998 1.99999C7.99998 1.63181 7.7015 1.33333 7.33331 1.33333C6.96512 1.33333 6.66665 1.63181 6.66665 1.99999C6.66665 2.36818 6.96512 2.66666 7.33331 2.66666ZM10.6666 1.99999C10.6666 2.36818 10.3682 2.66666 9.99998 2.66666C9.63179 2.66666 9.33331 2.36818 9.33331 1.99999C9.33331 1.63181 9.63179 1.33333 9.99998 1.33333C10.3682 1.33333 10.6666 1.63181 10.6666 1.99999ZM3.33331 3.99999C3.7015 3.99999 3.99998 3.70152 3.99998 3.33333C3.99998 2.96514 3.7015 2.66666 3.33331 2.66666C2.96512 2.66666 2.66665 2.96514 2.66665 3.33333C2.66665 3.70152 2.96512 3.99999 3.33331 3.99999ZM6.66665 3.33333C6.66665 3.70152 6.36817 3.99999 5.99998 3.99999C5.63179 3.99999 5.33331 3.70152 5.33331 3.33333C5.33331 2.96514 5.63179 2.66666 5.99998 2.66666C6.36817 2.66666 6.66665 2.96514 6.66665 3.33333ZM8.66665 3.99999C9.03484 3.99999 9.33331 3.70152 9.33331 3.33333C9.33331 2.96514 9.03484 2.66666 8.66665 2.66666C8.29846 2.66666 7.99998 2.96514 7.99998 3.33333C7.99998 3.70152 8.29846 3.99999 8.66665 3.99999ZM10.6666 4.66666C10.6666 5.03485 10.3682 5.33333 9.99998 5.33333C9.63179 5.33333 9.33331 5.03485 9.33331 4.66666C9.33331 4.29847 9.63179 3.99999 9.99998 3.99999C10.3682 3.99999 10.6666 4.29847 10.6666 4.66666ZM7.33331 5.33333C7.7015 5.33333 7.99998 5.03485 7.99998 4.66666C7.99998 4.29847 7.7015 3.99999 7.33331 3.99999C6.96512 3.99999 6.66665 4.29847 6.66665 4.66666C6.66665 5.03485 6.96512 5.33333 7.33331 5.33333ZM5.33331 4.66666C5.33331 5.03485 5.03484 5.33333 4.66665 5.33333C4.29846 5.33333 3.99998 5.03485 3.99998 4.66666C3.99998 4.29847 4.29846 3.99999 4.66665 3.99999C5.03484 3.99999 5.33331 4.29847 5.33331 4.66666ZM1.99998 5.33333C2.36817 5.33333 2.66665 5.03485 2.66665 4.66666C2.66665 4.29847 2.36817 3.99999 1.99998 3.99999C1.63179 3.99999 1.33331 4.29847 1.33331 4.66666C1.33331 5.03485 1.63179 5.33333 1.99998 5.33333ZM3.99998 5.99999C3.99998 6.36819 3.7015 6.66666 3.33331 6.66666C2.96512 6.66666 2.66665 6.36819 2.66665 5.99999C2.66665 5.6318 2.96512 5.33333 3.33331 5.33333C3.7015 5.33333 3.99998 5.6318 3.99998 5.99999ZM5.99998 6.66666C6.36817 6.66666 6.66665 6.36819 6.66665 5.99999C6.66665 5.6318 6.36817 5.33333 5.99998 5.33333C5.63179 5.33333 5.33331 5.6318 5.33331 5.99999C5.33331 6.36819 5.63179 6.66666 5.99998 6.66666ZM9.33331 5.99999C9.33331 6.36819 9.03484 6.66666 8.66665 6.66666C8.29846 6.66666 7.99998 6.36819 7.99998 5.99999C7.99998 5.6318 8.29846 5.33333 8.66665 5.33333C9.03484 5.33333 9.33331 5.6318 9.33331 5.99999ZM9.99998 8C10.3682 8 10.6666 7.70152 10.6666 7.33333C10.6666 6.96514 10.3682 6.66666 9.99998 6.66666C9.63179 6.66666 9.33331 6.96514 9.33331 7.33333C9.33331 7.70152 9.63179 8 9.99998 8ZM7.99998 7.33333C7.99998 7.70152 7.7015 8 7.33331 8C6.96512 8 6.66665 7.70152 6.66665 7.33333C6.66665 6.96514 6.96512 6.66666 7.33331 6.66666C7.7015 6.66666 7.99998 6.96514 7.99998 7.33333ZM4.66665 8C5.03484 8 5.33331 7.70152 5.33331 7.33333C5.33331 6.96514 5.03484 6.66666 4.66665 6.66666C4.29846 6.66666 3.99998 6.96514 3.99998 7.33333C3.99998 7.70152 4.29846 8 4.66665 8ZM2.66665 7.33333C2.66665 7.70152 2.36817 8 1.99998 8C1.63179 8 1.33331 7.70152 1.33331 7.33333C1.33331 6.96514 1.63179 6.66666 1.99998 6.66666C2.36817 6.66666 2.66665 6.96514 2.66665 7.33333Z" fill="url(#paint0_linear_503_3486)"/>
</g>
</g>
</g>
<defs>
<filter id="filter0_d_503_3486" x="1.33331" y="1.33333" width="9.33331" height="7.66667" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_503_3486"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_503_3486" result="shape"/>
</filter>
<linearGradient id="paint0_linear_503_3486" x1="1.33331" y1="1.33333" x2="1.33331" y2="7.99999" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#F0F0F0"/>
</linearGradient>
<clipPath id="clip0_503_3486">
<rect width="28" height="20" rx="2" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

6
l10n.yaml Normal file
View 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

9
l10n/app_en.arb Normal file
View File

@ -0,0 +1,9 @@
{
"@@locale": "en",
"search": "Search",
"liked": "Liked",
"disliked": "Disliked",
"arbEnding": "Чтобы не забыть про отсутствие запятой"
}

9
l10n/app_ru.arb Normal file
View File

@ -0,0 +1,9 @@
{
"@@locale": "ru",
"search": "Поиск",
"liked": "Понравилось",
"disliked": "Не нравится",
"arbEnding": "Чтобы не забыть про отсутствие запятой"
}

View File

@ -0,0 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:untitled/components/locale/l10n/app_locale.dart';
extension LocalContextX on BuildContext{
AppLocale get locale => AppLocale.of(this)!;
}

View File

@ -1,7 +1,7 @@
import 'dart:async';
import 'dart:ui';
class Debounce{
class Debounce {
factory Debounce() => _instance;
Debounce._();

View File

@ -1,5 +1,6 @@
import 'package:json_annotation/json_annotation.dart';
part 'pickaxes_dto.g.dart';
@JsonSerializable(createToJson: false)
class PickaxesDto {
final List<PickaxeDataDto>? data;
@ -8,8 +9,9 @@ class PickaxesDto {
factory PickaxesDto.fromJson(Map<String, dynamic> json) => _$PickaxesDtoFromJson(json);
}
@JsonSerializable(createToJson: false)
class PickaxeDataDto{
class PickaxeDataDto {
final String? id;
final String? name;
final String? description;
@ -20,8 +22,9 @@ class PickaxeDataDto{
factory PickaxeDataDto.fromJson(Map<String, dynamic> json) => _$PickaxeDataDtoFromJson(json);
}
@JsonSerializable(createToJson: false)
class PickaxeRarityDto{
class PickaxeRarityDto {
final String? value;
final String? displayValue;
@ -31,7 +34,7 @@ class PickaxeRarityDto{
}
@JsonSerializable(createToJson: false)
class PickaxeImagesDto{
class PickaxeImagesDto {
final String? smallIcon;
final String? icon;

View File

@ -1,12 +1,12 @@
import 'package:untitled/data/dtos/pickaxes_dto.dart';
import 'package:untitled/domain/models/card.dart';
extension PickaxeDataDtoToModel on PickaxeDataDto{
extension PickaxeDataDtoToModel on PickaxeDataDto {
CardData toDomain() => CardData(
name ?? 'UNKNOWN',
imageUrl: images?.icon,
descriptionText: description ?? 'NO DESCRIPTION',
rarity: rarity?.displayValue ?? 'NO RARITY',
id: id,
);
}

View File

@ -1,6 +1,7 @@
import 'package:untitled/domain/models/card.dart';
typedef OnErrorCallback = void Function(String? error);
abstract class ApiInterface {
Future<List<CardData>?> loadData();
}

View File

@ -5,7 +5,7 @@ import 'package:untitled/data/mappers/pickaxes_mapper.dart';
import 'package:untitled/data/repositories/api_interface.dart';
import 'package:untitled/domain/models/card.dart';
class FortniteRepository extends ApiInterface{
class FortniteRepository extends ApiInterface {
static final Dio _dio = Dio()
..interceptors.add(PrettyDioLogger(
requestHeader: true,
@ -16,21 +16,20 @@ class FortniteRepository extends ApiInterface{
@override
Future<List<CardData>?> loadData({String? q, OnErrorCallback? onError}) async {
try{
try {
String url = '$_baseUrl/v2/cosmetics/br/search/all?type=pickaxe';
if(q != null && q != ""){
if (q != null && q != "") {
url += '&matchMethod=contains&name=$q';
}
final Response<dynamic> response = await _dio.get<Map<dynamic,dynamic>>(url);
final Response<dynamic> response = await _dio.get<Map<dynamic, dynamic>>(url);
final PickaxesDto dto = PickaxesDto.fromJson(response.data as Map<String, dynamic>);
final List<CardData>? data = dto.data?.map((e) => e.toDomain()).toList();
return data;
} on DioException catch (e){
} on DioException catch (e) {
onError?.call(e.error?.toString());
return null;
}
}
}

View File

@ -1,8 +1,7 @@
import 'package:untitled/data/repositories/api_interface.dart';
import 'package:untitled/domain/models/card.dart';
class MockRepository extends ApiInterface{
class MockRepository extends ApiInterface {
@override
Future<List<CardData>?> loadData() async {
return [
@ -14,12 +13,10 @@ class MockRepository extends ApiInterface{
CardData("Blackstar Edge",
descriptionText: "Forged long ago in a world beyond the stars.",
rarity: "Epic",
imageUrl:
"https://fortnite-api.com/images/cosmetics/br/pickaxe_galaxyknight/icon.png"),
imageUrl: "https://fortnite-api.com/images/cosmetics/br/pickaxe_galaxyknight/icon.png"),
CardData(
"Black Talon",
descriptionText:
"His life brought peace to the Reef. His death brings a sword.",
descriptionText: "His life brought peace to the Reef. His death brings a sword.",
rarity: "Gaming Legends Series",
imageUrl:
"https://fortnite-api.com/images/cosmetics/br/pickaxe_id_848_wayfarefemale/icon.png",

View File

@ -3,11 +3,13 @@ class CardData {
final String rarity;
final String descriptionText;
final String? imageUrl;
final String? id;
CardData(
this.name, {
required this.descriptionText,
required this.rarity,
this.imageUrl,
this.id,
});
}

View File

@ -1,8 +1,15 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:untitled/data/repositories/fortnite_reppository.dart';
import 'package:untitled/presentation/home_page/bloc/bloc.dart';
import 'package:untitled/presentation/home_page/home_page.dart';
import 'package:untitled/presentation/home_page/like_bloc/like_bloc.dart';
import 'package:untitled/presentation/home_page/locale_bloc/locale_bloc.dart';
import 'package:untitled/presentation/home_page/locale_bloc/locale_state.dart';
import 'components/locale/l10n/app_locale.dart';
void main() {
runApp(const MyApp());
@ -13,8 +20,16 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
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.deepPurple),
useMaterial3: true,
@ -22,12 +37,19 @@ class MyApp extends StatelessWidget {
home: RepositoryProvider<FortniteRepository>(
lazy: true,
create: (_) => FortniteRepository(),
child: BlocProvider<LikeBloc>(
lazy: false,
create: (context) => LikeBloc(),
child: BlocProvider<HomeBlock>(
lazy: false,
create: (context) => HomeBlock(context.read<FortniteRepository>()),
child: const MyHomePage(title: "PIbd-31 Sagirov Marsel"),
),
),
),
);
}
),
);
}
}

View File

@ -0,0 +1,35 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:untitled/components/resources.g.dart';
import 'package:flutter_svg/flutter_svg.dart';
abstract class SvgObjects{
static void init(){
final pics = <String>[
R.ASSETS_SVG_RU_SVG,
R.ASSETS_SVG_US_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 SvgUs extends StatelessWidget {
const SvgUs ({super.key});
@override
Widget build(BuildContext context) {
return SvgPicture.asset(R.ASSETS_SVG_US_SVG);
}
}

View File

@ -44,10 +44,7 @@ class DetailsPage extends StatelessWidget {
left: 10,
child: Text(
data.rarity,
style: Theme.of(context)
.textTheme
.bodyLarge
?.copyWith(color: Colors.red),
style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.red),
),
),
]),

View File

@ -6,11 +6,11 @@ import 'package:untitled/presentation/home_page/bloc/state.dart';
class HomeBlock extends Bloc<HomeEvent, HomeState> {
final FortniteRepository repo;
HomeBlock(this.repo) : super(const HomeState()){
HomeBlock(this.repo) : super(const HomeState()) {
on<HomeLoadDataEvent>(_onLoadData);
}
Future<void> _onLoadData (HomeLoadDataEvent event, Emitter<HomeState> emit) async {
Future<void> _onLoadData(HomeLoadDataEvent event, Emitter<HomeState> emit) async {
emit(state.copyWith(isLoading: true));
String? error;

View File

@ -2,7 +2,7 @@ abstract class HomeEvent {
const HomeEvent();
}
class HomeLoadDataEvent extends HomeEvent{
class HomeLoadDataEvent extends HomeEvent {
final String? search;
const HomeLoadDataEvent({this.search});
}

View File

@ -1,15 +1,16 @@
import 'package:equatable/equatable.dart';
import 'package:untitled/domain/models/card.dart';
class HomeState extends Equatable{
class HomeState extends Equatable {
final List<CardData>? data;
final bool isLoading;
final String? error;
const HomeState({this.data, this.isLoading = false, this.error});
HomeState copyWith({List<CardData>? data, bool? isLoading, String? error}) => HomeState(data: data ?? this.data, isLoading: isLoading ?? this.isLoading, error: error ?? this.error);
HomeState copyWith({List<CardData>? data, bool? isLoading, String? error}) => HomeState(
data: data ?? this.data, isLoading: isLoading ?? this.isLoading, error: error ?? this.error);
@override
List<Object?> get props => [data,isLoading,error];
List<Object?> get props => [data, isLoading, error];
}

View File

@ -1,14 +1,16 @@
part of 'home_page.dart';
typedef OnLikeCallback = void Function(String name, bool isLiked)?;
typedef OnLikeCallback = void Function(String? id,String name, bool isLiked)?;
class _Card extends StatefulWidget {
class _Card extends StatelessWidget {
final String name;
final String rarity;
final String descriptionText;
final String? imageUrl;
final OnLikeCallback onLike;
final VoidCallback? onTap;
final String? id;
final bool isLiked;
const _Card(
this.name, {
required this.descriptionText,
@ -16,19 +18,113 @@ class _Card extends StatefulWidget {
this.imageUrl,
this.onLike,
this.onTap,
this.id,
this.isLiked = false,
});
factory _Card.fromData(CardData data,
{OnLikeCallback onLike, VoidCallback? onTap}) =>
_Card(
factory _Card.fromData(CardData data, {OnLikeCallback onLike, VoidCallback? onTap, bool isLiked = false}) => _Card(
data.name,
descriptionText: data.descriptionText,
rarity: data.rarity,
imageUrl: data.imageUrl,
onLike: onLike,
onTap: onTap,
);
isLiked: isLiked,
id: data.id
);
@override
State<_Card> createState() => _CardState();
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.all(10),
padding: const EdgeInsets.all(16),
constraints: const BoxConstraints(minHeight: 140),
decoration: BoxDecoration(
color: Colors.white70,
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(.5),
spreadRadius: 4,
offset: const Offset(0, 5),
blurRadius: 8,
),
],
),
child: IntrinsicHeight(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(20),
topLeft: Radius.circular(20),
),
child: SizedBox(
height: double.infinity,
width: 100,
child: Image.network(
imageUrl ?? '',
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Placeholder(),
),
),
),
Flexible(
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: Theme.of(context).textTheme.headlineLarge,
),
Text(
descriptionText,
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
"Rarity:${rarity}",
style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.red),
),
],
),
),
),
Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.only(
left: 8,
right: 8,
bottom: 8,
),
child: GestureDetector(
onTap: () => onLike?.call(id,name,isLiked),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: isLiked
? const Icon(
Icons.favorite,
color: Colors.redAccent,
key: ValueKey<int>(0),
)
: const Icon(
Icons.favorite_border,
key: ValueKey<int>(1),
),
),
),
),
),
],
),
),
),
);
}
}

View File

@ -1,9 +1,18 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:untitled/components/extensions/context_x.dart';
import 'package:untitled/presentation/common/svg_objects.dart';
import 'package:untitled/presentation/home_page/bloc/bloc.dart';
import 'package:untitled/presentation/home_page/bloc/events.dart';
import 'package:untitled/presentation/home_page/bloc/state.dart';
import 'package:untitled/presentation/home_page/like_bloc/like_bloc.dart';
import 'package:untitled/presentation/home_page/like_bloc/like_event.dart';
import 'package:untitled/presentation/home_page/like_bloc/like_state.dart';
import 'package:untitled/presentation/home_page/locale_bloc/locale_bloc.dart';
import 'package:untitled/presentation/home_page/locale_bloc/locale_events.dart';
import 'package:untitled/presentation/home_page/locale_bloc/locale_state.dart';
import '../../components/utils/debounce.dart';
import '../../domain/models/card.dart';
import '../details_page/details_page.dart';
@ -37,48 +46,83 @@ class Body extends StatefulWidget {
@override
State<Body> createState() => _BodyState();
}
class _BodyState extends State<Body> {
final searchController = TextEditingController();
@override
void initState() {
SvgObjects.init();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<HomeBlock>().add(const HomeLoadDataEvent());
context.read<LikeBloc>().add(const LoadLikesEvent());
});
super.initState();
}
@override
void dispose() {
searchController.dispose();
super.dispose();
}
Future<void> _onRefresh() {
context.read<HomeBlock>().add(HomeLoadDataEvent(search: searchController.text));
return Future.value(null);
}
void _onLike(String? id, String name, bool isLiked){
if(id!=null){
context.read<LikeBloc>().add(ChangeLikeEvent(id));
_showSnackBar(context, name, !isLiked);
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
Row(
children: [
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.all(12),
child: CupertinoSearchTextField(
controller: searchController,
placeholder: context.locale.search,
onSubmitted: (search) {
Debounce.run(() => context.read<HomeBlock>().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 SvgUs();
},
),
),
),
)
],
),
BlocBuilder<HomeBlock, HomeState>(
builder: (context,state) => state.error != null ?
Text(
builder: (context, state) => state.error != null
? Text(
state.error ?? "",
style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: Colors.red),
)
:
state.isLoading ?
const CircularProgressIndicator() :
Expanded(
: state.isLoading
? const CircularProgressIndicator()
: BlocBuilder<LikeBloc, LikeState>(
builder: (context, likeState) {
return Expanded(
child: RefreshIndicator(
onRefresh: _onRefresh,
child: ListView.builder(
@ -89,16 +133,16 @@ class _BodyState extends State<Body> {
return data != null
? _Card.fromData(
data,
onLike: (name, isLiked) =>
_showSnackBar(context, name, isLiked),
onTap: () => _navToDetails(context,data),
onLike: _onLike,
isLiked: likeState.likedIds?.contains(data.id) == true,
onTap: () => _navToDetails(context, data),
)
: const SizedBox.shrink();
},
),
)
)
)
));
}
))
],
);
}
@ -107,7 +151,7 @@ class _BodyState extends State<Body> {
WidgetsBinding.instance.addPostFrameCallback((_) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(
'You are ${isLiked ? 'liked' : 'disliked'} $name',
'${isLiked ? context.locale.liked : context.locale.disliked} $name',
style: Theme.of(context).textTheme.bodyLarge,
),
backgroundColor: Colors.pinkAccent,
@ -117,114 +161,6 @@ class _BodyState extends State<Body> {
}
void _navToDetails(BuildContext context, CardData data) {
Navigator.push(
context, CupertinoPageRoute(builder: (context) => DetailsPage(data)));
}
}
class _CardState extends State<_Card> {
bool isLiked = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: widget.onTap,
child: Container(
margin: const EdgeInsets.all(10),
padding: const EdgeInsets.all(16),
constraints: const BoxConstraints(minHeight: 140),
decoration: BoxDecoration(
color: Colors.white70,
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(.5),
spreadRadius: 4,
offset: const Offset(0, 5),
blurRadius: 8,
),
],
),
child: IntrinsicHeight(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(20),
topLeft: Radius.circular(20),
),
child: SizedBox(
height: double.infinity,
width: 100,
child: Image.network(
widget.imageUrl ?? '',
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Placeholder(),
),
),
),
Flexible(
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.name,
style: Theme.of(context).textTheme.headlineLarge,
),
Text(
widget.descriptionText,
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
"Rarity:${widget.rarity}",
style: Theme.of(context)
.textTheme
.bodyLarge
?.copyWith(color: Colors.red),
),
],
),
),
),
Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.only(
left: 8,
right: 8,
bottom: 8,
),
child: GestureDetector(
onTap: () {
setState(() {
isLiked = !isLiked;
});
widget.onLike?.call(widget.name, isLiked);
},
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: isLiked
? const Icon(
Icons.favorite,
color: Colors.redAccent,
key: ValueKey<int>(0),
)
: const Icon(
Icons.favorite_border,
key: ValueKey<int>(1),
),
),
),
),
),
],
),
),
),
);
Navigator.push(context, CupertinoPageRoute(builder: (context) => DetailsPage(data)));
}
}

View File

@ -0,0 +1,36 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'like_event.dart';
import 'like_state.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));
}
}

View File

@ -0,0 +1,13 @@
abstract class LikeEvent {
const LikeEvent();
}
class LoadLikesEvent extends LikeEvent {
const LoadLikesEvent();
}
class ChangeLikeEvent extends LikeEvent {
final String id;
const ChangeLikeEvent(this.id);
}

View File

@ -0,0 +1,12 @@
import 'package:equatable/equatable.dart';
class LikeState extends Equatable {
final List<String>? likedIds;
const LikeState({required this.likedIds});
@override
List<Object?> get props => [likedIds];
LikeState copyWith({List<String>? likedIds }) => LikeState(likedIds: likedIds ?? this.likedIds);
}

View File

@ -0,0 +1,18 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../components/locale/l10n/app_locale.dart';
import 'locale_events.dart';
import '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));
}
}

View File

@ -0,0 +1,7 @@
abstract class LocaleEvent {
const LocaleEvent();
}
class ChangeLocaleEvent extends LocaleEvent {
const ChangeLocaleEvent();
}

View File

@ -0,0 +1,14 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
class LocaleState extends Equatable {
final Locale currentLocale;
const LocaleState({required this.currentLocale});
@override
List<Object?> get props => [currentLocale];
LocaleState copyWith({Locale? currentLocale }) => LocaleState(currentLocale: currentLocale ?? this.currentLocale);
}

13
makefile Normal file
View File

@ -0,0 +1,13 @@
gen:
flutter pub run build_runner build --delete-conflicting-outputs
icon:
flutter pub run flutter_launcher_icons:main
init-res:
dart pub global activate flutter_asset_generator
format:
dart format . --line-length 100
res:
fgen --output lib/components/resources.g.dart --no-watch --no-preview
loc:
flutter gen-l10n

View File

@ -22,6 +22,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.7.0"
archive:
dependency: transitive
description:
name: archive
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
url: "https://pub.dev"
source: hosted
version: "3.6.1"
args:
dependency: transitive
description:
@ -134,6 +142,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@ -146,10 +162,10 @@ packages:
dependency: transitive
description:
name: code_builder
sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37
sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
url: "https://pub.dev"
source: hosted
version: "4.10.0"
version: "4.10.1"
collection:
dependency: transitive
description:
@ -162,18 +178,18 @@ packages:
dependency: transitive
description:
name: convert
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.1"
version: "3.1.2"
crypto:
dependency: transitive
description:
name: crypto
sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
version: "3.0.6"
cupertino_icons:
dependency: "direct main"
description:
@ -222,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:
@ -234,10 +258,10 @@ packages:
dependency: transitive
description:
name: fixnum
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
@ -251,6 +275,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.1.6"
flutter_launcher_icons:
dependency: "direct main"
description:
name: flutter_launcher_icons
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
url: "https://pub.dev"
source: hosted
version: "0.13.1"
flutter_lints:
dependency: "direct dev"
description:
@ -259,11 +291,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:
@ -288,6 +338,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:
@ -304,6 +362,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.2"
image:
dependency: transitive
description:
name: image
sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
url: "https://pub.dev"
source: hosted
version: "4.3.0"
intl:
dependency: "direct main"
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.19.0"
io:
dependency: transitive
description:
@ -372,10 +446,10 @@ packages:
dependency: transitive
description:
name: logging
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.3.0"
macros:
dependency: transitive
description:
@ -440,6 +514,62 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@ -480,6 +610,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:
@ -585,10 +771,34 @@ packages:
dependency: transitive
description:
name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.3.2"
version: "1.4.0"
vector_graphics:
dependency: transitive
description:
name: vector_graphics
sha256: "773c9522d66d523e1c7b25dfb95cc91c26a1e17b107039cfe147285e92de7878"
url: "https://pub.dev"
source: hosted
version: "1.1.14"
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: ab9ff38fc771e9ee1139320adbe3d18a60327370c218c60752068ebee4b49ab1
url: "https://pub.dev"
source: hosted
version: "1.1.15"
vector_math:
dependency: transitive
description:
@ -637,6 +847,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:
@ -647,4 +873,4 @@ packages:
version: "3.1.2"
sdks:
dart: ">=3.5.3 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
flutter: ">=3.24.0"

View File

@ -30,16 +30,32 @@ environment:
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
flutter_launcher_icons: 0.13.1
cupertino_icons: ^1.0.8
json_annotation: ^4.8.1
dio: ^5.4.2+1
pretty_dio_logger: ^1.4.0
equatable: ^2.0.5
flutter_bloc: ^8.1.5
flutter_svg: 2.0.7
intl: ^0.19.0
shared_preferences: 2.2.3
flutter_localizations:
sdk: flutter
flutter_icons:
image_path: "assets/icon.jpg"
min_sdk_android: 21
android: "ic_launcher"
build_runner: ^2.4.9
json_serializable: ^6.7.1
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
dev_dependencies:
flutter_test:
sdk: flutter
@ -58,11 +74,13 @@ dev_dependencies:
# 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.
uses-material-design: true
assets:
- assets/svg/
# To add assets to your application, add an assets section, like this:
# assets: