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..0f56889 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..ac1f165 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..2bd581c 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..3daa130 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..e785f8f 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/icon.png b/assets/icon.png
new file mode 100644
index 0000000..4f42819
Binary files /dev/null and b/assets/icon.png differ
diff --git a/assets/svg/gb.svg b/assets/svg/gb.svg
new file mode 100644
index 0000000..7991383
--- /dev/null
+++ b/assets/svg/gb.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/svg/ru.svg b/assets/svg/ru.svg
new file mode 100644
index 0000000..cf24301
--- /dev/null
+++ b/assets/svg/ru.svg
@@ -0,0 +1,5 @@
+
diff --git a/l10n.yaml b/l10n.yaml
new file mode 100644
index 0000000..c4b1c34
--- /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..adeccaa
--- /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..7dede04
--- /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/context_x.dart b/lib/Components/extensions/context_x.dart
new file mode 100644
index 0000000..69472f9
--- /dev/null
+++ b/lib/Components/extensions/context_x.dart
@@ -0,0 +1,7 @@
+
+import 'package:first_project/Components/locale/l10n/app_locale.dart';
+import 'package:flutter/cupertino.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..71a191e
--- /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..56a6d6c
--- /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..0fd0794
--- /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..41f3155
--- /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\89176\StudioProjects\first_project\assets\svg\gb.svg)
+ static const String ASSETS_SVG_GB_SVG = 'assets/svg/gb.svg';
+
+ /// ![preview](file://C:\Users\89176\StudioProjects\first_project\assets\svg\ru.svg)
+ static const String ASSETS_SVG_RU_SVG = 'assets/svg/ru.svg';
+}
diff --git a/lib/data/mappers/anime_mapper.dart b/lib/data/mappers/anime_mapper.dart
index 8c3be35..b3d355e 100644
--- a/lib/data/mappers/anime_mapper.dart
+++ b/lib/data/mappers/anime_mapper.dart
@@ -8,6 +8,7 @@ extension AnimeDataDtoToModel on AnimeDataDto {
imageUrl: images?.jpg?.image ?? "NONE",
score: score ?? 0,
description: synopsis == null ? "NONE" : synopsis!.split('\n').sublist(0, synopsis!.split('\n').length - 1).join('\n'),
+ id: id.toString(),
);
}
diff --git a/lib/main.dart b/lib/main.dart
index 4367f18..3034301 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,9 +1,16 @@
+import 'dart:io';
+
+import 'package:first_project/Components/locale/l10n/app_locale.dart';
import 'package:first_project/data/repositories/anime_repository.dart';
import 'package:first_project/presentation/home_page/bloc/bloc.dart';
import 'package:first_project/presentation/home_page/home_page.dart';
+import 'package:first_project/presentation/like_bloc/like_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
+import 'presentation/locale_bloc/locale_bloc.dart';
+import 'presentation/locale_bloc/locale_state.dart';
+
void main() {
runApp(const MyApp());
}
@@ -13,20 +20,35 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return MaterialApp(
- title: 'Flutter Demo',
- theme: ThemeData(
- colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
- useMaterial3: true,
- ),
- home: RepositoryProvider(
- lazy: true,
- create: (_) => AnimeRepository(),
- child: BlocProvider(
- lazy: false,
- create: (context) => HomeBloc(context.read()),
- child: const MyHomePage(title: 'Anime Nya >-<'),
- ),
+ return BlocProvider(
+ lazy: false,
+ create: (context) => LocaleBloc(Locale(Platform.localeName)),
+ child: BlocBuilder(
+ builder: (context, state) { return MaterialApp(
+ title: 'Flutter Demo',
+ locale: state.currentLocale,
+ localizationsDelegates: AppLocale.localizationsDelegates,
+ supportedLocales: AppLocale.supportedLocales,
+ debugShowCheckedModeBanner: false,
+ theme: ThemeData(
+ colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
+ useMaterial3: true,
+ ),
+ home: RepositoryProvider(
+ lazy: true,
+ create: (_) => AnimeRepository(),
+ child: BlocProvider(
+ lazy: false,
+ create: (context) => LikeBloc(),
+ child: BlocProvider(
+ lazy: false,
+ create: (context) => HomeBloc(context.read()),
+ child: const MyHomePage(title: 'Anime Nya >-<'),
+ ),
+ ),
+ ),
+ );
+ }
),
);
}
diff --git a/lib/presentation/common/svg_objects.dart b/lib/presentation/common/svg_objects.dart
new file mode 100644
index 0000000..03ffb5e
--- /dev/null
+++ b/lib/presentation/common/svg_objects.dart
@@ -0,0 +1,35 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:first_project/Components/resources.g.dart';
+
+abstract class SvgObjects {
+ static void init() {
+ final pics = [
+ R.ASSETS_SVG_RU_SVG,
+ R.ASSETS_SVG_GB_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 SvgGb extends StatelessWidget {
+ const SvgGb({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return SvgPicture.asset(R.ASSETS_SVG_GB_SVG);
+ }
+}
+
diff --git a/lib/presentation/home_page/card.dart b/lib/presentation/home_page/card.dart
index d3b5c77..7e6877c 100644
--- a/lib/presentation/home_page/card.dart
+++ b/lib/presentation/home_page/card.dart
@@ -1,6 +1,6 @@
part of 'home_page.dart';
-typedef OnLikeCallback = void Function(String title, bool isLiked)?;
+typedef OnLikeCallback = void Function(String? id, String title, bool isLiked)?;
class CardData {
final String text;
@@ -8,6 +8,7 @@ class CardData {
final IconData icon;
final double score;
final String imageUrl;
+ final String? id;
const CardData(
this.text, {
@@ -16,16 +17,19 @@ class CardData {
this.score = 0,
this.imageUrl =
"https://i.pinimg.com/736x/5f/14/b3/5f14b3f14fcd157bc4dffa39085396cc.jpg",
+ this.id,
});
}
-class _Card extends StatefulWidget {
+class _Card extends StatelessWidget {
final String text;
final String description;
final String? imageUrl;
final double score;
final OnLikeCallback onLike;
final VoidCallback? onTap;
+ final String? id;
+ final bool isLiked;
const _Card(
this.text, {
@@ -34,12 +38,15 @@ class _Card extends StatefulWidget {
required this.score,
this.onLike,
this.onTap,
+ this.id,
+ this.isLiked = false,
});
factory _Card.fromData(
CardData data, {
OnLikeCallback onLike,
VoidCallback? onTap,
+ bool isLiked = false,
}) =>
_Card(
data.text,
@@ -48,21 +55,16 @@ class _Card extends StatefulWidget {
score: data.score,
onLike: onLike,
onTap: onTap,
+ id: data.id,
+ isLiked: isLiked,
);
- @override
- State<_Card> createState() => _CardState();
-}
-
-class _CardState extends State<_Card> {
- bool isLiked = false;
-
@override
Widget build(BuildContext context) {
- List desc = widget.description.split("\n");
+ List desc = description.split("\n");
return GestureDetector(
- onTap: widget.onTap,
+ onTap: onTap,
child: Container(
margin: const EdgeInsets.all(10),
constraints: const BoxConstraints(minHeight: 140),
@@ -92,7 +94,7 @@ class _CardState extends State<_Card> {
height: 300,
width: 120,
child: Image.network(
- widget.imageUrl ?? '',
+ imageUrl ?? '',
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Placeholder(),
),
@@ -107,7 +109,7 @@ class _CardState extends State<_Card> {
topRight: Radius.circular(20),
bottomLeft: Radius.circular(18))),
padding: const EdgeInsets.fromLTRB(8, 2, 8, 2),
- child: Text(generateStars(widget.score),
+ child: Text(generateStars(score),
style: Theme.of(context)
.textTheme
.bodyMedium
@@ -123,7 +125,7 @@ class _CardState extends State<_Card> {
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
- widget.text,
+ text,
style: Theme.of(context).textTheme.headlineMedium,
),
Text(
@@ -143,10 +145,7 @@ class _CardState extends State<_Card> {
bottom: 8,
),
child: GestureDetector(
- onTap: () {
- setState(() => isLiked = !isLiked);
- widget.onLike?.call(widget.text, isLiked);
- },
+ onTap: () => onLike?.call(id, text, isLiked),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: isLiked
diff --git a/lib/presentation/home_page/home_page.dart b/lib/presentation/home_page/home_page.dart
index 2c8b411..18a7ecb 100644
--- a/lib/presentation/home_page/home_page.dart
+++ b/lib/presentation/home_page/home_page.dart
@@ -1,15 +1,24 @@
+import 'package:first_project/Components/extensions/context_x.dart';
import 'package:first_project/data/repositories/anime_repository.dart';
import 'package:first_project/data/repositories/mock_repository.dart';
import 'package:first_project/domain/models/card.dart';
+import 'package:first_project/presentation/common/svg_objects.dart';
import 'package:first_project/presentation/home_page/bloc/bloc.dart';
import 'package:first_project/presentation/home_page/bloc/events.dart';
import 'package:first_project/presentation/home_page/bloc/state.dart';
+import 'package:first_project/presentation/like_bloc/like_bloc.dart';
+import 'package:first_project/presentation/like_bloc/like_event.dart';
+import 'package:first_project/presentation/like_bloc/like_state.dart';
+import 'package:first_project/presentation/locale_bloc/locale_events.dart';
+import 'package:first_project/presentation/locale_bloc/locale_state.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:first_project/Components/utils/debounce.dart';
+import '../locale_bloc/locale_bloc.dart';
+
part 'card.dart';
class MyHomePage extends StatefulWidget {
@@ -62,8 +71,11 @@ class _BodyState extends State<_Body> {
@override
void initState() {
+ SvgObjects.init();
+
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read().add(const HomeLoadDataEvent());
+ context.read().add(const LoadLikesEvent());
});
scrollController.addListener(_onNextPageListener);
@@ -72,7 +84,8 @@ class _BodyState extends State<_Body> {
}
void _onNextPageListener() {
- if (scrollController.offset > scrollController.position.maxScrollExtent - 50) {
+ if (scrollController.offset >
+ scrollController.position.maxScrollExtent - 50) {
final bloc = context.read();
if (!bloc.state.isPaginationLoading) {
bloc.add(HomeLoadDataEvent(
@@ -104,37 +117,65 @@ class _BodyState extends State<_Body> {
: state.isLoading
? const CircularProgressIndicator()
: Column(children: [
- Padding(
- padding: const EdgeInsets.all(8),
- child: CupertinoSearchTextField(
- controller: searchController,
- onChanged: (search) {
- Debouce.run(() => context
- .read()
- .add(HomeLoadDataEvent(search: search)));
- },
+ Row(children: [
+ Expanded(
+ flex: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(8),
+ child: CupertinoSearchTextField(
+ controller: searchController,
+ placeholder: context.locale.search,
+ onChanged: (search) {
+ Debouce.run(() => context
+ .read()
+ .add(HomeLoadDataEvent(search: search)));
+ },
+ ),
+ ),
),
- ),
- 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) =>
- _showLiked(context, title, isLiked),
- onTap: () => _navToDetails(context, data),
- )
- : const SizedBox.shrink();
- }),
- ),
- ),
+ GestureDetector(
+ onTap: () => context
+ .read()
+ .add(const ChangeLocaleEvent()),
+ child: SizedBox.square(
+ dimension: 50,
+ child: Padding(
+ padding: const EdgeInsets.only(right: 15),
+ child: BlocBuilder(
+ builder: (context, state) {
+ return state.currentLocale.languageCode ==
+ 'ru'
+ ? const SvgRu()
+ : const SvgGb();
+ },
+ ),
+ ),
+ )),
+ ]),
+ 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,
+ isLiked: likeState.likedIds?.contains(data.id) == true,
+ onLike: _onLike,
+ onTap: () =>
+ _navToDetails(context, data),
+ )
+ : const SizedBox.shrink();
+ }),
+ ),
+ );
+ }),
BlocBuilder(
builder: (context, state) => state.isPaginationLoading
? const CircularProgressIndicator()
@@ -150,11 +191,19 @@ class _BodyState extends State<_Body> {
);
}
+ void _onLike(String? id, String title, bool isLiked) {
+ if (id != null)
+ {
+ context.read().add(ChangeLikeEvent(id));
+ _showLiked(context, title, !isLiked);
+ }
+ }
+
void _showLiked(BuildContext context, String title, bool isLiked) {
WidgetsBinding.instance.addPostFrameCallback((_) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(
- 'Аниме $title ${isLiked ? 'лайкнуто!' : 'дизлайнуто'}',
+ '$title ${isLiked ? context.locale.liked : context.locale.disliked}',
style: Theme.of(context).textTheme.bodyMedium,
),
backgroundColor: Colors.purple,
diff --git a/lib/presentation/like_bloc/like_bloc.dart b/lib/presentation/like_bloc/like_bloc.dart
new file mode 100644
index 0000000..2032915
--- /dev/null
+++ b/lib/presentation/like_bloc/like_bloc.dart
@@ -0,0 +1,37 @@
+
+import 'package:first_project/presentation/like_bloc/like_event.dart';
+import 'package:first_project/presentation/like_bloc/like_state.dart';
+import 'package:flutter_bloc/flutter_bloc.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_bloc/like_event.dart b/lib/presentation/like_bloc/like_event.dart
new file mode 100644
index 0000000..43032db
--- /dev/null
+++ b/lib/presentation/like_bloc/like_event.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);
+}
\ No newline at end of file
diff --git a/lib/presentation/like_bloc/like_state.dart b/lib/presentation/like_bloc/like_state.dart
new file mode 100644
index 0000000..77cf109
--- /dev/null
+++ b/lib/presentation/like_bloc/like_state.dart
@@ -0,0 +1,14 @@
+import 'package:equatable/equatable.dart';
+import 'package:copy_with_extension/copy_with_extension.dart';
+
+part "like_state.g.dart";
+
+@CopyWith()
+class LikeState extends Equatable {
+ final List? likedIds;
+
+ const LikeState({required this.likedIds});
+
+ @override
+ List