локаль дописать осталось :)

This commit is contained in:
Алексей Тихоненков 2024-12-19 02:16:35 +04:00
parent f492991a36
commit 2d49ce9f8c
17 changed files with 363 additions and 149 deletions

View File

@ -1,9 +1,13 @@
{
"@@locale": "en",
"search": "Search",
"title": "Quotes"
"search_word": "Search by word",
"search_author": "Search by author",
"liked": "Like :)",
"disliked": "Dislike :(",
"error": "Error :C"
"details": "Details of quote"
"arbEnding": ""
}

View File

@ -7,7 +7,7 @@ class AppLocaleEn extends AppLocale {
AppLocaleEn([String locale = 'en']) : super(locale);
@override
String get search => 'Search';
String get search => 'Поиск';
@override
String get liked => 'Like :)';

View File

@ -16,9 +16,10 @@ class QuoteDataDto {
final String? body;
final String? author;
final String? imageUrl;
final String? id;
final dynamic id;
final bool? isLiked;
const QuoteDataDto({this.body, this.author, this.imageUrl, this.id});
const QuoteDataDto({this.body, this.author, this.imageUrl, this.id, this.isLiked});
factory QuoteDataDto.fromJson(Map<String, dynamic> json) => _$QuoteDataDtoFromJson(json);
}

View File

@ -16,4 +16,6 @@ QuoteDataDto _$QuoteDataDtoFromJson(Map<String, dynamic> json) => QuoteDataDto(
body: json['body'] as String?,
author: json['author'] as String?,
imageUrl: json['imageUrl'] as String?,
id: json['id'],
isLiked: json['isLiked'] as bool?,
);

View File

@ -9,6 +9,7 @@ extension QuoteDtoToModel on QuoteDataDto {
body ?? 'Без текста',
author ?? 'Неизвестный автор',
imageUrl ?? _imagePlaceholder,
id ?? "",
id?.toString() ?? "",
isLiked ?? false,
);
}

View File

@ -3,12 +3,6 @@ class Quote {
final String author;
final String imagePath;
final String id;
bool isFavorite;
Quote(this.text, this.author, this.imagePath, this.id,[this.isFavorite = false]);
bool toggleFavorite() {
isFavorite = !isFavorite;
return isFavorite;
}
final bool isLiked;
Quote(this.text, this.author, this.imagePath, this.id, this.isLiked);
}

View File

@ -1,7 +1,11 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pmu_flutter_labs/presentation/home_page/bloc/events.dart';
import 'package:pmu_flutter_labs/presentation/like_bloc/like_bloc.dart';
import 'package:pmu_flutter_labs/presentation/locale_bloc/locale_bloc.dart';
import 'package:pmu_flutter_labs/presentation/locale_bloc/locale_state.dart';
import 'components/locale/l10n/app_locale.dart';
import 'data/repositories/quotes_repository.dart';
import '/presentation/home_page/bloc/bloc.dart';
@ -15,26 +19,36 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Цитаты',
localizationsDelegates: AppLocale.localizationsDelegates,
supportedLocales: AppLocale.supportedLocales,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: RepositoryProvider<QuotesRepository>(
lazy: true,
create: (_) => QuotesRepository(),
child: BlocProvider(
create: (context) => LikeBloc(), // Add LikeBloc here
child: BlocProvider(
create: (context) => HomeBloc(context.read<QuotesRepository>())
..add(const HomeLoadDataEvent()), // Ensure initial load
child: const MyHomePage(title: "Цитаты"),
),
),
),
return BlocProvider<LocaleBloc>(
lazy: false,
create:(context) => LocaleBloc(Locale(Platform.localeName)),
child: BlocBuilder<LocaleBloc, LocaleState>(
builder: (context, state) {
return MaterialApp(
title: 'Цитаты',
locale: state.currentLocale,
localizationsDelegates: AppLocale.localizationsDelegates,
supportedLocales: AppLocale.supportedLocales,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: RepositoryProvider<QuotesRepository>(
lazy: true,
create: (_) => QuotesRepository(),
child: BlocProvider<LikeBloc>(
lazy: false,
create: (context) => LikeBloc(), // Add LikeBloc here
child: BlocProvider<HomeBloc>(
lazy: false,
create: (context) => HomeBloc(context.read<QuotesRepository>()),
child: const MyHomePage(title: "Цитаты"),
),
),
),
);
}
),
);
}
}

View File

@ -3,7 +3,7 @@ import '../../../domain/quote.dart';
import '../home_page.dart';
class HomeState extends Equatable {
final Future<List<Quote>>? data; // Изменено
final Future<List<Quote>>? data;
const HomeState({this.data});

View File

@ -1,65 +1,63 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/quote.dart';
import '../like_bloc/like_bloc.dart';
import '../like_bloc/like_state.dart';
import 'home_page.dart';
class QuoteCard extends StatelessWidget {
final Quote quote;
final VoidCallback onFavoriteToggle;
final bool isLiked;
const QuoteCard({Key? key, required this.quote, required this.onFavoriteToggle}) : super(key: key);
const QuoteCard({
super.key,
required this.quote,
required this.onFavoriteToggle,
required this.isLiked,
});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.all(8.0),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
quote.text,
style: const TextStyle(fontSize: 18.0),
),
const SizedBox(height: 8.0),
Text(
'- ${quote.author}',
style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 16.0),
),
const SizedBox(height: 8.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ElevatedButton.icon(
onPressed: onFavoriteToggle,
icon: BlocBuilder<LikeBloc, LikeState>(
builder: (context, state) {
final isLiked = state.likedIds?.contains(quote.id) ?? false;
return Icon(
isLiked ? Icons.favorite : Icons.favorite_border,
color: isLiked ? Colors.red : Colors.grey,
);
},
),
label: BlocBuilder<LikeBloc, LikeState>(
builder: (context, state) {
final isLiked = state.likedIds?.contains(quote.id) ?? false;
return Text(isLiked ? 'Убрать из избранного' : 'Добавить в избранное');
},
),
),
],
),
],
margin: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 10.0,
),
child: ListTile(
contentPadding: const EdgeInsets.all(8.0),
leading: SizedBox(
width: 50.0,
child: Image.network(
quote.imagePath,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) =>
const Icon(Icons.error, color: Colors.red),
),
),
title: Text(
quote.text,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
),
subtitle: Text('- ${quote.author}'),
trailing: IconButton(
icon: Icon(
isLiked ? Icons.favorite : Icons.favorite_border,
color: isLiked ? Colors.red : null,
),
onPressed: onFavoriteToggle,
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => QuoteDetailScreen(quote: quote),
),
);
},
),
);
}
}
class QuoteDetailScreen extends StatelessWidget {
final Quote quote;
@ -80,7 +78,8 @@ class QuoteDetailScreen extends StatelessWidget {
Image.network(
quote.imagePath,
height: 150,
errorBuilder: (_, __, ___) => const Icon(Icons.error, color: Colors.red),
errorBuilder: (_, __, ___) =>
const Icon(Icons.error, color: Colors.red),
),
const SizedBox(height: 20),
Text(

View File

@ -1,11 +1,16 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pmu_flutter_labs/components/extensions/context_x.dart';
import 'package:pmu_flutter_labs/presentation/like_bloc/like_state.dart';
import '../../components/utils/debounce.dart';
import '../../domain/quote.dart';
import '../common/svg_objects.dart';
import '../like_bloc/like_bloc.dart';
import '../like_bloc/like_event.dart';
import '../locale_bloc/locale_bloc.dart';
import '../locale_bloc/locale_events.dart';
import '../locale_bloc/locale_state.dart';
import '/data/repositories/quotes_repository.dart';
import '/presentation/home_page/bloc/bloc.dart';
import '/presentation/home_page/bloc/events.dart';
@ -17,15 +22,46 @@ class MyHomePage extends StatelessWidget {
final String title;
get searchController => null;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => HomeBloc(QuotesRepository())..add(const HomeLoadDataEvent()),
create: (context) =>
HomeBloc(QuotesRepository())..add(const HomeLoadDataEvent()),
child: Scaffold(
appBar: AppBar(
title: Text(title),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title),
GestureDetector(
onTap: () =>
context.read<LocaleBloc>().add(const ChangeLocaleEvent()),
child: SizedBox.square(
dimension: 50,
child: Padding(
padding: const EdgeInsets.only(right: 12),
child: BlocBuilder<LocaleBloc, LocaleState>(
builder: (context, state) {
return state.currentLocale.languageCode == 'ru'
? const SvgRu()
: const SvgUk();
},
),
),
),
),
],
),
),
body: Column(
children: [
const Expanded(
child: _HomePageBody(),
),
],
),
body: const _HomePageBody(),
),
);
}
@ -50,22 +86,22 @@ class _HomePageBodyState extends State<_HomePageBody> {
searchController.addListener(() {
Debounce.run(() {
context.read<HomeBloc>().add(HomeLoadDataEvent(
search: searchController.text,
author: authorController.text,
));
search: searchController.text,
author: authorController.text,
));
});
});
authorController.addListener(() {
Debounce.run(() {
context.read<HomeBloc>().add(HomeLoadDataEvent(
search: searchController.text,
author: authorController.text,
));
search: searchController.text,
author: authorController.text,
));
});
});
context.read<HomeBloc>().add(const HomeLoadDataEvent());
context.read<LikeBloc>().add(const LoadLikesEvent());
}
void _showSnackbar(BuildContext context, bool isLiked) {
@ -88,9 +124,9 @@ class _HomePageBodyState extends State<_HomePageBody> {
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: searchController,
decoration: const InputDecoration(
labelText: 'Поиск по цитате',
prefixIcon: Icon(Icons.search),
decoration: InputDecoration(
labelText: context.locale.search,
prefixIcon: const Icon(Icons.search),
),
),
),
@ -98,60 +134,76 @@ class _HomePageBodyState extends State<_HomePageBody> {
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: authorController,
decoration: const InputDecoration(
labelText: 'Поиск по автору',
prefixIcon: Icon(Icons.person),
decoration: InputDecoration(
labelText: context.locale.search,
prefixIcon: const Icon(Icons.person),
),
),
),
Expanded(
child: state.data == null
? const Center(child: CircularProgressIndicator())
: FutureBuilder<List<Quote>?>(
future: state.data,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Ошибка: ${snapshot.error}'));
}
if (snapshot.data == null || snapshot.data!.isEmpty) {
return const Center(
child: Text(
'Нет цитат для отображения.',
style: TextStyle(fontSize: 18, color: Colors.grey),
),
);
}
final quotes = snapshot.data!;
return RefreshIndicator(
onRefresh: () async {
context.read<HomeBloc>().add(HomeRefreshEvent(
search: searchController.text,
author: authorController.text,
));
},
child: ListView.builder(
itemCount: quotes.length,
itemBuilder: (context, index) {
final quote = quotes[index];
return QuoteCard(
quote: quote,
onFavoriteToggle: () {
context.read<LikeBloc>().add(ChangeLikeEvent(quote.id));
BlocBuilder<LikeBloc, LikeState>(
builder: (context, likeState) {
return Expanded(
child: state.data == null
? const Center(child: CircularProgressIndicator())
: FutureBuilder<List<Quote>?>(
future: state.data,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Text('Ошибка: ${snapshot.error}'));
}
if (snapshot.data == null ||
snapshot.data!.isEmpty) {
return const Center(
child: Text(
'Нет цитат для отображения.',
style: TextStyle(
fontSize: 18, color: Colors.grey),
),
);
}
final quotes = snapshot.data!;
final likedIds = likeState.likedIds ?? [];
return RefreshIndicator(
onRefresh: () async {
context.read<HomeBloc>().add(HomeRefreshEvent(
search: searchController.text,
author: authorController.text,
));
},
child: ListView.builder(
itemCount: quotes.length,
itemBuilder: (context, index) {
final quote = quotes[index];
final isLiked = likedIds.contains(quote.id);
return QuoteCard(
quote: quote,
isLiked: isLiked,
onFavoriteToggle: () {
context
.read<LikeBloc>()
.add(ChangeLikeEvent(quote.id));
},
);
},
),
);
},
);
},
),
);
},
),
),
),
);
},
)
],
);
},
);
}
}

View File

@ -1,5 +1,5 @@
import 'package:flutter_bloc/flutter_bloc.dart';
//import 'package:shared_preferences/shared_preferences.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'like_event.dart';
import 'like_state.dart';
@ -13,10 +13,10 @@ class LikeBloc extends Bloc<LikeEvent, LikeState> {
}
Future<void> _onLoadLikes(LoadLikesEvent event, Emitter<LikeState> emit) async {
//final prefs = await SharedPreferences.getInstance();
//final data = prefs.getStringList(_likedPrefsKey);
final prefs = await SharedPreferences.getInstance();
final data = prefs.getStringList(_likedPrefsKey);
//emit(state.copyWith(likedIds: data));
emit(state.copyWith(likedIds: data));
}
Future<void> _onChangeLike(ChangeLikeEvent event, Emitter<LikeState> emit) async {
@ -28,8 +28,8 @@ class LikeBloc extends Bloc<LikeEvent, LikeState> {
updatedList.add(event.id);
}
//final prefs = await SharedPreferences.getInstance();
//prefs.setStringList(_likedPrefsKey, updatedList);
final prefs = await SharedPreferences.getInstance();
prefs.setStringList(_likedPrefsKey, updatedList);
emit(state.copyWith(likedIds: updatedList));
}

View File

@ -8,6 +8,5 @@ class LoadLikesEvent extends LikeEvent {
class ChangeLikeEvent extends LikeEvent {
final String id;
const ChangeLikeEvent(this.id);
}

View File

@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../components/locale/l10n/app_locale.dart';
import 'locale_events.dart';
import 'locale_state.dart';
class LocaleBloc extends Bloc<LocaleEvent, LocaleState> {
LocaleBloc(Locale defaultLocale) : super(LocaleState(currentLocale: defaultLocale)) {
on<ChangeLocaleEvent>(_onChangeLocale);
}
Future<void> _onChangeLocale(ChangeLocaleEvent event, Emitter<LocaleState> emit) async {
final toChange = AppLocale.supportedLocales
.firstWhere((e) => e.languageCode != state.currentLocale.languageCode);
emit(state.copyWith(currentLocale: toChange));
}
}

View File

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

View File

@ -0,0 +1,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<Object?> get props => [currentLocale];
}

View File

@ -325,6 +325,11 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
frontend_server_client:
dependency: transitive
description:
@ -533,6 +538,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
petitparser:
dependency: transitive
description:
@ -541,6 +570,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.2"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pool:
dependency: transitive
description:
@ -589,6 +634,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "3c7e73920c694a436afaf65ab60ce3453d91f84208d761fbd83fc21182134d93"
url: "https://pub.dev"
source: hosted
version: "2.3.4"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "02a7d8a9ef346c9af715811b01fbd8e27845ad2c41148eefd31321471b41863d"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shelf:
dependency: transitive
description:
@ -770,6 +871,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
@ -788,4 +897,4 @@ packages:
version: "3.1.2"
sdks:
dart: ">=3.5.4 <4.0.0"
flutter: ">=3.22.0"
flutter: ">=3.24.0"

View File

@ -7,7 +7,7 @@ environment:
sdk: ^3.5.4
dependencies:
# shared_preferences: ^2.3.3
shared_preferences: ^2.3.3
flutter:
sdk: flutter