From 483c8de0c0f70a3aa4dcc4056327c3580d225d1e Mon Sep 17 00:00:00 2001 From: Armand <4831c0@proton.me> Date: Mon, 2 Mar 2026 21:15:17 +0100 Subject: [PATCH] firka: add grade calculator bottom sheet from Figma --- firka/lib/l10n | 2 +- .../ui/components/common_bottom_sheets.dart | 284 +++++++++++++++++- 2 files changed, 283 insertions(+), 3 deletions(-) diff --git a/firka/lib/l10n b/firka/lib/l10n index d85741b4..a52a907c 160000 --- a/firka/lib/l10n +++ b/firka/lib/l10n @@ -1 +1 @@ -Subproject commit d85741b4dacc9409a5ba016750a91f2e18c5fbd8 +Subproject commit a52a907c9f02f90750b71e7144aeb5d0545465a8 diff --git a/firka/lib/ui/components/common_bottom_sheets.dart b/firka/lib/ui/components/common_bottom_sheets.dart index 8a43e443..6f1d230e 100644 --- a/firka/lib/ui/components/common_bottom_sheets.dart +++ b/firka/lib/ui/components/common_bottom_sheets.dart @@ -21,6 +21,7 @@ import 'package:go_router/go_router.dart'; import 'package:firka/ui/shared/class_icon.dart'; import 'package:firka/ui/components/firka_card.dart'; import 'package:firka/ui/components/grade.dart'; +import 'package:firka/ui/components/grade_helpers.dart'; Future showLessonBottomSheet( BuildContext context, @@ -982,7 +983,7 @@ Future showHomeworkBottomSheet( ); } -Future showSubjectBottomSheetSettings( +Future showGradeCalculatorBottomSheet( BuildContext context, AppInitialization data, Subject subject, @@ -1004,6 +1005,278 @@ Future showSubjectBottomSheetSettings( child: Container(color: Colors.transparent), ), ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + decoration: BoxDecoration( + color: appStyle.colors.background, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 40, 20, 20), + child: _GradeCalculatorSheetContent( + data: data, + subject: subject, + ), + ), + ), + ), + ], + ); + }, + ); +} + +class _GradeCalculatorSheetContent extends StatefulWidget { + final AppInitialization data; + final Subject subject; + + const _GradeCalculatorSheetContent({ + required this.data, + required this.subject, + }); + + @override + State<_GradeCalculatorSheetContent> createState() => + _GradeCalculatorSheetContentState(); +} + +class _GradeCalculatorSheetContentState + extends State<_GradeCalculatorSheetContent> { + int selectedGrade = 3; + int weightPercent = 100; + final List<(int grade, int weight)> entries = []; + + double get _weightedAverage { + if (entries.isEmpty) return 0; + double sum = 0; + double weightTotal = 0; + for (final e in entries) { + final w = e.$2 / 100.0; + weightTotal += w; + sum += e.$1 * w; + } + return weightTotal > 0 ? sum / weightTotal : 0; + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Center( + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: appStyle.colors.a15p, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + widget.data.l10n.grade_calculator, + style: appStyle.fonts.H_H2.apply( + color: appStyle.colors.textPrimary, + ), + ), + ), + GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: appStyle.colors.buttonSecondaryFill, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: appStyle.colors.shadowColor, + blurRadius: appStyle.colors.shadowBlur.toDouble(), + offset: Offset(0, 1), + ), + ], + ), + child: Icon( + Icons.close, + size: 20, + color: appStyle.colors.textPrimary, + ), + ), + ), + ], + ), + SizedBox(height: 20), + Container( + height: 64, + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: appStyle.colors.card, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: appStyle.colors.shadowColor, + blurRadius: appStyle.colors.shadowBlur.toDouble(), + offset: Offset(0, 1), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [1, 2, 3, 4, 5].map((grade) { + final isSelected = selectedGrade == grade; + final gradeColor = getGradeColor(grade.toDouble()); + return GestureDetector( + onTap: () => setState(() => selectedGrade = grade), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + decoration: BoxDecoration( + color: isSelected + ? appStyle.colors.buttonSecondaryFill + : Colors.transparent, + borderRadius: BorderRadius.circular(10), + ), + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: gradeColor.withAlpha(38), + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Text( + '$grade', + style: appStyle.fonts.H_14px.copyWith( + fontSize: 18, + color: gradeColor, + ), + ), + ), + ), + ); + }).toList(), + ), + ), + SizedBox(height: 12), + Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: appStyle.colors.card, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: appStyle.colors.shadowColor, + blurRadius: appStyle.colors.shadowBlur.toDouble(), + offset: Offset(0, 1), + ), + ], + ), + child: SliderTheme( + data: SliderThemeData( + activeTrackColor: appStyle.colors.accent, + inactiveTrackColor: appStyle.colors.card, + thumbColor: appStyle.colors.accent, + overlayColor: appStyle.colors.a10p, + trackHeight: 8, + ), + child: Slider( + value: weightPercent.toDouble(), + min: 0, + max: 100, + divisions: 100, + onChanged: (v) => setState(() => weightPercent = v.round()), + ), + ), + ), + ), + SizedBox(width: 12), + SizedBox( + width: 48, + child: Text( + '$weightPercent%', + style: appStyle.fonts.B_16R.apply( + color: appStyle.colors.textPrimary, + ), + ), + ), + ], + ), + SizedBox(height: 12), + SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: appStyle.colors.accent, + foregroundColor: appStyle.colors.textPrimary, + elevation: 1, + shadowColor: appStyle.colors.shadowColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: () { + setState(() { + entries.add((selectedGrade, weightPercent)); + }); + }, + child: Text( + widget.data.l10n.grade_calculator_add, + style: appStyle.fonts.H_18px.apply( + color: appStyle.colors.textPrimary, + ), + ), + ), + ), + if (entries.isNotEmpty) ...[ + SizedBox(height: 16), + Text( + '${widget.data.l10n.subject_avg}: ${_weightedAverage.toStringAsFixed(2)}', + style: appStyle.fonts.B_14R.apply( + color: appStyle.colors.textPrimary, + ), + ), + ], + ], + ); + } +} + +Future showSubjectBottomSheetSettings( + BuildContext context, + AppInitialization data, + Subject subject, +) async { + final parentContext = context; + showModalBottomSheet( + context: context, + elevation: 100, + isScrollControlled: true, + enableDrag: true, + backgroundColor: Colors.transparent, + barrierColor: appStyle.colors.a15p, + builder: (BuildContext sheetContext) { + return Stack( + children: [ + Positioned.fill( + child: GestureDetector( + onTap: () => Navigator.pop(sheetContext), + behavior: HitTestBehavior.opaque, + child: Container(color: Colors.transparent), + ), + ), Align( alignment: Alignment.bottomCenter, child: Container( @@ -1036,7 +1309,14 @@ Future showSubjectBottomSheetSettings( ), SizedBox(height: 20), GestureDetector( - onTap: () => Navigator.pop(context), + onTap: () { + Navigator.pop(sheetContext); + showGradeCalculatorBottomSheet( + parentContext, + data, + subject, + ); + }, child: Container( height: 56, padding: const EdgeInsets.symmetric(