что-не очень

This commit is contained in:
Алексей Тихоненков 2024-12-18 13:10:57 +04:00
parent 1c46c1c0fb
commit 78350b99e4
29 changed files with 295 additions and 103 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 42 KiB

BIN
assets/launcher.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

6
l10n.yaml Normal file
View File

@ -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

9
l10n/app_en.arb Normal file
View File

@ -0,0 +1,9 @@
{
"@@locale": "en",
"search": "Search",
"liked": "Like :)",
"disliked": "Dislike :(",
"arbEnding": ""
}

9
l10n/app_ru.arb Normal file
View File

@ -0,0 +1,9 @@
{
"@@locale": "ru",
"search": "Поиск",
"liked": "Круто :)",
"disliked": "Не круто :с",
"arbEnding": ""
}

View File

@ -0,0 +1,6 @@
import 'package:flutter/widgets.dart';
import '../locale/l10n/app_locale.dart';
extension LocalContextX on BuildContext {
AppLocale get locale => AppLocale.of(this)!;
}

View File

@ -11,10 +11,10 @@ class Debounce {
static Timer? _timer;
static void run(
VoidCallback action, {
Duration delay = const Duration(milliseconds: 1000),
}) {
VoidCallback action, {
Duration delay = const Duration(milliseconds: 1000),
}) {
_timer?.cancel();
_timer = Timer(delay, action);
}
}
}

View File

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

View File

@ -2,13 +2,13 @@ import '../../domain/quote.dart';
import '/data/dtos/quotes_dto.dart';
import '/presentation/home_page/home_page.dart';
const _imagePlaceholder =
'https://cdn-icons-png.flaticon.com/128/17818/17818874.png';
const _imagePlaceholder = 'https://cdn-icons-png.flaticon.com/128/17818/17818874.png';
extension QuoteDtoToModel on QuoteDataDto {
Quote toDomain() => Quote(
body ?? 'Без текста',
author ?? 'Неизвестный автор',
imageUrl ?? _imagePlaceholder,
);
}
body ?? 'Без текста',
author ?? 'Неизвестный автор',
imageUrl ?? _imagePlaceholder,
id ?? "",
);
}

View File

@ -24,12 +24,10 @@ class QuotesRepository extends ApiInterface {
try {
final Map<String, dynamic> queryParams = {};
if (q != null && q.isNotEmpty) {
queryParams['filter'] = q;
}
if (author != null && author.isNotEmpty) {
queryParams['type'] = 'author';
queryParams['filter'] = author;
@ -51,4 +49,3 @@ class QuotesRepository extends ApiInterface {
}
}
}

View File

@ -2,11 +2,13 @@ class Quote {
final String text;
final String author;
final String imagePath;
final String id;
bool isFavorite;
Quote(this.text, this.author, this.imagePath, [this.isFavorite = false]);
Quote(this.text, this.author, this.imagePath, this.id,[this.isFavorite = false]);
void toggleFavorite() {
bool toggleFavorite() {
isFavorite = !isFavorite;
return isFavorite;
}
}

View File

@ -1,9 +1,11 @@
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 'components/locale/l10n/app_locale.dart';
import 'data/repositories/quotes_repository.dart';
import '/presentation/home_page/bloc/bloc.dart';
import '/presentation/home_page/home_page.dart';
void main() {
runApp(const MyApp());
}
@ -15,6 +17,8 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Цитаты',
localizationsDelegates: AppLocale.localizationsDelegates,
supportedLocales: AppLocale.supportedLocales,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
@ -22,12 +26,16 @@ class MyApp extends StatelessWidget {
home: RepositoryProvider<QuotesRepository>(
lazy: true,
create: (_) => QuotesRepository(),
child: BlocProvider<HomeBloc>(
lazy: false,
create: (context) => HomeBloc(context.read<QuotesRepository>()),
child: const MyHomePage(title: "Цитаты",),
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: "Цитаты"),
),
),
),
);
}
}

View File

@ -0,0 +1,34 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '/components/resources.g.dart';
abstract class SvgObjects {
static void init() {
final pics = <String>[
R.ASSETS_SVG_RU_SVG,
R.ASSETS_SVG_US_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_US_SVG);
}
}

View File

@ -30,4 +30,4 @@ class ErrorDialog extends StatelessWidget {
),
);
}
}
}

View File

@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
import 'error_dialog.dart';
void showErrorDialog(
BuildContext context, {
required String error,
}) {
BuildContext context, {
required String error,
}) {
showDialog(
context: context,
builder: (_) => ErrorDialog(error),
);
}
}

View File

@ -24,12 +24,14 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
} else {
// Если автор указан, фильтруем данные по тексту цитаты
try {
final currentData = await repo.loadData(q:search, author: author,
final currentData = await repo.loadData(
q: search,
author: author,
);
final filteredData = currentData?.where((quote) {
final matchesAuthor = quote.author.toLowerCase().contains(author.toLowerCase());
final matchesSearch = search.isEmpty ||
quote.text.toLowerCase().contains(search.toLowerCase());
final matchesSearch =
search.isEmpty || quote.text.toLowerCase().contains(search.toLowerCase());
return matchesAuthor && matchesSearch;
}).toList();
emit(state.copyWith(data: Future.value(filteredData)));
@ -39,13 +41,8 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
}
}
Future<void> _onRefreshData(HomeRefreshEvent event, Emitter<HomeState> emit) async {
add(HomeLoadDataEvent(search: event.search, author: event.author)); // Просто перезапускаем загрузку
add(HomeLoadDataEvent(
search: event.search, author: event.author)); // Просто перезапускаем загрузку
}
}

View File

@ -2,7 +2,6 @@ import 'package:equatable/equatable.dart';
import '../../../domain/quote.dart';
import '../home_page.dart';
class HomeState extends Equatable {
final Future<List<Quote>>? data; // Изменено

View File

@ -1,55 +1,60 @@
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;
const QuoteCard({
super.key,
required this.quote,
required this.onFavoriteToggle,
});
const QuoteCard({Key? key, required this.quote, required this.onFavoriteToggle}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
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(
quote.isFavorite ? Icons.favorite : Icons.favorite_border,
color: quote.isFavorite ? Colors.red : null,
),
onPressed: onFavoriteToggle,
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => QuoteDetailScreen(quote: quote),
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 ? 'Убрать из избранного' : 'Добавить в избранное');
},
),
),
],
),
],
),
),
);
}
@ -75,8 +80,7 @@ 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,7 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pmu_flutter_labs/components/extensions/context_x.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 '/data/repositories/quotes_repository.dart';
import '/presentation/home_page/bloc/bloc.dart';
import '/presentation/home_page/bloc/events.dart';
@ -40,11 +44,15 @@ class _HomePageBodyState extends State<_HomePageBody> {
@override
void initState() {
SvgObjects.init();
super.initState();
searchController.addListener(() {
Debounce.run(() {
context.read<HomeBloc>().add(HomeLoadDataEvent(search: searchController.text));
context.read<HomeBloc>().add(HomeLoadDataEvent(
search: searchController.text,
author: authorController.text,
));
});
});
@ -56,6 +64,18 @@ class _HomePageBodyState extends State<_HomePageBody> {
));
});
});
context.read<LikeBloc>().add(const LoadLikesEvent());
}
void _showSnackbar(BuildContext context, bool isLiked) {
final message = isLiked ? context.locale.liked : context.locale.disliked;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
duration: const Duration(seconds: 2),
),
);
}
@override
@ -119,9 +139,7 @@ class _HomePageBodyState extends State<_HomePageBody> {
return QuoteCard(
quote: quote,
onFavoriteToggle: () {
setState(() {
quote.toggleFavorite();
});
context.read<LikeBloc>().add(ChangeLikeEvent(quote.id));
},
);
},
@ -136,3 +154,4 @@ class _HomePageBodyState extends State<_HomePageBody> {
);
}
}

View File

@ -0,0 +1,36 @@
import 'package:flutter_bloc/flutter_bloc.dart';
//import 'package:shared_preferences/shared_preferences.dart';
import 'like_event.dart';
import 'like_state.dart';
const String _likedPrefsKey = 'liked';
class LikeBloc extends Bloc<LikeEvent, LikeState> {
LikeBloc() : super(const LikeState(likedIds: [])) {
on<ChangeLikeEvent>(_onChangeLike);
on<LoadLikesEvent>(_onLoadLikes);
}
Future<void> _onLoadLikes(LoadLikesEvent event, Emitter<LikeState> emit) async {
//final prefs = await SharedPreferences.getInstance();
//final data = prefs.getStringList(_likedPrefsKey);
//emit(state.copyWith(likedIds: data));
}
Future<void> _onChangeLike(ChangeLikeEvent event, Emitter<LikeState> emit) async {
final updatedList = List<String>.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));
}
}

View File

@ -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);
}

View File

@ -0,0 +1,14 @@
import 'package:copy_with_extension/copy_with_extension.dart';
import 'package:equatable/equatable.dart';
part 'like_state.g.dart';
@CopyWith()
class LikeState extends Equatable {
final List<String>? likedIds;
const LikeState({required this.likedIds});
@override
List<Object?> get props => [likedIds];
}

14
makefile Normal file
View File

@ -0,0 +1,14 @@
gen:
flutter pub run build_runner build --delete-conflicting-outputs
icon:
flutter pub run flutter_launcher_icons:main
init_res:
dart pub global activate flutter_asset_generator
format:
dart format . --line-length 100
res:
fgen --output lib/components/resources.g.dart --no-watch --no-preview;\
dart format . --line-length 100
loc:
flutter gen-l10n
dart format . --line-length 100

View File

@ -26,10 +26,10 @@ packages:
dependency: transitive
description:
name: archive
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
sha256: "08064924cbf0ab88280a0c3f60db9dd24fec693927e725ecb176f16c629d1cb8"
url: "https://pub.dev"
source: hosted
version: "3.6.1"
version: "4.0.1"
args:
dependency: transitive
description:
@ -254,6 +254,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
file:
dependency: transitive
description:
@ -353,10 +361,10 @@ packages:
dependency: transitive
description:
name: http_multi_server
sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.1"
version: "3.2.2"
http_parser:
dependency: transitive
description:
@ -369,10 +377,10 @@ packages:
dependency: transitive
description:
name: image
sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
sha256: "20842a5ad1555be624c314b0c0cc0566e8ece412f61e859a42efeb6d4101a26c"
url: "https://pub.dev"
source: hosted
version: "4.3.0"
version: "4.5.0"
intl:
dependency: "direct main"
description:
@ -505,10 +513,10 @@ packages:
dependency: transitive
description:
name: package_config
sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.1"
path:
dependency: transitive
description:
@ -541,6 +549,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.1"
posix:
dependency: transitive
description:
name: posix
sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a
url: "https://pub.dev"
source: hosted
version: "6.0.1"
pretty_dio_logger:
dependency: "direct main"
description:
@ -561,10 +577,10 @@ packages:
dependency: transitive
description:
name: pub_semver
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.1.5"
pubspec_parse:
dependency: transitive
description:
@ -606,10 +622,10 @@ packages:
dependency: transitive
description:
name: source_helper
sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd"
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c"
url: "https://pub.dev"
source: hosted
version: "1.3.4"
version: "1.3.5"
source_span:
dependency: transitive
description:
@ -638,10 +654,10 @@ packages:
dependency: transitive
description:
name: stream_transform
sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f"
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.1"
string_scanner:
dependency: transitive
description:
@ -670,10 +686,10 @@ packages:
dependency: transitive
description:
name: timing
sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32"
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
version: "1.0.2"
typed_data:
dependency: transitive
description:
@ -726,10 +742,10 @@ packages:
dependency: transitive
description:
name: watcher
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.1.1"
web:
dependency: transitive
description:

View File

@ -7,6 +7,7 @@ environment:
sdk: ^3.5.4
dependencies:
# shared_preferences: ^2.3.3
flutter:
sdk: flutter
@ -29,9 +30,10 @@ dependencies:
sdk: flutter
intl: ^0.19.0
#shared_preferences: ^2.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
@ -41,9 +43,15 @@ dev_dependencies:
build_runner: ^2.4.9
json_serializable: ^6.7.1
flutter_icons:
android: "ic_launcher"
image_path: "assets/launcher.png"
min_sdk_android: 21
flutter:
generate: true
uses-material-design: true
assets:
- assets/svg/