CourseWork - isDone

This commit is contained in:
DyCTaTOR 2024-12-12 16:36:29 +04:00
parent 66fcb3fa3a
commit a251967084
48 changed files with 1225 additions and 876 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,9 +1,13 @@
{
"@@locale": "en",
"search": "Search",
"liked": "liked!",
"disliked": "disliked :(",
"studentWord": "Student",
"liked": "liked",
"disliked": "disliked",
"activityWord": "Activity",
"startTime": "Start:",
"endTime": "End:",
"description": "Description:",
"duration": "Duration:",
"arbEnding": "Чтобы не забыть про отсутствие запятой :)"
}

View File

@ -1,9 +1,13 @@
{
"@@locale": "ru",
"search": "Поиск",
"liked": "Понравился!",
"disliked": "разонравился :(",
"studentWord": "Студент",
"liked": "понравился",
"disliked": "разонравился",
"activityWord": "Активность",
"startTime": "Начало:",
"endTime": "Окончание:",
"description": "Описание:",
"duration": "Длительность:",
"arbEnding": "Чтобы не забыть про отсутствие запятой :)"
}

View File

@ -82,7 +82,8 @@ abstract class AppLocale {
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
@ -90,10 +91,7 @@ abstract class AppLocale {
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
Locale('en'),
Locale('ru')
];
static const List<Locale> supportedLocales = <Locale>[Locale('en'), Locale('ru')];
/// No description provided for @search.
///
@ -104,20 +102,44 @@ abstract class AppLocale {
/// No description provided for @liked.
///
/// In ru, this message translates to:
/// **'Понравился!'**
/// **'понравился'**
String get liked;
/// No description provided for @disliked.
///
/// In ru, this message translates to:
/// **'разонравился :('**
/// **'разонравился'**
String get disliked;
/// No description provided for @studentWord.
/// No description provided for @activityWord.
///
/// In ru, this message translates to:
/// **'Студент'**
String get studentWord;
/// **'Активность'**
String get activityWord;
/// No description provided for @startTime.
///
/// In ru, this message translates to:
/// **'Начало:'**
String get startTime;
/// No description provided for @endTime.
///
/// In ru, this message translates to:
/// **'Окончание:'**
String get endTime;
/// No description provided for @description.
///
/// In ru, this message translates to:
/// **'Описание:'**
String get description;
/// No description provided for @duration.
///
/// In ru, this message translates to:
/// **'Длительность:'**
String get duration;
/// No description provided for @arbEnding.
///
@ -142,18 +164,17 @@ class _AppLocaleDelegate extends LocalizationsDelegate<AppLocale> {
}
AppLocale lookupAppLocale(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'en': return AppLocaleEn();
case 'ru': return AppLocaleRu();
case 'en':
return AppLocaleEn();
case 'ru':
return AppLocaleRu();
}
throw FlutterError(
'AppLocale.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.'
);
'AppLocale.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.');
}

View File

@ -10,13 +10,25 @@ class AppLocaleEn extends AppLocale {
String get search => 'Search';
@override
String get liked => 'liked!';
String get liked => 'liked';
@override
String get disliked => 'disliked :(';
String get disliked => 'disliked';
@override
String get studentWord => 'Student';
String get activityWord => 'Activity';
@override
String get startTime => 'Start:';
@override
String get endTime => 'End:';
@override
String get description => 'Description:';
@override
String get duration => 'Duration:';
@override
String get arbEnding => 'Чтобы не забыть про отсутствие запятой :)';

View File

@ -10,13 +10,25 @@ class AppLocaleRu extends AppLocale {
String get search => 'Поиск';
@override
String get liked => 'Понравился!';
String get liked => 'понравился';
@override
String get disliked => 'разонравился :(';
String get disliked => 'разонравился';
@override
String get studentWord => 'Студент';
String get activityWord => 'Активность';
@override
String get startTime => 'Начало:';
@override
String get endTime => 'Окончание:';
@override
String get description => 'Описание:';
@override
String get duration => 'Длительность:';
@override
String get arbEnding => 'Чтобы не забыть про отсутствие запятой :)';

View File

@ -0,0 +1,68 @@
import 'package:json_annotation/json_annotation.dart';
part 'activity_dto.g.dart'; // Этот файл будет сгенерирован автоматически
@JsonSerializable(createToJson: false)
class ActivitiesDto {
final List<ActivityDataDto>? data;
final MetaDto? meta;
const ActivitiesDto({this.data, this.meta});
factory ActivitiesDto.fromJson(Map<String, dynamic> json) => _$ActivitiesDtoFromJson(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 ActivityDataDto {
final String? id;
final String? type;
final ActivityAttributesDataDto? attributes;
const ActivityDataDto({this.id, this.type, this.attributes});
factory ActivityDataDto.fromJson(Map<String, dynamic> json) => _$ActivityDataDtoFromJson(json);
}
@JsonSerializable(createToJson: false)
class ActivityAttributesDataDto {
final String? name;
final String? description;
final String? startTime;
final String? endTime;
final String? category;
const ActivityAttributesDataDto({
this.name,
this.description,
this.startTime,
this.endTime,
this.category,
});
factory ActivityAttributesDataDto.fromJson(Map<String, dynamic> json) =>
_$ActivityAttributesDataDtoFromJson(json);
}

View File

@ -0,0 +1,43 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'activity_dto.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ActivitiesDto _$ActivitiesDtoFromJson(Map<String, dynamic> json) => ActivitiesDto(
data: (json['data'] as List<dynamic>?)
?.map((e) => ActivityDataDto.fromJson(e as Map<String, dynamic>))
.toList(),
meta: json['meta'] == null ? null : MetaDto.fromJson(json['meta'] as Map<String, dynamic>),
);
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(),
);
ActivityDataDto _$ActivityDataDtoFromJson(Map<String, dynamic> json) => ActivityDataDto(
id: json['id'] as String?,
type: json['type'] as String?,
attributes: json['attributes'] == null
? null
: ActivityAttributesDataDto.fromJson(json['attributes'] as Map<String, dynamic>),
);
ActivityAttributesDataDto _$ActivityAttributesDataDtoFromJson(Map<String, dynamic> json) =>
ActivityAttributesDataDto(
name: json['name'] as String?,
description: json['description'] as String?,
startTime: json['startTime'] as String?,
endTime: json['endTime'] as String?,
category: json['category'] as String?,
);

View File

@ -0,0 +1,21 @@
import 'package:labs/domain/models/Activity.dart';
import 'package:labs/domain/models/home.dart';
import '../dtos/activity_dto.dart';
extension ActivityDataDtoToModel on ActivityDataDto {
Activity toDomain() => Activity(
id: id ?? '0',
name: attributes?.name ?? 'Без названия',
description: attributes?.description ?? 'Нет описания',
startTime: attributes?.startTime ?? '00:00',
endTime: attributes?.endTime ?? '00:00',
);
}
extension ActivitiesDtoToModel on ActivitiesDto {
HomeData toDomain() => HomeData(
data: data?.map((e) => e.toDomain()).toList(),
nextPage: meta?.pagination?.next,
);
}

View File

@ -10,9 +10,9 @@ extension CharacterDataDtoToModel on CharacterDataDto {
'https://gryazoveckij-r19.gosweb.gosuslugi.ru/netcat_files/460/2008/net_foto_muzh.jpg');
}
extension CharactersDtoToModel on CharactersDto {
/*extension CharactersDtoToModel on CharactersDto {
HomeData toDomain() => HomeData(
data: data?.map((e) => e.toDomain()).toList(),
nextPage: meta?.pagination?.next,
);
}
}*/

View File

@ -0,0 +1,17 @@
class Activity {
final String name;
final String description;
final String startTime;
final String endTime;
final String? id;
final String? image;
Activity({
required this.name,
required this.description,
required this.startTime,
required this.endTime,
this.id,
this.image,
});
}

View File

@ -1,7 +1,9 @@
import 'package:labs/domain/models/Activity.dart';
import 'Student.dart';
class HomeData {
final List<Student>? data;
final List<Activity>? data;
final int? nextPage;
HomeData({this.data, this.nextPage});

View File

@ -1,14 +1,12 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:labs/presentation/home_page/bloc/bloc.dart';
import 'package:labs/presentation/home_page/home_page.dart';
import 'package:labs/presentation/like_bloc/like_bloc.dart';
import 'package:labs/repo/potter_repo.dart';
import 'University.dart';
import 'package:labs/repo/activity_repo.dart';
import 'components/locale/l10n/app_locale.dart';
import 'domain/models/Student.dart';
import 'domain/models/Activity.dart';
import 'presentation/locale_bloc/locale_bloc.dart';
import 'presentation/locale_bloc/locale_state.dart';
@ -20,46 +18,24 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'University App',
title: 'Time Management App',
theme: ThemeData(
primarySwatch: Colors.blue,
scaffoldBackgroundColor: Colors.white, // Устанавливаем белый фон
scaffoldBackgroundColor: Colors.white,
),
home: ScaffoldMessenger(
child: UniversityScreen(),
child: ActivityScreen(),
),
);
}
}
class UniversityScreen extends StatefulWidget {
class ActivityScreen extends StatefulWidget {
@override
_UniversityScreenState createState() => _UniversityScreenState();
_ActivityScreenState createState() => _ActivityScreenState();
}
class _UniversityScreenState extends State<UniversityScreen> {
final University university = University();
final TextEditingController nameController = TextEditingController();
final TextEditingController ageController = TextEditingController();
List<String> selectedCourses = [];
void _addStudent() async {
await Future.delayed(Duration(seconds: 1));
String name = nameController.text;
int age = int.tryParse(ageController.text) ?? 0;
if (name.isNotEmpty && age > 0 && selectedCourses.isNotEmpty) {
setState(() {
// Создаем новый список курсов для каждого студента
List<String> studentCourses = List.from(selectedCourses);
university.addStudent(Student(name, age, studentCourses));
nameController.clear();
ageController.clear();
selectedCourses.clear();
});
}
}
class _ActivityScreenState extends State<ActivityScreen> {
@override
Widget build(BuildContext context) {
return BlocProvider<LocaleBloc>(
@ -68,7 +44,7 @@ class _UniversityScreenState extends State<UniversityScreen> {
child: BlocBuilder<LocaleBloc, LocaleState>(
builder: (context, state) {
return MaterialApp(
title: 'Строев Владимир, ПИбд-32',
title: 'Time Management',
locale: state.currentLocale,
localizationsDelegates: AppLocale.localizationsDelegates,
supportedLocales: AppLocale.supportedLocales,
@ -76,17 +52,17 @@ class _UniversityScreenState extends State<UniversityScreen> {
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.orangeAccent),
useMaterial3: true,
scaffoldBackgroundColor: Colors.white12, // Устанавливаем белый фон
scaffoldBackgroundColor: Colors.white12,
),
home: RepositoryProvider<PotterRepo>(
home: RepositoryProvider<ActivityRepository>(
lazy: true,
create: (_) => PotterRepo(),
create: (_) => ActivityRepository(),
child: BlocProvider<LikeBloc>(
lazy: false,
create: (context) => LikeBloc(),
child: BlocProvider<HomeBloc>(
lazy: false,
create: (context) => HomeBloc(context.read<PotterRepo>()),
create: (context) => HomeBloc(context.read<ActivityRepository>()),
child: const MyHomePage(),
),
),

View File

@ -1,41 +1,134 @@
import 'package:flutter/material.dart';
import '../../domain/models/Student.dart';
import 'package:labs/components/extensions/context_x.dart';
import 'package:labs/domain/models/Activity.dart';
class DetailsPage extends StatelessWidget {
final Student data;
final Activity data;
const DetailsPage(this.data, {super.key});
// Метод для преобразования времени в минуты
int _timeToMinutes(String time) {
final parts = time.split(':');
final hours = int.parse(parts[0]);
final minutes = int.parse(parts[1]);
return hours * 60 + minutes;
}
// Метод для вычисления разницы между временем
String _calculateDuration(String startTime, String endTime) {
final startMinutes = _timeToMinutes(startTime);
final endMinutes = _timeToMinutes(endTime);
final duration = endMinutes - startMinutes;
if (duration < 0) {
return 'Ошибка: время окончания меньше времени начала';
}
final hours = duration ~/ 60;
final minutes = duration % 60;
return 'Длительность: ${hours}ч ${minutes}м';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Image.network(
data.image ?? '',
appBar: AppBar(
title: Text(
data.name,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
backgroundColor: Colors.blueAccent,
),
body: Container(
width: double.infinity,
height: double.infinity, // Растягиваем контейнер на всю ширину
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.blueAccent, Colors.lightBlueAccent],
),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
Text(
context.locale.description,
style: TextStyle(
fontSize: 16,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
Text(
data.description,
style: const TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 24),
Text(
context.locale.startTime,
style: TextStyle(
fontSize: 16,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
Text(
data.startTime,
style: const TextStyle(
fontSize: 18,
color: Colors.white70,
),
),
const SizedBox(height: 16),
Text(
context.locale.endTime,
style: TextStyle(
fontSize: 16,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
Text(
data.endTime,
style: const TextStyle(
fontSize: 18,
color: Colors.white70,
),
),
const SizedBox(height: 24),
Text(
context.locale.duration,
style: TextStyle(
fontSize: 16,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
Text(
_calculateDuration(data.startTime, data.endTime),
style: const TextStyle(
fontSize: 20,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
),
Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Text(
data.name,
style: Theme.of(context).textTheme.headlineLarge,
),
),
Text(
Student.getYearWord(data.age),
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
'Courses: ${data.courses.join(", ")}',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
);
}

View File

@ -1,10 +1,10 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:labs/presentation/home_page/bloc/events.dart';
import 'package:labs/presentation/home_page/bloc/state.dart';
import 'package:labs/repo/potter_repo.dart';
import 'package:labs/repo/activity_repo.dart';
class HomeBloc extends Bloc<HomeEvent, HomeState> {
final PotterRepo repo;
final ActivityRepository repo;
HomeBloc(this.repo) : super(const HomeState()) {
on<HomeLoadDataEvent>(_onLoadData);

View File

@ -4,9 +4,9 @@ typedef OnLikeCallBack = void Function(String? id, String title, bool isLiked)?;
class _Card extends StatelessWidget {
final String name;
final int age;
final List<String> courses;
final String? imageUrl;
final String description;
final String startTime;
final String endTime;
final OnLikeCallBack onLike;
final VoidCallback? onTap;
final String? id;
@ -14,22 +14,22 @@ class _Card extends StatelessWidget {
const _Card(
this.name, {
required this.age,
required this.courses,
this.imageUrl,
required this.description,
required this.startTime,
required this.endTime,
this.onLike,
this.onTap,
this.id,
this.isLiked = false,
});
factory _Card.fromData(Student data,
factory _Card.fromData(Activity data,
{OnLikeCallBack onLike, VoidCallback? onTap, bool isLiked = false}) =>
_Card(
data.name,
age: data.age,
courses: data.courses,
imageUrl: data.image,
description: data.description,
startTime: data.startTime,
endTime: data.endTime,
onLike: onLike,
onTap: onTap,
isLiked: isLiked,
@ -39,86 +39,67 @@ class _Card extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.only(top: 16, left: 16, right: 16, bottom: 16),
constraints: const BoxConstraints(minHeight: 140),
decoration: BoxDecoration(
color: Colors.blueAccent,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
spreadRadius: 2,
blurRadius: 5,
offset: Offset(0, 3),
onTap: onTap,
child: Card(
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blueAccent,
),
],
),
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
),
const SizedBox(height: 8),
Text(
description,
style: const TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
const SizedBox(height: 8),
Text(
'${context.locale.startTime} $startTime',
style: const TextStyle(
fontSize: 14,
color: Colors.black54,
),
),
Text(
'${context.locale.endTime} $endTime',
style: const TextStyle(
fontSize: 14,
color: Colors.black54,
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Align(
alignment: Alignment.bottomLeft,
child: Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 16,
bottom: 16,
),
child: GestureDetector(
onTap: () => onLike?.call(id, name, isLiked),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: isLiked
? const Icon(
Icons.favorite,
color: Colors.lightBlueAccent,
key: ValueKey<int>(0),
)
: const Icon(Icons.favorite_border),
key: ValueKey<int>(0),
),
),
),
),
SizedBox(width: 16),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(name, style: Theme.of(context).textTheme.bodyLarge),
SizedBox(height: 12),
Text('Age: ${Student.getYearWord(age)}',
style: Theme.of(context).textTheme.bodyLarge),
SizedBox(height: 12),
Text('Courses: ${courses.join(", ")}',
style: Theme.of(context).textTheme.bodyMedium),
],
),
),
Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.only(
bottomRight: Radius.circular(20),
topRight: Radius.circular(20),
),
child: Stack(
children: [
Positioned.fill(
child: Image.network(
imageUrl ?? '',
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Placeholder(),
),
),
],
),
GestureDetector(
onTap: () => onLike?.call(id, name, isLiked),
child: Icon(
isLiked ? Icons.favorite : Icons.favorite_border,
color: isLiked ? Colors.red : Colors.grey,
),
),
],
),
)));
],
),
),
),
);
}
}

View File

@ -6,9 +6,9 @@ import 'package:labs/presentation/details_page/details_page.dart';
import 'package:labs/presentation/home_page/bloc/events.dart';
import 'package:labs/presentation/like_bloc/like_event.dart';
import 'package:labs/presentation/like_bloc/like_state.dart';
import 'package:labs/repo/potter_repo.dart';
import 'package:labs/repo/activity_repo.dart';
import '../../components/utils/debounce.dart';
import '../../domain/models/Student.dart';
import '../../domain/models/Activity.dart';
import '../common/svg_objects.dart';
import '../like_bloc/like_bloc.dart';
import '../locale_bloc/locale_bloc.dart';
@ -16,6 +16,7 @@ import '../locale_bloc/locale_events.dart';
import '../locale_bloc/locale_state.dart';
import 'bloc/bloc.dart';
import 'bloc/state.dart';
part 'card.dart';
class MyHomePage extends StatefulWidget {
@ -42,8 +43,8 @@ class _Body extends StatefulWidget {
class _BodyState extends State<_Body> {
final searchController = TextEditingController();
final scrollController = ScrollController();
final PotterRepo repo = PotterRepo();
late Future<List<Student>?> data;
final ActivityRepository repo = ActivityRepository();
late Future<List<Activity>?> data;
@override
void initState() {
@ -88,83 +89,94 @@ class _BodyState extends State<_Body> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: CupertinoSearchTextField(
controller: searchController,
placeholder: context.locale.search,
onChanged: (search) {
Debounce.run(
() => context.read<HomeBloc>().add(HomeLoadDataEvent(search: search)));
},
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.blueAccent, Colors.lightBlueAccent],
),
),
child: Padding(
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: CupertinoSearchTextField(
controller: searchController,
placeholder: context.locale.search,
onChanged: (search) {
Debounce.run(
() => context.read<HomeBloc>().add(HomeLoadDataEvent(search: search)));
},
),
),
),
GestureDetector(
onTap: () => context.read<LocaleBloc>().add(const ChangeLocaleEvent()),
child: SizedBox.square(
dimension: 50,
child: Padding(
padding: const EdgeInsets.only(right: 12),
child: BlocBuilder<LocaleBloc, LocaleState>(
builder: (context, state) {
return state.currentLocale.languageCode == 'ru'
? const SvgRu()
: const SvgUk();
},
GestureDetector(
onTap: () => context.read<LocaleBloc>().add(const ChangeLocaleEvent()),
child: SizedBox.square(
dimension: 50,
child: Padding(
padding: const EdgeInsets.only(right: 12),
child: BlocBuilder<LocaleBloc, LocaleState>(
builder: (context, state) {
return state.currentLocale.languageCode == 'ru'
? const SvgRu()
: const SvgUk();
},
),
),
),
),
),
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()
: BlocBuilder<LikeBloc, LikeState>(
builder: (context, likeState) {
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();
},
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()
: BlocBuilder<LikeBloc, LikeState>(
builder: (context, likeState) {
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();
},
),
),
),
);
},
),
),
BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) => state.isPaginationLoading
? const CircularProgressIndicator()
: const SizedBox.shrink(),
)
],
);
},
),
),
BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) => state.isPaginationLoading
? const CircularProgressIndicator()
: const SizedBox.shrink(),
)
],
),
),
),
);
}
void _navToDetails(BuildContext context, Student data) {
void _navToDetails(BuildContext context, Activity data) {
Navigator.push(
context,
CupertinoPageRoute(builder: (context) => DetailsPage(data)),
@ -180,7 +192,7 @@ class _BodyState extends State<_Body> {
WidgetsBinding.instance.addPostFrameCallback((_) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(
'${context.locale.studentWord} $title ${isLiked ? context.locale.liked : context.locale.disliked}',
'${context.locale.activityWord} $title ${isLiked ? context.locale.liked : context.locale.disliked}',
style: Theme.of(context).textTheme.bodyLarge,
),
backgroundColor: Colors.lightBlueAccent,

View File

@ -0,0 +1,66 @@
import 'package:labs/domain/models/Activity.dart';
import 'package:labs/domain/models/home.dart';
import 'api_interface.dart';
class ActivityRepository extends ApiInterface {
@override
Future<HomeData?> loadData(
{OnErrorCallback? onError, String? q, int page = 1, int pageSize = 25}) async {
try {
// Мок-данные для активностей
final List<Activity> mockActivities = [
Activity(
id: '1',
name: 'Утренняя пробежка',
description: 'Пробежка в парке',
startTime: '07:00',
endTime: '07:30',
),
Activity(
id: '2',
name: 'Работа над проектом',
description: 'Завершение курсовой работы',
startTime: '10:00',
endTime: '12:00',
),
Activity(
id: '3',
name: 'Обед',
description: 'Обед в кафе',
startTime: '13:00',
endTime: '14:00',
),
Activity(
id: '4',
name: 'Чтение книги',
description: 'Чтение главы из книги "Тайм-менеджмент"',
startTime: '15:00',
endTime: '16:00',
),
];
// Фильтрация данных по поисковой строке
final filteredActivities = mockActivities.where((activity) {
return q == null ||
activity.name.toLowerCase().contains(q.toLowerCase()) ||
activity.description.toLowerCase().contains(q.toLowerCase());
}).toList();
// Пагинация
final startIndex = (page - 1) * pageSize;
final endIndex = startIndex + pageSize;
final paginatedActivities =
filteredActivities.sublist(startIndex, endIndex.clamp(0, filteredActivities.length));
// Возвращаем данные в формате HomeData
return HomeData(
data: paginatedActivities,
nextPage: endIndex < filteredActivities.length ? page + 1 : null,
);
} catch (e) {
// Обработка ошибок
onError?.call(e.toString());
return null;
}
}
}

View File

@ -32,8 +32,8 @@ class PotterRepo extends ApiInterface {
);
final CharactersDto dto = CharactersDto.fromJson(response.data as Map<String, dynamic>);
final HomeData data = dto.toDomain();
return data;
/*final HomeData data = dto.toDomain();
return data;*/
} on DioException catch (e) {
onError?.call(e.error?.toString());
return null;

View File

@ -1,6 +1,14 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_discoveryapis_commons:
dependency: transitive
description:
name: _discoveryapis_commons
sha256: "113c4100b90a5b70a983541782431b82168b3cae166ab130649c36eb3559d498"
url: "https://pub.dev"
source: hosted
version: "1.0.7"
_fe_analyzer_shared:
dependency: transitive
description:
@ -346,6 +354,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
googleapis:
dependency: "direct main"
description:
name: googleapis
sha256: "864f222aed3f2ff00b816c675edf00a39e2aaf373d728d8abec30b37bee1a81c"
url: "https://pub.dev"
source: hosted
version: "13.2.0"
graphs:
dependency: transitive
description:

View File

@ -14,12 +14,13 @@ dependencies:
cupertino_icons: ^1.0.8
json_annotation: ^4.9.0
dio: ^5.4.2+1
dio: ^5.7.0
pretty_dio_logger: ^1.3.1
equatable: ^2.0.5
flutter_bloc: ^8.1.6
copy_with_extension_gen: ^5.0.4
flutter_svg: 2.0.7
googleapis: ^13.2.0
dev_dependencies:
flutter_test: