Чисто для вовы пишу чтоб был комит
This commit is contained in:
parent
168a9e287c
commit
e674af146a
Binary file not shown.
Before Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
@ -2,8 +2,15 @@
|
||||
"@@locale": "en",
|
||||
|
||||
"search": "Search",
|
||||
"liked": "liked!",
|
||||
"disliked": "disliked :(",
|
||||
"complete": "Complete!",
|
||||
"uncomplete": "Not complete :(",
|
||||
"delete": "Delete",
|
||||
"addtask": "Add Task",
|
||||
"cancel": "Cancel",
|
||||
"addbutton": "Add",
|
||||
"nameplaceholder": "Enter task name",
|
||||
"descriptionplaceholder": "Enter task description",
|
||||
"imageplaceholder": "Select image",
|
||||
|
||||
"arbEnding": "Чтобы не забыть про отсутствие запятой :)"
|
||||
}
|
@ -2,8 +2,15 @@
|
||||
"@@locale": "ru",
|
||||
|
||||
"search": "Поиск",
|
||||
"liked": "понравился!",
|
||||
"disliked": "Уже не нравится :(",
|
||||
"complete": "Выполнено!",
|
||||
"uncomplete": "Ещё не выполнена :(",
|
||||
"delete": "Удалено",
|
||||
"addtask": "Добавить задачу",
|
||||
"cancel": "Отмена",
|
||||
"addbutton": "Добавить",
|
||||
"nameplaceholder": "Введите название задачи",
|
||||
"descriptionplaceholder": "Введите описание задачи",
|
||||
"imageplaceholder": "выберите изображение",
|
||||
|
||||
"arbEnding": "Чтобы не забыть про отсутствие запятой :)"
|
||||
}
|
@ -101,17 +101,59 @@ abstract class AppLocale {
|
||||
/// **'Поиск'**
|
||||
String get search;
|
||||
|
||||
/// No description provided for @liked.
|
||||
/// No description provided for @complete.
|
||||
///
|
||||
/// In ru, this message translates to:
|
||||
/// **'понравился!'**
|
||||
String get liked;
|
||||
/// **'Выполнено!'**
|
||||
String get complete;
|
||||
|
||||
/// No description provided for @disliked.
|
||||
/// No description provided for @uncomplete.
|
||||
///
|
||||
/// In ru, this message translates to:
|
||||
/// **'Уже не нравится :('**
|
||||
String get disliked;
|
||||
/// **'Ещё не выполнена :('**
|
||||
String get uncomplete;
|
||||
|
||||
/// No description provided for @delete.
|
||||
///
|
||||
/// In ru, this message translates to:
|
||||
/// **'Удалено'**
|
||||
String get delete;
|
||||
|
||||
/// No description provided for @addtask.
|
||||
///
|
||||
/// In ru, this message translates to:
|
||||
/// **'Добавить задачу'**
|
||||
String get addtask;
|
||||
|
||||
/// No description provided for @cancel.
|
||||
///
|
||||
/// In ru, this message translates to:
|
||||
/// **'Отмена'**
|
||||
String get cancel;
|
||||
|
||||
/// No description provided for @addbutton.
|
||||
///
|
||||
/// In ru, this message translates to:
|
||||
/// **'Добавить'**
|
||||
String get addbutton;
|
||||
|
||||
/// No description provided for @nameplaceholder.
|
||||
///
|
||||
/// In ru, this message translates to:
|
||||
/// **'Введите название задачи'**
|
||||
String get nameplaceholder;
|
||||
|
||||
/// No description provided for @descriptionplaceholder.
|
||||
///
|
||||
/// In ru, this message translates to:
|
||||
/// **'Введите описание задачи'**
|
||||
String get descriptionplaceholder;
|
||||
|
||||
/// No description provided for @imageplaceholder.
|
||||
///
|
||||
/// In ru, this message translates to:
|
||||
/// **'выберите изображение'**
|
||||
String get imageplaceholder;
|
||||
|
||||
/// No description provided for @arbEnding.
|
||||
///
|
||||
|
@ -10,10 +10,31 @@ class AppLocaleEn extends AppLocale {
|
||||
String get search => 'Search';
|
||||
|
||||
@override
|
||||
String get liked => 'liked!';
|
||||
String get complete => 'Complete!';
|
||||
|
||||
@override
|
||||
String get disliked => 'disliked :(';
|
||||
String get uncomplete => 'Not complete :(';
|
||||
|
||||
@override
|
||||
String get delete => 'Delete';
|
||||
|
||||
@override
|
||||
String get addtask => 'Add Task';
|
||||
|
||||
@override
|
||||
String get cancel => 'Cancel';
|
||||
|
||||
@override
|
||||
String get addbutton => 'Add';
|
||||
|
||||
@override
|
||||
String get nameplaceholder => 'Enter task name';
|
||||
|
||||
@override
|
||||
String get descriptionplaceholder => 'Enter task description';
|
||||
|
||||
@override
|
||||
String get imageplaceholder => 'Select image';
|
||||
|
||||
@override
|
||||
String get arbEnding => 'Чтобы не забыть про отсутствие запятой :)';
|
||||
|
@ -10,10 +10,31 @@ class AppLocaleRu extends AppLocale {
|
||||
String get search => 'Поиск';
|
||||
|
||||
@override
|
||||
String get liked => 'понравился!';
|
||||
String get complete => 'Выполнено!';
|
||||
|
||||
@override
|
||||
String get disliked => 'Уже не нравится :(';
|
||||
String get uncomplete => 'Ещё не выполнена :(';
|
||||
|
||||
@override
|
||||
String get delete => 'Удалено';
|
||||
|
||||
@override
|
||||
String get addtask => 'Добавить задачу';
|
||||
|
||||
@override
|
||||
String get cancel => 'Отмена';
|
||||
|
||||
@override
|
||||
String get addbutton => 'Добавить';
|
||||
|
||||
@override
|
||||
String get nameplaceholder => 'Введите название задачи';
|
||||
|
||||
@override
|
||||
String get descriptionplaceholder => 'Введите описание задачи';
|
||||
|
||||
@override
|
||||
String get imageplaceholder => 'выберите изображение';
|
||||
|
||||
@override
|
||||
String get arbEnding => 'Чтобы не забыть про отсутствие запятой :)';
|
||||
|
33
lib/data/dtos/Task.dart
Normal file
33
lib/data/dtos/Task.dart
Normal file
@ -0,0 +1,33 @@
|
||||
class Task {
|
||||
final int id;
|
||||
final String name;
|
||||
final String description;
|
||||
final String date;
|
||||
final String imageUrl;
|
||||
|
||||
Task({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.date,
|
||||
required this.imageUrl
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'description': description,
|
||||
'date': date,
|
||||
'imageUrl': imageUrl
|
||||
};
|
||||
}
|
||||
factory Task.fromJson(Map<String, dynamic> json) {
|
||||
return Task(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
description: json['description'],
|
||||
date: json['date'],
|
||||
imageUrl: json['imageUrl']
|
||||
);
|
||||
}
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'films_dto.g.dart';
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class FilmsDto {
|
||||
final List<FilmDataDto>? data;
|
||||
final MetaDto? meta;
|
||||
|
||||
const FilmsDto({
|
||||
this.data,
|
||||
this.meta,
|
||||
});
|
||||
|
||||
factory FilmsDto.fromJson(Map<String, dynamic> json) => _$FilmsDtoFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class FilmDataDto {
|
||||
final String? id;
|
||||
final String? type;
|
||||
final FilmAttributesDataDto? attributes;
|
||||
|
||||
const FilmDataDto({this.id, this.type, this.attributes});
|
||||
|
||||
factory FilmDataDto.fromJson(Map<String, dynamic> json) => _$FilmDataDtoFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class FilmAttributesDataDto {
|
||||
final String? title;
|
||||
final DateTime? release_date;
|
||||
final String? budget;
|
||||
final String? poster;
|
||||
|
||||
const FilmAttributesDataDto({this.title, this.release_date, this.budget, this.poster});
|
||||
|
||||
factory FilmAttributesDataDto.fromJson(Map<String, dynamic> json) =>
|
||||
_$FilmAttributesDataDtoFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class MetaDto {
|
||||
final PaginationDto? pagination;
|
||||
|
||||
const MetaDto({this.pagination});
|
||||
|
||||
factory MetaDto.fromJson(Map<String, dynamic> json) => _$MetaDtoFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class PaginationDto {
|
||||
final int? current;
|
||||
final int? next;
|
||||
final int? last;
|
||||
|
||||
const PaginationDto({this.current, this.next, this.last});
|
||||
|
||||
factory PaginationDto.fromJson(Map<String, dynamic> json) => _$PaginationDtoFromJson(json);
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
import 'package:testlab/data/dtos/films_dto.dart';
|
||||
import 'package:testlab/domain/models/card.dart';
|
||||
import 'package:testlab/domain/models/home.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
const _imagePlaceholder =
|
||||
'https://ralfvanveen.com/wp-content/uploads/2021/06/Placeholder-_-Glossary.svg';
|
||||
|
||||
extension FilmsDtoToModel on FilmsDto {
|
||||
HomeData toDomain() => HomeData(
|
||||
data: data?.map((e) => e.toDomain()).toList(),
|
||||
nextPage: meta?.pagination?.next,
|
||||
);
|
||||
}
|
||||
|
||||
extension FilmDataDtoToModel on FilmDataDto {
|
||||
CardData toDomain() => CardData(
|
||||
attributes?.title ?? 'UNKNOWN',
|
||||
imageUrl: attributes?.poster ?? _imagePlaceholder,
|
||||
descriptionText:
|
||||
_makeDescriptionText(formatDateToString(attributes?.release_date), attributes?.budget),
|
||||
id: id,
|
||||
);
|
||||
|
||||
String formatDateToString(DateTime? date) {
|
||||
return date != null ? DateFormat('yyyy-MM-dd').format(date) : 'UNKNOWN DATE';
|
||||
}
|
||||
|
||||
String _makeDescriptionText(String? release_date, String? budget) {
|
||||
return release_date != null && budget != null
|
||||
? '$release_date \n $budget'
|
||||
: release_date != null
|
||||
? 'release: $release_date'
|
||||
: budget != null
|
||||
? 'budget: $budget'
|
||||
: '';
|
||||
}
|
||||
}
|
@ -3,5 +3,5 @@ import 'package:testlab/domain/models/home.dart';
|
||||
typedef OnErrorCallback = void Function(String? error);
|
||||
|
||||
abstract class ApiInterface {
|
||||
Future<HomeData?> loadData({OnErrorCallback? onError});
|
||||
Future<HomeData?> loadData(String name);
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import 'package:testlab/domain/models/home.dart';
|
||||
|
||||
class MockRepository extends ApiInterface {
|
||||
@override
|
||||
Future<HomeData?> loadData({OnErrorCallback? onError}) async {
|
||||
Future<HomeData?> loadData(String name) async {
|
||||
return HomeData(data: [
|
||||
CardData(
|
||||
'Mobile dev',
|
||||
|
@ -1,44 +0,0 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:testlab/data/dtos/films_dto.dart';
|
||||
import 'package:testlab/data/mapper/films_mapper.dart';
|
||||
import 'package:testlab/data/repositories/api_interface.dart';
|
||||
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
||||
import 'package:testlab/domain/models/home.dart';
|
||||
|
||||
class PotterFilmsRepository extends ApiInterface {
|
||||
static final Dio _dio = Dio()
|
||||
..interceptors.add(PrettyDioLogger(
|
||||
requestHeader: true,
|
||||
requestBody: true,
|
||||
));
|
||||
|
||||
static const String _baseUrl = 'https://api.potterdb.com';
|
||||
|
||||
@override
|
||||
Future<HomeData?> loadData({
|
||||
OnErrorCallback? onError,
|
||||
String? q,
|
||||
int page = 1,
|
||||
int pageSize = 5,
|
||||
}) async {
|
||||
try {
|
||||
const String url = '$_baseUrl/v1/movies';
|
||||
|
||||
final Response<dynamic> response = await _dio.get<Map<dynamic, dynamic>>(
|
||||
url,
|
||||
queryParameters: {
|
||||
'filter[title_cont]': q,
|
||||
'page[number]': page,
|
||||
'page[size]': pageSize,
|
||||
},
|
||||
);
|
||||
|
||||
final FilmsDto dto = FilmsDto.fromJson(response.data as Map<String, dynamic>);
|
||||
final HomeData data = dto.toDomain();
|
||||
return data;
|
||||
} on DioException catch (e) {
|
||||
onError?.call(e.error?.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
82
lib/data/repositories/repository.dart
Normal file
82
lib/data/repositories/repository.dart
Normal file
@ -0,0 +1,82 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../domain/models/card.dart';
|
||||
import '../../domain/models/home.dart';
|
||||
import '../dtos/Task.dart';
|
||||
import 'api_interface.dart';
|
||||
|
||||
class repository extends ApiInterface {
|
||||
Future<HomeData?> loadData(String name) async {
|
||||
final response = await http.get(Uri.parse('http://192.168.1.67:8080/api/1.0/task/name/$name'));
|
||||
//final response = await http.get(Uri.parse('http://192.168.112.64:8080/api/1.0/task/name/$name'));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
List jsonResponse = json.decode(utf8.decode(response.bodyBytes));
|
||||
|
||||
List<CardData> cardDataList = jsonResponse.map<CardData>((taskJson) {
|
||||
Task task = Task.fromJson(taskJson);
|
||||
|
||||
return CardData(
|
||||
task.name,
|
||||
descriptionText: '${task.description}\nДата: ${task.date}',
|
||||
imageUrl: task.imageUrl,
|
||||
icon: Icons.close,
|
||||
id: task.id.toString(),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return HomeData(data: cardDataList);
|
||||
} else {
|
||||
throw Exception('Не удалось загрузить задачи');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addTask(String taskName, String taskDescription, String? taskImageUrl) async {
|
||||
|
||||
DateTime now = DateTime.now();
|
||||
|
||||
String formattedDate = DateFormat('dd.MM.yyyy').format(now);
|
||||
|
||||
String defaultImageUrl = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTOwRConBYl2t6L8QMOAQqa5FDmPB_bg7EnGA&s";
|
||||
|
||||
String finalImageUrl = taskImageUrl ?? defaultImageUrl;
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse('http://192.168.1.67:8080/api/1.0/task'),
|
||||
headers: <String, String>{
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
},
|
||||
body: jsonEncode(Task(
|
||||
id: 0,
|
||||
name: taskName,
|
||||
description: taskDescription,
|
||||
date: formattedDate,
|
||||
imageUrl: finalImageUrl,
|
||||
).toJson()),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
} else {
|
||||
throw Exception('Ошибка при добавлении задачи');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteTask(int taskId) async {
|
||||
final response = await http.delete(
|
||||
Uri.parse('http://192.168.1.67:8080/api/1.0/task/$taskId'),
|
||||
headers: <String, String>{
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// Задача успешно удалена
|
||||
} else {
|
||||
throw Exception('Ошибка при удалении задачи');
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@ import 'card.dart';
|
||||
|
||||
class HomeData {
|
||||
final List<CardData>? data;
|
||||
final int? nextPage;
|
||||
|
||||
HomeData({this.data, this.nextPage});
|
||||
HomeData({this.data});
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:testlab/presentaition/home_page/home_page.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:testlab/presentaition/home_page/bloc/bloc.dart';
|
||||
import 'data/repositories/potter_films_repository.dart';
|
||||
import 'data/repositories/repository.dart';
|
||||
import 'package:testlab/presentaition/like_bloc/like_bloc.dart';
|
||||
import 'package:testlab/presentaition/locale_bloc/locale_bloc.dart';
|
||||
import 'package:testlab/presentaition/locale_bloc/locale_state.dart';
|
||||
@ -34,15 +34,15 @@ class MyApp extends StatelessWidget {
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: RepositoryProvider<PotterFilmsRepository>(
|
||||
home: RepositoryProvider<repository>(
|
||||
lazy: true,
|
||||
create: (_) => PotterFilmsRepository(),
|
||||
create: (_) => repository(),
|
||||
child: BlocProvider<LikeBloc>(
|
||||
lazy: false,
|
||||
create: (context) => LikeBloc(),
|
||||
child: BlocProvider<HomeBloc>(
|
||||
lazy: false,
|
||||
create: (context) => HomeBloc(context.read<PotterFilmsRepository>()),
|
||||
create: (context) => HomeBloc(context.read<repository>()),
|
||||
child: const MyHomePage(title: 'Фирсов Кирилл Алексеевич'),
|
||||
),
|
||||
),
|
||||
|
@ -1,39 +1,39 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:testlab/data/repositories/potter_films_repository.dart';
|
||||
import 'package:testlab/presentaition/home_page/bloc/events.dart';
|
||||
import 'package:testlab/presentaition/home_page/bloc/state.dart';
|
||||
|
||||
import '../../../data/repositories/repository.dart';
|
||||
|
||||
class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
final PotterFilmsRepository repo;
|
||||
final repository repo;
|
||||
|
||||
HomeBloc(this.repo) : super(const HomeState()) {
|
||||
on<HomeLoadDataEvent>(_onLoadData);
|
||||
on<HomeAddTaskEvent>(_onAddTask);
|
||||
}
|
||||
|
||||
Future<void> _onLoadData(HomeLoadDataEvent event, Emitter<HomeState> emit) async {
|
||||
if (event.nextPage == null) {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
} else {
|
||||
emit(state.copyWith(isPaginationLoading: true));
|
||||
}
|
||||
emit(state.copyWith(isLoading: true));
|
||||
|
||||
String? error;
|
||||
|
||||
final data = await repo.loadData(
|
||||
q: event.search,
|
||||
page: event.nextPage ?? 1,
|
||||
onError: (e) => error = e,
|
||||
);
|
||||
final data = await repo.loadData(event.search.toString());
|
||||
|
||||
if (event.nextPage != null) {
|
||||
data?.data?.insertAll(0, state.data?.data ?? []);
|
||||
}
|
||||
|
||||
//data?.data?.insertAll(0, state.data?.data ?? []);
|
||||
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
isPaginationLoading: false,
|
||||
data: data,
|
||||
error: error,
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> _onAddTask(HomeAddTaskEvent eventTask, Emitter<HomeState> emit) async {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
|
||||
String? error;
|
||||
|
||||
await repo.addTask(eventTask.name.toString(), eventTask.name.toString(), eventTask.name.toString(),);
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,12 @@ abstract class HomeEvent {
|
||||
|
||||
class HomeLoadDataEvent extends HomeEvent {
|
||||
final String? search;
|
||||
final int? nextPage;
|
||||
|
||||
const HomeLoadDataEvent({this.search, this.nextPage});
|
||||
const HomeLoadDataEvent({this.search});
|
||||
}
|
||||
|
||||
class HomeAddTaskEvent extends HomeEvent {
|
||||
final String? name;
|
||||
|
||||
const HomeAddTaskEvent({this.name});
|
||||
}
|
||||
|
@ -9,13 +9,11 @@ part 'state.g.dart';
|
||||
class HomeState extends Equatable {
|
||||
final HomeData? data;
|
||||
final bool isLoading;
|
||||
final bool isPaginationLoading;
|
||||
final String? error;
|
||||
|
||||
const HomeState({
|
||||
this.data,
|
||||
this.isLoading = false,
|
||||
this.isPaginationLoading = false,
|
||||
this.error,
|
||||
});
|
||||
|
||||
@ -23,7 +21,6 @@ class HomeState extends Equatable {
|
||||
List<Object?> get props => [
|
||||
data,
|
||||
isLoading,
|
||||
isPaginationLoading,
|
||||
error,
|
||||
];
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
part of 'home_page.dart';
|
||||
|
||||
typedef OnLikeCallback = void Function(String? id, String title, bool isLiked)?;
|
||||
typedef OnDeleteCallback = void Function(String? id, String title)?;
|
||||
|
||||
class _Card extends StatelessWidget {
|
||||
final String text;
|
||||
@ -8,6 +9,7 @@ class _Card extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String? imageUrl;
|
||||
final OnLikeCallback onLike;
|
||||
final OnDeleteCallback onDelete;
|
||||
final VoidCallback? onTap;
|
||||
final String? id;
|
||||
final bool isLiked;
|
||||
@ -18,6 +20,7 @@ class _Card extends StatelessWidget {
|
||||
required this.descriptionText,
|
||||
this.imageUrl,
|
||||
this.onLike,
|
||||
this.onDelete,
|
||||
this.onTap,
|
||||
this.id,
|
||||
this.isLiked = false,
|
||||
@ -26,6 +29,7 @@ class _Card extends StatelessWidget {
|
||||
factory _Card.fromData(
|
||||
CardData data, {
|
||||
OnLikeCallback onLike,
|
||||
OnDeleteCallback onDelete,
|
||||
VoidCallback? onTap,
|
||||
bool isLiked = false,
|
||||
}) =>
|
||||
@ -35,18 +39,12 @@ class _Card extends StatelessWidget {
|
||||
icon: data.icon,
|
||||
imageUrl: data.imageUrl,
|
||||
onLike: onLike,
|
||||
onDelete: onDelete,
|
||||
onTap: onTap,
|
||||
isLiked: isLiked,
|
||||
id: data.id,
|
||||
);
|
||||
|
||||
/*@override
|
||||
State<_Card> createState() => _CardState();
|
||||
}
|
||||
|
||||
class _CardState extends State<_Card> {
|
||||
bool isLiked = false;*/
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
@ -89,19 +87,6 @@ class _CardState extends State<_Card> {
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8.0,
|
||||
right: 8.0,
|
||||
bottom: 16.0,
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 3.0, top: 3.0),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
@ -124,30 +109,38 @@ class _CardState extends State<_Card> {
|
||||
),
|
||||
],
|
||||
)),
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 16.0,
|
||||
bottom: 16.0,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () => onLike?.call(id, text, isLiked),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: isLiked
|
||||
? const Icon(
|
||||
Icons.thumb_up,
|
||||
color: Colors.redAccent,
|
||||
key: ValueKey<int>(0),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.thumb_up_off_alt,
|
||||
key: ValueKey<int>(1),
|
||||
),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => onDelete?.call(id, text),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Icon(
|
||||
icon,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: GestureDetector(
|
||||
onTap: () => onLike?.call(id, text, isLiked),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: isLiked
|
||||
? const Icon(
|
||||
Icons.check_box_rounded,
|
||||
color: Colors.redAccent,
|
||||
key: ValueKey<int>(0),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.check_box,
|
||||
key: ValueKey<int>(1),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -8,6 +8,7 @@ import 'package:testlab/presentaition/home_page/bloc/bloc.dart';
|
||||
import 'package:testlab/presentaition/home_page/bloc/events.dart';
|
||||
import 'package:testlab/presentaition/home_page/bloc/state.dart';
|
||||
import 'package:testlab/domain/models/card.dart';
|
||||
import '../../data/repositories/repository.dart';
|
||||
import '../common/svg_objects.dart';
|
||||
import '../like_bloc/like_bloc.dart';
|
||||
import '../like_bloc/like_event.dart';
|
||||
@ -58,7 +59,6 @@ class Body extends StatefulWidget {
|
||||
|
||||
class BodyState extends State<Body> {
|
||||
final searchController = TextEditingController();
|
||||
final scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -67,27 +67,12 @@ class BodyState extends State<Body> {
|
||||
context.read<HomeBloc>().add(const HomeLoadDataEvent());
|
||||
context.read<LikeBloc>().add(const LoadLikesEvent());
|
||||
});
|
||||
|
||||
scrollController.addListener(_onNextPageListener);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _onNextPageListener() {
|
||||
if (scrollController.offset > scrollController.position.maxScrollExtent) {
|
||||
final bloc = context.read<HomeBloc>();
|
||||
if (!bloc.state.isPaginationLoading) {
|
||||
bloc.add(HomeLoadDataEvent(
|
||||
search: searchController.text,
|
||||
nextPage: bloc.state.data?.nextPage,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
searchController.dispose();
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -129,7 +114,7 @@ class BodyState extends State<Body> {
|
||||
CupertinoIcons.clear,
|
||||
color: Colors.white,
|
||||
),
|
||||
placeholder: 'Search...',
|
||||
placeholder: context.locale.search,
|
||||
placeholderStyle: const TextStyle(
|
||||
color: Colors.white54,
|
||||
),
|
||||
@ -167,37 +152,51 @@ class BodyState extends State<Body> {
|
||||
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();
|
||||
},
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: state.data?.data?.length ?? 0,
|
||||
itemBuilder: (context, index) {
|
||||
final data = state.data?.data?[index];
|
||||
return data != null
|
||||
? _Card.fromData(
|
||||
data,
|
||||
onDelete: _onDelete,
|
||||
onLike: _onLike,
|
||||
isLiked: likeState.likedIds?.contains(data.id) == true,
|
||||
onTap: () => _navToDetails(context, data),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
child: FloatingActionButton(
|
||||
onPressed: () {
|
||||
_showAddTaskDialog(context);
|
||||
},
|
||||
tooltip: 'Добавить задачу',
|
||||
child: Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
BlocBuilder<HomeBloc, HomeState>(
|
||||
builder: (context, state) => state.isPaginationLoading
|
||||
? const CircularProgressIndicator()
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Future<void> _onRefresh() {
|
||||
context.read<HomeBloc>().add(HomeLoadDataEvent(search: searchController.text));
|
||||
return Future.value(null);
|
||||
@ -214,7 +213,7 @@ class BodyState extends State<Body> {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
'$title ${isLiked ? context.locale.liked : context.locale.disliked}',
|
||||
'$title ${isLiked ? context.locale.complete : context.locale.uncomplete}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
@ -227,10 +226,142 @@ class BodyState extends State<Body> {
|
||||
});
|
||||
}
|
||||
|
||||
void _showDeleteBar(BuildContext context, String title) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
'$title ${context.locale.delete}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.black,
|
||||
duration: const Duration(seconds: 1),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
final repository repo = new repository();
|
||||
|
||||
|
||||
|
||||
Future<void> _showAddTaskDialog(BuildContext context) async {
|
||||
|
||||
final TextEditingController _taskNameController = TextEditingController();
|
||||
final TextEditingController _taskDescriptionController = TextEditingController();
|
||||
String? selectedImageUrl;
|
||||
|
||||
List<String> imageUrls = [
|
||||
'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtsv2O-lLpxro9HH9CUejcymOOnGdQJwjasg&s',
|
||||
'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT--ZMDGsa_8y9r9u0RjAn576ajsVVfsOIcDA&s',
|
||||
'https://img-webcalypt.ru/storage/memes/YhGTdxW7HnbmhqAd37XjsMCTj1fHUW7AADagYMDbmyU9VBEhDBbq4d3XNGDWo34FibwtjyeVsyxudpzeViSmHdJ7e6C50tLbZnkZZAP81AV7aM0R8VSi4YEnqgcZ1EtE-md.jpeg',
|
||||
];
|
||||
|
||||
|
||||
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false, // Разрешить закрытие диалога по нажатию вне его
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(context.locale.addtask),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
controller: _taskNameController,
|
||||
decoration: InputDecoration(hintText: context.locale.nameplaceholder),
|
||||
),
|
||||
SizedBox(height: 16.0),
|
||||
TextField(
|
||||
controller: _taskDescriptionController,
|
||||
decoration: InputDecoration(hintText: context.locale.descriptionplaceholder),
|
||||
maxLines: 5,
|
||||
minLines: 1,
|
||||
),
|
||||
SizedBox(height: 16.0),
|
||||
if (selectedImageUrl != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Image.network(
|
||||
selectedImageUrl!,
|
||||
width: 70,
|
||||
height: 70,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.0),
|
||||
DropdownButton<String>(
|
||||
hint: Text(context.locale.imageplaceholder),
|
||||
value: selectedImageUrl,
|
||||
isExpanded: true,
|
||||
items: imageUrls.map((String url) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: url,
|
||||
child: Row(
|
||||
children: [
|
||||
Image.network(url, width: 50, height: 50),
|
||||
SizedBox(width: 10),
|
||||
Expanded(child: Text(url, overflow: TextOverflow.ellipsis)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (String? value) {
|
||||
setState(() {
|
||||
selectedImageUrl = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text(context.locale.cancel),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(context.locale.addbutton),
|
||||
onPressed: () async {
|
||||
await repo.addTask(
|
||||
_taskNameController.text,
|
||||
_taskDescriptionController.text,
|
||||
selectedImageUrl
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
_onRefresh();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _onLike(String? id, String title, bool isLiked) {
|
||||
if (id != null) {
|
||||
context.read<LikeBloc>().add(ChangeLikeEvent(id));
|
||||
_showSnackBar(context, title, !isLiked);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onDelete(String? id, String title) async {
|
||||
if (id != null) {
|
||||
try {
|
||||
int taskId = int.parse(id);
|
||||
await repo.deleteTask(taskId);
|
||||
_onRefresh();
|
||||
_showDeleteBar(context, title);
|
||||
} catch (e) {
|
||||
print("Ошибка преобразования id в int: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
8
local.properties
Normal file
8
local.properties
Normal file
@ -0,0 +1,8 @@
|
||||
## This file must *NOT* be checked into Version Control Systems,
|
||||
# as it contains information specific to your local configuration.
|
||||
#
|
||||
# Location of the SDK. This is only used by Gradle.
|
||||
# For customization when using a Version Control System, please read the
|
||||
# header note.
|
||||
#Mon Dec 09 15:56:48 GMT+04:00 2024
|
||||
sdk.dir=C\:\\Users\\Admin\\AppData\\Local\\Android\\Sdk
|
5
makefile
5
makefile
@ -12,4 +12,7 @@ format:
|
||||
|
||||
res:
|
||||
fgen --output lib/components/resources.g.dart --no-watch --no-preview; \
|
||||
make format
|
||||
make format
|
||||
|
||||
loc:
|
||||
flutter gen l10n
|
@ -71,7 +71,6 @@ dev_dependencies:
|
||||
|
||||
flutter_icons:
|
||||
android: "ic_launcher"
|
||||
ios: true
|
||||
image_path: "assets/launcher.jpg"
|
||||
min_sdk_android: 21
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user