курсовая готова
This commit is contained in:
parent
d3dc6956f1
commit
f8afb81def
@ -3,5 +3,6 @@
|
||||
"search": "Search",
|
||||
"liked": "liked!",
|
||||
"disliked": "disliked :(",
|
||||
"arbEnding": "Чтобы не забыть про отсутствие запятой :)"
|
||||
"arbEnding": "Чтобы не забыть про отсутствие запятой :)",
|
||||
"goToLink": "Go to website"
|
||||
}
|
@ -3,5 +3,6 @@
|
||||
"search": "Поиск",
|
||||
"liked": "понравился!",
|
||||
"disliked": "разонравился :(",
|
||||
"arbEnding": "Чтобы не забыть про отсутствие запятой :)"
|
||||
"arbEnding": "Чтобы не забыть про отсутствие запятой :)",
|
||||
"goToLink": "Перейти на сайт"
|
||||
}
|
@ -1,19 +1,20 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'news_dto.g.dart';
|
||||
|
||||
//сборник классов, необходимых для десериализации с методами fromJson созданными автоматически в g файле. классы paginationdto, metadto - нахрен не упали насколько я понимаю
|
||||
@JsonSerializable(createToJson: false)
|
||||
class NewsDto {
|
||||
@JsonKey(name: 'articles')
|
||||
final List<NewAttributesDataDto>? data;
|
||||
final MetaDto? meta;
|
||||
// final MetaDto? meta;
|
||||
|
||||
const NewsDto({
|
||||
this.data,
|
||||
this.meta,
|
||||
// this.meta,
|
||||
});
|
||||
|
||||
factory NewsDto.fromJson(Map<String, dynamic> json) => _$NewsDtoFromJson(json);
|
||||
factory NewsDto.fromJson(Map<String, dynamic> json) =>
|
||||
_$NewsDtoFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
@ -24,29 +25,39 @@ class NewAttributesDataDto {
|
||||
final String? imagelink;
|
||||
@JsonKey(name: 'url')
|
||||
final String? id;
|
||||
@JsonKey(name: 'publishedAt')
|
||||
final String date;
|
||||
|
||||
const NewAttributesDataDto({this.id, this.title, this.description, this.imagelink});
|
||||
const NewAttributesDataDto({
|
||||
this.id,
|
||||
this.title,
|
||||
this.description,
|
||||
this.imagelink,
|
||||
required this.date
|
||||
});
|
||||
|
||||
factory NewAttributesDataDto.fromJson(Map<String, dynamic> json) =>
|
||||
_$NewAttributesDataDtoFromJson(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);
|
||||
}
|
||||
//
|
||||
// @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);
|
||||
// }
|
||||
|
@ -10,9 +10,9 @@ NewsDto _$NewsDtoFromJson(Map<String, dynamic> json) => NewsDto(
|
||||
data: (json['articles'] as List<dynamic>?)
|
||||
?.map((e) => NewAttributesDataDto.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
meta: json['meta'] == null
|
||||
? null
|
||||
: MetaDto.fromJson(json['meta'] as Map<String, dynamic>),
|
||||
// meta: json['meta'] == null
|
||||
// ? null
|
||||
// : MetaDto.fromJson(json['meta'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
NewAttributesDataDto _$NewAttributesDataDtoFromJson(
|
||||
@ -22,17 +22,18 @@ NewAttributesDataDto _$NewAttributesDataDtoFromJson(
|
||||
title: json['title'] as String?,
|
||||
description: json['description'] as String?,
|
||||
imagelink: json['urlToImage'] as String?,
|
||||
date: json['publishedAt'] as String,
|
||||
);
|
||||
|
||||
MetaDto _$MetaDtoFromJson(Map<String, dynamic> json) => MetaDto(
|
||||
pagination: json['pagination'] == null
|
||||
? null
|
||||
: PaginationDto.fromJson(json['pagination'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
PaginationDto _$PaginationDtoFromJson(Map<String, dynamic> json) =>
|
||||
PaginationDto(
|
||||
current: (json['current'] as num?)?.toInt(),
|
||||
next: (json['next'] as num?)?.toInt(),
|
||||
last: (json['last'] as num?)?.toInt(),
|
||||
);
|
||||
// MetaDto _$MetaDtoFromJson(Map<String, dynamic> json) => MetaDto(
|
||||
// pagination: json['pagination'] == null
|
||||
// ? null
|
||||
// : PaginationDto.fromJson(json['pagination'] as Map<String, dynamic>),
|
||||
// );
|
||||
//
|
||||
// PaginationDto _$PaginationDtoFromJson(Map<String, dynamic> json) =>
|
||||
// PaginationDto(
|
||||
// current: (json['current'] as num?)?.toInt(),
|
||||
// next: (json['next'] as num?)?.toInt(),
|
||||
// last: (json['last'] as num?)?.toInt(),
|
||||
// );
|
||||
|
@ -1,25 +1,39 @@
|
||||
import 'package:pmu/data/dtos/news_dto.dart';
|
||||
import 'package:pmu/domain/models/card.dart';
|
||||
import 'package:pmu/domain/models/home.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:uri/uri.dart';
|
||||
int _id = 0;
|
||||
|
||||
extension NewDataDtoToModel on NewAttributesDataDto {
|
||||
CardData toDomain() => CardData(
|
||||
text: title ?? 'UNKNOWN',
|
||||
imageUrl: imagelink,
|
||||
imageUrl: imagelink ?? 'https://smart.mag-river.ru/uploads/goods/img/445-360/fit/no-image.png',
|
||||
descText: description ?? 'NOTHING',
|
||||
id: id,
|
||||
date: toNormalDate(date) ?? 'UNKNOWN',
|
||||
);
|
||||
}
|
||||
|
||||
extension NewsDtoToModel on NewsDto {
|
||||
HomeData toDomain() => HomeData(
|
||||
data: data?.map((e) => e.toDomain()).toList(),
|
||||
nextPage: meta?.pagination?.next,
|
||||
// nextPage: meta?.pagination?.next,
|
||||
);
|
||||
}
|
||||
|
||||
int _getId(){
|
||||
_id+=1;
|
||||
return _id;
|
||||
}
|
||||
}
|
||||
|
||||
String toNormalDate(String date){
|
||||
// Парсинг строки в объект DateTime
|
||||
DateTime dateTime = DateTime.parse(date);
|
||||
|
||||
// Форматирование даты в нужный формат
|
||||
String formattedDate = DateFormat('dd.MM.yyyy').format(dateTime);
|
||||
return formattedDate;
|
||||
}
|
||||
|
||||
|
@ -5,15 +5,17 @@ import 'package:pmu/data/repositories/api_interface.dart';
|
||||
import 'package:pmu/domain/models/card.dart';
|
||||
import 'package:pmu/domain/models/home.dart';
|
||||
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
||||
|
||||
const int page = 3;
|
||||
// класс необходим для отправки запросов в апи и формирования конечных объектов homeData из response
|
||||
class BbcRepository extends ApiInterface {
|
||||
static const String _baseUrl = 'https://newsapi.org/v2';
|
||||
static const String _apiKey = '&apiKey=b9848c2aa43e4a0ba12dfe925db8513c';
|
||||
static final Dio _dio = Dio()
|
||||
..interceptors.add(PrettyDioLogger(
|
||||
requestHeader: true,
|
||||
requestBody: true,
|
||||
));
|
||||
static const String _baseUrl = 'https://newsapi.org/v2';
|
||||
static const String _apiKey = '&apiKey=b9848c2aa43e4a0ba12dfe925db8513c';
|
||||
|
||||
@override
|
||||
Future<HomeData?> loadData({
|
||||
OnErrorCallback? onError,
|
||||
@ -25,20 +27,25 @@ class BbcRepository extends ApiInterface {
|
||||
// final String url = '$_baseUrl/everything?q=$q&page=$page&pageSize=$pageSize$_apiKey';
|
||||
final String url = '$_baseUrl/everything';
|
||||
|
||||
final Response<dynamic> response = await _dio.get<Map<dynamic, dynamic>>(url,
|
||||
queryParameters: ((q != null && !q.isEmpty)
|
||||
? {
|
||||
'q': q,
|
||||
'page': page,
|
||||
'pageSize': pageSize,
|
||||
'apikey': 'b9848c2aa43e4a0ba12dfe925db8513c'
|
||||
}
|
||||
: {
|
||||
'q': 'news',
|
||||
'page': page,
|
||||
'pageSize': pageSize,
|
||||
'apikey': 'b9848c2aa43e4a0ba12dfe925db8513c'
|
||||
}));
|
||||
final Response<dynamic> response =
|
||||
await _dio.get<Map<dynamic, dynamic>>(url,
|
||||
queryParameters: ((q != null && !q.isEmpty)
|
||||
? {
|
||||
'q': q,
|
||||
'page': page,
|
||||
'pageSize': pageSize,
|
||||
'sortBy': 'publishedAt',
|
||||
// 'sortBy': 'relevancy',
|
||||
'apikey': 'b9848c2aa43e4a0ba12dfe925db8513c',
|
||||
}
|
||||
: {
|
||||
'q': 'россия',
|
||||
'page': page,
|
||||
'pageSize': pageSize,
|
||||
// 'sortBy': 'popularity',
|
||||
'sortBy': 'publishedAt',
|
||||
'apikey': 'b9848c2aa43e4a0ba12dfe925db8513c'
|
||||
}));
|
||||
final NewsDto dto = NewsDto.fromJson(response.data as Map<String, dynamic>);
|
||||
final HomeData data = dto.toDomain();
|
||||
return data;
|
||||
|
@ -1,58 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pmu/data/repositories/api_interface.dart';
|
||||
import 'package:pmu/domain/models/card.dart';
|
||||
import 'package:pmu/domain/models/home.dart';
|
||||
|
||||
class MockRepository extends ApiInterface {
|
||||
@override
|
||||
Future<HomeData?> loadData() async {
|
||||
return HomeData(
|
||||
data: [
|
||||
CardData(
|
||||
text: 'Что',
|
||||
descText:
|
||||
'Как следует из заключения, которое имеется в распоряжении агентства, документ направлен на сохранение и укрепление традиционных семейных ценностей. Он предусматривает установление защиты от распространения в интернете, СМИ, кинофильмах и рекламе информации, пропагандирующей отказ от рождения детей.' *
|
||||
10,
|
||||
imageUrl:
|
||||
'https://sun9-80.userapi.com/impg/6zgTFCriDQjiAQITCZIMb6jCjViUBgo1fzMLwA/4MJOK5aBZt8.jpg?size=1622x2160&quality=95&sign=57b1751fe201d3728998e96ac3a7ed7a&type=album',
|
||||
),
|
||||
CardData(
|
||||
text: 'Вершит',
|
||||
descText:
|
||||
'Речь идет о двух нежилых помещениях: одно — площадью почти 650 квадратных метров, второе — свыше 330. Cогласно сервисам проверки недвижимости, оба помещения относятся к особняку в Чистом переулке, одно из них оценивается почти в 73 миллиона рублей (по кадастровой стоимости).',
|
||||
imageUrl:
|
||||
'https://sun9-32.userapi.com/impg/0GkPBnqsxmWufKQrO7kA8y6JpnVwVPuezGhwvQ/_h1wPKl1-s0.jpg?size=600x600&quality=96&sign=e1f3eb74cf15263de35e82c83aedb894&type=album'),
|
||||
CardData(
|
||||
text: 'Судьбу',
|
||||
descText:
|
||||
'На контроле ситуация с эвакуацией курян, проживающих в 15-километровой зоне в Рыльском и Хомутовском районах. Всего за прошедшую неделю из приграничья в безопасные районы убыли самостоятельно и организованными колоннами более шести тысяч человек", — говорится в публикации по итогам заседания регионального правительства.',
|
||||
imageUrl:
|
||||
'https://sun9-70.userapi.com/impg/pzUP2M-ekHGD-0hvftyyrAYoMGB91-aCPwcPEA/dEc1PemeQpk.jpg?size=474x600&quality=95&sign=d83d8106137e012bfda2de9e70010f43&type=album',
|
||||
),
|
||||
CardData(
|
||||
text: 'Человечества',
|
||||
descText:
|
||||
'"Запущена официальная процедура смены фамилии", — прокомментировали РИА Новости в пресс-службе объединенной компании Wildberries и Russ (РВБ).',
|
||||
icon: Icons.add_call,
|
||||
imageUrl: 'https://i.pinimg.com/736x/df/91/dc/df91dc3de2580cffc66d01000c0c6d82.jpg',
|
||||
),
|
||||
CardData(
|
||||
text: 'В этом мире?',
|
||||
descText:
|
||||
'"Мы не располагаем никакой информацией на этот счет. Это скорее вопрос, который относится к компетенции наших военных, поэтому я рекомендую туда обращаться", — сказал пресс-секретарь.',
|
||||
icon: Icons.add_call,
|
||||
imageUrl:
|
||||
'https://sun9-80.userapi.com/impg/EzIhe8VsYrt0Eq-LzYf9DMitSISIDNVUATkz0w/qzIsFaog46s.jpg?size=483x604&quality=96&sign=5bb2247498cc216af4518677af87c9d6&type=album',
|
||||
),
|
||||
CardData(
|
||||
text: 'Некая незримая сущность?',
|
||||
descText:
|
||||
'"Мы не располагаем никакой информацией на этот счет. Это скорее вопрос, который относится к компетенции наших военных, поэтому я рекомендую туда обращаться", — сказал пресс-секретарь.',
|
||||
icon: Icons.add_call,
|
||||
imageUrl:
|
||||
'https://sun9-30.userapi.com/impg/5CPSfy33XVIKNTZBd5dgONJrPeCMFn6lOE81kg/eL8SLoL530I.jpg?size=1278x1280&quality=95&sign=f809d63142dac3559e7f7c3ddf817614&type=album',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ class CardData {
|
||||
final String descText;
|
||||
final String? imageUrl;
|
||||
final String? id;
|
||||
final String date;
|
||||
|
||||
|
||||
CardData({
|
||||
@ -14,5 +15,6 @@ class CardData {
|
||||
required this.descText,
|
||||
this.imageUrl,
|
||||
this.id,
|
||||
required this.date,
|
||||
});
|
||||
}
|
||||
|
@ -2,7 +2,9 @@ import 'package:pmu/domain/models/card.dart';
|
||||
|
||||
class HomeData {
|
||||
final List<CardData>? data;
|
||||
final int? nextPage;
|
||||
//final int? nextPage;
|
||||
|
||||
HomeData({this.data, this.nextPage});
|
||||
HomeData({this.data,
|
||||
// this.nextPage
|
||||
});
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pmu/domain/models/card.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:pmu/components/extensions/context_x.dart';
|
||||
|
||||
|
||||
class DetailsPage extends StatelessWidget {
|
||||
final CardData data;
|
||||
@ -9,21 +12,26 @@ class DetailsPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(backgroundColor: Colors.lightBlue, foregroundColor: Colors.white),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.lightBlue, foregroundColor: Colors.white),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0, top: 8.0, left: 8.0, right: 8.0),
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 4.0, top: 8.0, left: 8.0, right: 8.0),
|
||||
child: Text(
|
||||
data.text,
|
||||
style:
|
||||
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500, fontSize: 26),
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 26),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0, top: 8.0, left: 8.0, right: 8.0),
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 16.0, top: 8.0, left: 8.0, right: 8.0),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ClipRRect(
|
||||
@ -34,12 +42,33 @@ class DetailsPage extends StatelessWidget {
|
||||
)),
|
||||
)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0, top: 8.0, left: 8.0, right: 8.0),
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 4.0, top: 8.0, left: 8.0, right: 8.0),
|
||||
child: Text(
|
||||
data.descText,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 4.0, top: 8.0, left: 8.0, right: 8.0),
|
||||
child: Text(
|
||||
data.date,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 4.0, top: 8.0, left: 0.0, right: 8.0),
|
||||
child: TextButton(
|
||||
onPressed: () => launchUrl(Uri.parse(data.id ??
|
||||
'https://newsapi.org/docs/endpoints/everything')),
|
||||
child: Text('${context.locale.goToLink}'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -2,8 +2,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:pmu/data/repositories/bbc_repository.dart';
|
||||
import 'package:pmu/presentation/home_page/bloc/events.dart';
|
||||
import 'package:pmu/presentation/home_page/bloc/state.dart';
|
||||
|
||||
//класс для управления состоянием главной страницы приложения, взаимодействует с репозиторием, отвечает за подгрузку данных
|
||||
class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
// объект для доступа к репозиторию
|
||||
final BbcRepository repo;
|
||||
|
||||
HomeBloc(this.repo) : super(const HomeState()) {
|
||||
@ -12,20 +13,24 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
|
||||
// мы должны изменить наше состояние, передаем внутрь новое состояние
|
||||
Future<void> _onLoadData(HomeLoadDataEvent event, Emitter<HomeState> emit) async {
|
||||
// изменение состояния загрузки
|
||||
if (event.nextPage == null) {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
} else {
|
||||
emit(state.copyWith(isPaginationLoading: true));
|
||||
}
|
||||
//загрузка данных
|
||||
String? error;
|
||||
final data = await repo.loadData(
|
||||
q: event.search,
|
||||
page: event.nextPage ?? 1,
|
||||
page: event.nextPage ?? 5,
|
||||
onError: (e) => error = e,
|
||||
);
|
||||
//объединение новых данных со старыми
|
||||
if (event.nextPage != null) {
|
||||
data?.data?.insertAll(0, state.data?.data ?? []);
|
||||
}
|
||||
// эмит нового состояния
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
isPaginationLoading: false,
|
||||
|
@ -19,16 +19,18 @@ class HomeState extends Equatable {
|
||||
this.error,
|
||||
});
|
||||
|
||||
// HomeState copyWith({
|
||||
// HomeData? data,
|
||||
// bool? isLoading,
|
||||
// bool? isPaginationLoading,
|
||||
// }) =>
|
||||
// HomeState(
|
||||
// data: data ?? this.data,
|
||||
// isLoading: isLoading ?? this.isLoading,
|
||||
// isPaginationLoading: isPaginationLoading ?? this.isPaginationLoading,
|
||||
// );
|
||||
HomeState copyWith({
|
||||
HomeData? data,
|
||||
bool? isLoading,
|
||||
bool? isPaginationLoading,
|
||||
String? error,
|
||||
}) =>
|
||||
HomeState(
|
||||
data: data ?? this.data,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
isPaginationLoading: isPaginationLoading ?? this.isPaginationLoading,
|
||||
error: error ?? this.error,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
|
@ -11,6 +11,7 @@ class _Card extends StatelessWidget {
|
||||
final VoidCallback? onTap;
|
||||
final String? id;
|
||||
final bool isLiked;
|
||||
final String date;
|
||||
|
||||
const _Card(
|
||||
this.text, {
|
||||
@ -21,6 +22,7 @@ class _Card extends StatelessWidget {
|
||||
this.onTap,
|
||||
this.id,
|
||||
this.isLiked = false,
|
||||
required this.date,
|
||||
});
|
||||
|
||||
factory _Card.fromData(
|
||||
@ -38,6 +40,7 @@ class _Card extends StatelessWidget {
|
||||
onTap: onTap,
|
||||
isLiked: isLiked,
|
||||
id: data.id,
|
||||
date: data.date,
|
||||
);
|
||||
|
||||
@override
|
||||
|
@ -4,7 +4,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:pmu/components/extensions/context_x.dart';
|
||||
import 'package:pmu/components/utils/debounce.dart';
|
||||
import 'package:pmu/data/repositories/bbc_repository.dart';
|
||||
import 'package:pmu/data/repositories/mock_repository.dart';
|
||||
import 'package:pmu/domain/models/card.dart';
|
||||
import 'package:pmu/presentation/common/svg_objects.dart';
|
||||
import 'package:pmu/presentation/details_page/details_page.dart';
|
||||
@ -20,17 +19,14 @@ import 'package:pmu/presentation/locale_bloc/locale_events.dart';
|
||||
import 'package:pmu/presentation/locale_bloc/locale_state.dart';
|
||||
|
||||
part 'card.dart';
|
||||
|
||||
//основной виджет со скаффолд и боди внутри
|
||||
class MyHomePage extends StatefulWidget {
|
||||
//const MyHomePage({super.key, required this.title});
|
||||
|
||||
//final String title;
|
||||
const MyHomePage({super.key});
|
||||
|
||||
@override
|
||||
State<MyHomePage> createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
// состояние его
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -39,45 +35,51 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// виджет с соновной логикой(на самом деле вся логика в состоянии)
|
||||
class _Body extends StatefulWidget {
|
||||
const _Body({super.key});
|
||||
|
||||
@override
|
||||
State<_Body> createState() => _BodyState();
|
||||
}
|
||||
|
||||
// состояние боди с основной лог8икой
|
||||
class _BodyState extends State<_Body> {
|
||||
// создание объектов контроллеров для скролла и поиска
|
||||
final searchController = TextEditingController();
|
||||
final scrollController = ScrollController();
|
||||
|
||||
// late Future<List<CardData>?> data;
|
||||
// final repo = BbcRepository();
|
||||
int page = 1;
|
||||
// инициализация
|
||||
@override
|
||||
void initState() {
|
||||
//инициализация иконок флагов вроде
|
||||
SvgObjects.init();
|
||||
// после завершения сборки виджетов отправляет события загрузки данных и лайков в соответсвующие блоки, дальнейшая работа происходит там
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<HomeBloc>().add(const HomeLoadDataEvent());
|
||||
context.read<LikeBloc>().add(const LoadLikesEvent());
|
||||
});
|
||||
//data = repo.loadData(onError: (e) => showErrorDialog(context, error: e));
|
||||
// вешает слушатель на скролл контроллер(для пагинации)
|
||||
scrollController.addListener(_onNextPageListener);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
//слушатель на скролл контроллер
|
||||
void _onNextPageListener() {
|
||||
if (scrollController.offset > scrollController.position.maxScrollExtent) {
|
||||
// проверка на то что список прокручен не до конца
|
||||
if (scrollController.offset >= scrollController.position.maxScrollExtent) {
|
||||
// preventing multiple pagination request on multiple swipes
|
||||
final bloc = context.read<HomeBloc>();
|
||||
if (!bloc.state.isPaginationLoading) {
|
||||
bloc.add(HomeLoadDataEvent(
|
||||
final homebloc = context.read<HomeBloc>();
|
||||
// проверка на то что подгрузка пагинации уже не активна
|
||||
if (!homebloc.state.isPaginationLoading) {
|
||||
page ++;
|
||||
// отправляем в хомблок событие на подгрузку данных со следующей страницы
|
||||
homebloc.add(HomeLoadDataEvent(
|
||||
search: searchController.text,
|
||||
nextPage: 2,
|
||||
nextPage: page,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// освобождает контроллеры(чистит)
|
||||
void dispose() {
|
||||
searchController.dispose();
|
||||
scrollController.dispose();
|
||||
@ -96,6 +98,7 @@ class _BodyState extends State<_Body> {
|
||||
flex: 4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
// поисковая строка и немного ее обработки: на изменение текста в строке происходит ран дебаунса, я пока не знаю что это. и посылается в хомблок событие на загрузку новых данных
|
||||
child: CupertinoSearchTextField(
|
||||
controller: searchController,
|
||||
placeholder: context.locale.search,
|
||||
@ -108,15 +111,17 @@ class _BodyState extends State<_Body> {
|
||||
),
|
||||
),
|
||||
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'
|
||||
builder: (context, localeState) {
|
||||
return localeState.currentLocale.languageCode == 'ru'
|
||||
? const SvgRu()
|
||||
: const SvgUk();
|
||||
},
|
||||
@ -127,15 +132,15 @@ class _BodyState extends State<_Body> {
|
||||
],
|
||||
),
|
||||
BlocBuilder<HomeBloc, HomeState>(
|
||||
builder: (context, state) => state.error != null
|
||||
builder: (context, homeState) => homeState.error != null
|
||||
? Text(
|
||||
state.error ?? '',
|
||||
homeState.error ?? '',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineSmall
|
||||
?.copyWith(color: Colors.red),
|
||||
)
|
||||
: state.isLoading
|
||||
: homeState.isLoading
|
||||
? const CircularProgressIndicator()
|
||||
: BlocBuilder<LikeBloc, LikeState>(
|
||||
builder: (context, likeState) {
|
||||
@ -145,9 +150,9 @@ class _BodyState extends State<_Body> {
|
||||
child: ListView.builder(
|
||||
controller: scrollController,
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: state.data?.data?.length ?? 0,
|
||||
itemCount: homeState.data?.data?.length ?? 0,
|
||||
itemBuilder: (context, index) {
|
||||
final data = state.data?.data?[index];
|
||||
final data = homeState.data?.data?[index];
|
||||
return data != null
|
||||
? _Card.fromData(
|
||||
data,
|
||||
@ -167,19 +172,20 @@ class _BodyState extends State<_Body> {
|
||||
),
|
||||
),
|
||||
BlocBuilder<HomeBloc, HomeState>(
|
||||
builder: (context, state) => state.isPaginationLoading
|
||||
builder: (context, homeState) => homeState.isPaginationLoading
|
||||
? const CircularProgressIndicator()
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
// функция показа снэкбара
|
||||
void _showSnackBar(BuildContext context, String title, bool isLiked) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
'The New ${isLiked ? context.locale.liked : context.locale.disliked}',
|
||||
// в зависимомти от локализациии и от того понравилась или разонравилась новость отображается итоговый снэкбар
|
||||
'${isLiked ? context.locale.liked : context.locale.disliked}',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
backgroundColor: Colors.lightBlue,
|
||||
@ -187,23 +193,23 @@ class _BodyState extends State<_Body> {
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
// функция для перехода к детальной странице
|
||||
void _navToDetails(BuildContext context, CardData data) {
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(builder: (context) => DetailsPage(data)),
|
||||
);
|
||||
}
|
||||
|
||||
// функция вызывается при нажатии на кнопку лайка и вызывает функцию показа снэкбара, так же, запоминается айди лайкнутой новости
|
||||
void _onLike(String? id, String title, bool isLiked) {
|
||||
if (id != null) {
|
||||
context.read<LikeBloc>().add(ChangeLikeEvent(id));
|
||||
_showSnackBar(context, title, !isLiked);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// функция вызывается при тяге вниз основной части для обновления и вызывает событие загрузки данных для homedata
|
||||
Future<void> _onRefresh() {
|
||||
page = 1;
|
||||
context
|
||||
.read<HomeBloc>()
|
||||
.add(HomeLoadDataEvent(search: searchController.text));
|
||||
|
@ -9,17 +9,17 @@ class LikeBloc extends Bloc<LikeEvent, LikeState> {
|
||||
on<ChangeLikeEvent>(_onChangeLike);
|
||||
on<LoadLikesEvent>(_onLoadLikes);
|
||||
}
|
||||
Future<void> _onLoadLikes(LoadLikesEvent event, Emitter<LikeState> emit) async {
|
||||
Future<void> _onLoadLikes(LoadLikesEvent loadLikesEvent, 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 {
|
||||
Future<void> _onChangeLike(ChangeLikeEvent changeLikeEvent, Emitter<LikeState> emit) async {
|
||||
final updatedList = List<String>.from(state.likedIds ?? []);
|
||||
if (updatedList.contains(event.id)) {
|
||||
updatedList.remove(event.id);
|
||||
if (updatedList.contains(changeLikeEvent.id)) {
|
||||
updatedList.remove(changeLikeEvent.id);
|
||||
} else {
|
||||
updatedList.add(event.id);
|
||||
updatedList.add(changeLikeEvent.id);
|
||||
}
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
prefs.setStringList(_likedPrefsKey, updatedList);
|
||||
|
@ -6,6 +6,10 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
@ -6,7 +6,9 @@ import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import shared_preferences_foundation
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
||||
|
82
pubspec.lock
82
pubspec.lock
@ -331,7 +331,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
http:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
|
||||
@ -594,6 +594,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
quiver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: quiver
|
||||
sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -759,6 +767,78 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
uri:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: uri
|
||||
sha256: "889eea21e953187c6099802b7b4cf5219ba8f3518f604a1033064d45b1b8268a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.1"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.14"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.2"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.3"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
vector_graphics:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -42,6 +42,9 @@ dependencies:
|
||||
equatable: ^2.0.5
|
||||
flutter_bloc: ^8.1.5
|
||||
copy_with_extension_gen: ^5.0.4
|
||||
http: ^1.2.2
|
||||
uri: ^1.0.0
|
||||
url_launcher: ^6.3.1
|
||||
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
|
@ -6,6 +6,9 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
Loading…
x
Reference in New Issue
Block a user