LabWork7 icon is done
@@ -2,7 +2,7 @@
|
||||
<application
|
||||
android:label="flutter_project"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@mipmap/ic_cosmetics">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_cosmetics.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 544 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_cosmetics.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 442 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_cosmetics.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 721 B |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_cosmetics.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_cosmetics.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
BIN
assets/cosmetics.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
1
assets/svg/ru.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 9 6" width="900" height="600"><path fill="#fff" d="M0 0h9v3H0z"/><path fill="#d52b1e" d="M0 3h9v3H0z"/><path fill="#0039a6" d="M0 2h9v2H0z"/></svg>
|
||||
|
After Width: | Height: | Size: 200 B |
9
assets/svg/uk.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 30" width="1000" height="600">
|
||||
<clipPath id="t">
|
||||
<path d="M25,15h25v15zv15h-25zh-25v-15zv-15h25z"/>
|
||||
</clipPath>
|
||||
<path d="M0,0v30h50v-30z" fill="#012169"/>
|
||||
<path d="M0,0 50,30M50,0 0,30" stroke="#fff" stroke-width="6"/>
|
||||
<path d="M0,0 50,30M50,0 0,30" clip-path="url(#t)" stroke="#C8102E" stroke-width="4"/>
|
||||
<path d="M-1 11h22v-12h8v12h22v8h-22v12h-8v-12h-22z" fill="#C8102E" stroke="#FFF" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 477 B |
@@ -6,11 +6,8 @@ class Debounce {
|
||||
Debounce._();
|
||||
static final Debounce _instance = Debounce._();
|
||||
static Timer? _timer;
|
||||
static void run(
|
||||
VoidCallback action, {
|
||||
Duration delay = const Duration(milliseconds: 500),
|
||||
}) {
|
||||
static void run(VoidCallback action, {Duration delay = const Duration(milliseconds: 500)}) {
|
||||
_timer?.cancel();
|
||||
_timer = Timer(delay, action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,4 +23,4 @@ class CosmecticDataDto {
|
||||
const CosmecticDataDto({this.id, this.productType, this.name, this.imageLink});
|
||||
|
||||
factory CosmecticDataDto.fromJson(Map<String, dynamic> json) => _$CosmecticDataDtoFromJson(json);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,9 @@ CosmeticsDto _$CosmeticsDtoFromJson(Map<String, dynamic> json) => CosmeticsDto(
|
||||
.toList(),
|
||||
);
|
||||
|
||||
CosmecticDataDto _$CosmecticDataDtoFromJson(Map<String, dynamic> json) =>
|
||||
CosmecticDataDto(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
productType: json['product_type'] as String?,
|
||||
name: json['name'] as String?,
|
||||
imageLink: json['image_link'] as String?,
|
||||
);
|
||||
CosmecticDataDto _$CosmecticDataDtoFromJson(Map<String, dynamic> json) => CosmecticDataDto(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
productType: json['product_type'] as String?,
|
||||
name: json['name'] as String?,
|
||||
imageLink: json['image_link'] as String?,
|
||||
);
|
||||
|
||||
@@ -15,9 +15,7 @@ class MyApp extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
),
|
||||
theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple)),
|
||||
home: RepositoryProvider<CosmeticsRepository>(
|
||||
lazy: true,
|
||||
create: (_) => CosmeticsRepository(),
|
||||
|
||||
@@ -19,10 +19,7 @@ class DetailsPage extends StatelessWidget {
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: Text(
|
||||
data.name,
|
||||
style: Theme.of(context).textTheme.headlineLarge,
|
||||
),
|
||||
child: Text(data.name, style: Theme.of(context).textTheme.headlineLarge),
|
||||
),
|
||||
Text(data.description, style: Theme.of(context).textTheme.bodyLarge),
|
||||
],
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:flutter_project/presentation/home_page/bloc/events.dart';
|
||||
import 'package:flutter_project/presentation/home_page/bloc/state.dart';
|
||||
import 'package:flutter_project/repositories/cosmetic_repository.dart';
|
||||
|
||||
|
||||
class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
final CosmeticsRepository repo;
|
||||
|
||||
@@ -11,13 +10,13 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
on<HomeLoadDataEvent>(_onLoadData);
|
||||
}
|
||||
|
||||
void _onLoadData(HomeLoadDataEvent event, Emitter<HomeState> emit) async {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
void _onLoadData(HomeLoadDataEvent event, Emitter<HomeState> emit) async {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
|
||||
String? error;
|
||||
String? error;
|
||||
|
||||
final data = await repo.loadData(q: event.search, onError: (e) => error = e?.toString(),);
|
||||
final data = await repo.loadData(q: event.search, onError: (e) => error = e?.toString());
|
||||
|
||||
emit(state.copyWith(isLoading: false, data: data, error: error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ abstract class HomeEvent {
|
||||
}
|
||||
|
||||
class HomeLoadDataEvent extends HomeEvent {
|
||||
|
||||
final String? search;
|
||||
const HomeLoadDataEvent({this.search}); // событие для загрузки данных
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,17 +5,14 @@ import '../../../domain/models/card.dart';
|
||||
part 'state.g.dart';
|
||||
|
||||
@CopyWith()
|
||||
class HomeState extends Equatable { // для сравнения состояний
|
||||
class HomeState extends Equatable {
|
||||
// для сравнения состояний
|
||||
final List<CardData>? data;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
|
||||
const HomeState({
|
||||
this.data,
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
});
|
||||
const HomeState({this.data, this.isLoading = false, this.error});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [data, isLoading, error];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,7 @@ class _Card extends StatefulWidget {
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
factory _Card.fromData(
|
||||
CardData data, {
|
||||
OnLikeCallBack onLike,
|
||||
VoidCallback? onTap,
|
||||
}) => _Card(
|
||||
factory _Card.fromData(CardData data, {OnLikeCallBack onLike, VoidCallback? onTap}) => _Card(
|
||||
data.name,
|
||||
description: data.description,
|
||||
imageUrl: data.imageUrl,
|
||||
@@ -80,15 +76,14 @@ class _CardState extends State<_Card> {
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.deepPurple,
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
borderRadius: BorderRadius.only(topRight: Radius.circular(20)),
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(8, 2, 8, 2),
|
||||
child: Text(
|
||||
'Cкидка 5%',
|
||||
style: Theme.of(context).textTheme.bodyMedium
|
||||
?.copyWith(color: Colors.white),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -107,10 +102,7 @@ class _CardState extends State<_Card> {
|
||||
style: Theme.of(context).textTheme.headlineLarge,
|
||||
maxLines: 3,
|
||||
),
|
||||
Text(
|
||||
widget.description,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
Text(widget.description, style: Theme.of(context).textTheme.bodyLarge),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -125,11 +117,7 @@ class _CardState extends State<_Card> {
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 16,
|
||||
bottom: 16,
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 8, right: 16, bottom: 16),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
@@ -145,10 +133,7 @@ class _CardState extends State<_Card> {
|
||||
color: Colors.redAccent,
|
||||
key: ValueKey<int>(0),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.favorite_border,
|
||||
key: ValueKey<int>(1),
|
||||
),
|
||||
: const Icon(Icons.favorite_border, key: ValueKey<int>(1)),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:flutter_project/domain/models/card.dart';
|
||||
import 'package:flutter_project/presentation/details_page/details_page.dart';
|
||||
import 'package:flutter_project/presentation/home_page/bloc/state.dart';
|
||||
|
||||
|
||||
import '../../components/utils/debounce.dart';
|
||||
import 'bloc/bloc.dart';
|
||||
import 'bloc/events.dart';
|
||||
@@ -28,10 +27,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Приветствуем!',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
content: Text('Приветствуем!', style: Theme.of(context).textTheme.bodyLarge),
|
||||
backgroundColor: Colors.purple,
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
@@ -86,38 +82,38 @@ class _BodyState extends State<Body> {
|
||||
child: CupertinoSearchTextField(
|
||||
controller: searchController,
|
||||
onChanged: (search) {
|
||||
Debounce.run(() => context.read<HomeBloc>().add(HomeLoadDataEvent(search: search)));
|
||||
Debounce.run(() => context.read<HomeBloc>().add(HomeLoadDataEvent(search: search)));
|
||||
},
|
||||
),
|
||||
),
|
||||
BlocBuilder<HomeBloc, HomeState>(
|
||||
builder: (context, state) => state.error != null
|
||||
? Text(
|
||||
state.error ?? '',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: Colors.red),
|
||||
)
|
||||
:state.isLoading
|
||||
? const CircularProgressIndicator()
|
||||
? Text(
|
||||
state.error ?? '',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: Colors.red),
|
||||
)
|
||||
: state.isLoading
|
||||
? const CircularProgressIndicator()
|
||||
: Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _onRefresh,
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: state.data?.length ?? 0,
|
||||
itemBuilder: (context, index) {
|
||||
final data = state.data?[index];
|
||||
return data != null
|
||||
? _Card.fromData(
|
||||
data,
|
||||
onLike: (title, isLiked) =>
|
||||
_showSnackBar(context, title, isLiked),
|
||||
onTap: () => _navToDetails(context, data),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
)
|
||||
),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _onRefresh,
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: state.data?.length ?? 0,
|
||||
itemBuilder: (context, index) {
|
||||
final data = state.data?[index];
|
||||
return data != null
|
||||
? _Card.fromData(
|
||||
data,
|
||||
onLike: (title, isLiked) =>
|
||||
_showSnackBar(context, title, isLiked),
|
||||
onTap: () => _navToDetails(context, data),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -125,10 +121,7 @@ class _BodyState extends State<Body> {
|
||||
}
|
||||
|
||||
void _navToDetails(BuildContext context, CardData data) {
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(builder: (context) => DetailsPage(data)),
|
||||
);
|
||||
Navigator.push(context, CupertinoPageRoute(builder: (context) => DetailsPage(data)));
|
||||
}
|
||||
|
||||
void _showSnackBar(BuildContext context, String title, bool isLiked) {
|
||||
@@ -145,12 +138,9 @@ class _BodyState extends State<Body> {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onRefresh() {
|
||||
context.read<HomeBloc>().add(
|
||||
HomeLoadDataEvent(
|
||||
search: searchController.text,
|
||||
),
|
||||
);
|
||||
context.read<HomeBloc>().add(HomeLoadDataEvent(search: searchController.text));
|
||||
return Future.value(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ typedef OnErrorCallback = void Function(Object? error);
|
||||
|
||||
abstract class ApiInterface {
|
||||
Future<List<CardData>?> loadData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,7 @@ import 'api_interface.dart';
|
||||
|
||||
class CosmeticsRepository implements ApiInterface {
|
||||
static final Dio _dio = Dio()
|
||||
..interceptors.add(PrettyDioLogger(
|
||||
requestHeader: true,
|
||||
requestBody: true,
|
||||
));
|
||||
..interceptors.add(PrettyDioLogger(requestHeader: true, requestBody: true));
|
||||
|
||||
static const String _baseUrl = 'https://makeup-api.herokuapp.com';
|
||||
|
||||
@@ -21,8 +18,8 @@ class CosmeticsRepository implements ApiInterface {
|
||||
final Response<dynamic> response = await _dio.get<List<dynamic>>(
|
||||
url,
|
||||
queryParameters: {
|
||||
'brand': 'maybelline', // фиксированный бренд
|
||||
if (q != null && q.isNotEmpty) // тип продукта из поиска
|
||||
'brand': 'maybelline', // фиксированный бренд
|
||||
if (q != null && q.isNotEmpty) // тип продукта из поиска
|
||||
'product_type': q.toLowerCase(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ class MockRepository extends ApiInterface {
|
||||
'Darling',
|
||||
description: 'Тушь для ресниц',
|
||||
imageUrl:
|
||||
'https://avatars.mds.yandex.net/get-vertis-journal/4469561/7_Byuti_nabory.png_1757063983772/845x845',
|
||||
'https://avatars.mds.yandex.net/get-vertis-journal/4469561/7_Byuti_nabory.png_1757063983772/845x845',
|
||||
icon: Icons.shopping_cart,
|
||||
price: 1922,
|
||||
),
|
||||
@@ -30,4 +30,4 @@ class MockRepository extends ApiInterface {
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
makefile
Normal 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; \
|
||||
make format
|
||||
loc:
|
||||
flutter gen-l10n; \
|
||||
make format
|
||||
64
pubspec.lock
@@ -17,6 +17,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.4.1"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.7"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -113,6 +121,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_util
|
||||
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.2"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -217,6 +233,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -246,6 +270,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.1.1"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_launcher_icons
|
||||
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.1"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -291,6 +323,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.4"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -411,6 +451,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -419,6 +467,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.3"
|
||||
pretty_dio_logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -600,6 +656,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -24,12 +24,21 @@ dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
|
||||
build_runner: ^2.4.9
|
||||
json_serializable: ^6.7.1
|
||||
copy_with_extension_gen: ^10.0.1
|
||||
|
||||
flutter_lints: ^5.0.0
|
||||
|
||||
flutter_icons:
|
||||
android: "ic_cosmetics"
|
||||
image_path: "assets/cosmetics.png"
|
||||
min_sdk_android: 21
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
||||
assets:
|
||||
- assets/cosmetics.png
|
||||