fixed localizations issues

changed application name
updated svg icons
This commit is contained in:
ShabOl 2024-12-16 22:59:15 +04:00
parent a676d454ca
commit 04120c4847
26 changed files with 212 additions and 414 deletions

View File

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

View File

@ -1,5 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-ru" viewBox="0 0 640 480"> <?xml version="1.0" encoding="utf-8"?>
<path fill="#fff" d="M0 0h640v160H0z"/> <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<path fill="#0039a6" d="M0 160h640v160H0z"/> <svg width="800px" height="800px" viewBox="0 -4 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill="#d52b1e" d="M0 320h640v160H0z"/> <g clip-path="url(#clip0_503_2726)">
</svg> <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>

Before

Width:  |  Height:  |  Size: 225 B

After

Width:  |  Height:  |  Size: 968 B

View File

@ -1,9 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-us" viewBox="0 0 640 480"> <?xml version="1.0" encoding="utf-8"?>
<path fill="#bd3d44" d="M0 0h640v480H0"/> <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<path stroke="#fff" stroke-width="37" d="M0 55.3h640M0 129h640M0 203h640M0 277h640M0 351h640M0 425h640"/> <svg width="800px" height="800px" viewBox="0 -4 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill="#192f5d" d="M0 0h364.8v258.5H0"/> <g clip-path="url(#clip0_503_3486)">
<marker id="us-a" markerHeight="30" markerWidth="30"> <rect width="28" height="20" rx="2" fill="white"/>
<path fill="#fff" d="m14 0 9 27L0 10h28L5 27z"/> <mask id="mask0_503_3486" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="28" height="20">
</marker> <rect width="28" height="20" rx="2" fill="white"/>
<path fill="none" marker-mid="url(#us-a)" d="m0 0 16 11h61 61 61 61 60L47 37h61 61 60 61L16 63h61 61 61 61 60L47 89h61 61 60 61L16 115h61 61 61 61 60L47 141h61 61 60 61L16 166h61 61 61 61 60L47 192h61 61 60 61L16 218h61 61 61 61 60z"/> </mask>
</svg> <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>

Before

Width:  |  Height:  |  Size: 648 B

After

Width:  |  Height:  |  Size: 5.5 KiB

3
devtools_options.yaml Normal file
View File

@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

View File

@ -1,5 +1,5 @@
arb-dir: l10n arb-dir: l10n
template-arb-file: app_ru.arb template-arb-file: app_en.arb
output-localization: app_locale.dart output-localization: app_locale.dart
output-dir: lib/components/locale/l10n output-dir: lib/components/locale/l10n
output-class: AppLocale output-class: AppLocale

View File

@ -1,21 +1,15 @@
{ {
"@@locale": "en", "@@locale": "en",
"appBarTitle": "Harry Potter characters", "mainAppBarTitle": "Cryptocurrency Exchange",
"detailsPageAppBarTitle": "Cryptocurrency info",
"settingsPageAppBarTitle": "Settings",
"search": "Search", "searchHint": "Search",
"liked": "You liked", "addedToFavourite": "is added to favourites",
"unliked": "Like removed from", "removedFromFavourite": "is removed from favourites",
"errorOccurred": "Error occurred",
"noErrorMsg": "No message provided",
"retry": "Retry",
"unknown": "Unknown",
"apiYear": "Year", "coinDataPriceChange": "for the last 24 hours",
"apiType": "Type",
"apiRating": "Rating",
"apiDesc": "",
"apiNoDesc": "No description provided",
"arbEnding": "t" "settingsLanguage": "Language"
} }

View File

@ -1,21 +1,15 @@
{ {
"@@locale": "ru", "@@locale": "ru",
"appBarTitle": "Персонажи из Гарри Поттера", "mainAppBarTitle": "Криптобиржа",
"detailsPageAppBarTitle": "Сведения о валюте",
"settingsPageAppBarTitle": "Настройки",
"search": "Поиск", "searchHint": "Поиск",
"liked": "Вы добавили в избранное", "addedToFavourite": "добавлен в избранное",
"unliked": "Вы удалили из избранного", "removedFromFavourite": "удален из избранного",
"errorOccured": "Произошла ошибка",
"noErrorMsg": "Нет сообщения",
"retry": "Повторить",
"unknown": "Неизвестно",
"apiYear": "Год", "coinDataPriceChange": "за последние 24 часа",
"apiType": "Тип",
"apiRating": "Рейтинг",
"apiDesc": "(Описание доступно только на английском языке)",
"apiNoDesc": "Нет описания",
"arbEnding": "t" "settingsLanguage": "Язык"
} }

View File

@ -95,89 +95,53 @@ abstract class AppLocale {
Locale('ru') Locale('ru')
]; ];
/// No description provided for @appBarTitle. /// No description provided for @mainAppBarTitle.
/// ///
/// In ru, this message translates to: /// In en, this message translates to:
/// **'Персонажи из Гарри Поттера'** /// **'Cryptocurrency Exchange'**
String get appBarTitle; String get mainAppBarTitle;
/// No description provided for @search. /// No description provided for @detailsPageAppBarTitle.
/// ///
/// In ru, this message translates to: /// In en, this message translates to:
/// **'Поиск'** /// **'Cryptocurrency info'**
String get search; String get detailsPageAppBarTitle;
/// No description provided for @liked. /// No description provided for @settingsPageAppBarTitle.
/// ///
/// In ru, this message translates to: /// In en, this message translates to:
/// **'Вы добавили в избранное'** /// **'Settings'**
String get liked; String get settingsPageAppBarTitle;
/// No description provided for @unliked. /// No description provided for @searchHint.
/// ///
/// In ru, this message translates to: /// In en, this message translates to:
/// **'Вы удалили из избранного'** /// **'Search'**
String get unliked; String get searchHint;
/// No description provided for @errorOccured. /// No description provided for @addedToFavourite.
/// ///
/// In ru, this message translates to: /// In en, this message translates to:
/// **'Произошла ошибка'** /// **'is added to favourites'**
String get errorOccured; String get addedToFavourite;
/// No description provided for @noErrorMsg. /// No description provided for @removedFromFavourite.
/// ///
/// In ru, this message translates to: /// In en, this message translates to:
/// **'Нет сообщения'** /// **'is removed from favourites'**
String get noErrorMsg; String get removedFromFavourite;
/// No description provided for @retry. /// No description provided for @coinDataPriceChange.
/// ///
/// In ru, this message translates to: /// In en, this message translates to:
/// **'Повторить'** /// **'for the last 24 hours'**
String get retry; String get coinDataPriceChange;
/// No description provided for @unknown. /// No description provided for @settingsLanguage.
/// ///
/// In ru, this message translates to: /// In en, this message translates to:
/// **'Неизвестно'** /// **'Language'**
String get unknown; String get settingsLanguage;
/// No description provided for @apiYear.
///
/// In ru, this message translates to:
/// **'Год'**
String get apiYear;
/// No description provided for @apiType.
///
/// In ru, this message translates to:
/// **'Тип'**
String get apiType;
/// No description provided for @apiRating.
///
/// In ru, this message translates to:
/// **'Рейтинг'**
String get apiRating;
/// No description provided for @apiDesc.
///
/// In ru, this message translates to:
/// **'(Описание доступно только на английском языке)'**
String get apiDesc;
/// No description provided for @apiNoDesc.
///
/// In ru, this message translates to:
/// **'Нет описания'**
String get apiNoDesc;
/// No description provided for @arbEnding.
///
/// In ru, this message translates to:
/// **'t'**
String get arbEnding;
} }
class _AppLocaleDelegate extends LocalizationsDelegate<AppLocale> { class _AppLocaleDelegate extends LocalizationsDelegate<AppLocale> {

View File

@ -7,44 +7,26 @@ class AppLocaleEn extends AppLocale {
AppLocaleEn([String locale = 'en']) : super(locale); AppLocaleEn([String locale = 'en']) : super(locale);
@override @override
String get appBarTitle => 'Harry Potter characters'; String get mainAppBarTitle => 'Cryptocurrency Exchange';
@override @override
String get search => 'Search'; String get detailsPageAppBarTitle => 'Cryptocurrency info';
@override @override
String get liked => 'You liked'; String get settingsPageAppBarTitle => 'Settings';
@override @override
String get unliked => 'Like removed from'; String get searchHint => 'Search';
@override @override
String get errorOccured => 'Произошла ошибка'; String get addedToFavourite => 'is added to favourites';
@override @override
String get noErrorMsg => 'No message provided'; String get removedFromFavourite => 'is removed from favourites';
@override @override
String get retry => 'Retry'; String get coinDataPriceChange => 'for the last 24 hours';
@override @override
String get unknown => 'Unknown'; String get settingsLanguage => 'Language';
@override
String get apiYear => 'Year';
@override
String get apiType => 'Type';
@override
String get apiRating => 'Rating';
@override
String get apiDesc => '';
@override
String get apiNoDesc => 'No description provided';
@override
String get arbEnding => 't';
} }

View File

@ -7,44 +7,26 @@ class AppLocaleRu extends AppLocale {
AppLocaleRu([String locale = 'ru']) : super(locale); AppLocaleRu([String locale = 'ru']) : super(locale);
@override @override
String get appBarTitle => 'Персонажи из Гарри Поттера'; String get mainAppBarTitle => 'Криптобиржа';
@override @override
String get search => 'Поиск'; String get detailsPageAppBarTitle => 'Сведения о валюте';
@override @override
String get liked => 'Вы добавили в избранное'; String get settingsPageAppBarTitle => 'Настройки';
@override @override
String get unliked => 'Вы удалили из избранного'; String get searchHint => 'Поиск';
@override @override
String get errorOccured => 'Произошла ошибка'; String get addedToFavourite => 'добавлен в избранное';
@override @override
String get noErrorMsg => 'Нет сообщения'; String get removedFromFavourite => 'удален из избранного';
@override @override
String get retry => 'Повторить'; String get coinDataPriceChange => 'за последние 24 часа';
@override @override
String get unknown => 'Неизвестно'; String get settingsLanguage => 'Язык';
@override
String get apiYear => 'Год';
@override
String get apiType => 'Тип';
@override
String get apiRating => 'Рейтинг';
@override
String get apiDesc => '(Описание доступно только на английском языке)';
@override
String get apiNoDesc => 'Нет описания';
@override
String get arbEnding => 't';
} }

View File

@ -1,56 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
part 'characters_dto.g.dart';
@JsonSerializable(createToJson: false)
class CharactersDto {
final List<CharacterDataDto>? data;
final MetaDto? meta;
const CharactersDto({this.data, this.meta});
factory CharactersDto.fromJson(Map<String, dynamic> json) => _$CharactersDtoFromJson(json);
}
@JsonSerializable(createToJson: false)
class CharacterDataDto {
final String? id;
final String? type;
final CharacterAttributesDataDto? attributes;
const CharacterDataDto({this.id, this.type, this.attributes});
factory CharacterDataDto.fromJson(Map<String, dynamic> json) => _$CharacterDataDtoFromJson(json);
}
@JsonSerializable(createToJson: false)
class CharacterAttributesDataDto {
final String? name;
final String? born;
final String? died;
final String? image;
const CharacterAttributesDataDto({this.name, this.born, this.died, this.image});
factory CharacterAttributesDataDto.fromJson(Map<String, dynamic> json) => _$CharacterAttributesDataDtoFromJson(json);
}
@JsonSerializable(createToJson: false)
class MetaDto {
final PaginationDto? pagination;
const MetaDto({this.pagination});
factory MetaDto.fromJson(Map<String, dynamic> json) => _$MetaDtoFromJson(json);
}
@JsonSerializable(createToJson: false)
class PaginationDto {
final int? current;
final int? next;
final int? last;
const PaginationDto({this.current, this.next, this.last});
factory PaginationDto.fromJson(Map<String, dynamic> json) => _$PaginationDtoFromJson(json);
}

View File

@ -1,49 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'characters_dto.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CharactersDto _$CharactersDtoFromJson(Map<String, dynamic> json) =>
CharactersDto(
data: (json['data'] as List<dynamic>?)
?.map((e) => CharacterDataDto.fromJson(e as Map<String, dynamic>))
.toList(),
meta: json['meta'] == null
? null
: MetaDto.fromJson(json['meta'] as Map<String, dynamic>),
);
CharacterDataDto _$CharacterDataDtoFromJson(Map<String, dynamic> json) =>
CharacterDataDto(
id: json['id'] as String?,
type: json['type'] as String?,
attributes: json['attributes'] == null
? null
: CharacterAttributesDataDto.fromJson(
json['attributes'] as Map<String, dynamic>),
);
CharacterAttributesDataDto _$CharacterAttributesDataDtoFromJson(
Map<String, dynamic> json) =>
CharacterAttributesDataDto(
name: json['name'] as String?,
born: json['born'] as String?,
died: json['died'] as String?,
image: json['image'] as String?,
);
MetaDto _$MetaDtoFromJson(Map<String, dynamic> json) => MetaDto(
pagination: json['pagination'] == null
? null
: PaginationDto.fromJson(json['pagination'] as Map<String, dynamic>),
);
PaginationDto _$PaginationDtoFromJson(Map<String, dynamic> json) =>
PaginationDto(
current: (json['current'] as num?)?.toInt(),
next: (json['next'] as num?)?.toInt(),
last: (json['last'] as num?)?.toInt(),
);

View File

@ -2,13 +2,14 @@ import 'package:json_annotation/json_annotation.dart';
part 'coins_dto.g.dart'; part 'coins_dto.g.dart';
@JsonSerializable(createToJson: false)
class CoinsDto { class CoinsDto {
final List<CoinDataDto>? coins; final List<CoinDataDto>? coins;
CoinsDto({this.coins}); CoinsDto({this.coins});
factory CoinsDto.fromJson(Map<String, dynamic> json) => _$CoinsDtoFromJson(json); factory CoinsDto.fromJson(List<dynamic> json) => CoinsDto(
coins: json.map((e) => CoinDataDto.fromJson(e as Map<String, dynamic>)).toList(),
);
} }
@JsonSerializable(createToJson: false) @JsonSerializable(createToJson: false)
@ -16,7 +17,9 @@ class CoinDataDto {
final String? id; final String? id;
final String? name; final String? name;
final String? image; final String? image;
@JsonKey(name: 'current_price')
final double? currentPrice; final double? currentPrice;
@JsonKey(name: 'price_change_24h')
final double? priceChange24h; final double? priceChange24h;
CoinDataDto({ CoinDataDto({

View File

@ -6,16 +6,10 @@ part of 'coins_dto.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
CoinsDto _$CoinsDtoFromJson(Map<String, dynamic> json) => CoinsDto(
coins: (json['coins'] as List<dynamic>?)
?.map((e) => CoinDataDto.fromJson(e as Map<String, dynamic>))
.toList(),
);
CoinDataDto _$CoinDataDtoFromJson(Map<String, dynamic> json) => CoinDataDto( CoinDataDto _$CoinDataDtoFromJson(Map<String, dynamic> json) => CoinDataDto(
id: json['id'] as String?, id: json['id'] as String?,
name: json['name'] as String?, name: json['name'] as String?,
image: json['image'] as String?, image: json['image'] as String?,
currentPrice: (json['currentPrice'] as num?)?.toDouble(), currentPrice: (json['current_price'] as num?)?.toDouble(),
priceChange24h: (json['priceChange24h'] as num?)?.toDouble(), priceChange24h: (json['price_change_24h'] as num?)?.toDouble(),
); );

View File

@ -1,34 +0,0 @@
import 'package:flutter_android_app/data/dtos/characters_dto.dart';
import 'package:flutter_android_app/domain/models/card.dart';
import '../../domain/models/home.dart';
const _imagePlaceholder = 'https://gryazoveckij-r19.gosweb.gosuslugi.ru/netcat_files/460/2008/net_foto_muzh.jpg';
/*
extension CharactersDataDto on CharactersDto {
HomeData toDomain() => HomeData(
data: data?.map((e) => e.toDomain()).toList(),
nextPage: meta?.pagination?.next,
);
}
extension CharacterDataDtoToModel on CharacterDataDto {
CardData toDomain() => CardData(
title: attributes?.name ?? 'UNKNOWN',
imageUrl: attributes?.image ?? _imagePlaceholder,
description: _makeDescriptionText(attributes?.born, attributes?.died),
id: id,
);
}
String _makeDescriptionText(String? born, String? died) {
return born != null && died != null
? '$born - $died'
: born != null
? 'born: $born'
: died != null
? 'died: $died'
: '';
}
*/

View File

@ -35,13 +35,13 @@ extension CoinDataDtoToModel on CoinDataDto {
retVal += _getLocalizedPrice(priceChange, locale?.localeName); retVal += _getLocalizedPrice(priceChange, locale?.localeName);
return '$retVal за последние 24 часа'; return '$retVal ${locale?.coinDataPriceChange}';
} }
} }
extension CoinsDtoToModel on CoinsDto { extension CoinsDtoToModel on CoinsDto {
HomeData toDomain(AppLocale? locale) => HomeData( HomeData toDomain(AppLocale? locale, int currentPage) => HomeData(
data: coins?.map((e) => e.toDomain(locale)).toList(), data: coins?.map((e) => e.toDomain(locale)).toList(),
nextPage: 1, nextPage: currentPage + 1,
); );
} }

View File

@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
class CardData { class CardData {
final String id; final String id;
final String title; final String title;

View File

@ -1,12 +1,13 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_android_app/components/extensions/context_x.dart';
import 'package:flutter_android_app/presentation/home_page/bloc/bloc.dart'; import 'package:flutter_android_app/presentation/home_page/bloc/bloc.dart';
import 'package:flutter_android_app/presentation/home_page/home_page.dart'; import 'package:flutter_android_app/presentation/home_page/home_page.dart';
import 'package:flutter_android_app/presentation/like_bloc/like_bloc.dart'; import 'package:flutter_android_app/presentation/like_bloc/like_bloc.dart';
import 'package:flutter_android_app/presentation/locale_bloc/locale_bloc.dart'; import 'package:flutter_android_app/presentation/locale_bloc/locale_bloc.dart';
import 'package:flutter_android_app/presentation/locale_bloc/locale_state.dart'; import 'package:flutter_android_app/presentation/locale_bloc/locale_state.dart';
import 'package:flutter_android_app/repositories/mock_repository.dart'; import 'package:flutter_android_app/repositories/crypto_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'components/locale/l10n/app_localizations.dart'; import 'components/locale/l10n/app_localizations.dart';
@ -25,7 +26,7 @@ class MyApp extends StatelessWidget {
create: (context) => LikeBloc(), create: (context) => LikeBloc(),
child: BlocProvider<LocaleBloc>( child: BlocProvider<LocaleBloc>(
lazy: false, lazy: false,
create: (context) => LocaleBloc(Locale(Platform.localeName)), create: (context) => LocaleBloc(Locale(_getLangCode(Platform.localeName))),
child: BlocBuilder<LocaleBloc, LocaleState>( child: BlocBuilder<LocaleBloc, LocaleState>(
builder: (context, state) => MaterialApp( builder: (context, state) => MaterialApp(
title: 'Cryptocurrency Exchange App', title: 'Cryptocurrency Exchange App',
@ -37,13 +38,13 @@ class MyApp extends StatelessWidget {
useMaterial3: true, useMaterial3: true,
), ),
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
home: RepositoryProvider<MockRepository>( home: RepositoryProvider<CryptoRepository>(
lazy: true, lazy: true,
create: (_) => MockRepository(), create: (_) => CryptoRepository(),
child: BlocProvider<HomeBloc>( child: BlocProvider<HomeBloc>(
lazy: false, lazy: false,
create: (context) => HomeBloc(context.read<MockRepository>()), create: (context) => HomeBloc(context.read<CryptoRepository>()),
child: const MyHomePage(title: 'Cryptocurrency Exchange'), child: const MyHomePage(),
), ),
), ),
), ),
@ -51,4 +52,9 @@ class MyApp extends StatelessWidget {
), ),
); );
} }
String _getLangCode(String fullLocaleName) {
int index = fullLocaleName.indexOf('_');
return index != -1 ? fullLocaleName.substring(0, index) : fullLocaleName;
}
} }

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_android_app/components/extensions/context_x.dart';
import 'package:flutter_android_app/domain/models/card.dart'; import 'package:flutter_android_app/domain/models/card.dart';
class DetailsPage extends StatelessWidget { class DetailsPage extends StatelessWidget {
@ -9,7 +10,10 @@ class DetailsPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(), appBar: AppBar(
title: Text(context.locale.detailsPageAppBarTitle),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Column( body: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [

View File

@ -23,6 +23,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
search: event.search, search: event.search,
page: event.nextPage ?? 1, page: event.nextPage ?? 1,
onError: (e) => error = e, onError: (e) => error = e,
locale: event.locale,
); );
if (event.nextPage != null) { if (event.nextPage != null) {

View File

@ -1,3 +1,5 @@
import 'package:flutter_android_app/components/locale/l10n/app_localizations.dart';
abstract class HomeEvent { abstract class HomeEvent {
const HomeEvent(); const HomeEvent();
} }
@ -5,6 +7,7 @@ abstract class HomeEvent {
class HomeLoadDataEvent extends HomeEvent { class HomeLoadDataEvent extends HomeEvent {
final String? search; final String? search;
final int? nextPage; final int? nextPage;
final AppLocale? locale;
const HomeLoadDataEvent({this.search, this.nextPage}); const HomeLoadDataEvent({this.search, this.nextPage, this.locale});
} }

View File

@ -8,7 +8,6 @@ class _Card extends StatelessWidget {
final String? imageUrl; final String? imageUrl;
final String currentPrice; final String currentPrice;
final String priceChange; final String priceChange;
final AppLocale locale;
final OnLikeCallback onLike; final OnLikeCallback onLike;
final VoidCallback? onTap; final VoidCallback? onTap;
final bool isLiked; final bool isLiked;
@ -20,14 +19,12 @@ class _Card extends StatelessWidget {
this.imageUrl, this.imageUrl,
required this.currentPrice, required this.currentPrice,
required this.priceChange, required this.priceChange,
required this.locale,
this.onLike, this.onLike,
this.onTap, this.onTap,
this.isLiked = false, this.isLiked = false,
}); });
factory _Card.fromData( factory _Card.fromData(
AppLocale locale,
CardData data, { CardData data, {
OnLikeCallback onLike, OnLikeCallback onLike,
VoidCallback? onTap, VoidCallback? onTap,
@ -38,7 +35,6 @@ class _Card extends StatelessWidget {
imageUrl: data.imageUrl, imageUrl: data.imageUrl,
currentPrice: data.currentPrice, currentPrice: data.currentPrice,
priceChange: data.priceChange, priceChange: data.priceChange,
locale: locale,
onLike: onLike, onLike: onLike,
onTap: onTap, onTap: onTap,
isLiked: isLiked, isLiked: isLiked,

View File

@ -1,7 +1,5 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_android_app/components/extensions/context_x.dart'; import 'package:flutter_android_app/components/extensions/context_x.dart';
import 'package:flutter_android_app/components/locale/l10n/app_localizations.dart';
import 'package:flutter_android_app/components/utils/debounce.dart'; import 'package:flutter_android_app/components/utils/debounce.dart';
import 'package:flutter_android_app/domain/models/card.dart'; import 'package:flutter_android_app/domain/models/card.dart';
import 'package:flutter_android_app/presentation/details_page/details_page.dart'; import 'package:flutter_android_app/presentation/details_page/details_page.dart';
@ -14,24 +12,19 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../common/svg_objects.dart'; import '../common/svg_objects.dart';
import '../like_bloc/like_events.dart'; import '../like_bloc/like_events.dart';
import '../locale_bloc/locale_bloc.dart';
import '../locale_bloc/locale_events.dart';
import '../locale_bloc/locale_state.dart';
import '../settings_page/settings_page.dart'; import '../settings_page/settings_page.dart';
part 'card.dart'; part 'card.dart';
class MyHomePage extends StatelessWidget { class MyHomePage extends StatelessWidget {
const MyHomePage({super.key, required this.title}); const MyHomePage({super.key});
final String title;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary, backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(title), title: Text(context.locale.mainAppBarTitle),
actions: [ actions: [
Padding( Padding(
padding: const EdgeInsets.only(right: 12.0), padding: const EdgeInsets.only(right: 12.0),
@ -43,7 +36,6 @@ class MyHomePage extends StatelessWidget {
), ),
), ),
], ],
), ),
body: const Body(), body: const Body(),
); );
@ -66,7 +58,7 @@ class _BodyState extends State<Body> {
SvgObjects.init(); SvgObjects.init();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<HomeBloc>().add(const HomeLoadDataEvent()); context.read<HomeBloc>().add(HomeLoadDataEvent(locale: context.locale));
context.read<LikeBloc>().add(const LoadLikesEvent()); context.read<LikeBloc>().add(const LoadLikesEvent());
}); });
@ -90,23 +82,25 @@ class _BodyState extends State<Body> {
child: SearchBar( child: SearchBar(
controller: searchController, controller: searchController,
onChanged: (search) { onChanged: (search) {
Debounce.run(() => context.read<HomeBloc>().add(HomeLoadDataEvent(search: search))); Debounce.run(() => context.read<HomeBloc>().add(HomeLoadDataEvent(search: search, locale: context.locale)));
}, },
leading: const Icon(Icons.search), leading: const Icon(Icons.search),
trailing: [ trailing: [
IconButton( IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () { searchController.clear(); }, onPressed: () {
searchController.clear();
context.read<HomeBloc>().add(HomeLoadDataEvent(locale: context.locale));
},
), ),
], ],
hintText: context.locale.search, hintText: context.locale.searchHint,
elevation: const WidgetStatePropertyAll(0.0), elevation: const WidgetStatePropertyAll(0.0),
padding: const WidgetStatePropertyAll(EdgeInsets.only(left: 18, right: 10)), padding: const WidgetStatePropertyAll(EdgeInsets.only(left: 18, right: 10)),
backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.secondaryContainer), backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.secondaryContainer),
), ),
), ),
), ),
], ],
), ),
BlocBuilder<HomeBloc, HomeState>( BlocBuilder<HomeBloc, HomeState>(
@ -129,7 +123,6 @@ class _BodyState extends State<Body> {
final data = state.data?.data?[index]; final data = state.data?.data?[index];
return data != null return data != null
? _Card.fromData( ? _Card.fromData(
context.locale,
data, data,
isLiked: likeState.likedIds?.contains(data.id) == true, isLiked: likeState.likedIds?.contains(data.id) == true,
onLike: _onLike, onLike: _onLike,
@ -164,7 +157,7 @@ class _BodyState extends State<Body> {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text( content: Text(
'$title ${isLiked ? context.locale.liked : context.locale.unliked}', '$title ${isLiked ? context.locale.addedToFavourite : context.locale.removedFromFavourite}',
style: Theme.of(context).textTheme.bodyLarge style: Theme.of(context).textTheme.bodyLarge
), ),
backgroundColor: Colors.deepPurple.shade200, backgroundColor: Colors.deepPurple.shade200,
@ -174,11 +167,13 @@ class _BodyState extends State<Body> {
} }
void _navToDetails(BuildContext context, CardData data) { void _navToDetails(BuildContext context, CardData data) {
Navigator.push(context, CupertinoPageRoute(builder: (context) => DetailsPage(data))); Navigator.push(context, MaterialPageRoute<void>(
builder: (context) => DetailsPage(data)),
);
} }
Future<void> _onRefresh() { Future<void> _onRefresh() {
context.read<HomeBloc>().add(HomeLoadDataEvent(search: searchController.text)); context.read<HomeBloc>().add(HomeLoadDataEvent(search: searchController.text, locale: context.locale));
return Future.value(null); return Future.value(null);
} }
@ -189,6 +184,7 @@ class _BodyState extends State<Body> {
bloc.add(HomeLoadDataEvent( bloc.add(HomeLoadDataEvent(
search: searchController.text, search: searchController.text,
nextPage: bloc.state.data?.nextPage, nextPage: bloc.state.data?.nextPage,
locale: context.locale,
)); ));
} }
} }

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_android_app/components/extensions/context_x.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../common/svg_objects.dart'; import '../common/svg_objects.dart';
@ -12,22 +13,39 @@ class SettingsPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Settings')), appBar: AppBar(
body: Center( title: Text(context.locale.settingsPageAppBarTitle),
child: GestureDetector( backgroundColor: Theme.of(context).colorScheme.inversePrimary,
onTap: () => context.read<LocaleBloc>().add(const ChangeLocaleEvent()), ),
child: SizedBox.square( body: Padding(
dimension: 50, padding: const EdgeInsets.only(left: 20, right: 16, top: 8),
child: Padding( child: Column(
padding: const EdgeInsets.only(right: 12), children: [
child: BlocBuilder<LocaleBloc, LocaleState>( Row(
builder: (context, state) { mainAxisAlignment: MainAxisAlignment.spaceBetween,
return state.currentLocale.languageCode == 'ru' children: [
? const SvgRu() Text(
: const SvgUs(); '${context.locale.settingsLanguage}:',
}), style: Theme.of(context).textTheme.titleMedium,
),
GestureDetector(
onTap: () => context.read<LocaleBloc>().add(const ChangeLocaleEvent()),
child: SizedBox.square(
dimension: 50,
child: Padding(
padding: const EdgeInsets.only(right: 0),
child: BlocBuilder<LocaleBloc, LocaleState>(
builder: (context, state) {
return state.currentLocale.languageCode == 'ru'
? const SvgRu()
: const SvgUs();
}),
),
),
),
],
), ),
), ],
), ),
), ),
); );

View File

@ -32,12 +32,13 @@ class CryptoRepository extends ApiInterface {
}) async { }) async {
try { try {
Map<String, dynamic> queryParams = { Map<String, dynamic> queryParams = {
'x_cg_demo_api_key': _apiKey,
'vs_currency': _getCurrencyName(locale?.localeName), 'vs_currency': _getCurrencyName(locale?.localeName),
'per_page': pageSize, 'per_page': pageSize,
'page': page, 'page': page,
}; };
if (search != null) { if (search != null && search.isNotEmpty) {
final Response<dynamic> searchResponse = await _dio.get<Map<dynamic, dynamic>>( final Response<dynamic> searchResponse = await _dio.get<Map<dynamic, dynamic>>(
'$_baseUrl$_searchUrl', '$_baseUrl$_searchUrl',
queryParameters: { queryParameters: {
@ -52,17 +53,22 @@ class CryptoRepository extends ApiInterface {
for (var coinData in searchCoinsDto.coins!) { for (var coinData in searchCoinsDto.coins!) {
ids += coinData.id != null ? '${coinData.id},' : ''; ids += coinData.id != null ? '${coinData.id},' : '';
} }
if (ids.isEmpty) {
return HomeData();
}
queryParams['ids'] = ids; queryParams['ids'] = ids;
} }
} }
final Response<dynamic> response = await _dio.get<Map<dynamic, dynamic>>( final response = await _dio.get(
'$_baseUrl$_coinsDataUrl', '$_baseUrl$_coinsDataUrl',
queryParameters: queryParams, queryParameters: queryParams,
); );
final CoinsDto dto = CoinsDto.fromJson(response.data as Map<String, dynamic>); final CoinsDto dto = CoinsDto.fromJson(response.data as List<dynamic>);
final HomeData data = dto.toDomain(locale); final HomeData data = dto.toDomain(locale, page);
return data; return data;
} on DioException catch (e) { } on DioException catch (e) {

View File

@ -1,46 +0,0 @@
import 'package:dio/dio.dart';
import 'package:flutter_android_app/data/dtos/characters_dto.dart';
import 'package:flutter_android_app/data/mappers/characters_mapper.dart';
import 'package:flutter_android_app/domain/models/home.dart';
import 'package:flutter_android_app/repositories/api_interface.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
import '../components/utils/error_callback.dart';
/*
class PotterRepository extends ApiInterface {
static final Dio _dio = Dio()
..interceptors.add(PrettyDioLogger(
requestHeader: true,
requestBody: true,
));
static const String _baseUrl = 'https://api.potterdb.com';
@override
Future<HomeData?> loadData({
OnErrorCallback? onError,
String? q, int page = 1,
int pageSize = 20,
}) async {
try {
const String url = '$_baseUrl/v1/characters';
final Response<dynamic> response = await _dio.get<Map<dynamic, dynamic>>(
url,
queryParameters: {
'filter[name_cont]': q,
'page[number]': page,
'page[size]': pageSize,
},
);
final CharactersDto dto = CharactersDto.fromJson(response.data as Map<String, dynamic>);
final HomeData data = dto.toDomain();
return data;
} on DioException catch (e) {
onError?.call(e.error?.toString());
return null;
}
}
}
*/