diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..345888d
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index db77bb4..334610a 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
new file mode 100644
index 0000000..1966948
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..745b050
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000..745b050
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 17987b7..1adae92 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
new file mode 100644
index 0000000..75025cf
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..b489390
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000..b489390
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 09d4391..51a3dcf 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
new file mode 100644
index 0000000..9784f16
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..512f1d8
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000..512f1d8
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index d5f1c8d..00e8478 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
new file mode 100644
index 0000000..04ef206
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..1bc12f7
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000..1bc12f7
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 4d6372e..b049e56 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
new file mode 100644
index 0000000..66a5487
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..cdc749c
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000..cdc749c
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png differ
diff --git a/assets/launcher.jpeg b/assets/launcher.jpeg
new file mode 100644
index 0000000..8375fbc
Binary files /dev/null and b/assets/launcher.jpeg differ
diff --git a/assets/svg/ru.svg b/assets/svg/ru.svg
new file mode 100644
index 0000000..ae12982
--- /dev/null
+++ b/assets/svg/ru.svg
@@ -0,0 +1,19 @@
+
+
+
\ No newline at end of file
diff --git a/assets/svg/uk.svg b/assets/svg/uk.svg
new file mode 100644
index 0000000..88e2211
--- /dev/null
+++ b/assets/svg/uk.svg
@@ -0,0 +1,23 @@
+
+
+
\ No newline at end of file
diff --git a/l10n.yaml b/l10n.yaml
new file mode 100644
index 0000000..d26d702
--- /dev/null
+++ b/l10n.yaml
@@ -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
\ No newline at end of file
diff --git a/l10n/app_en.arb b/l10n/app_en.arb
new file mode 100644
index 0000000..e7068b9
--- /dev/null
+++ b/l10n/app_en.arb
@@ -0,0 +1,15 @@
+{
+ "@@locale": "en",
+
+ "title": "Comments App",
+ "credits": "made by",
+ "search": "Search",
+ "comment": "{nickname}'s comment",
+ "liked": "liked",
+ "disliked": "disliked",
+ "neutralized": "neutralized",
+ "born": "Born",
+ "died": "Died",
+
+ "arbEnding": "Чтобы не забыть про отсутствие запятой :)"
+}
\ No newline at end of file
diff --git a/l10n/app_ru.arb b/l10n/app_ru.arb
new file mode 100644
index 0000000..7499d00
--- /dev/null
+++ b/l10n/app_ru.arb
@@ -0,0 +1,15 @@
+{
+ "@@locale": "ru",
+
+ "title": "Комментарии",
+ "credits": "сделано",
+ "search": "Поиск",
+ "comment": "Комментарий {nickname}",
+ "liked": "лайкнут",
+ "disliked": "дизлайкнут",
+ "neutralized": "без реакции",
+ "born": "Родился",
+ "died": "Умер",
+
+ "arbEnding": "Чтобы не забыть про отсутствие запятой :)"
+}
\ No newline at end of file
diff --git a/lib/components/extensions/context_x.dart b/lib/components/extensions/context_x.dart
new file mode 100644
index 0000000..a25731e
--- /dev/null
+++ b/lib/components/extensions/context_x.dart
@@ -0,0 +1,6 @@
+import 'package:flutter/widgets.dart';
+import 'package:pmu_labworks/components/locale/l10n/app_locale.dart';
+
+extension LocalContextX on BuildContext {
+ AppLocale get locale => AppLocale.of(this)!;
+}
diff --git a/lib/components/locale/l10n/app_locale.dart b/lib/components/locale/l10n/app_locale.dart
new file mode 100644
index 0000000..1f2eb4e
--- /dev/null
+++ b/lib/components/locale/l10n/app_locale.dart
@@ -0,0 +1,189 @@
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_localizations/flutter_localizations.dart';
+import 'package:intl/intl.dart' as intl;
+
+import 'app_locale_en.dart';
+import 'app_locale_ru.dart';
+
+// ignore_for_file: type=lint
+
+/// Callers can lookup localized strings with an instance of AppLocale
+/// returned by `AppLocale.of(context)`.
+///
+/// Applications need to include `AppLocale.delegate()` in their app's
+/// `localizationDelegates` list, and the locales they support in the app's
+/// `supportedLocales` list. For example:
+///
+/// ```dart
+/// import 'l10n/app_locale.dart';
+///
+/// return MaterialApp(
+/// localizationsDelegates: AppLocale.localizationsDelegates,
+/// supportedLocales: AppLocale.supportedLocales,
+/// home: MyApplicationHome(),
+/// );
+/// ```
+///
+/// ## Update pubspec.yaml
+///
+/// Please make sure to update your pubspec.yaml to include the following
+/// packages:
+///
+/// ```yaml
+/// dependencies:
+/// # Internationalization support.
+/// flutter_localizations:
+/// sdk: flutter
+/// intl: any # Use the pinned version from flutter_localizations
+///
+/// # Rest of dependencies
+/// ```
+///
+/// ## iOS Applications
+///
+/// iOS applications define key application metadata, including supported
+/// locales, in an Info.plist file that is built into the application bundle.
+/// To configure the locales supported by your app, you’ll need to edit this
+/// file.
+///
+/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
+/// Then, in the Project Navigator, open the Info.plist file under the Runner
+/// project’s Runner folder.
+///
+/// Next, select the Information Property List item, select Add Item from the
+/// Editor menu, then select Localizations from the pop-up menu.
+///
+/// Select and expand the newly-created Localizations item then, for each
+/// locale your application supports, add a new item and select the locale
+/// you wish to add from the pop-up menu in the Value field. This list should
+/// be consistent with the languages listed in the AppLocale.supportedLocales
+/// property.
+abstract class AppLocale {
+ AppLocale(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString());
+
+ final String localeName;
+
+ static AppLocale? of(BuildContext context) {
+ return Localizations.of(context, AppLocale);
+ }
+
+ static const LocalizationsDelegate delegate = _AppLocaleDelegate();
+
+ /// A list of this localizations delegate along with the default localizations
+ /// delegates.
+ ///
+ /// Returns a list of localizations delegates containing this delegate along with
+ /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
+ /// and GlobalWidgetsLocalizations.delegate.
+ ///
+ /// Additional delegates can be added by appending to this list in
+ /// MaterialApp. This list does not have to be used at all if a custom list
+ /// of delegates is preferred or required.
+ static const List> localizationsDelegates = >[
+ delegate,
+ GlobalMaterialLocalizations.delegate,
+ GlobalCupertinoLocalizations.delegate,
+ GlobalWidgetsLocalizations.delegate,
+ ];
+
+ /// A list of this localizations delegate's supported locales.
+ static const List supportedLocales = [
+ Locale('en'),
+ Locale('ru')
+ ];
+
+ /// No description provided for @title.
+ ///
+ /// In ru, this message translates to:
+ /// **'Комментарии'**
+ String get title;
+
+ /// No description provided for @credits.
+ ///
+ /// In ru, this message translates to:
+ /// **'сделано'**
+ String get credits;
+
+ /// No description provided for @search.
+ ///
+ /// In ru, this message translates to:
+ /// **'Поиск'**
+ String get search;
+
+ /// No description provided for @comment.
+ ///
+ /// In ru, this message translates to:
+ /// **'Комментарий {nickname}'**
+ String comment(Object nickname);
+
+ /// 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 @neutralized.
+ ///
+ /// In ru, this message translates to:
+ /// **'без реакции'**
+ String get neutralized;
+
+ /// No description provided for @born.
+ ///
+ /// In ru, this message translates to:
+ /// **'Родился'**
+ String get born;
+
+ /// No description provided for @died.
+ ///
+ /// In ru, this message translates to:
+ /// **'Умер'**
+ String get died;
+
+ /// No description provided for @arbEnding.
+ ///
+ /// In ru, this message translates to:
+ /// **'Чтобы не забыть про отсутствие запятой :)'**
+ String get arbEnding;
+}
+
+class _AppLocaleDelegate extends LocalizationsDelegate {
+ const _AppLocaleDelegate();
+
+ @override
+ Future load(Locale locale) {
+ return SynchronousFuture(lookupAppLocale(locale));
+ }
+
+ @override
+ bool isSupported(Locale locale) => ['en', 'ru'].contains(locale.languageCode);
+
+ @override
+ bool shouldReload(_AppLocaleDelegate old) => false;
+}
+
+AppLocale lookupAppLocale(Locale locale) {
+
+
+ // Lookup logic when only language code is specified.
+ switch (locale.languageCode) {
+ case 'en': return AppLocaleEn();
+ case 'ru': return AppLocaleRu();
+ }
+
+ throw FlutterError(
+ 'AppLocale.delegate failed to load unsupported locale "$locale". This is likely '
+ 'an issue with the localizations generation tool. Please file an issue '
+ 'on GitHub with a reproducible sample app and the gen-l10n configuration '
+ 'that was used.'
+ );
+}
diff --git a/lib/components/locale/l10n/app_locale_en.dart b/lib/components/locale/l10n/app_locale_en.dart
new file mode 100644
index 0000000..9b3f173
--- /dev/null
+++ b/lib/components/locale/l10n/app_locale_en.dart
@@ -0,0 +1,40 @@
+import 'app_locale.dart';
+
+// ignore_for_file: type=lint
+
+/// The translations for English (`en`).
+class AppLocaleEn extends AppLocale {
+ AppLocaleEn([String locale = 'en']) : super(locale);
+
+ @override
+ String get title => 'Comments App';
+
+ @override
+ String get credits => 'made by';
+
+ @override
+ String get search => 'Search';
+
+ @override
+ String comment(Object nickname) {
+ return '$nickname\'s comment';
+ }
+
+ @override
+ String get liked => 'liked';
+
+ @override
+ String get disliked => 'disliked';
+
+ @override
+ String get neutralized => 'neutralized';
+
+ @override
+ String get born => 'Born';
+
+ @override
+ String get died => 'Died';
+
+ @override
+ String get arbEnding => 'Чтобы не забыть про отсутствие запятой :)';
+}
diff --git a/lib/components/locale/l10n/app_locale_ru.dart b/lib/components/locale/l10n/app_locale_ru.dart
new file mode 100644
index 0000000..6d7a80d
--- /dev/null
+++ b/lib/components/locale/l10n/app_locale_ru.dart
@@ -0,0 +1,40 @@
+import 'app_locale.dart';
+
+// ignore_for_file: type=lint
+
+/// The translations for Russian (`ru`).
+class AppLocaleRu extends AppLocale {
+ AppLocaleRu([String locale = 'ru']) : super(locale);
+
+ @override
+ String get title => 'Комментарии';
+
+ @override
+ String get credits => 'сделано';
+
+ @override
+ String get search => 'Поиск';
+
+ @override
+ String comment(Object nickname) {
+ return 'Комментарий $nickname';
+ }
+
+ @override
+ String get liked => 'лайкнут';
+
+ @override
+ String get disliked => 'дизлайкнут';
+
+ @override
+ String get neutralized => 'без реакции';
+
+ @override
+ String get born => 'Родился';
+
+ @override
+ String get died => 'Умер';
+
+ @override
+ String get arbEnding => 'Чтобы не забыть про отсутствие запятой :)';
+}
diff --git a/lib/components/resources.g.dart b/lib/components/resources.g.dart
new file mode 100644
index 0000000..4ce026a
--- /dev/null
+++ b/lib/components/resources.g.dart
@@ -0,0 +1,12 @@
+/// Generate by [asset_generator](https://github.com/fluttercandies/flutter_asset_generator) library.
+/// PLEASE DO NOT EDIT MANUALLY.
+// ignore_for_file: constant_identifier_names
+class R {
+ const R._();
+
+ /// ![preview](file://C:\Users\masen\StudioProjects\pmu_labworks\assets\svg\ru.svg)
+ static const String ASSETS_SVG_RU_SVG = 'assets/svg/ru.svg';
+
+ /// ![preview](file://C:\Users\masen\StudioProjects\pmu_labworks\assets\svg\uk.svg)
+ static const String ASSETS_SVG_UK_SVG = 'assets/svg/uk.svg';
+}
diff --git a/lib/data/dtos/characters_dto.dart b/lib/data/dtos/characters_dto.dart
index 67c42b3..2bccf76 100644
--- a/lib/data/dtos/characters_dto.dart
+++ b/lib/data/dtos/characters_dto.dart
@@ -12,8 +12,7 @@ class CharactersDto {
this.meta,
});
- factory CharactersDto.fromJson(Map json) =>
- _$CharactersDtoFromJson(json);
+ factory CharactersDto.fromJson(Map json) => _$CharactersDtoFromJson(json);
}
@JsonSerializable(createToJson: false)
@@ -24,8 +23,7 @@ class CharacterDataDto {
const CharacterDataDto({this.id, this.type, this.attributes});
- factory CharacterDataDto.fromJson(Map json) =>
- _$CharacterDataDtoFromJson(json);
+ factory CharacterDataDto.fromJson(Map json) => _$CharacterDataDtoFromJson(json);
}
@JsonSerializable(createToJson: false)
@@ -35,8 +33,7 @@ class CharacterAttributesDataDto {
final String? died;
final String? image;
- const CharacterAttributesDataDto(
- {this.name, this.born, this.died, this.image});
+ const CharacterAttributesDataDto({this.name, this.born, this.died, this.image});
factory CharacterAttributesDataDto.fromJson(Map json) =>
_$CharacterAttributesDataDtoFromJson(json);
@@ -48,8 +45,7 @@ class MetaDto {
const MetaDto({this.pagination});
- factory MetaDto.fromJson(Map json) =>
- _$MetaDtoFromJson(json);
+ factory MetaDto.fromJson(Map json) => _$MetaDtoFromJson(json);
}
@JsonSerializable(createToJson: false)
@@ -60,6 +56,5 @@ class PaginationDto {
const PaginationDto({this.current, this.next, this.last});
- factory PaginationDto.fromJson(Map json) =>
- _$PaginationDtoFromJson(json);
+ factory PaginationDto.fromJson(Map json) => _$PaginationDtoFromJson(json);
}
diff --git a/lib/data/dtos/characters_dto.g.dart b/lib/data/dtos/characters_dto.g.dart
index c4e1b40..6066b05 100644
--- a/lib/data/dtos/characters_dto.g.dart
+++ b/lib/data/dtos/characters_dto.g.dart
@@ -6,28 +6,22 @@ part of 'characters_dto.dart';
// JsonSerializableGenerator
// **************************************************************************
-CharactersDto _$CharactersDtoFromJson(Map json) =>
- CharactersDto(
+CharactersDto _$CharactersDtoFromJson(Map json) => CharactersDto(
data: (json['data'] as List?)
?.map((e) => CharacterDataDto.fromJson(e as Map))
.toList(),
- meta: json['meta'] == null
- ? null
- : MetaDto.fromJson(json['meta'] as Map),
+ meta: json['meta'] == null ? null : MetaDto.fromJson(json['meta'] as Map),
);
-CharacterDataDto _$CharacterDataDtoFromJson(Map json) =>
- CharacterDataDto(
+CharacterDataDto _$CharacterDataDtoFromJson(Map json) => CharacterDataDto(
id: json['id'] as String?,
type: json['type'] as String?,
attributes: json['attributes'] == null
? null
- : CharacterAttributesDataDto.fromJson(
- json['attributes'] as Map),
+ : CharacterAttributesDataDto.fromJson(json['attributes'] as Map),
);
-CharacterAttributesDataDto _$CharacterAttributesDataDtoFromJson(
- Map json) =>
+CharacterAttributesDataDto _$CharacterAttributesDataDtoFromJson(Map json) =>
CharacterAttributesDataDto(
name: json['name'] as String?,
born: json['born'] as String?,
@@ -41,8 +35,7 @@ MetaDto _$MetaDtoFromJson(Map json) => MetaDto(
: PaginationDto.fromJson(json['pagination'] as Map),
);
-PaginationDto _$PaginationDtoFromJson(Map json) =>
- PaginationDto(
+PaginationDto _$PaginationDtoFromJson(Map json) => PaginationDto(
current: (json['current'] as num?)?.toInt(),
next: (json['next'] as num?)?.toInt(),
last: (json['last'] as num?)?.toInt(),
diff --git a/lib/data/mappers/characters_mapper.dart b/lib/data/mappers/characters_mapper.dart
index 79c6009..b3d8ef0 100644
--- a/lib/data/mappers/characters_mapper.dart
+++ b/lib/data/mappers/characters_mapper.dart
@@ -12,11 +12,10 @@ extension CharactersDtoToModel on CharactersDto {
extension CharacterDataDtoToModel on CharacterDataDto {
CommentData toDomain() => CommentData(
+ id: id,
title: type ?? 'UNKNOWN',
text: _makeDescriptionText(attributes?.born, attributes?.died),
- user: UserData(
- nickname: attributes?.name ?? 'Noname',
- avatarUrl: attributes?.image),
+ user: UserData(nickname: attributes?.name ?? 'Noname', avatarUrl: attributes?.image),
);
String _makeDescriptionText(String? born, String? died) {
diff --git a/lib/data/repositories/mock_repository.dart b/lib/data/repositories/mock_repository.dart
index 4e714f9..da9a711 100644
--- a/lib/data/repositories/mock_repository.dart
+++ b/lib/data/repositories/mock_repository.dart
@@ -12,8 +12,7 @@ class MockRepository extends ApiInterface {
text: 'this app is so cool ' * 10,
user: UserData(
nickname: 'Steve',
- avatarUrl:
- 'https://preview.free3d.com/img/2016/03/1875481443430303321/hld8c0oa.jpg',
+ avatarUrl: 'https://preview.free3d.com/img/2016/03/1875481443430303321/hld8c0oa.jpg',
),
),
CommentData(
@@ -27,8 +26,7 @@ class MockRepository extends ApiInterface {
),
CommentData(
title: 'BREAKING!!!',
- text:
- 'ONLY NOW you can purchase the ability to put likes and dislikes for only 299\$',
+ text: 'ONLY NOW you can purchase the ability to put likes and dislikes for only 299\$',
user: UserData(
nickname: 'Bethesda',
avatarUrl: 'https://i.playground.ru/p/BWixorSTeZQfoPvdVL9lgA.jpeg',
diff --git a/lib/data/repositories/potterdb_repository.dart b/lib/data/repositories/potterdb_repository.dart
index 482f7d1..e17389a 100644
--- a/lib/data/repositories/potterdb_repository.dart
+++ b/lib/data/repositories/potterdb_repository.dart
@@ -33,8 +33,7 @@ class PotterDBRepository extends ApiInterface {
},
);
- final CharactersDto dto =
- CharactersDto.fromJson(response.data as Map);
+ final CharactersDto dto = CharactersDto.fromJson(response.data as Map);
final HomeData data = dto.toDomain();
return data;
} on DioException catch (e) {
diff --git a/lib/domain/models/comment.dart b/lib/domain/models/comment.dart
index 9d706e8..02d79ac 100644
--- a/lib/domain/models/comment.dart
+++ b/lib/domain/models/comment.dart
@@ -1,11 +1,13 @@
import 'user.dart';
class CommentData {
+ final String? id;
final String title;
final String text;
final UserData user;
CommentData({
+ this.id,
required this.title,
required this.text,
required this.user,
diff --git a/lib/main.dart b/lib/main.dart
index f95b776..afe524b 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,8 +1,15 @@
+import 'dart:io';
+
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:pmu_labworks/components/extensions/context_x.dart';
+import 'package:pmu_labworks/components/locale/l10n/app_locale.dart';
import 'package:pmu_labworks/data/repositories/potterdb_repository.dart';
import 'package:pmu_labworks/view/home_page/bloc/bloc.dart';
import 'package:pmu_labworks/view/home_page/home_page.dart';
+import 'package:pmu_labworks/view/reaction_bloc/reaction_bloc.dart';
+import 'package:pmu_labworks/view/locale_bloc/locale_bloc.dart';
+import 'package:pmu_labworks/view/locale_bloc/locale_state.dart';
void main() {
runApp(const MyApp());
@@ -13,21 +20,36 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return MaterialApp(
- title: 'Comments App',
- debugShowCheckedModeBanner: false,
- theme: ThemeData(
- colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
- useMaterial3: true,
- ),
- home: RepositoryProvider(
- lazy: true,
- create: (_) => PotterDBRepository(),
- child: BlocProvider(
- lazy: false,
- create: (context) => HomeBloc(context.read()),
- child: const HomePage(title: 'Comments App'),
- ),
+ return BlocProvider(
+ lazy: false,
+ create: (context) => LocaleBloc(Locale(Platform.localeName)),
+ child: BlocBuilder(
+ builder: (context, state) {
+ return MaterialApp(
+ title: "Comments App",
+ locale: state.currentLocale,
+ localizationsDelegates: AppLocale.localizationsDelegates,
+ supportedLocales: AppLocale.supportedLocales,
+ debugShowCheckedModeBanner: false,
+ theme: ThemeData(
+ colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
+ useMaterial3: true,
+ ),
+ home: RepositoryProvider(
+ lazy: true,
+ create: (_) => PotterDBRepository(),
+ child: BlocProvider(
+ lazy: false,
+ create: (context) => ReactionBloc(),
+ child: BlocProvider(
+ lazy: false,
+ create: (context) => HomeBloc(context.read()),
+ child: HomePage(title: "Comments App"),
+ ),
+ ),
+ ),
+ );
+ },
),
);
}
diff --git a/lib/view/common/svg_objects.dart b/lib/view/common/svg_objects.dart
new file mode 100644
index 0000000..8823fe9
--- /dev/null
+++ b/lib/view/common/svg_objects.dart
@@ -0,0 +1,34 @@
+import 'package:flutter/widgets.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:pmu_labworks/components/resources.g.dart';
+
+abstract class SvgObjects {
+ static void init() {
+ final pics = [
+ R.ASSETS_SVG_RU_SVG,
+ R.ASSETS_SVG_UK_SVG,
+ ];
+ for (final String p in pics) {
+ final loader = SvgAssetLoader(p);
+ svg.cache.putIfAbsent(loader.cacheKey(null), () => loader.loadBytes(null));
+ }
+ }
+}
+
+class SvgRu extends StatelessWidget {
+ const SvgRu({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return SvgPicture.asset(R.ASSETS_SVG_RU_SVG);
+ }
+}
+
+class SvgUk extends StatelessWidget {
+ const SvgUk({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return SvgPicture.asset(R.ASSETS_SVG_UK_SVG);
+ }
+}
diff --git a/lib/view/details_page/details_page.dart b/lib/view/details_page/details_page.dart
index e139772..a36fc54 100644
--- a/lib/view/details_page/details_page.dart
+++ b/lib/view/details_page/details_page.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import 'package:pmu_labworks/components/extensions/context_x.dart';
import 'package:pmu_labworks/domain/models/comment.dart';
class DetailsPage extends StatelessWidget {
@@ -11,7 +12,7 @@ class DetailsPage extends StatelessWidget {
return Scaffold(
appBar: AppBar(
title: Text(
- '${_shortenNickname(model.user.nickname)}\'s comment',
+ context.locale.comment(model.user.nickname),
),
),
body: Padding(
@@ -41,8 +42,7 @@ class DetailsPage extends StatelessWidget {
left: 8,
bottom: 8,
child: Container(
- padding: const EdgeInsets.symmetric(
- vertical: 4.0, horizontal: 8.0),
+ padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
color: Colors.black.withOpacity(0.6),
child: Text(
_shortenNickname(model.user.nickname, maxLength: 13),
diff --git a/lib/view/dialogs/error_dialog.dart b/lib/view/dialogs/error_dialog.dart
index 7366703..8de67d2 100644
--- a/lib/view/dialogs/error_dialog.dart
+++ b/lib/view/dialogs/error_dialog.dart
@@ -24,10 +24,7 @@ class ErrorDialog extends StatelessWidget {
const SizedBox(height: 12),
Text(
error ?? 'UNKNOWN',
- style: Theme.of(context)
- .textTheme
- .bodyLarge
- ?.copyWith(color: Colors.white),
+ style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.white),
),
],
),
diff --git a/lib/view/home_page/bloc/bloc.dart b/lib/view/home_page/bloc/bloc.dart
index 7cacfda..07545fc 100644
--- a/lib/view/home_page/bloc/bloc.dart
+++ b/lib/view/home_page/bloc/bloc.dart
@@ -36,4 +36,4 @@ class HomeBloc extends Bloc {
error: error,
));
}
-}
\ No newline at end of file
+}
diff --git a/lib/view/home_page/bloc/state.g.dart b/lib/view/home_page/bloc/state.g.dart
index 114ac25..258a914 100644
--- a/lib/view/home_page/bloc/state.g.dart
+++ b/lib/view/home_page/bloc/state.g.dart
@@ -72,8 +72,7 @@ class _$HomeStateCWProxyImpl implements _$HomeStateCWProxy {
// ignore: cast_nullable_to_non_nullable
: isLoading as bool,
isPaginationLoading:
- isPaginationLoading == const $CopyWithPlaceholder() ||
- isPaginationLoading == null
+ isPaginationLoading == const $CopyWithPlaceholder() || isPaginationLoading == null
? _value.isPaginationLoading
// ignore: cast_nullable_to_non_nullable
: isPaginationLoading as bool,
diff --git a/lib/view/home_page/comment.dart b/lib/view/home_page/comment.dart
index 1931d96..25d54b2 100644
--- a/lib/view/home_page/comment.dart
+++ b/lib/view/home_page/comment.dart
@@ -1,72 +1,65 @@
part of 'home_page.dart';
-typedef OnActionCallback = void Function(
- String nickname, bool isLiked, bool isDisliked)?;
+typedef OnLikeCallback = void Function(
+ String? id, String nickname, bool isLiked, bool isDisliked)?;
+typedef OnDislikeCallback = void Function(
+ String? id, String nickname, bool isLiked, bool isDisliked)?;
-class _Comment extends StatefulWidget {
+class _Comment extends StatelessWidget {
+ final String? id;
final String title;
final String text;
- final UserData? user;
- final OnActionCallback onAction;
+ final UserData user;
+ final bool isLiked;
+ final bool isDisliked;
+ final OnLikeCallback? onLike;
+ final OnDislikeCallback? onDislike;
final VoidCallback? onTap;
const _Comment({
+ this.id,
required this.title,
required this.text,
- this.user,
- this.onAction,
+ required this.user,
+ this.isLiked = false,
+ this.isDisliked = false,
+ this.onLike,
+ this.onDislike,
this.onTap,
});
factory _Comment.fromData(
CommentData data, {
- OnActionCallback onAction,
+ bool isLiked = false,
+ bool isDisliked = false,
+ OnLikeCallback? onLike,
+ OnDislikeCallback? onDislike,
VoidCallback? onTap,
}) =>
_Comment(
+ id: data.id,
title: data.title,
text: data.text,
user: data.user,
- onAction: onAction,
+ isLiked: isLiked,
+ isDisliked: isDisliked,
+ onLike: onLike,
+ onDislike: onDislike,
onTap: onTap,
);
- @override
- State<_Comment> createState() => _CommentState();
-}
-
-class _CommentState extends State<_Comment> {
- bool isLiked = false;
- bool isDisliked = false;
-
void _onLike() {
- setState(() {
- isLiked = !isLiked;
- if (isLiked && isDisliked) {
- isDisliked = false;
- }
- });
- if (widget.onAction != null) {
- widget.onAction?.call(widget.user!.nickname, isLiked, isDisliked);
- }
+ onLike?.call(id, user.nickname, isLiked, isDisliked);
}
void _onDislike() {
- setState(() {
- isDisliked = !isDisliked;
- if (isDisliked && isLiked) {
- isLiked = false;
- }
- });
- if (widget.onAction != null) {
- widget.onAction?.call(widget.user!.nickname, isLiked, isDisliked);
- }
+ onDislike?.call(id, user.nickname, isLiked, isDisliked);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
- onTap: widget.onTap,
+ onTap: onTap,
child: Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
@@ -93,10 +86,9 @@ class _CommentState extends State<_Comment> {
child: SizedBox(
height: 80,
width: 80,
- child: widget.user!.avatarUrl != null &&
- widget.user!.avatarUrl!.isNotEmpty
+ child: user.avatarUrl != null && user.avatarUrl!.isNotEmpty
? Image.network(
- widget.user!.avatarUrl!,
+ user.avatarUrl!,
fit: BoxFit.cover,
)
: Container(
@@ -111,7 +103,7 @@ class _CommentState extends State<_Comment> {
SizedBox(
width: 80,
child: Text(
- widget.user!.nickname,
+ user.nickname,
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
@@ -131,9 +123,7 @@ class _CommentState extends State<_Comment> {
IconButton(
iconSize: 20,
icon: Icon(
- isDisliked
- ? Icons.thumb_down
- : Icons.thumb_down_outlined,
+ isDisliked ? Icons.thumb_down : Icons.thumb_down_outlined,
color: isDisliked ? Colors.red : Colors.grey,
),
onPressed: _onDislike,
@@ -149,12 +139,12 @@ class _CommentState extends State<_Comment> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
- widget.title,
+ title,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text(
- widget.text,
+ text,
style: Theme.of(context).textTheme.bodyLarge,
),
],
diff --git a/lib/view/home_page/home_page.dart b/lib/view/home_page/home_page.dart
index 4fead2a..91f8082 100644
--- a/lib/view/home_page/home_page.dart
+++ b/lib/view/home_page/home_page.dart
@@ -1,15 +1,24 @@
+import 'dart:io';
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:pmu_labworks/components/extensions/context_x.dart';
import 'package:pmu_labworks/components/utils/debounce.dart';
import 'package:pmu_labworks/domain/models/comment.dart';
import 'package:pmu_labworks/domain/models/user.dart';
+import 'package:pmu_labworks/view/common/svg_objects.dart';
import 'package:pmu_labworks/view/details_page/details_page.dart';
import 'package:pmu_labworks/view/home_page/bloc/bloc.dart';
import 'package:pmu_labworks/view/home_page/bloc/events.dart';
import 'package:pmu_labworks/view/home_page/bloc/state.dart';
+import 'package:pmu_labworks/view/reaction_bloc/reaction_bloc.dart';
+import 'package:pmu_labworks/view/reaction_bloc/reaction_event.dart';
+import 'package:pmu_labworks/view/reaction_bloc/reaction_state.dart';
+import 'package:pmu_labworks/view/locale_bloc/locale_bloc.dart';
+import 'package:pmu_labworks/view/locale_bloc/locale_events.dart';
+import 'package:pmu_labworks/view/locale_bloc/locale_state.dart';
part 'comment.dart';
@@ -44,8 +53,8 @@ class _HomePageState extends State {
fontSize: 20,
fontWeight: FontWeight.bold,
)),
- const Text(
- 'made by Factorino',
+ Text(
+ context.locale.credits + ' Factorino',
style: TextStyle(
fontSize: 12,
fontStyle: FontStyle.italic,
@@ -81,8 +90,11 @@ class _BodyState extends State<_Body> {
@override
void initState() {
+ SvgObjects.init();
+
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read().add(const HomeLoadDataEvent());
+ context.read().add(const LoadReactionsEvent());
});
scrollController.addListener(_onNextPageListener);
@@ -103,50 +115,76 @@ class _BodyState extends State<_Body> {
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
child: Column(
children: [
- Padding(
- padding: const EdgeInsets.all(12),
- child: CupertinoSearchTextField(
- controller: searchController,
- onChanged: (search) {
- Debounce.run(() => context
- .read()
- .add(HomeLoadDataEvent(search: search)));
- },
- ),
+ Row(
+ children: [
+ Expanded(
+ flex: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(12),
+ child: CupertinoSearchTextField(
+ controller: searchController,
+ placeholder: context.locale.search,
+ onChanged: (search) {
+ Debounce.run(
+ () => context.read().add(HomeLoadDataEvent(search: search)));
+ },
+ ),
+ ),
+ ),
+ GestureDetector(
+ onTap: () => context.read().add(const ChangeLocaleEvent()),
+ child: SizedBox.square(
+ dimension: 50,
+ child: Padding(
+ padding: const EdgeInsets.only(right: 12),
+ child: BlocBuilder(
+ builder: (context, state) {
+ return state.currentLocale.languageCode == 'ru'
+ ? const SvgRu()
+ : const SvgUk();
+ },
+ ),
+ ),
+ ),
+ ),
+ ],
),
BlocBuilder(
builder: (context, state) => state.error != null
? Text(
state.error ?? '',
- style: Theme.of(context)
- .textTheme
- .headlineSmall
- ?.copyWith(color: Colors.red),
+ style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: Colors.red),
)
: state.isLoading
? const CircularProgressIndicator()
- : Expanded(
- child: RefreshIndicator(
- onRefresh: _onRefresh,
- child: ListView.builder(
- controller: scrollController,
- padding: EdgeInsets.zero,
- itemCount: state.data?.data?.length ?? 0,
- itemBuilder: (context, index) {
- final data = state.data?.data?[index];
- return data != null
- ? _Comment.fromData(
- data,
- onAction:
- (nickname, isLiked, isDisliked) =>
- _showSnackBar(context, nickname,
- isLiked, isDisliked),
- onTap: () => _navToDetails(context, data),
- )
- : const SizedBox.shrink();
- },
- ),
- ),
+ : BlocBuilder(
+ builder: (context, reactionState) {
+ 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
+ ? _Comment.fromData(
+ data,
+ onLike: _onLike,
+ onDislike: _onDislike,
+ isLiked:
+ reactionState.likedIds?.contains(data.id) == true,
+ isDisliked:
+ reactionState.dislikedIds?.contains(data.id) == true,
+ onTap: () => _navToDetails(context, data),
+ )
+ : const SizedBox.shrink();
+ },
+ ),
+ ),
+ );
+ },
),
),
BlocBuilder(
@@ -160,12 +198,30 @@ class _BodyState extends State<_Body> {
}
Future _onRefresh() {
- context
- .read()
- .add(HomeLoadDataEvent(search: searchController.text));
+ context.read().add(HomeLoadDataEvent(search: searchController.text));
return Future.value(null);
}
+ void _onLike(String? id, String nickname, bool isLiked, bool isDisliked) {
+ if (id != null) {
+ if (isDisliked) {
+ isDisliked = false;
+ }
+ context.read().add(ChangeLikeEvent(id));
+ _showSnackBar(context, nickname, !isLiked, isDisliked);
+ }
+ }
+
+ void _onDislike(String? id, String nickname, bool isLiked, bool isDisliked) {
+ if (id != null) {
+ if (isLiked) {
+ isLiked = false;
+ }
+ context.read().add(ChangeDislikeEvent(id));
+ _showSnackBar(context, nickname, isLiked, !isDisliked);
+ }
+ }
+
void _navToDetails(BuildContext context, CommentData data) {
Navigator.push(
context,
@@ -173,22 +229,22 @@ class _BodyState extends State<_Body> {
);
}
- void _showSnackBar(
- BuildContext context, String nickname, bool isLiked, bool isDisliked) {
+ void _showSnackBar(BuildContext context, String nickname, bool isLiked, bool isDisliked) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final String action;
- if (isLiked) {
- action = 'liked';
- } else if (isDisliked) {
- action = 'disliked';
+ if (isLiked && !isDisliked) {
+ action = context.locale.liked;
+ } else if (!isLiked && isDisliked) {
+ action = context.locale.disliked;
} else {
- action = 'neutralized'; // Состояние без лайков/дизлайков
+ action = context.locale.neutralized; // Состояние без лайков/дизлайков
}
+ print('$isLiked $isDisliked = $action');
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(
- '$nickname\'s comment $action',
+ context.locale.comment(nickname) + ' ' + action,
style: Theme.of(context).textTheme.bodyLarge,
),
backgroundColor: Colors.white70,
diff --git a/lib/view/locale_bloc/locale_bloc.dart b/lib/view/locale_bloc/locale_bloc.dart
new file mode 100644
index 0000000..6610d69
--- /dev/null
+++ b/lib/view/locale_bloc/locale_bloc.dart
@@ -0,0 +1,17 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:pmu_labworks/components/locale/l10n/app_locale.dart';
+import 'package:pmu_labworks/view/locale_bloc/locale_events.dart';
+import 'package:pmu_labworks/view/locale_bloc/locale_state.dart';
+
+class LocaleBloc extends Bloc {
+ LocaleBloc(Locale defaultLocale) : super(LocaleState(currentLocale: defaultLocale)) {
+ on(_onChangeLocale);
+ }
+
+ Future _onChangeLocale(ChangeLocaleEvent event, Emitter emit) async {
+ final toChange = AppLocale.supportedLocales
+ .firstWhere((e) => e.languageCode != state.currentLocale.languageCode);
+ emit(state.copyWith(currentLocale: toChange));
+ }
+}
diff --git a/lib/view/locale_bloc/locale_events.dart b/lib/view/locale_bloc/locale_events.dart
new file mode 100644
index 0000000..c08cd1b
--- /dev/null
+++ b/lib/view/locale_bloc/locale_events.dart
@@ -0,0 +1,7 @@
+abstract class LocaleEvent {
+ const LocaleEvent();
+}
+
+class ChangeLocaleEvent extends LocaleEvent {
+ const ChangeLocaleEvent();
+}
diff --git a/lib/view/locale_bloc/locale_state.dart b/lib/view/locale_bloc/locale_state.dart
new file mode 100644
index 0000000..b9221ca
--- /dev/null
+++ b/lib/view/locale_bloc/locale_state.dart
@@ -0,0 +1,15 @@
+import 'package:copy_with_extension/copy_with_extension.dart';
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+
+part 'locale_state.g.dart';
+
+@CopyWith()
+class LocaleState extends Equatable {
+ final Locale currentLocale;
+
+ const LocaleState({required this.currentLocale});
+
+ @override
+ List