fix: grade subjects page design

This commit is contained in:
checkedear
2026-04-21 22:34:58 +02:00
parent 55dbf12a2f
commit ad9e2c6676
2 changed files with 477 additions and 502 deletions

View File

@@ -1,5 +1,6 @@
import 'package:firka/ui/phone/widgets/grade_summary_bar.dart';
import 'package:firka/ui/phone/widgets/info_card.dart';
import 'package:firka_common/ui/components/filled_circle.dart';
import 'package:kreta_api/kreta_api.dart';
import 'package:firka/core/extensions.dart';
import 'package:firka/ui/components/common_bottom_sheets.dart';
@@ -106,305 +107,266 @@ class _HomeGradesSubjectScreen extends FirkaState<HomeGradesSubjectScreen> {
}
Widget _buildContent(BuildContext context) {
if (grades != null && grades!.isNotEmpty && activeSubjectUid != "") {
var aGrade = grades!.first;
var groups = grades!.groupList((grade) => grade.recordDate);
final ghostGradeWidgets = _ghostEntries.reversed.map((e) {
return GestureDetector(
child: FirkaCard(
left: [
Row(
children: [
GradeWidget.gradeValue(e.$1, gradeWeight: e.$2),
SizedBox(width: 8),
Text(
'${widget.data.l10n.ghost_grade} ${e.$2}%',
style: appStyle.fonts.B_16SB.apply(
if (grades == null || grades!.isEmpty || activeSubjectUid == "") {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Transform.translate(
offset: const Offset(-4, 0),
child: GestureDetector(
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.chevronLeftLine,
color: appStyle.colors.textSecondary,
),
onTap: () {
context.pop();
},
),
),
Transform.translate(
offset: const Offset(-4, 1),
child: Text(
widget.data.l10n.subjects,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
],
),
],
),
onTap: () {},
);
}).toList();
var gradeWidgets = List<Widget>.empty(growable: true);
if (ghostGradeWidgets.isNotEmpty) {
gradeWidgets.add(
Text(
widget.data.l10n.ghost_grades,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
],
),
),
);
gradeWidgets.addAll(ghostGradeWidgets);
}
for (var group in groups.entries) {
gradeWidgets.add(SizedBox(height: 8));
gradeWidgets.add(
Text(
group.key.format(widget.data.l10n, FormatMode.grades),
style: appStyle.fonts.H_14px.apply(
color: appStyle.colors.textPrimary,
),
),
);
gradeWidgets.add(SizedBox(height: 8));
for (var grade in group.value) {
gradeWidgets.add(InfoCard.gradeDesc(grade));
}
}
return Material(
color: appStyle.colors.background,
child: Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 12),
Column(
SizedBox(height: 16),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
Card(
shadowColor: const Color.fromRGBO(0, 0, 0, 0),
color: appStyle.colors.a15p,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: EdgeInsetsGeometry.all(6),
child: ClassIconWidget(
uid: subjectId,
className: subjectName,
category: subjectCategory,
color: appStyle.colors.accent,
),
),
),
SizedBox(height: 8),
Text(
subjectName,
style: appStyle.fonts.H_H2.apply(
color: appStyle.colors.textPrimary,
),
),
SizedBox(height: 2),
Text(
widget.data.l10n.unknown_teacher,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
Expanded(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Transform.translate(
offset: const Offset(-4, 0),
child: GestureDetector(
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.chevronLeftLine,
color: appStyle.colors.textSecondary,
),
onTap: () {
context.pop();
},
),
SvgPicture.asset(
"assets/images/logos/dave.svg",
width: 48,
height: 48,
),
Transform.translate(
offset: const Offset(-4, 0),
child: Text(
widget.data.l10n.subjects,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
SizedBox(height: 12),
Text(
widget.data.l10n.no_grades,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
],
),
GestureDetector(
child: Card(
color: appStyle.colors.buttonSecondaryFill,
child: Padding(
padding: const EdgeInsets.all(4),
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.menuSolid,
size: 26.0,
color: appStyle.colors.accent,
),
),
),
onTap: () {
showSubjectBottomSheetSettings(
context,
widget.data,
aGrade.subject,
onAddFromCalculator: (g, w) {
setState(() => _ghostEntries.add((g, w)));
},
);
},
),
],
),
),
],
),
SizedBox(height: 16),
Expanded(
child: ListView(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Card(
shadowColor: const Color.fromRGBO(0, 0, 0, 0),
color: appStyle.colors.a15p,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: EdgeInsetsGeometry.all(6),
child: ClassIconWidget(
uid: aGrade.subject.uid,
className: aGrade.subject.name,
category: aGrade.subject.category.name!,
color: appStyle.colors.accent,
),
),
),
SizedBox(height: 8),
Text(
aGrade.subject.name,
style: appStyle.fonts.H_H2.apply(
color: appStyle.colors.textPrimary,
),
),
SizedBox(height: 2),
Text(
aGrade.teacher,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
SizedBox(height: 15),
],
),
GradeChartWithInteraction(
grades: _gradesWithGhosts(aGrade.subject),
),
SizedBox(height: 2),
GradeSummaryBar(
grades: _gradesWithGhosts(aGrade.subject),
l10n: widget.data.l10n,
showAverage: ghostGradeWidgets.isNotEmpty,
),
SizedBox(height: 12),
Padding(
padding: EdgeInsets.only(left: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: gradeWidgets,
),
),
],
),
),
],
),
),
);
} else {
return Material(
color: appStyle.colors.background,
child: Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
children: [
Row(
children: [
Transform.translate(
offset: const Offset(-4, 0),
child: GestureDetector(
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.chevronLeftLine,
color: appStyle.colors.textSecondary,
),
onTap: () {
context.pop();
},
),
),
Transform.translate(
offset: const Offset(-4, 1),
child: Text(
widget.data.l10n.subjects,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
),
],
),
],
),
SizedBox(height: 16),
SizedBox(
height:
MediaQuery.of(context).size.height -
MediaQuery.of(context).padding.top -
230,
child: ListView(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Card(
shadowColor: const Color.fromRGBO(0, 0, 0, 0),
color: appStyle.colors.a15p,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: EdgeInsetsGeometry.all(6),
child: ClassIconWidget(
uid: subjectId,
className: subjectName,
category: subjectCategory,
color: appStyle.colors.accent,
),
),
),
SizedBox(height: 8),
Text(
subjectName,
style: appStyle.fonts.H_H2.apply(
color: appStyle.colors.textPrimary,
),
),
SizedBox(height: 2),
Text(
widget.data.l10n.unknown_teacher,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
],
),
SizedBox(
height:
MediaQuery.of(context).size.height -
MediaQuery.of(context).padding.top -
320,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
"assets/images/logos/dave.svg",
width: 48,
height: 48,
),
SizedBox(height: 12),
Text(
widget.data.l10n.no_grades,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
],
),
),
),
],
),
),
],
),
),
],
),
);
}
var aGrade = grades!.first;
final ghostGradeWidgets = _ghostEntries.indexed
.map((e) {
return InfoCard.gradeGhost(
e.$2.$1,
e.$2.$2,
onTap: (ctx) {
setState(() {
_ghostEntries.removeAt(e.$1);
});
},
);
})
.toList()
.reversed;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Transform.translate(
offset: const Offset(-4, 0),
child: GestureDetector(
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.chevronLeftLine,
color: appStyle.colors.textSecondary,
),
onTap: () {
context.pop();
},
),
),
Transform.translate(
offset: const Offset(-4, 0),
child: Text(
widget.data.l10n.subjects,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
),
],
),
GestureDetector(
child: Card(
color: appStyle.colors.buttonSecondaryFill,
child: Padding(
padding: const EdgeInsets.all(4),
child: FirkaIconWidget(
FirkaIconType.majesticons,
Majesticon.menuSolid,
size: 26.0,
color: appStyle.colors.accent,
),
),
),
onTap: () {
showSubjectBottomSheetSettings(
context,
widget.data,
aGrade.subject,
onAddFromCalculator: (g, w) {
setState(() => _ghostEntries.add((g, w)));
},
);
},
),
],
),
SizedBox(height: 24),
Expanded(
child: ListView(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FilledCircle(
diameter: 36,
color: appStyle.colors.a15p,
child: ClassIconWidget(
uid: aGrade.subject.uid,
className: aGrade.subject.name,
category: aGrade.subject.category.name!,
color: appStyle.colors.accent,
size: 24,
),
),
SizedBox(height: 16),
Text(
aGrade.subject.name,
style: appStyle.fonts.H_H2.apply(
color: appStyle.colors.textPrimary,
),
),
SizedBox(height: 4),
Text(
aGrade.teacher,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
],
),
SizedBox(height: 24),
GradeChartWithInteraction(
grades: _gradesWithGhosts(aGrade.subject),
),
SizedBox(height: 10),
GradeSummaryBar(
grades: _gradesWithGhosts(aGrade.subject),
l10n: widget.data.l10n,
showAverage: ghostGradeWidgets.isNotEmpty,
),
SizedBox(height: 20),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 20,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 10,
children: [
if (ghostGradeWidgets.isNotEmpty)
Text(
initData.l10n.ghost_grades,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textSecondary,
),
),
...ghostGradeWidgets,
],
),
...grades!
.groupList((e) => e.recordDate)
.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) => InfoCard.gradeDesc(v)),
],
),
),
],
),
],
),
),
],
),
);
}
}

View File

@@ -1,219 +1,232 @@
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<String> texts;
final List<Widget> right;
final List<TextStyle> 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<bool>(
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<String> 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<Text> 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);
},
);
}
}
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<String> texts;
final List<Widget> right;
final List<TextStyle> 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<bool>(
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.gradeGhost(
int gradeValue,
int gradeWeigth, {
void Function(BuildContext)? onTap,
}) {
return InfoCard(
icon: GradeWidget.gradeValue(gradeValue, gradeWeight: gradeWeigth),
texts: ["${initData.l10n.ghost_grade} ($gradeWeigth%)"],
right: [],
onTap: onTap,
);
}
factory InfoCard.gradeDesc(
Grade grade, {
void Function(BuildContext)? onTap,
}) {
List<String> 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<Text> 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);
},
);
}
}