From 55dbf12a2f1596637906cba114b4dae1d539b48e Mon Sep 17 00:00:00 2001 From: checkedear <271323618+checkedear@users.noreply.github.com> Date: Tue, 21 Apr 2026 22:33:46 +0200 Subject: [PATCH] feat: main page with dates --- firka/lib/core/extensions.dart | 110 ++--- firka/lib/ui/phone/pages/home/home_main.dart | 22 +- firka/lib/ui/phone/widgets/info_card.dart | 438 +++++++++---------- 3 files changed, 279 insertions(+), 291 deletions(-) diff --git a/firka/lib/core/extensions.dart b/firka/lib/core/extensions.dart index b98b4f1..570ddf0 100644 --- a/firka/lib/core/extensions.dart +++ b/firka/lib/core/extensions.dart @@ -73,6 +73,8 @@ extension DurationExtension on Duration { enum FormatMode { yearly, + mmmd, + main, grades, welcome, hmm, @@ -88,7 +90,7 @@ enum FormatMode { enum Cycle { morning, day, afternoon, night } extension DateExtension on DateTime { - String format(AppLocalizations l10n, FormatMode mode) { + String? translatedDay(AppLocalizations l10n) { var today = timeNow().getMidnight(); var tomorrowLim = today.add(Duration(days: 2)); @@ -96,32 +98,43 @@ extension DateExtension on DateTime { var yesterday = today.subtract(Duration(days: 1)); var yesterdayLim = today.subtract(Duration(days: 2)); - var weekStart = subtract(Duration(days: weekday - 1)); + if (isAfter(yesterdayLim) && isBefore(today)) { + return l10n.yesterday; + } + if (isAfter(yesterday) && isBefore(tomorrow)) { + return l10n.today; + } + if (isAfter(today) && isBefore(tomorrowLim)) { + return l10n.tomorrow; + } + + return null; + } + + String format(AppLocalizations l10n, FormatMode mode) { + var weekStart = getMonday(); var weekEnd = weekStart.add(Duration(days: 6)); switch (mode) { + case FormatMode.main: + var lastWeek = timeNow().getMidnight().subtract(Duration(days: 8)); + var isLastWeek = + lastWeek.millisecondsSinceEpoch < + getMidnight().millisecondsSinceEpoch; + final dayName = DateFormat( + 'EEEE', + l10n.localeName, + ).format(this).firstUpper(); + return translatedDay(l10n) ?? + (isLastWeek + ? "$dayName (${format(l10n, FormatMode.mmmd).firstUpper()})" + : "${format(l10n, FormatMode.yearly).firstUpper()} ($dayName)"); case FormatMode.grades: - if (isBefore(yesterdayLim)) { - final month = DateFormat( - 'MMMM', - l10n.localeName, - ).format(this).firstUpper(); - final day = DateFormat('d', l10n.localeName).format(this); - return "$month $day"; - } - if (isAfter(yesterdayLim) && isBefore(today)) { - return l10n.yesterday; - } - if (isAfter(yesterday) && isBefore(tomorrow)) { - return l10n.today; - } - if (isAfter(today) && isBefore(tomorrowLim)) { - return l10n.tomorrow; - } - - return format(l10n, FormatMode.yearly); + return translatedDay(l10n) ?? format(l10n, FormatMode.yearly); case FormatMode.yearly: - return DateFormat('MMMM dd', l10n.localeName).format(this); + return DateFormat('MMMM d', l10n.localeName).format(this); + case FormatMode.mmmd: + return DateFormat('MMM d', l10n.localeName).format(this); case FormatMode.hmm: return DateFormat('H:mm', l10n.localeName).format(this); case FormatMode.welcome: @@ -155,7 +168,10 @@ extension DateExtension on DateTime { case FormatMode.yyyymmdd: return DateFormat('yyyy. MM. dd.', l10n.localeName).format(this); case FormatMode.yyyymmddhhmmss: - return DateFormat('yyyy-MM-dd hh:mm:ss', l10n.localeName).format(this); + return DateFormat( + 'yyyy. MM. dd. H:mm:ss', + l10n.localeName, + ).format(this); } } @@ -207,55 +223,9 @@ extension DateGrouper on Iterable { Map> groupList(DateTime Function(T elem) getDate) { Map> newList = {}; - var today = timeNow(); - today = today.subtract( - Duration( - hours: today.hour, - minutes: today.minute, - seconds: today.second, - milliseconds: today.millisecond, - ), - ); - - var tomorrow = today.add(Duration(days: 1)); - var yesterday = today.subtract(Duration(days: 1)); - for (var elem in this) { var date = getDate(elem); - var day = date.subtract( - Duration( - hours: date.hour, - minutes: date.minute, - seconds: date.second, - milliseconds: date.millisecond, - ), - ); - - if (date.isAfter(tomorrow.add(Duration(days: 1)))) { - if (newList[day] == null) { - newList[day] = List.empty(growable: true); - } - - newList[day]!.add(elem); - continue; - } - if (date.isAfter(today)) { - if (newList[tomorrow] == null) { - newList[tomorrow] = List.empty(growable: true); - } - - newList[tomorrow]!.add(elem); - continue; - } - if (date.isAfter(yesterday.subtract(Duration(days: 1))) && - date.isBefore(today)) { - if (newList[yesterday] == null) { - newList[yesterday] = List.empty(growable: true); - } - - newList[yesterday]!.add(elem); - continue; - } + var day = date.getMidnight(); if (newList[day] == null) { newList[day] = List.empty(growable: true); diff --git a/firka/lib/ui/phone/pages/home/home_main.dart b/firka/lib/ui/phone/pages/home/home_main.dart index 60acbea..00abcb7 100644 --- a/firka/lib/ui/phone/pages/home/home_main.dart +++ b/firka/lib/ui/phone/pages/home/home_main.dart @@ -5,7 +5,6 @@ import 'package:firka/ui/phone/widgets/info_card.dart'; import 'package:kreta_api/kreta_api.dart'; import 'package:firka/core/extensions.dart'; import 'package:firka/ui/phone/widgets/home_main_starting_soon.dart'; -import 'package:firka/ui/phone/widgets/homework.dart'; import 'package:firka/ui/phone/widgets/lesson_small.dart'; import 'package:firka/ui/shared/delayed_spinner.dart'; import 'package:flutter/material.dart'; @@ -338,7 +337,26 @@ class _HomeMainScreen extends FirkaState { nextTest != null ? SizedBox(height: 12) : SizedBox(height: 0), Expanded( child: ListView( - children: noticeBoardWidgets.map((e) => e.$1).toList(), + children: noticeBoardWidgets + .groupList((e) => e.$2) + .entries + .map( + (e) => Column( + spacing: 10, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + e.key.format(widget.data.l10n, FormatMode.main), + style: appStyle.fonts.B_16R.apply( + color: appStyle.colors.textSecondary, + ), + ), + ...e.value.map((v) => v.$1), + SizedBox(height: 10), + ], + ), + ) + .toList(), ), ), ], diff --git a/firka/lib/ui/phone/widgets/info_card.dart b/firka/lib/ui/phone/widgets/info_card.dart index 60c391f..0bb638e 100644 --- a/firka/lib/ui/phone/widgets/info_card.dart +++ b/firka/lib/ui/phone/widgets/info_card.dart @@ -1,219 +1,219 @@ -import 'package:firka/app/app_state.dart'; -import 'package:firka/core/extensions.dart'; -import 'package:firka/data/models/homework_cache_model.dart'; -import 'package:firka/ui/components/common_bottom_sheets.dart'; -import 'package:firka/ui/components/firka_card.dart'; -import 'package:firka/ui/components/grade.dart'; -import 'package:firka/ui/components/grade_helpers.dart'; -import 'package:firka/ui/shared/class_icon.dart'; -import 'package:firka/ui/shared/firka_icon.dart'; -import 'package:firka/ui/theme/style.dart'; -import 'package:firka_common/ui/components/filled_circle.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -import 'package:kreta_api/kreta_api.dart'; -import 'package:majesticons_flutter/majesticons_flutter.dart'; - -class InfoCard extends StatelessWidget { - final void Function(BuildContext)? onTap; - final Widget icon; - final List texts; - final List right; - - final List textSyles = [ - appStyle.fonts.B_16SB.apply(color: appStyle.colors.textPrimary), - appStyle.fonts.B_14R.apply(color: appStyle.colors.textSecondary), - ]; - - InfoCard({ - required this.icon, - required this.texts, - this.right = const [], - this.onTap, - super.key, - }); - - static Widget buildSubject(Color color, Subject subject) { - return FilledCircle( - diameter: 32, - color: color.withAlpha(38), - child: ClassIconWidget( - uid: subject.uid, - className: subject.name, - category: subject.category.name!, - color: color, - size: 20, - ), - ); - } - - factory InfoCard.test(Test test) { - final color = appStyle.colors.accent; - - return InfoCard( - icon: FilledCircle( - diameter: 36, - color: color.withAlpha(38), - child: FirkaIconWidget( - FirkaIconType.majesticons, - Majesticon.editPen4Solid, - color: color, - size: 24, - ), - ), - texts: [test.theme.firstUpper(), test.subject.name.firstUpper()], - right: [buildSubject(color, test.subject)], - ); - } - - factory InfoCard.testDesc(Test test) { - final color = appStyle.colors.accent; - - return InfoCard( - icon: FilledCircle( - diameter: 36, - color: color.withAlpha(38), - child: FirkaIconWidget( - FirkaIconType.majesticons, - Majesticon.editPen4Solid, - color: color, - size: 24, - ), - ), - texts: [test.theme.firstUpper(), test.method.description.firstUpper()], - right: [buildSubject(color, test.subject)], - ); - } - - factory InfoCard.messageItem(MessageItem item) { - return InfoCard( - icon: FilledCircle( - diameter: 36, - color: appStyle.colors.accent, - child: Text( - item.author[0], - style: appStyle.fonts.H_H2.apply(color: appStyle.colors.textPrimary), - ), - ), - texts: [item.title, item.author], - onTap: (context) => context.push('/message', extra: item), - ); - } - - factory InfoCard.homework(Homework homework) { - return InfoCard( - icon: FilledCircle( - diameter: 36, - color: appStyle.colors.accent.withAlpha(38), - child: FutureBuilder( - future: isHomeworkDone(initData.isar, homework.uid), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return SizedBox(); - } - final done = snapshot.data!; - return done - ? FirkaIconWidget( - FirkaIconType.majesticonsLocal, - "homeWithMark", - color: appStyle.colors.accent, - size: 24, - ) - : FirkaIconWidget( - FirkaIconType.majesticons, - Majesticon.homeSolid, - color: appStyle.colors.accent, - size: 24, - ); - }, - ), - ), - texts: [initData.l10n.homework, homework.subjectName], - right: [buildSubject(appStyle.colors.accent, homework.subject)], - onTap: (context) => showHomeworkBottomSheet(context, initData, homework), - ); - } - - factory InfoCard.gradeSubj( - Grade grade, { - void Function(BuildContext)? onTap, - }) { - String? value = grade.numericValue == null ? grade.strValue : null; - return InfoCard( - icon: GradeWidget(grade), - texts: [ - (value ?? - grade.topic ?? - grade.mode?.description ?? - grade.type.description!) - .firstUpper(), - grade.subject.name.firstUpper(), - ], - right: [buildSubject(appStyle.colors.accent, grade.subject)], - onTap: - onTap ?? (context) => showGradeBottomSheet(context, initData, grade), - ); - } - - factory InfoCard.gradeDesc( - Grade grade, { - void Function(BuildContext)? onTap, - }) { - List texts = [ - (grade.mode?.description ?? grade.type.description!).firstUpper(), - ]; - - if (grade.topic != null) { - texts = [grade.topic!.firstUpper(), ...texts]; - } - - return InfoCard( - icon: GradeWidget(grade), - texts: texts, - right: [buildSubject(appStyle.colors.accent, grade.subject)], - onTap: - onTap ?? (context) => showGradeBottomSheet(context, initData, grade), - ); - } - - @override - Widget build(BuildContext context) { - List children = []; - int i = 0; - for (var text in texts) { - children.add( - Text(text, style: textSyles[i], overflow: TextOverflow.ellipsis), - ); - if (i < textSyles.length) { - i++; - } - } - return GestureDetector( - child: FirkaCard.single( - height: 68, - child: Row( - spacing: 12, - children: [ - SizedBox(width: 4), - icon, - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - spacing: 2, - children: children, - ), - ), - ...right, - SizedBox(width: 4), - ], - ), - ), - onTap: () { - if (onTap == null) return; - onTap!.call(context); - }, - ); - } -} +import 'package:firka/app/app_state.dart'; +import 'package:firka/core/extensions.dart'; +import 'package:firka/data/models/homework_cache_model.dart'; +import 'package:firka/ui/components/common_bottom_sheets.dart'; +import 'package:firka/ui/components/firka_card.dart'; +import 'package:firka/ui/components/grade.dart'; +import 'package:firka/ui/components/grade_helpers.dart'; +import 'package:firka/ui/shared/class_icon.dart'; +import 'package:firka/ui/shared/firka_icon.dart'; +import 'package:firka/ui/theme/style.dart'; +import 'package:firka_common/ui/components/filled_circle.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import 'package:kreta_api/kreta_api.dart'; +import 'package:majesticons_flutter/majesticons_flutter.dart'; + +class InfoCard extends StatelessWidget { + final void Function(BuildContext)? onTap; + final Widget icon; + final List texts; + final List right; + + final List textSyles = [ + appStyle.fonts.B_16SB.apply(color: appStyle.colors.textPrimary), + appStyle.fonts.B_14R.apply(color: appStyle.colors.textSecondary), + ]; + + InfoCard({ + required this.icon, + required this.texts, + this.right = const [], + this.onTap, + super.key, + }); + + static Widget buildSubject(Color color, Subject subject) { + return FilledCircle( + diameter: 32, + color: color.withAlpha(38), + child: ClassIconWidget( + uid: subject.uid, + className: subject.name, + category: subject.category.name!, + color: color, + size: 20, + ), + ); + } + + factory InfoCard.test(Test test) { + final color = appStyle.colors.accent; + + return InfoCard( + icon: FilledCircle( + diameter: 36, + color: color.withAlpha(38), + child: FirkaIconWidget( + FirkaIconType.majesticons, + Majesticon.editPen4Solid, + color: color, + size: 24, + ), + ), + texts: [test.theme.firstUpper(), test.subject.name.firstUpper()], + right: [buildSubject(color, test.subject)], + ); + } + + factory InfoCard.testDesc(Test test) { + final color = appStyle.colors.accent; + + return InfoCard( + icon: FilledCircle( + diameter: 36, + color: color.withAlpha(38), + child: FirkaIconWidget( + FirkaIconType.majesticons, + Majesticon.editPen4Solid, + color: color, + size: 24, + ), + ), + texts: [test.theme.firstUpper(), test.method.description.firstUpper()], + right: [buildSubject(color, test.subject)], + ); + } + + factory InfoCard.messageItem(MessageItem item) { + return InfoCard( + icon: FilledCircle( + diameter: 36, + color: appStyle.colors.accent, + child: Text( + item.author[0], + style: appStyle.fonts.H_H2.apply(color: appStyle.colors.textPrimary), + ), + ), + texts: [item.title, item.author], + onTap: (context) => context.push('/message', extra: item), + ); + } + + factory InfoCard.homework(Homework homework) { + return InfoCard( + icon: FilledCircle( + diameter: 36, + color: appStyle.colors.accent.withAlpha(38), + child: FutureBuilder( + future: isHomeworkDone(initData.isar, homework.uid), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return SizedBox(); + } + final done = snapshot.data!; + return done + ? FirkaIconWidget( + FirkaIconType.majesticonsLocal, + "homeWithMark", + color: appStyle.colors.accent, + size: 24, + ) + : FirkaIconWidget( + FirkaIconType.majesticons, + Majesticon.homeSolid, + color: appStyle.colors.accent, + size: 24, + ); + }, + ), + ), + texts: [initData.l10n.homework, homework.subjectName], + right: [buildSubject(appStyle.colors.accent, homework.subject)], + onTap: (context) => showHomeworkBottomSheet(context, initData, homework), + ); + } + + factory InfoCard.gradeSubj( + Grade grade, { + void Function(BuildContext)? onTap, + }) { + String? value = grade.numericValue == null ? grade.strValue : null; + return InfoCard( + icon: GradeWidget(grade), + texts: [ + (value ?? + grade.topic ?? + grade.mode?.description ?? + grade.type.description!) + .firstUpper(), + grade.subject.name.firstUpper(), + ], + right: [buildSubject(appStyle.colors.accent, grade.subject)], + onTap: + onTap ?? (context) => showGradeBottomSheet(context, initData, grade), + ); + } + + factory InfoCard.gradeDesc( + Grade grade, { + void Function(BuildContext)? onTap, + }) { + List texts = [ + (grade.mode?.description ?? grade.type.description!).firstUpper(), + ]; + + if (grade.topic != null) { + texts = [grade.topic!.firstUpper(), ...texts]; + } + + return InfoCard( + icon: GradeWidget(grade), + texts: texts, + right: [buildSubject(appStyle.colors.accent, grade.subject)], + onTap: + onTap ?? (context) => showGradeBottomSheet(context, initData, grade), + ); + } + + @override + Widget build(BuildContext context) { + List children = []; + int i = 0; + for (var text in texts) { + children.add( + Text(text, style: textSyles[i], overflow: TextOverflow.ellipsis), + ); + if (i < textSyles.length) { + i++; + } + } + return GestureDetector( + child: FirkaCard.single( + height: 68, + padding: EdgeInsets.symmetric(horizontal: 16), + margin: EdgeInsets.all(0), + child: Row( + spacing: 12, + children: [ + icon, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + spacing: 2, + children: children, + ), + ), + ...right, + ], + ), + ), + onTap: () { + if (onTap == null) return; + onTap!.call(context); + }, + ); + } +}