diff --git a/Makefile b/Makefile
index 808a68a..6b8b867 100644
--- a/Makefile
+++ b/Makefile
@@ -1,26 +1,24 @@
-# Устанавливаем переменную для команды flutter
-FLUTTER = flutter
+gen:
+ flutter pub run build_runner build --delete-conflicting-outputs
-# Команда для генерации кода с build_runner
-gen: install-dependencies
- $(FLUTTER) pub run build_runner build --delete-conflicting-outputs
+hello:
+ echo "Hi!"; \
+ echo "I'm makefile"; \
+ echo "^_^"
-# Устанавливаем зависимости (если их нет)
-install-dependencies:
- $(FLUTTER) pub get
+icon:
+ flutter pub run flutter_launcher_icons:main
-# Очистка проекта (по желанию)
-clean:
- $(FLUTTER) clean
+init_res:
+ dart pub global activate flutter_asset_generator
-# Для генерации кода с очисткой
-clean-generate: clean generate
+format:
+ dart format . --line-length 100
-# Отображение доступных команд
-help:
- @echo "Доступные команды:"
- @echo " generate - Запуск генерации кода с build_runner"
- @echo " install-dependencies - Установить зависимости"
- @echo " clean - Очистить проект"
- @echo " clean-generate - Очистка и генерация кода"
- @echo " help - Показать эту справку"
+res:
+ fgen --output lib/components/resources.g.dart --no-watch --no-preview; \
+ make format
+
+loc:
+ flutter gen-l10n; \
+ make format
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..fa73b82 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-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 17987b7..8f36c9c 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-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 09d4391..a759bf4 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-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index d5f1c8d..6d4b029 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-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 4d6372e..f29ff94 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/assets/launcher.jpg b/assets/launcher.jpg
new file mode 100644
index 0000000..00f9cab
Binary files /dev/null and b/assets/launcher.jpg differ
diff --git a/assets/svg/ru.svg b/assets/svg/ru.svg
new file mode 100644
index 0000000..b7c6623
--- /dev/null
+++ b/assets/svg/ru.svg
@@ -0,0 +1,10 @@
+
diff --git a/assets/svg/uk.svg b/assets/svg/uk.svg
new file mode 100644
index 0000000..781b415
--- /dev/null
+++ b/assets/svg/uk.svg
@@ -0,0 +1,16 @@
+
diff --git a/devtools_options.yaml b/devtools_options.yaml
new file mode 100644
index 0000000..fa0b357
--- /dev/null
+++ b/devtools_options.yaml
@@ -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:
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..a01051c
--- /dev/null
+++ b/l10n/app_en.arb
@@ -0,0 +1,9 @@
+{
+ "@@locale": "en",
+
+ "search": "Search",
+ "liked": "liked!",
+ "disliked": "disliked :(",
+
+ "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..7b30d41
--- /dev/null
+++ b/l10n/app_ru.arb
@@ -0,0 +1,9 @@
+{
+ "@@locale": "ru",
+
+ "search": "Поиск",
+ "liked": "понравился!",
+ "disliked": "разонравился :(",
+
+ "arbEnding": "Чтобы не забыть про отсутствие запятой :)"
+}
\ No newline at end of file
diff --git a/lib/components/extensions/content_x.dart b/lib/components/extensions/content_x.dart
new file mode 100644
index 0000000..69e02c6
--- /dev/null
+++ b/lib/components/extensions/content_x.dart
@@ -0,0 +1,6 @@
+import 'package:flutter/widgets.dart';
+import 'package:pmu/components/locale/l10n/app_locale.dart';
+
+extension LocalContextX on BuildContext {
+ AppLocale get locale => AppLocale.of(this)!;
+}
\ No newline at end of file
diff --git a/lib/components/locale/l10n/app_locale.dart b/lib/components/locale/l10n/app_locale.dart
new file mode 100644
index 0000000..0922eb1
--- /dev/null
+++ b/lib/components/locale/l10n/app_locale.dart
@@ -0,0 +1,153 @@
+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 @search.
+ ///
+ /// In ru, this message translates to:
+ /// **'Поиск'**
+ String get search;
+
+ /// No description provided for @liked.
+ ///
+ /// In ru, this message translates to:
+ /// **'понравился!'**
+ String get liked;
+
+ /// No description provided for @disliked.
+ ///
+ /// In ru, this message translates to:
+ /// **'разонравился :('**
+ String get disliked;
+
+ /// No description provided for @arbEnding.
+ ///
+ /// 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..599548c
--- /dev/null
+++ b/lib/components/locale/l10n/app_locale_en.dart
@@ -0,0 +1,20 @@
+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 search => 'Search';
+
+ @override
+ String get liked => 'liked!';
+
+ @override
+ String get disliked => 'disliked :(';
+
+ @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..b8d5444
--- /dev/null
+++ b/lib/components/locale/l10n/app_locale_ru.dart
@@ -0,0 +1,20 @@
+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 search => 'Поиск';
+
+ @override
+ String get liked => 'понравился!';
+
+ @override
+ String get disliked => 'разонравился :(';
+
+ @override
+ String get arbEnding => 'Чтобы не забыть про отсутствие запятой :)';
+}
diff --git a/lib/components/resources.g.dart b/lib/components/resources.g.dart
new file mode 100644
index 0000000..0627a52
--- /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://D:\code\pmu_nikita\PMU-PIbd-31-Potapov-N-S\assets\svg\ru.svg)
+ static const String ASSETS_SVG_RU_SVG = 'assets/svg/ru.svg';
+
+ /// ![preview](file://D:\code\pmu_nikita\PMU-PIbd-31-Potapov-N-S\assets\svg\uk.svg)
+ static const String ASSETS_SVG_UK_SVG = 'assets/svg/uk.svg';
+}
diff --git a/lib/components/resoures.g.dart b/lib/components/resoures.g.dart
new file mode 100644
index 0000000..276bfa0
--- /dev/null
+++ b/lib/components/resoures.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://D:\code\pmu_labs\pmu_labs_3_course\assets\svg\ru.svg)
+ static const String ASSETS_SVG_RU_SVG = 'assets/svg/ru.svg';
+
+ /// ![preview](file://D:\code\pmu_labs\pmu_labs_3_course\assets\svg\uk.svg)
+ static const String ASSETS_SVG_UK_SVG = 'assets/svg/uk.svg';
+}
diff --git a/lib/data/mappers/user_mapper.dart b/lib/data/mappers/user_mapper.dart
index ee80ae2..132fef8 100644
--- a/lib/data/mappers/user_mapper.dart
+++ b/lib/data/mappers/user_mapper.dart
@@ -21,8 +21,8 @@ extension UserDtoToModel on UserDto {
? "https://avatar.iran.liara.run/public/boy"
: "https://avatar.iran.liara.run/public/girl")
.toString(),
- isLiked: false,
age: age,
- distance: distance);
+ distance: distance,
+ id: id?.toString());
}
}
diff --git a/lib/data/repositories/api_repository.dart b/lib/data/repositories/api_repository.dart
index e2594b1..b32d015 100644
--- a/lib/data/repositories/api_repository.dart
+++ b/lib/data/repositories/api_repository.dart
@@ -41,7 +41,8 @@ class ApiRepository extends ApiInterface {
HomeData homeData = HomeData(
data: data,
pageNumber: pageDto.pageNumber,
- nextPageNumber: pageDto.nextPageNumber);
+ nextPageNumber: pageDto.nextPageNumber,
+ lastPageNumber: pageDto.lastPageNumber);
return homeData;
} on DioException catch (e) {
diff --git a/lib/data/repositories/mock_repository.dart b/lib/data/repositories/mock_repository.dart
index 69d4654..af2f395 100644
--- a/lib/data/repositories/mock_repository.dart
+++ b/lib/data/repositories/mock_repository.dart
@@ -13,7 +13,7 @@ class MockRepository extends ApiInterface {
imageUrl: "https://placehold.co/600x400/png",
age: 21,
distance: 24.5,
- isLiked: false),
+ ),
const CardData(
name: "Константин",
surname: "Злобин",
@@ -21,7 +21,7 @@ class MockRepository extends ApiInterface {
imageUrl: "https://placehold.co/600x400/png",
age: 24,
distance: 478.3,
- isLiked: false)
- ], pageNumber: 0);
+ )
+ ], pageNumber: 0, lastPageNumber: 0);
}
}
diff --git a/lib/domain/models/card.dart b/lib/domain/models/card.dart
index e347b3c..cb185e7 100644
--- a/lib/domain/models/card.dart
+++ b/lib/domain/models/card.dart
@@ -1,18 +1,21 @@
class CardData {
+
final String? name;
final String? surname;
final String? description;
final String? imageUrl;
final int? age;
final double? distance;
- final bool? isLiked;
+
+ final String? id;
+
const CardData(
{this.name,
this.surname,
this.description,
this.imageUrl,
- this.isLiked,
this.age,
- this.distance});
+ this.distance,
+ this.id});
}
diff --git a/lib/domain/models/home.dart b/lib/domain/models/home.dart
index 0249aed..35f6102 100644
--- a/lib/domain/models/home.dart
+++ b/lib/domain/models/home.dart
@@ -4,6 +4,7 @@ class HomeData {
final List? data;
final int? pageNumber;
final int? nextPageNumber;
+ final int? lastPageNumber;
- HomeData({this.data, this.pageNumber, this.nextPageNumber});
+ HomeData({this.data, this.pageNumber, this.nextPageNumber, this.lastPageNumber});
}
diff --git a/lib/main.dart b/lib/main.dart
index 8e907d4..9872901 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/data/repositories/api_repository.dart';
import 'package:pmu/presentation/home_page/bloc/bloc.dart';
import 'package:pmu/presentation/home_page/home_page.dart';
+import 'package:pmu/presentation/like/bloc.dart';
+import 'package:pmu/presentation/locale/bloc.dart';
+import 'package:pmu/presentation/locale/state.dart';
+
+import 'components/locale/l10n/app_locale.dart';
void main() {
runApp(const MyApp());
@@ -13,15 +20,33 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return MaterialApp(
- title: 'LoveSearch',
- home: RepositoryProvider(
- lazy: true,
- create: (_) => ApiRepository(),
- child: BlocProvider(
- lazy: false,
- create: (context) => HomeBloc(context.read()),
- child: const HomePage())),
+ return BlocProvider(
+ lazy: false,
+ create: (context) => LocaleBloc(Locale(Platform.localeName)),
+ child: BlocBuilder(
+ builder: (context, state) {
+ return MaterialApp(
+ title: 'LoveSearch',
+ locale: state.currentLocale,
+ localizationsDelegates: AppLocale.localizationsDelegates,
+ supportedLocales: AppLocale.supportedLocales,
+ debugShowCheckedModeBanner: false,
+ home: RepositoryProvider(
+ lazy: true,
+ create: (_) => ApiRepository(),
+ child: BlocProvider(
+ lazy: true,
+ create: (context) => LikeBloc(),
+ child: BlocProvider(
+ lazy: false,
+ create: (context) =>
+ HomeBloc(context.read()),
+ child: const HomePage()),
+ ),
+ ),
+ );
+ },
+ ),
);
}
}
diff --git a/lib/presentation/common/svg_objects.dart b/lib/presentation/common/svg_objects.dart
new file mode 100644
index 0000000..13b85f2
--- /dev/null
+++ b/lib/presentation/common/svg_objects.dart
@@ -0,0 +1,36 @@
+import 'package:flutter/widgets.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+
+import '../../components/resoures.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/presentation/home_page/bloc/bloc.dart b/lib/presentation/home_page/bloc/bloc.dart
index 46e4c5e..2fd0569 100644
--- a/lib/presentation/home_page/bloc/bloc.dart
+++ b/lib/presentation/home_page/bloc/bloc.dart
@@ -14,6 +14,15 @@ class HomeBloc extends Bloc {
Future _onLoadData(
HomeLoadDataEvent event, Emitter emit) async {
+
+ if (event.pageNumber == event.lastPageNumber && event.pageNumber != null) {
+ emit(state.copyWith(
+ isLoading: false,
+ isPaginationLoading: false,
+ ));
+ return;
+ }
+
if (event.nextPageNumber == null) {
emit(state.copyWith(isLoading: true));
} else {
diff --git a/lib/presentation/home_page/bloc/events.dart b/lib/presentation/home_page/bloc/events.dart
index 52d4190..0f86dab 100644
--- a/lib/presentation/home_page/bloc/events.dart
+++ b/lib/presentation/home_page/bloc/events.dart
@@ -6,6 +6,8 @@ class HomeLoadDataEvent extends HomeEvent {
final String? search;
final int? pageNumber;
final int? nextPageNumber;
+ final int? lastPageNumber;
- const HomeLoadDataEvent({this.search, this.pageNumber, this.nextPageNumber});
+ const HomeLoadDataEvent(
+ {this.search, this.pageNumber, this.nextPageNumber, this.lastPageNumber});
}
diff --git a/lib/presentation/home_page/bloc/state.g.dart b/lib/presentation/home_page/bloc/state.g.dart
index 114ac25..cd19bc1 100644
--- a/lib/presentation/home_page/bloc/state.g.dart
+++ b/lib/presentation/home_page/bloc/state.g.dart
@@ -23,8 +23,8 @@ abstract class _$HomeStateCWProxy {
/// ````
HomeState call({
HomeData? data,
- bool? isLoading,
- bool? isPaginationLoading,
+ bool isLoading,
+ bool isPaginationLoading,
String? error,
});
}
@@ -67,16 +67,14 @@ class _$HomeStateCWProxyImpl implements _$HomeStateCWProxy {
? _value.data
// ignore: cast_nullable_to_non_nullable
: data as HomeData?,
- isLoading: isLoading == const $CopyWithPlaceholder() || isLoading == null
+ isLoading: isLoading == const $CopyWithPlaceholder()
? _value.isLoading
// ignore: cast_nullable_to_non_nullable
: isLoading as bool,
- isPaginationLoading:
- isPaginationLoading == const $CopyWithPlaceholder() ||
- isPaginationLoading == null
- ? _value.isPaginationLoading
- // ignore: cast_nullable_to_non_nullable
- : isPaginationLoading as bool,
+ isPaginationLoading: isPaginationLoading == const $CopyWithPlaceholder()
+ ? _value.isPaginationLoading
+ // ignore: cast_nullable_to_non_nullable
+ : isPaginationLoading as bool,
error: error == const $CopyWithPlaceholder()
? _value.error
// ignore: cast_nullable_to_non_nullable
diff --git a/lib/presentation/home_page/card.dart b/lib/presentation/home_page/card.dart
index 8eeec16..1d4d9dc 100644
--- a/lib/presentation/home_page/card.dart
+++ b/lib/presentation/home_page/card.dart
@@ -1,14 +1,15 @@
part of 'home_page.dart';
-typedef OnLikeCallback = void Function(String title, bool isLiked)?;
+typedef OnLikeCallback = void Function(String? id, String title, bool isLiked)?;
const double NORMAL_ICON_SCALE = 2.0;
const double SCALED_ICON_SCALE = 2.5;
-class _Card extends StatefulWidget {
+class _Card extends StatelessWidget {
final String? description;
final String? imageUrl;
- final bool? isLiked;
+ final bool isLiked;
+ final String? id;
final String? name;
final String? surname;
@@ -20,34 +21,28 @@ class _Card extends StatefulWidget {
this.surname,
this.description,
this.imageUrl,
- this.isLiked,
+ this.isLiked = false,
this.onLike,
- this.onTap});
+ this.onTap,
+ this.id});
factory _Card.fromData(CardData data,
- {OnLikeCallback onLike, VoidCallback? onTap}) =>
+ {OnLikeCallback onLike, VoidCallback? onTap, bool isLiked = false}) =>
_Card(
- name: data.name,
- surname: data.surname,
- description: data.description,
- imageUrl: data.imageUrl,
- isLiked: data.isLiked,
- onLike: onLike,
- onTap: onTap);
-
- @override
- State<_Card> createState() => _CardState();
-}
-
-class _CardState extends State<_Card> {
- bool isLiked = false;
-
- double iconScale = NORMAL_ICON_SCALE;
+ name: data.name,
+ surname: data.surname,
+ description: data.description,
+ imageUrl: data.imageUrl,
+ isLiked: isLiked,
+ onLike: onLike,
+ onTap: onTap,
+ id: data.id,
+ );
@override
Widget build(BuildContext context) {
return GestureDetector(
- onTap: widget.onTap,
+ onTap: onTap,
child: Container(
margin: const EdgeInsets.all(10),
padding: const EdgeInsets.all(10),
@@ -77,7 +72,7 @@ class _CardState extends State<_Card> {
children: [
Flexible(
child: Text(
- (widget.name ?? "") + (" ") + (widget.surname ?? ""),
+ (name ?? "") + (" ") + (surname ?? ""),
style: const TextStyle(
color: Colors.white,
fontSize: 30,
@@ -93,7 +88,7 @@ class _CardState extends State<_Card> {
children: [
Flexible(
child: Text(
- widget.description ?? "",
+ description ?? "",
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
@@ -104,43 +99,40 @@ class _CardState extends State<_Card> {
],
),
),
- if (widget.imageUrl != null)
+ if (imageUrl != null)
Padding(
padding: const EdgeInsets.all(8.0),
- child: Image.network(widget.imageUrl!,
+ child: Image.network(imageUrl!,
height: 500,
width: double.infinity,
fit: BoxFit.fitWidth),
),
Padding(
padding: const EdgeInsets.all(4.0),
+
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
- onTap: () => {
- setState(() {
- isLiked = !isLiked;
- widget.onLike?.call(widget.name ?? "", isLiked);
- })
- },
- child: AnimatedScale(
- scale: iconScale,
- duration: const Duration(milliseconds: 250),
- child: AnimatedSwitcher(
- duration: const Duration(milliseconds: 250),
- child: isLiked
- ? const Icon(
- Icons.favorite,
- color: Colors.red,
- key: ValueKey(1),
- )
- : const Icon(Icons.favorite_border,
- color: Colors.black, key: ValueKey(1)))),
+ onTap: () => onLike?.call(id, "${name!} ${surname!}", isLiked),
+ child: AnimatedSwitcher(
+ duration: const Duration(milliseconds: 250),
+ child: !isLiked
+ ? const Icon(
+ Icons.favorite_border,
+ color: Colors.black,
+ key: ValueKey(0),
+ )
+ : const Icon(
+ Icons.favorite,
+ color: Colors.red,
+ key: ValueKey(1),
+ ),
+ ),
),
],
),
- )
+ ),
],
),
));
diff --git a/lib/presentation/home_page/home_page.dart b/lib/presentation/home_page/home_page.dart
index afba72d..c0801b4 100644
--- a/lib/presentation/home_page/home_page.dart
+++ b/lib/presentation/home_page/home_page.dart
@@ -2,11 +2,20 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pmu/components/debounce.dart';
+import 'package:pmu/components/extensions/content_x.dart';
import 'package:pmu/domain/models/card.dart';
+import 'package:pmu/presentation/common/svg_objects.dart';
import 'package:pmu/presentation/detail_pages/card_detail_page.dart';
import 'package:pmu/presentation/home_page/bloc/bloc.dart';
import 'package:pmu/presentation/home_page/bloc/events.dart';
import 'package:pmu/presentation/home_page/bloc/state.dart';
+import 'package:pmu/presentation/locale/bloc.dart';
+import 'package:pmu/presentation/locale/events.dart';
+import 'package:pmu/presentation/locale/state.dart';
+
+import '../like/bloc.dart';
+import '../like/events.dart';
+import '../like/state.dart';
part 'card.dart';
@@ -39,6 +48,7 @@ class _BodyState extends State<_Body> {
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read().add(const HomeLoadDataEvent());
+ context.read().add(const LoadLikesEvent());
});
scrollController.addListener(_onNextPageListener);
@@ -53,7 +63,9 @@ class _BodyState extends State<_Body> {
if (!bloc.state.isPaginationLoading) {
bloc.add(HomeLoadDataEvent(
search: searchController.text,
+ pageNumber: bloc.state.data?.pageNumber,
nextPageNumber: bloc.state.data?.nextPageNumber,
+ lastPageNumber: bloc.state.data?.lastPageNumber
));
}
}
@@ -72,16 +84,41 @@ 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(8.0),
+ 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.all(12),
+ child: BlocBuilder(
+ builder: (context, state) {
+ return state.currentLocale.languageCode == 'ru'
+ ? const SvgRu()
+ : const SvgUk();
+ },
+ ),
+ ),
+ ),
+ )
+ ],
),
BlocBuilder(
builder: (context, state) => state.error != null
@@ -94,26 +131,33 @@ class _BodyState extends State<_Body> {
)
: state.isLoading
? const CircularProgressIndicator()
- : Expanded(
- child: RefreshIndicator(
- onRefresh: _onRefresh,
- child: ListView.builder(
- controller: scrollController,
- padding: EdgeInsets.zero,
- itemCount: state.data?.data?.length ?? 0,
- itemBuilder: (context, index) {
- final data = state.data?.data?[index];
- return data != null
- ? _Card.fromData(
- data,
- onLike: (title, isLiked) => _showSnackBar(
- context, title, isLiked),
- onTap: () => _navToDetails(context, data),
- )
- : const SizedBox.shrink();
- },
- ),
- ),
+ : BlocBuilder(
+ builder: (context, likeState) {
+ return Expanded(
+ child: RefreshIndicator(
+ onRefresh: _onRefresh,
+ child: ListView.builder(
+ controller: scrollController,
+ padding: EdgeInsets.zero,
+ itemCount: state.data?.data?.length ?? 0,
+ itemBuilder: (context, index) {
+ final data = state.data?.data?[index];
+ return data != null
+ ? _Card.fromData(
+ data,
+ onLike: _onLike,
+ isLiked: likeState.likedIds
+ ?.contains(data.id) ==
+ true,
+ onTap: () =>
+ _navToDetails(context, data),
+ )
+ : const SizedBox.shrink();
+ },
+ ),
+ ),
+ );
+ },
),
),
BlocBuilder(
@@ -126,6 +170,13 @@ class _BodyState extends State<_Body> {
);
}
+ void _onLike(String? id, String title, bool isLiked) {
+ if (id != null) {
+ context.read().add(ChangeLikeEvent(id));
+ _showSnackBar(context, title, !isLiked);
+ }
+ }
+
Future _onRefresh() {
context
.read()
diff --git a/lib/presentation/like/bloc.dart b/lib/presentation/like/bloc.dart
new file mode 100644
index 0000000..76e46c8
--- /dev/null
+++ b/lib/presentation/like/bloc.dart
@@ -0,0 +1,38 @@
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:pmu/presentation/like/events.dart';
+import 'package:pmu/presentation/like/state.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+
+const String _likedPrefsKey = 'liked';
+
+class LikeBloc extends Bloc {
+ LikeBloc() : super(const LikeState(likedIds: [])) {
+ on(_onChangeLike);
+ on(_onLoadLikes);
+ }
+
+ Future _onLoadLikes(
+ LoadLikesEvent event, Emitter emit) async {
+ final prefs = await SharedPreferences.getInstance();
+ final data = prefs.getStringList(_likedPrefsKey);
+
+ emit(state.copyWith(likedIds: data));
+ }
+
+ Future _onChangeLike(
+ ChangeLikeEvent event, Emitter emit) async {
+ final updatedList = List.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));
+ }
+}
diff --git a/lib/presentation/like/events.dart b/lib/presentation/like/events.dart
new file mode 100644
index 0000000..d0326d8
--- /dev/null
+++ b/lib/presentation/like/events.dart
@@ -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);
+}
diff --git a/lib/presentation/like/state.dart b/lib/presentation/like/state.dart
new file mode 100644
index 0000000..7b60777
--- /dev/null
+++ b/lib/presentation/like/state.dart
@@ -0,0 +1,14 @@
+import 'package:copy_with_extension/copy_with_extension.dart';
+import 'package:equatable/equatable.dart';
+
+part 'state.g.dart';
+
+@CopyWith()
+class LikeState extends Equatable {
+ final List? likedIds;
+
+ const LikeState({required this.likedIds});
+
+ @override
+ List