changed everything from filcnaplo to refilc finally
This commit is contained in:
106
refilc_mobile_ui/lib/pages/grades/average_selector.dart
Normal file
106
refilc_mobile_ui/lib/pages/grades/average_selector.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
// import 'package:refilc_plus/models/premium_scopes.dart';
|
||||
// import 'package:refilc_plus/providers/premium_provider.dart';
|
||||
// import 'package:refilc_plus/ui/mobile/premium/upsell.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:refilc_mobile_ui/pages/grades/grades_page.i18n.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
// import 'package:provider/provider.dart';
|
||||
|
||||
final Map<int, String> avgDropItems = {
|
||||
0: "annual_average",
|
||||
90: "3_months_average",
|
||||
30: "30_days_average",
|
||||
14: "14_days_average",
|
||||
7: "7_days_average",
|
||||
};
|
||||
|
||||
class AverageSelector extends StatefulWidget {
|
||||
const AverageSelector({super.key, this.onChanged, required this.value});
|
||||
|
||||
final Function(int?)? onChanged;
|
||||
final int value;
|
||||
|
||||
@override
|
||||
AverageSelectorState createState() => AverageSelectorState();
|
||||
}
|
||||
|
||||
class AverageSelectorState extends State<AverageSelector> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<DropdownMenuItem<int>> dropdownItems = avgDropItems.keys.map((item) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: item,
|
||||
child: Text(
|
||||
avgDropItems[item]!.i18n,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.of(context).text,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return DropdownButton2<int>(
|
||||
items: dropdownItems,
|
||||
onChanged: (int? value) {
|
||||
// if (Provider.of<PremiumProvider>(context, listen: false)
|
||||
// .hasScope(PremiumScopes.gradeStats)) {
|
||||
if (widget.onChanged != null) {
|
||||
setState(() {
|
||||
widget.onChanged!(value);
|
||||
});
|
||||
}
|
||||
// } else {
|
||||
// PremiumLockedFeatureUpsell.show(
|
||||
// context: context, feature: PremiumFeature.gradestats);
|
||||
// }
|
||||
},
|
||||
value: widget.value,
|
||||
iconSize: 14,
|
||||
iconEnabledColor: AppColors.of(context).text,
|
||||
iconDisabledColor: AppColors.of(context).text,
|
||||
underline: const SizedBox(),
|
||||
itemHeight: 40,
|
||||
itemPadding: const EdgeInsets.only(left: 14, right: 14),
|
||||
dropdownWidth: 200,
|
||||
dropdownPadding: null,
|
||||
buttonDecoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
dropdownDecoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
dropdownElevation: 8,
|
||||
scrollbarRadius: const Radius.circular(40),
|
||||
scrollbarThickness: 6,
|
||||
scrollbarAlwaysShow: true,
|
||||
offset: const Offset(-10, -10),
|
||||
buttonSplashColor: Colors.transparent,
|
||||
customButton: SizedBox(
|
||||
height: 30,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
avgDropItems[widget.value]!.i18n,
|
||||
style: Theme.of(context).textTheme.titleSmall!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.of(context).text.withOpacity(0.65)),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
Icon(
|
||||
FeatherIcons.chevronDown,
|
||||
size: 16,
|
||||
color: AppColors.of(context).text,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:refilc_kreta_api/models/category.dart';
|
||||
import 'package:refilc_kreta_api/models/grade.dart';
|
||||
import 'package:refilc_kreta_api/models/subject.dart';
|
||||
import 'package:refilc_kreta_api/models/teacher.dart';
|
||||
import 'package:refilc_mobile_ui/common/custom_snack_bar.dart';
|
||||
import 'package:refilc_mobile_ui/common/material_action_button.dart';
|
||||
import 'package:refilc/ui/widgets/grade/grade_tile.dart';
|
||||
import 'package:refilc_mobile_ui/pages/grades/calculator/grade_calculator_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'grade_calculator.i18n.dart';
|
||||
|
||||
class GradeCalculator extends StatefulWidget {
|
||||
const GradeCalculator(this.subject, {super.key});
|
||||
|
||||
final GradeSubject? subject;
|
||||
|
||||
@override
|
||||
GradeCalculatorState createState() => GradeCalculatorState();
|
||||
}
|
||||
|
||||
class GradeCalculatorState extends State<GradeCalculator> {
|
||||
late GradeCalculatorProvider calculatorProvider;
|
||||
|
||||
final _weightController = TextEditingController(text: "100");
|
||||
|
||||
double newValue = 5.0;
|
||||
double newWeight = 100.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
calculatorProvider = Provider.of<GradeCalculatorProvider>(context);
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
"Grade Calculator".i18n,
|
||||
style:
|
||||
const TextStyle(fontSize: 20.0, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
|
||||
// Grade value
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: Slider(
|
||||
thumbColor: Theme.of(context).colorScheme.secondary,
|
||||
activeColor: Theme.of(context).colorScheme.secondary,
|
||||
value: newValue,
|
||||
min: 1.0,
|
||||
max: 5.0,
|
||||
divisions: 4,
|
||||
label: "${newValue.toInt()}",
|
||||
onChanged: (value) => setState(() => newValue = value),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 80.0,
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: Center(
|
||||
child: GradeValueWidget(
|
||||
GradeValue(newValue.toInt(), "", "", 0))),
|
||||
),
|
||||
]),
|
||||
|
||||
// Grade weight
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: Slider(
|
||||
thumbColor: Theme.of(context).colorScheme.secondary,
|
||||
activeColor: Theme.of(context).colorScheme.secondary,
|
||||
value: newWeight.clamp(50, 400),
|
||||
min: 50.0,
|
||||
max: 400.0,
|
||||
divisions: 7,
|
||||
label: "${newWeight.toInt()}%",
|
||||
onChanged: (value) => setState(() {
|
||||
newWeight = value;
|
||||
_weightController.text = newWeight.toInt().toString();
|
||||
}),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 80.0,
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: Center(
|
||||
child: TextField(
|
||||
controller: _weightController,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600, fontSize: 22.0),
|
||||
autocorrect: false,
|
||||
textAlign: TextAlign.right,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
|
||||
LengthLimitingTextInputFormatter(3),
|
||||
],
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
hintText: "100",
|
||||
suffixText: "%",
|
||||
suffixStyle: TextStyle(fontSize: 18.0),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
newWeight = double.tryParse(value) ?? 100.0;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
Container(
|
||||
width: 120.0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: MaterialActionButton(
|
||||
child: Text("Add Grade".i18n),
|
||||
onPressed: () {
|
||||
if (calculatorProvider.ghosts.length >= 50) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(
|
||||
content: Text("limit_reached".i18n), context: context));
|
||||
return;
|
||||
}
|
||||
|
||||
DateTime date;
|
||||
|
||||
if (calculatorProvider.ghosts.isNotEmpty) {
|
||||
List<Grade> grades = calculatorProvider.ghosts;
|
||||
grades.sort((a, b) => -a.writeDate.compareTo(b.writeDate));
|
||||
date = grades.first.date.add(const Duration(days: 7));
|
||||
} else {
|
||||
List<Grade> grades = calculatorProvider.grades
|
||||
.where((e) =>
|
||||
e.type == GradeType.midYear &&
|
||||
(e.subject == widget.subject ||
|
||||
widget.subject == null))
|
||||
.toList();
|
||||
grades.sort((a, b) => -a.writeDate.compareTo(b.writeDate));
|
||||
date = grades.first.date;
|
||||
}
|
||||
|
||||
calculatorProvider.addGhost(Grade(
|
||||
id: randomId(),
|
||||
date: date,
|
||||
writeDate: date,
|
||||
description: "Ghost Grade".i18n,
|
||||
value:
|
||||
GradeValue(newValue.toInt(), "", "", newWeight.toInt()),
|
||||
teacher: Teacher.fromString("Ghost"),
|
||||
type: GradeType.ghost,
|
||||
form: "",
|
||||
subject: widget.subject ??
|
||||
GradeSubject(
|
||||
id: randomId(),
|
||||
category: Category(id: randomId()),
|
||||
name: 'All',
|
||||
),
|
||||
mode: Category.fromJson({}),
|
||||
seenDate: DateTime(0),
|
||||
groupId: "",
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String randomId() {
|
||||
var rng = Random();
|
||||
return rng.nextInt(1000000000).toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"Grades": "Grades",
|
||||
"Ghost Grade": "Ghost Grade",
|
||||
"Grade Calculator": "Average calculator",
|
||||
"Add Grade": "Add Grade",
|
||||
"limit_reached": "You cannot add more Ghost Grades.",
|
||||
},
|
||||
"hu_hu": {
|
||||
"Grades": "Jegyek",
|
||||
"Ghost Grade": "Szellem jegy",
|
||||
"Grade Calculator": "Átlag számoló",
|
||||
"Add Grade": "Hozzáadás",
|
||||
"limit_reached": "Nem adhatsz hozzá több jegyet.",
|
||||
},
|
||||
"de_de": {
|
||||
"Grades": "Noten",
|
||||
"Ghost Grade": "Geist Noten",
|
||||
"Grade Calculator": "Mittelwert-Rechner",
|
||||
"Add Grade": "Hinzufügen",
|
||||
"limit_reached": "Sie können keine weiteren Noten hinzufügen.",
|
||||
},
|
||||
};
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
String fill(List<Object> params) => localizeFill(this, params);
|
||||
String plural(int value) => localizePlural(value, this, _t);
|
||||
String version(Object modifier) => localizeVersion(modifier, this, _t);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// import 'package:refilc/api/providers/database_provider.dart';
|
||||
// import 'package:refilc/api/providers/user_provider.dart';
|
||||
// import 'package:refilc/models/settings.dart';
|
||||
// import 'package:refilc_kreta_api/client/client.dart';
|
||||
import 'package:refilc_kreta_api/providers/grade_provider.dart';
|
||||
import 'package:refilc_kreta_api/models/grade.dart';
|
||||
|
||||
class GradeCalculatorProvider extends GradeProvider {
|
||||
GradeCalculatorProvider({
|
||||
super.initialGrades,
|
||||
required super.settings,
|
||||
required super.user,
|
||||
required super.database,
|
||||
required super.kreta,
|
||||
});
|
||||
|
||||
List<Grade> _grades = [];
|
||||
List<Grade> _ghosts = [];
|
||||
@override
|
||||
List<Grade> get grades => _grades + _ghosts;
|
||||
List<Grade> get ghosts => _ghosts;
|
||||
|
||||
void addGhost(Grade grade) {
|
||||
_ghosts.add(grade);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void addGrade(Grade grade) {
|
||||
_grades.add(grade);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void removeGrade(Grade ghost) {
|
||||
_ghosts.removeWhere((e) => ghost.id == e.id);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void addAllGrades(List<Grade> grades) {
|
||||
_grades.addAll(grades);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_grades = [];
|
||||
_ghosts = [];
|
||||
}
|
||||
}
|
||||
40
refilc_mobile_ui/lib/pages/grades/fail_warning.dart
Normal file
40
refilc_mobile_ui/lib/pages/grades/fail_warning.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'package:refilc_kreta_api/models/subject.dart';
|
||||
import 'package:refilc_mobile_ui/common/panel/panel.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'grades_page.i18n.dart';
|
||||
|
||||
class FailWarning extends StatelessWidget {
|
||||
const FailWarning({super.key, required this.subjectAvgs});
|
||||
|
||||
final Map<GradeSubject, double> subjectAvgs;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final failingSubjectCount =
|
||||
subjectAvgs.values.where((avg) => avg < 2.0).length;
|
||||
|
||||
if (failingSubjectCount == 0) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24.0),
|
||||
child: Panel(
|
||||
title: Text("fail_warning".i18n),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
FeatherIcons.alertTriangle,
|
||||
color: Colors.orange.withOpacity(.5),
|
||||
size: 20.0,
|
||||
),
|
||||
const SizedBox(width: 12.0),
|
||||
Text("fail_warning_description".i18n.fill([failingSubjectCount])),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
367
refilc_mobile_ui/lib/pages/grades/grade_subject_view.dart
Normal file
367
refilc_mobile_ui/lib/pages/grades/grade_subject_view.dart
Normal file
@@ -0,0 +1,367 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:refilc/api/providers/database_provider.dart';
|
||||
import 'package:refilc/api/providers/user_provider.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
// import 'package:refilc_kreta_api/client/api.dart';
|
||||
// import 'package:refilc_kreta_api/client/client.dart';
|
||||
import 'package:refilc_kreta_api/providers/grade_provider.dart';
|
||||
import 'package:refilc/helpers/average_helper.dart';
|
||||
import 'package:refilc/helpers/subject.dart';
|
||||
import 'package:refilc_kreta_api/models/grade.dart';
|
||||
import 'package:refilc_kreta_api/models/subject.dart';
|
||||
import 'package:refilc_mobile_ui/common/average_display.dart';
|
||||
import 'package:refilc_mobile_ui/common/bottom_sheet_menu/rounded_bottom_sheet.dart';
|
||||
import 'package:refilc_mobile_ui/common/panel/panel.dart';
|
||||
import 'package:refilc_mobile_ui/common/trend_display.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/cretification/certification_tile.dart';
|
||||
import 'package:refilc/ui/widgets/grade/grade_tile.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/grade/grade_viewable.dart';
|
||||
import 'package:refilc_mobile_ui/common/hero_scrollview.dart';
|
||||
import 'package:refilc_mobile_ui/pages/grades/calculator/grade_calculator.dart';
|
||||
import 'package:refilc_mobile_ui/pages/grades/calculator/grade_calculator_provider.dart';
|
||||
import 'package:refilc_mobile_ui/pages/grades/grades_count.dart';
|
||||
import 'package:refilc_mobile_ui/pages/grades/graph.dart';
|
||||
import 'package:refilc_mobile_ui/pages/grades/subject_grades_container.dart';
|
||||
import 'package:refilc_plus/ui/mobile/goal_planner/goal_planner_screen.dart';
|
||||
// import 'package:refilc_plus/models/premium_scopes.dart';
|
||||
// import 'package:refilc_plus/providers/premium_provider.dart';
|
||||
import 'package:refilc_plus/ui/mobile/goal_planner/goal_state_screen.dart';
|
||||
// import 'package:refilc_plus/ui/mobile/premium/upsell.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'grades_page.i18n.dart';
|
||||
// import 'package:refilc_plus/ui/mobile/goal_planner/new_goal.dart';
|
||||
|
||||
class GradeSubjectView extends StatefulWidget {
|
||||
const GradeSubjectView(this.subject, {super.key, this.groupAverage = 0.0});
|
||||
|
||||
final GradeSubject subject;
|
||||
final double groupAverage;
|
||||
|
||||
void push(BuildContext context, {bool root = false}) {
|
||||
Navigator.of(context, rootNavigator: root)
|
||||
.push(CupertinoPageRoute(builder: (context) => this));
|
||||
}
|
||||
|
||||
@override
|
||||
State<GradeSubjectView> createState() => _GradeSubjectViewState();
|
||||
}
|
||||
|
||||
class _GradeSubjectViewState extends State<GradeSubjectView> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
// Controllers
|
||||
PersistentBottomSheetController? _sheetController;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
List<Widget> gradeTiles = [];
|
||||
|
||||
// Providers
|
||||
late GradeProvider gradeProvider;
|
||||
late GradeCalculatorProvider calculatorProvider;
|
||||
late SettingsProvider settingsProvider;
|
||||
late DatabaseProvider dbProvider;
|
||||
late UserProvider user;
|
||||
|
||||
late double average;
|
||||
late Widget gradeGraph;
|
||||
|
||||
bool gradeCalcMode = false;
|
||||
|
||||
String plan = '';
|
||||
|
||||
List<Grade> getSubjectGrades(GradeSubject subject) => !gradeCalcMode
|
||||
? gradeProvider.grades.where((e) => e.subject == subject).toList()
|
||||
: calculatorProvider.grades.where((e) => e.subject == subject).toList();
|
||||
|
||||
bool showGraph(List<Grade> subjectGrades) {
|
||||
if (gradeCalcMode) return true;
|
||||
|
||||
final gradeDates = subjectGrades.map((e) => e.date.millisecondsSinceEpoch);
|
||||
final maxGradeDate = gradeDates.fold(0, max);
|
||||
final minGradeDate = gradeDates.fold(0, min);
|
||||
if (maxGradeDate - minGradeDate < const Duration(days: 5).inMilliseconds) {
|
||||
return false; // naplo/#78
|
||||
}
|
||||
|
||||
return subjectGrades.where((e) => e.type == GradeType.midYear).length > 1;
|
||||
}
|
||||
|
||||
void buildTiles(List<Grade> subjectGrades) {
|
||||
List<Widget> tiles = [];
|
||||
|
||||
if (showGraph(subjectGrades)) {
|
||||
tiles.add(gradeGraph);
|
||||
} else {
|
||||
tiles.add(Container(height: 24.0));
|
||||
}
|
||||
|
||||
tiles.add(Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24.0),
|
||||
child: Panel(
|
||||
child: GradesCount(grades: getSubjectGrades(widget.subject).toList()),
|
||||
),
|
||||
));
|
||||
|
||||
// ignore: no_leading_underscores_for_local_identifiers
|
||||
List<Widget> _gradeTiles = [];
|
||||
|
||||
if (!gradeCalcMode) {
|
||||
subjectGrades.sort((a, b) => -a.date.compareTo(b.date));
|
||||
for (var grade in subjectGrades) {
|
||||
if (grade.type == GradeType.midYear) {
|
||||
_gradeTiles.add(GradeViewable(grade));
|
||||
} else {
|
||||
_gradeTiles.add(CertificationTile(grade, padding: EdgeInsets.zero));
|
||||
}
|
||||
}
|
||||
} else if (subjectGrades.isNotEmpty) {
|
||||
subjectGrades.sort((a, b) => -a.date.compareTo(b.date));
|
||||
for (var grade in subjectGrades) {
|
||||
_gradeTiles.add(GradeTile(grade));
|
||||
}
|
||||
}
|
||||
tiles.add(
|
||||
PageTransitionSwitcher(
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.vertical,
|
||||
fillColor: Colors.transparent,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: _gradeTiles.isNotEmpty
|
||||
? Panel(
|
||||
key: ValueKey(gradeCalcMode),
|
||||
title: Text(
|
||||
gradeCalcMode ? "Ghost Grades".i18n : "Grades".i18n,
|
||||
),
|
||||
child: Column(
|
||||
children: _gradeTiles,
|
||||
))
|
||||
: const SizedBox(),
|
||||
),
|
||||
);
|
||||
|
||||
tiles.add(Padding(
|
||||
padding: EdgeInsets.only(bottom: !gradeCalcMode ? 24.0 : 250.0)));
|
||||
gradeTiles = List.castFrom(tiles);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
user = Provider.of<UserProvider>(context, listen: false);
|
||||
dbProvider = Provider.of<DatabaseProvider>(context, listen: false);
|
||||
}
|
||||
|
||||
void fetchGoalPlans() async {
|
||||
plan = (await dbProvider.userQuery
|
||||
.subjectGoalPlans(userId: user.id!))[widget.subject.id] ??
|
||||
'';
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
gradeProvider = Provider.of<GradeProvider>(context);
|
||||
calculatorProvider = Provider.of<GradeCalculatorProvider>(context);
|
||||
settingsProvider = Provider.of<SettingsProvider>(context);
|
||||
|
||||
List<Grade> subjectGrades = getSubjectGrades(widget.subject).toList();
|
||||
average = AverageHelper.averageEvals(subjectGrades);
|
||||
final prevAvg = subjectGrades.isNotEmpty
|
||||
? AverageHelper.averageEvals(subjectGrades
|
||||
.where((e) => e.date.isBefore(subjectGrades
|
||||
.reduce((v, e) => e.date.isAfter(v.date) ? e : v)
|
||||
.date
|
||||
.subtract(const Duration(days: 30))))
|
||||
.toList())
|
||||
: 0.0;
|
||||
|
||||
gradeGraph = Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0, bottom: 8.0),
|
||||
child: Panel(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("annual_average".i18n),
|
||||
if (average != prevAvg)
|
||||
TrendDisplay(current: average, previous: prevAvg),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 16.0, right: 12.0),
|
||||
child: GradeGraph(subjectGrades,
|
||||
dayThreshold: 5, classAvg: widget.groupAverage),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (!gradeCalcMode) {
|
||||
buildTiles(subjectGrades);
|
||||
} else {
|
||||
List<Grade> ghostGrades = calculatorProvider.ghosts
|
||||
.where((e) => e.subject == widget.subject)
|
||||
.toList();
|
||||
buildTiles(ghostGrades);
|
||||
}
|
||||
|
||||
fetchGoalPlans();
|
||||
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
floatingActionButtonLocation: ExpandableFab.location,
|
||||
floatingActionButton: Visibility(
|
||||
visible: !gradeCalcMode &&
|
||||
subjectGrades
|
||||
.where((e) => e.type == GradeType.midYear)
|
||||
.isNotEmpty,
|
||||
child: ExpandableFab(
|
||||
type: ExpandableFabType.up,
|
||||
distance: 50,
|
||||
childrenOffset: const Offset(-3.8, 0.0),
|
||||
children: [
|
||||
FloatingActionButton.small(
|
||||
heroTag: "btn_ghost_grades",
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
onPressed: () {
|
||||
gradeCalc(context);
|
||||
},
|
||||
child: const Icon(FeatherIcons.plus),
|
||||
),
|
||||
FloatingActionButton.small(
|
||||
heroTag: "btn_goal_planner",
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
onPressed: () {
|
||||
// if (!Provider.of<PremiumProvider>(context, listen: false)
|
||||
// .hasScope(PremiumScopes.goalPlanner)) {
|
||||
// PremiumLockedFeatureUpsell.show(
|
||||
// context: context, feature: PremiumFeature.goalplanner);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// ScaffoldMessenger.of(context).showSnackBar(
|
||||
// const SnackBar(content: Text("Hamarosan...")));
|
||||
|
||||
Navigator.of(context).push(CupertinoPageRoute(
|
||||
builder: (context) =>
|
||||
GoalPlannerScreen(subject: widget.subject)));
|
||||
},
|
||||
child: const Icon(FeatherIcons.flag, size: 20.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {},
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
child: HeroScrollView(
|
||||
onClose: () {
|
||||
if (_sheetController != null && gradeCalcMode) {
|
||||
_sheetController!.close();
|
||||
} else {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
navBarItems: [
|
||||
const SizedBox(width: 6.0),
|
||||
if (widget.groupAverage != 0)
|
||||
Center(
|
||||
child: AverageDisplay(
|
||||
average: widget.groupAverage, border: true)),
|
||||
const SizedBox(width: 6.0),
|
||||
if (average != 0)
|
||||
Center(child: AverageDisplay(average: average)),
|
||||
const SizedBox(width: 6.0),
|
||||
if (plan != '')
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(CupertinoPageRoute(
|
||||
builder: (context) =>
|
||||
GoalStateScreen(subject: widget.subject)));
|
||||
},
|
||||
child: Container(
|
||||
width: 54.0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(45.0),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(.15),
|
||||
),
|
||||
child: Icon(
|
||||
FeatherIcons.flag,
|
||||
size: 17.0,
|
||||
weight: 2.5,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12.0),
|
||||
],
|
||||
icon: SubjectIcon.resolveVariant(
|
||||
subject: widget.subject, context: context),
|
||||
scrollController: _scrollController,
|
||||
title: widget.subject.renamedTo ?? widget.subject.name.capital(),
|
||||
italic: settingsProvider.renamedSubjectsItalics &&
|
||||
widget.subject.isRenamed,
|
||||
child: SubjectGradesContainer(
|
||||
child: CupertinoScrollbar(
|
||||
child: ListView.builder(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) => gradeTiles[index],
|
||||
itemCount: gradeTiles.length,
|
||||
),
|
||||
),
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
||||
void gradeCalc(BuildContext context) {
|
||||
// Scroll to the top of the page
|
||||
_scrollController.animateTo(100,
|
||||
duration: const Duration(milliseconds: 500), curve: Curves.ease);
|
||||
|
||||
calculatorProvider.clear();
|
||||
calculatorProvider.addAllGrades(gradeProvider.grades);
|
||||
|
||||
_sheetController = _scaffoldKey.currentState?.showBottomSheet(
|
||||
(context) => RoundedBottomSheet(
|
||||
borderRadius: 14.0, child: GradeCalculator(widget.subject)),
|
||||
backgroundColor: const Color(0x00000000),
|
||||
elevation: 12.0,
|
||||
);
|
||||
|
||||
// Hide the fab and grades
|
||||
setState(() {
|
||||
gradeCalcMode = true;
|
||||
});
|
||||
|
||||
_sheetController!.closed.then((value) {
|
||||
// Show fab and grades
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
gradeCalcMode = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
70
refilc_mobile_ui/lib/pages/grades/grades_count.dart
Normal file
70
refilc_mobile_ui/lib/pages/grades/grades_count.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:refilc_kreta_api/models/grade.dart';
|
||||
import 'package:refilc_mobile_ui/pages/grades/grades_count_item.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
class GradesCount extends StatelessWidget {
|
||||
const GradesCount({super.key, required this.grades});
|
||||
|
||||
final List<Grade> grades;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<int> gradesCount = List.generate(5,
|
||||
(int index) => grades.where((e) => e.value.value == index + 1).length);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// TODO: make a new widget here, cuz this will not fit
|
||||
// Text.rich(
|
||||
// TextSpan(children: [
|
||||
// TextSpan(
|
||||
// text: gradesCount.reduce((a, b) => a + b).toString(),
|
||||
// style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
// ),
|
||||
// const TextSpan(
|
||||
// text: "x",
|
||||
// style: TextStyle(
|
||||
// fontSize: 13.0,
|
||||
// ),
|
||||
// ),
|
||||
// ]),
|
||||
// style: const TextStyle(fontSize: 15.0),
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// width: 10.0,
|
||||
// ),
|
||||
// ClipRRect(
|
||||
// borderRadius: BorderRadius.circular(10.0),
|
||||
// child: VerticalDivider(
|
||||
// width: 2,
|
||||
// thickness: 2,
|
||||
// indent: 2,
|
||||
// endIndent: 2,
|
||||
// color: MediaQuery.of(context).platformBrightness ==
|
||||
// Brightness.light
|
||||
// ? Colors.grey.shade300
|
||||
// : Colors.grey.shade700,
|
||||
// ),
|
||||
// ),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: gradesCount
|
||||
.mapIndexed((index, e) => Padding(
|
||||
padding: const EdgeInsets.only(left: 9.69),
|
||||
child: GradesCountItem(count: e, value: index + 1)))
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
34
refilc_mobile_ui/lib/pages/grades/grades_count_item.dart
Normal file
34
refilc_mobile_ui/lib/pages/grades/grades_count_item.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'package:refilc/ui/widgets/grade/grade_tile.dart';
|
||||
import 'package:refilc_kreta_api/models/grade.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GradesCountItem extends StatelessWidget {
|
||||
const GradesCountItem({super.key, required this.count, required this.value});
|
||||
|
||||
final int count;
|
||||
final int value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(children: [
|
||||
TextSpan(
|
||||
text: count.toString(),
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const TextSpan(
|
||||
text: "x",
|
||||
style: TextStyle(fontSize: 13.0),
|
||||
),
|
||||
]),
|
||||
style: const TextStyle(fontSize: 15.0),
|
||||
),
|
||||
const SizedBox(width: 3.0),
|
||||
GradeValueWidget(GradeValue(value, "Value", "Value", 100),
|
||||
size: 18.0, fill: true, shadow: false),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
624
refilc_mobile_ui/lib/pages/grades/grades_page.dart
Normal file
624
refilc_mobile_ui/lib/pages/grades/grades_page.dart
Normal file
@@ -0,0 +1,624 @@
|
||||
// ignore_for_file: no_leading_underscores_for_local_identifiers
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:refilc/api/providers/update_provider.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/ui/widgets/grade/grade_tile.dart';
|
||||
import 'package:refilc_kreta_api/models/exam.dart';
|
||||
import 'package:refilc_kreta_api/providers/exam_provider.dart';
|
||||
// import 'package:refilc_kreta_api/client/api.dart';
|
||||
// import 'package:refilc_kreta_api/client/client.dart';
|
||||
import 'package:refilc_kreta_api/providers/grade_provider.dart';
|
||||
import 'package:refilc/api/providers/user_provider.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc_kreta_api/models/grade.dart';
|
||||
import 'package:refilc_kreta_api/models/subject.dart';
|
||||
import 'package:refilc_kreta_api/models/group_average.dart';
|
||||
import 'package:refilc_kreta_api/providers/homework_provider.dart';
|
||||
import 'package:refilc_mobile_ui/common/average_display.dart';
|
||||
import 'package:refilc_mobile_ui/common/bottom_sheet_menu/rounded_bottom_sheet.dart';
|
||||
import 'package:refilc_mobile_ui/common/empty.dart';
|
||||
import 'package:refilc_mobile_ui/common/panel/panel.dart';
|
||||
import 'package:refilc_mobile_ui/common/profile_image/profile_button.dart';
|
||||
import 'package:refilc_mobile_ui/common/profile_image/profile_image.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/exam/exam_viewable.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/statistics_tile.dart';
|
||||
import 'package:refilc_mobile_ui/common/widgets/grade/grade_subject_tile.dart';
|
||||
import 'package:refilc_mobile_ui/common/trend_display.dart';
|
||||
import 'package:refilc_mobile_ui/pages/grades/fail_warning.dart';
|
||||
import 'package:refilc_mobile_ui/pages/grades/grades_count.dart';
|
||||
import 'package:refilc_mobile_ui/pages/grades/graph.dart';
|
||||
import 'package:refilc_mobile_ui/pages/grades/grade_subject_view.dart';
|
||||
import 'package:refilc_plus/models/premium_scopes.dart';
|
||||
import 'package:refilc_plus/providers/premium_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:refilc/helpers/average_helper.dart';
|
||||
import 'package:refilc_plus/ui/mobile/premium/upsell.dart';
|
||||
import 'average_selector.dart';
|
||||
import 'package:refilc_plus/ui/mobile/premium/premium_inline.dart';
|
||||
import 'calculator/grade_calculator.dart';
|
||||
import 'calculator/grade_calculator_provider.dart';
|
||||
import 'grades_page.i18n.dart';
|
||||
|
||||
class GradesPage extends StatefulWidget {
|
||||
const GradesPage({super.key});
|
||||
|
||||
@override
|
||||
GradesPageState createState() => GradesPageState();
|
||||
}
|
||||
|
||||
class GradesPageState extends State<GradesPage> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
PersistentBottomSheetController? _sheetController;
|
||||
|
||||
late UserProvider user;
|
||||
late GradeProvider gradeProvider;
|
||||
late UpdateProvider updateProvider;
|
||||
late GradeCalculatorProvider calculatorProvider;
|
||||
late HomeworkProvider homeworkProvider;
|
||||
late ExamProvider examProvider;
|
||||
|
||||
late String firstName;
|
||||
late Widget yearlyGraph;
|
||||
late Widget gradesCount;
|
||||
List<Widget> subjectTiles = [];
|
||||
|
||||
int avgDropValue = 0;
|
||||
|
||||
bool gradeCalcMode = false;
|
||||
|
||||
List<Grade> getSubjectGrades(GradeSubject subject,
|
||||
{int days = 0}) =>
|
||||
!gradeCalcMode
|
||||
? gradeProvider
|
||||
.grades
|
||||
.where((e) =>
|
||||
e
|
||||
.subject ==
|
||||
subject &&
|
||||
e.type == GradeType.midYear &&
|
||||
(days ==
|
||||
0 ||
|
||||
e.date.isBefore(
|
||||
DateTime.now().subtract(Duration(days: days)))))
|
||||
.toList()
|
||||
: calculatorProvider.grades
|
||||
.where((e) => e.subject == subject)
|
||||
.toList();
|
||||
|
||||
void generateTiles() {
|
||||
List<GradeSubject> subjects = gradeProvider.grades
|
||||
.map((e) => e.subject)
|
||||
.toSet()
|
||||
.toList()
|
||||
..sort((a, b) => a.name.compareTo(b.name));
|
||||
List<Widget> tiles = [];
|
||||
|
||||
Map<GradeSubject, double> subjectAvgs = {};
|
||||
|
||||
if (!gradeCalcMode) {
|
||||
var i = 0;
|
||||
|
||||
tiles.addAll(subjects.map((subject) {
|
||||
List<Grade> subjectGrades = getSubjectGrades(subject);
|
||||
|
||||
double avg = AverageHelper.averageEvals(subjectGrades);
|
||||
double averageBefore = 0.0;
|
||||
|
||||
if (avgDropValue != 0) {
|
||||
List<Grade> gradesBefore =
|
||||
getSubjectGrades(subject, days: avgDropValue);
|
||||
averageBefore = avgDropValue == 0
|
||||
? 0.0
|
||||
: AverageHelper.averageEvals(gradesBefore);
|
||||
}
|
||||
var nullavg = GroupAverage(average: 0.0, subject: subject, uid: "0");
|
||||
double groupAverage = gradeProvider.groupAverages
|
||||
.firstWhere((e) => e.subject == subject, orElse: () => nullavg)
|
||||
.average;
|
||||
|
||||
if (avg != 0) subjectAvgs[subject] = avg;
|
||||
|
||||
i++;
|
||||
|
||||
int homeworkCount = homeworkProvider.homework
|
||||
.where((e) =>
|
||||
e.subject.id == subject.id &&
|
||||
e.deadline.isAfter(DateTime.now()))
|
||||
.length;
|
||||
bool hasHomework = homeworkCount > 0;
|
||||
|
||||
List<Exam> allExams = examProvider.exams;
|
||||
allExams.sort((a, b) => a.date.compareTo(b.date));
|
||||
|
||||
Exam? nearestExam = allExams.firstWhereOrNull((e) =>
|
||||
e.subject.id == subject.id && e.writeDate.isAfter(DateTime.now()));
|
||||
|
||||
bool hasUnder = hasHomework || nearestExam != null;
|
||||
|
||||
return Padding(
|
||||
padding: i > 1 ? const EdgeInsets.only(top: 9.0) : EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
if (Provider.of<SettingsProvider>(context, listen: false)
|
||||
.shadowEffect)
|
||||
BoxShadow(
|
||||
offset: const Offset(0, 21),
|
||||
blurRadius: 23.0,
|
||||
color: Theme.of(context).shadowColor,
|
||||
)
|
||||
],
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: const Radius.circular(16.0),
|
||||
topRight: const Radius.circular(16.0),
|
||||
bottomLeft: hasUnder
|
||||
? const Radius.circular(8.0)
|
||||
: const Radius.circular(16.0),
|
||||
bottomRight: hasUnder
|
||||
? const Radius.circular(8.0)
|
||||
: const Radius.circular(16.0),
|
||||
),
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0, horizontal: 6.0),
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
highlightColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
),
|
||||
child: GradeSubjectTile(
|
||||
subject,
|
||||
averageBefore: averageBefore,
|
||||
average: avg,
|
||||
groupAverage: avgDropValue == 0 ? groupAverage : 0.0,
|
||||
onTap: () {
|
||||
GradeSubjectView(subject, groupAverage: groupAverage)
|
||||
.push(context, root: true);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (hasUnder)
|
||||
const SizedBox(
|
||||
height: 6.0,
|
||||
),
|
||||
if (hasHomework)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
if (Provider.of<SettingsProvider>(context, listen: false)
|
||||
.shadowEffect)
|
||||
BoxShadow(
|
||||
offset: const Offset(0, 21),
|
||||
blurRadius: 23.0,
|
||||
color: Theme.of(context).shadowColor,
|
||||
)
|
||||
],
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: const Radius.circular(8.0),
|
||||
topRight: const Radius.circular(8.0),
|
||||
bottomLeft: nearestExam != null
|
||||
? const Radius.circular(8.0)
|
||||
: const Radius.circular(16.0),
|
||||
bottomRight: nearestExam != null
|
||||
? const Radius.circular(8.0)
|
||||
: const Radius.circular(16.0),
|
||||
),
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 8.0,
|
||||
bottom: 8.0,
|
||||
left: 15.0,
|
||||
right: 8.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'you_have_hw'.i18n.fill([homeworkCount]),
|
||||
style: const TextStyle(
|
||||
fontSize: 15.0, fontWeight: FontWeight.w500),
|
||||
),
|
||||
// const Icon(
|
||||
// FeatherIcons.chevronRight,
|
||||
// grade: 0.5,
|
||||
// size: 20.0,
|
||||
// )
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (nearestExam != null)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
if (Provider.of<SettingsProvider>(context, listen: false)
|
||||
.shadowEffect)
|
||||
BoxShadow(
|
||||
offset: const Offset(0, 21),
|
||||
blurRadius: 23.0,
|
||||
color: Theme.of(context).shadowColor,
|
||||
)
|
||||
],
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(8.0),
|
||||
topRight: Radius.circular(8.0),
|
||||
bottomLeft: Radius.circular(16.0),
|
||||
bottomRight: Radius.circular(16.0),
|
||||
),
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
),
|
||||
child: ExamViewable(
|
||||
nearestExam,
|
||||
showSubject: false,
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}));
|
||||
} else {
|
||||
tiles.clear();
|
||||
|
||||
List<Grade> ghostGrades = calculatorProvider.ghosts;
|
||||
ghostGrades.sort((a, b) => -a.date.compareTo(b.date));
|
||||
|
||||
List<GradeTile> _gradeTiles = [];
|
||||
for (Grade grade in ghostGrades) {
|
||||
_gradeTiles.add(GradeTile(
|
||||
grade,
|
||||
viewOverride: true,
|
||||
));
|
||||
}
|
||||
|
||||
tiles.add(
|
||||
_gradeTiles.isNotEmpty
|
||||
? Panel(
|
||||
key: ValueKey(gradeCalcMode),
|
||||
title: Text(
|
||||
"Ghost Grades".i18n,
|
||||
),
|
||||
child: Column(
|
||||
children: _gradeTiles,
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
);
|
||||
}
|
||||
|
||||
if (tiles.isNotEmpty || gradeCalcMode) {
|
||||
tiles.insert(0, yearlyGraph);
|
||||
tiles.insert(1, gradesCount);
|
||||
if (!gradeCalcMode) {
|
||||
tiles.insert(2, FailWarning(subjectAvgs: subjectAvgs));
|
||||
tiles.insert(
|
||||
3,
|
||||
PanelTitle(
|
||||
title: Text(
|
||||
avgDropValue == 0 ? "Subjects".i18n : "Subjects_changes".i18n,
|
||||
)),
|
||||
);
|
||||
|
||||
// tiles.insert(4, const PanelHeader(padding: EdgeInsets.only(top: 12.0)));
|
||||
// tiles.add(const PanelFooter(padding: EdgeInsets.only(bottom: 12.0)));
|
||||
}
|
||||
tiles.add(Padding(
|
||||
padding: EdgeInsets.only(bottom: !gradeCalcMode ? 24.0 : 250.0),
|
||||
));
|
||||
} else {
|
||||
tiles.insert(
|
||||
0,
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 24.0),
|
||||
child: Empty(subtitle: "empty".i18n),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double subjectAvg = subjectAvgs.isNotEmpty
|
||||
? subjectAvgs.values.fold(0.0, (double a, double b) => a + b) /
|
||||
subjectAvgs.length
|
||||
: 0.0;
|
||||
final double classAvg = gradeProvider.groupAverages.isNotEmpty
|
||||
? gradeProvider.groupAverages
|
||||
.map((e) => e.average)
|
||||
.fold(0.0, (double a, double b) => a + b) /
|
||||
gradeProvider.groupAverages.length
|
||||
: 0.0;
|
||||
|
||||
if (subjectAvg > 0 && !gradeCalcMode) {
|
||||
tiles.add(
|
||||
PanelTitle(title: Text("data".i18n)),
|
||||
);
|
||||
tiles.add(Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: StatisticsTile(
|
||||
fill: true,
|
||||
title: AutoSizeText(
|
||||
"subjectavg".i18n,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
value: subjectAvg,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 24.0),
|
||||
Expanded(
|
||||
child: StatisticsTile(
|
||||
outline: true,
|
||||
title: AutoSizeText(
|
||||
"classavg".i18n,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
wrapWords: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
value: classAvg,
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
tiles.add(Provider.of<PremiumProvider>(context, listen: false).hasPremium
|
||||
? const SizedBox()
|
||||
: const Padding(
|
||||
padding: EdgeInsets.only(top: 24.0),
|
||||
child: PremiumInline(features: [
|
||||
PremiumInlineFeature.goal,
|
||||
PremiumInlineFeature.stats,
|
||||
]),
|
||||
));
|
||||
|
||||
// padding
|
||||
tiles.add(const SizedBox(height: 32.0));
|
||||
|
||||
subjectTiles = List.castFrom(tiles);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
user = Provider.of<UserProvider>(context);
|
||||
gradeProvider = Provider.of<GradeProvider>(context);
|
||||
updateProvider = Provider.of<UpdateProvider>(context);
|
||||
calculatorProvider = Provider.of<GradeCalculatorProvider>(context);
|
||||
homeworkProvider = Provider.of<HomeworkProvider>(context);
|
||||
examProvider = Provider.of<ExamProvider>(context);
|
||||
|
||||
context.watch<PremiumProvider>();
|
||||
|
||||
List<String> nameParts = user.displayName?.split(" ") ?? ["?"];
|
||||
firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0];
|
||||
|
||||
final double totalClassAvg = gradeProvider.groupAverages.isEmpty
|
||||
? 0.0
|
||||
: gradeProvider.groupAverages
|
||||
.map((e) => e.average)
|
||||
.fold(0.0, (double a, double b) => a + b) /
|
||||
gradeProvider.groupAverages.length;
|
||||
|
||||
final now = gradeProvider.grades.isNotEmpty
|
||||
? gradeProvider.grades
|
||||
.reduce((v, e) => e.date.isAfter(v.date) ? e : v)
|
||||
.date
|
||||
: DateTime.now();
|
||||
|
||||
final currentStudentAvg = AverageHelper.averageEvals(!gradeCalcMode
|
||||
? gradeProvider.grades
|
||||
.where((e) => e.type == GradeType.midYear)
|
||||
.toList()
|
||||
: calculatorProvider.grades);
|
||||
|
||||
final prevStudentAvg = AverageHelper.averageEvals(gradeProvider.grades
|
||||
.where((e) => e.type == GradeType.midYear)
|
||||
.where((e) => e.date.isBefore(now.subtract(const Duration(days: 30))))
|
||||
.toList());
|
||||
|
||||
List<Grade> graphGrades = !gradeCalcMode
|
||||
? gradeProvider.grades
|
||||
.where((e) =>
|
||||
e.type == GradeType.midYear &&
|
||||
(avgDropValue == 0 ||
|
||||
e.date.isAfter(
|
||||
DateTime.now().subtract(Duration(days: avgDropValue)))))
|
||||
.toList()
|
||||
: calculatorProvider.grades
|
||||
.where(((e) =>
|
||||
avgDropValue == 0 ||
|
||||
e.date.isAfter(
|
||||
DateTime.now().subtract(Duration(days: avgDropValue)))))
|
||||
.toList();
|
||||
|
||||
yearlyGraph = Padding(
|
||||
padding: const EdgeInsets.only(top: 12.0, bottom: 8.0),
|
||||
child: Panel(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
AverageSelector(
|
||||
value: avgDropValue,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
avgDropValue = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
// if (totalClassAvg >= 1.0) AverageDisplay(average: totalClassAvg, border: true),
|
||||
// const SizedBox(width: 4.0),
|
||||
TrendDisplay(
|
||||
previous: prevStudentAvg, current: currentStudentAvg),
|
||||
if (gradeProvider.grades
|
||||
.where((e) => e.type == GradeType.midYear)
|
||||
.isNotEmpty)
|
||||
AverageDisplay(average: currentStudentAvg),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 12.0, right: 12.0),
|
||||
child:
|
||||
GradeGraph(graphGrades, dayThreshold: 2, classAvg: totalClassAvg),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
gradesCount = Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24.0),
|
||||
child: Panel(child: GradesCount(grades: graphGrades)),
|
||||
);
|
||||
|
||||
generateTiles();
|
||||
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.only(top: 9.0),
|
||||
child: NestedScrollView(
|
||||
physics: const BouncingScrollPhysics(
|
||||
parent: AlwaysScrollableScrollPhysics()),
|
||||
headerSliverBuilder: (context, _) => [
|
||||
SliverAppBar(
|
||||
centerTitle: false,
|
||||
pinned: true,
|
||||
floating: false,
|
||||
snap: false,
|
||||
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
actions: [
|
||||
if (!gradeCalcMode)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 5.0, vertical: 0.0),
|
||||
child: IconButton(
|
||||
splashRadius: 24.0,
|
||||
onPressed: () {
|
||||
if (!Provider.of<PremiumProvider>(context,
|
||||
listen: false)
|
||||
.hasScope(PremiumScopes.totalGradeCalculator)) {
|
||||
PremiumLockedFeatureUpsell.show(
|
||||
context: context,
|
||||
feature: PremiumFeature.gradeCalculation);
|
||||
return;
|
||||
}
|
||||
|
||||
// SoonAlert.show(context: context);
|
||||
gradeCalcTotal(context);
|
||||
},
|
||||
icon: Icon(
|
||||
FeatherIcons.plus,
|
||||
color: AppColors.of(context).text,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// profile Icon
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 24.0),
|
||||
child: ProfileButton(
|
||||
child: ProfileImage(
|
||||
heroTag: "profile",
|
||||
name: firstName,
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary, //ColorUtils.stringToColor(user.displayName ?? "?"),
|
||||
badge: updateProvider.available,
|
||||
role: user.role,
|
||||
profilePictureString: user.picture,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
automaticallyImplyLeading: false,
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Text(
|
||||
"Grades".i18n,
|
||||
style: TextStyle(
|
||||
color: AppColors.of(context).text,
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
shadowColor: Theme.of(context).shadowColor,
|
||||
),
|
||||
],
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () => gradeProvider.fetch(),
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemCount: max(subjectTiles.length, 1),
|
||||
itemBuilder: (context, index) {
|
||||
if (subjectTiles.isNotEmpty) {
|
||||
EdgeInsetsGeometry panelPadding =
|
||||
const EdgeInsets.symmetric(horizontal: 24.0);
|
||||
|
||||
if (subjectTiles[index].runtimeType == GradeSubjectTile) {
|
||||
return Padding(
|
||||
padding: panelPadding,
|
||||
child: PanelBody(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: subjectTiles[index],
|
||||
));
|
||||
} else {
|
||||
return Padding(
|
||||
padding: panelPadding, child: subjectTiles[index]);
|
||||
}
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void gradeCalcTotal(BuildContext context) {
|
||||
calculatorProvider.clear();
|
||||
calculatorProvider.addAllGrades(gradeProvider.grades);
|
||||
|
||||
_sheetController = _scaffoldKey.currentState?.showBottomSheet(
|
||||
(context) => const RoundedBottomSheet(
|
||||
borderRadius: 14.0, child: GradeCalculator(null)),
|
||||
backgroundColor: const Color(0x00000000),
|
||||
elevation: 12.0,
|
||||
);
|
||||
|
||||
// Hide the fab and grades
|
||||
setState(() {
|
||||
gradeCalcMode = true;
|
||||
});
|
||||
|
||||
_sheetController!.closed.then((value) {
|
||||
// Show fab and grades
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
gradeCalcMode = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
66
refilc_mobile_ui/lib/pages/grades/grades_page.i18n.dart
Normal file
66
refilc_mobile_ui/lib/pages/grades/grades_page.i18n.dart
Normal file
@@ -0,0 +1,66 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"Grades": "Subjects",
|
||||
"Ghost Grades": "Grades",
|
||||
"Subjects": "Your Subjects",
|
||||
"Subjects_changes": "Subject Differences",
|
||||
"empty": "You don't have any subjects.",
|
||||
"annual_average": "Annual average",
|
||||
"3_months_average": "3 Monthly Average",
|
||||
"30_days_average": "Monthly Average",
|
||||
"14_days_average": "2 Weekly Average",
|
||||
"7_days_average": "Weekly Average",
|
||||
"subjectavg": "Subject Average",
|
||||
"classavg": "Class Average",
|
||||
"fail_warning": "Failure warning",
|
||||
"fail_warning_description": "You are failing %d subject(s)",
|
||||
"data": "Data",
|
||||
"you_have_hw": "You have %s homework(s) to do",
|
||||
},
|
||||
"hu_hu": {
|
||||
"Grades": "Tantárgyak",
|
||||
"Ghost Grades": "Szellem jegyek",
|
||||
"Subjects": "Tantárgyaid",
|
||||
"Subjects_changes": "Tantárgyi változások",
|
||||
"empty": "Még nincs egy tárgyad sem.",
|
||||
"annual_average": "Éves átlag",
|
||||
"3_months_average": "Háromhavi átlag",
|
||||
"30_days_average": "Havi átlag",
|
||||
"14_days_average": "Kétheti átlag",
|
||||
"7_days_average": "Heti átlag",
|
||||
"subjectavg": "Tantárgyi átlag",
|
||||
"classavg": "Osztályátlag",
|
||||
"fail_warning": "Bukás figyelmeztető",
|
||||
"fail_warning_description": "Bukásra állsz %d tantárgyból",
|
||||
"data": "Adatok",
|
||||
"you_have_hw": "%s házi feladat vár rád",
|
||||
},
|
||||
"de_de": {
|
||||
"Grades": "Fächer",
|
||||
"Ghost Grades": "Geist Noten",
|
||||
"Subjects": "Ihre Themen",
|
||||
"Subjects_changes": "Betreff Änderungen",
|
||||
"empty": "Sie haben keine Fächer.",
|
||||
"annual_average": "Jahresdurchschnitt",
|
||||
"3_months_average": "Drei-Monats-Durchschnitt",
|
||||
"30_days_average": "Monatsdurchschnitt",
|
||||
"14_days_average": "Vierzehntägiger Durchschnitt",
|
||||
"7_days_average": "Wöchentlicher Durchschnitt",
|
||||
"subjectavg": "Fächer Durchschnitt",
|
||||
"classavg": "Klassendurchschnitt",
|
||||
"fail_warning": "Ausfallwarnung",
|
||||
"fail_warning_description": "Sie werden in %d des Fachs durchfallen",
|
||||
"data": "Daten",
|
||||
"you_have_hw": "Du hast %s Hausaufgaben",
|
||||
},
|
||||
};
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
String fill(List<Object> params) => localizeFill(this, params);
|
||||
String plural(int value) => localizePlural(value, this, _t);
|
||||
String version(Object modifier) => localizeVersion(modifier, this, _t);
|
||||
}
|
||||
377
refilc_mobile_ui/lib/pages/grades/graph.dart
Normal file
377
refilc_mobile_ui/lib/pages/grades/graph.dart
Normal file
@@ -0,0 +1,377 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:refilc/helpers/average_helper.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc_kreta_api/models/grade.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:i18n_extension/i18n_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'graph.i18n.dart';
|
||||
|
||||
class GradeGraph extends StatefulWidget {
|
||||
const GradeGraph(this.data,
|
||||
{super.key, this.dayThreshold = 7, this.classAvg});
|
||||
|
||||
final List<Grade> data;
|
||||
final int dayThreshold;
|
||||
final double? classAvg;
|
||||
|
||||
@override
|
||||
GradeGraphState createState() => GradeGraphState();
|
||||
}
|
||||
|
||||
class GradeGraphState extends State<GradeGraph> {
|
||||
late SettingsProvider settings;
|
||||
|
||||
List<Color> getColors(List<Grade> data) {
|
||||
List<Color> colors = [];
|
||||
List<List<Grade>> sortedData = [[]];
|
||||
|
||||
// Sort by date descending
|
||||
data.sort((a, b) => -a.writeDate.compareTo(b.writeDate));
|
||||
|
||||
// Sort data to points by treshold
|
||||
for (var element in data) {
|
||||
if (sortedData.last.isNotEmpty &&
|
||||
sortedData.last.last.writeDate.difference(element.writeDate).inDays >
|
||||
widget.dayThreshold) {
|
||||
sortedData.add([]);
|
||||
}
|
||||
for (var dataList in sortedData) {
|
||||
dataList.add(element);
|
||||
}
|
||||
}
|
||||
|
||||
// Create FlSpots from points
|
||||
for (var dataList in sortedData) {
|
||||
double average = AverageHelper.averageEvals(dataList);
|
||||
|
||||
Color clr = average >= 1 && average <= 5
|
||||
? ColorTween(
|
||||
begin: settings.gradeColors[average.floor() - 1],
|
||||
end: settings.gradeColors[average.ceil() - 1])
|
||||
.transform(average - average.floor())!
|
||||
: Theme.of(context).colorScheme.secondary;
|
||||
|
||||
colors.add(clr);
|
||||
}
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
List<FlSpot> getSpots(List<Grade> data) {
|
||||
List<FlSpot> subjectData = [];
|
||||
List<List<Grade>> sortedData = [[]];
|
||||
|
||||
// Sort by date descending
|
||||
data.sort((a, b) => -a.writeDate.compareTo(b.writeDate));
|
||||
|
||||
// Sort data to points by treshold
|
||||
for (var element in data) {
|
||||
if (sortedData.last.isNotEmpty &&
|
||||
sortedData.last.last.writeDate.difference(element.writeDate).inDays >
|
||||
widget.dayThreshold) {
|
||||
sortedData.add([]);
|
||||
}
|
||||
for (var dataList in sortedData) {
|
||||
dataList.add(element);
|
||||
}
|
||||
}
|
||||
|
||||
// Create FlSpots from points
|
||||
for (var dataList in sortedData) {
|
||||
double average = AverageHelper.averageEvals(dataList);
|
||||
|
||||
if (dataList.isNotEmpty) {
|
||||
subjectData.add(FlSpot(
|
||||
dataList[0].writeDate.month +
|
||||
(dataList[0].writeDate.day / 31) +
|
||||
((dataList[0].writeDate.year - data.last.writeDate.year) * 12),
|
||||
double.parse(average.toStringAsFixed(2)),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return subjectData;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
settings = Provider.of<SettingsProvider>(context);
|
||||
|
||||
List<FlSpot> subjectSpots = [];
|
||||
List<FlSpot> ghostSpots = [];
|
||||
List<VerticalLine> extraLinesV = [];
|
||||
List<HorizontalLine> extraLinesH = [];
|
||||
|
||||
// Filter data
|
||||
List<Grade> data = widget.data
|
||||
.where((e) => e.value.weight != 0)
|
||||
.where((e) => e.type == GradeType.midYear)
|
||||
.where((e) => e.gradeType?.name == "Osztalyzat")
|
||||
.toList();
|
||||
|
||||
// Filter ghost data
|
||||
List<Grade> ghostData = widget.data
|
||||
.where((e) => e.value.weight != 0)
|
||||
.where((e) => e.type == GradeType.ghost)
|
||||
.toList();
|
||||
|
||||
// Calculate average
|
||||
double average = AverageHelper.averageEvals(data);
|
||||
|
||||
// Calculate graph color
|
||||
Color averageColor = average >= 1 && average <= 5
|
||||
? ColorTween(
|
||||
begin: settings.gradeColors[average.floor() - 1],
|
||||
end: settings.gradeColors[average.ceil() - 1])
|
||||
.transform(average - average.floor())!
|
||||
: Theme.of(context).colorScheme.secondary;
|
||||
|
||||
List<Color> averageColors = getColors(data);
|
||||
subjectSpots = getSpots(data);
|
||||
|
||||
// naplo/#73
|
||||
if (subjectSpots.isNotEmpty) {
|
||||
ghostSpots = getSpots(data + ghostData);
|
||||
|
||||
// hax
|
||||
ghostSpots = ghostSpots
|
||||
.where((e) => e.x >= subjectSpots.map((f) => f.x).reduce(max))
|
||||
.toList();
|
||||
ghostSpots = ghostSpots.map((e) => FlSpot(e.x + 0.1, e.y)).toList();
|
||||
ghostSpots.add(subjectSpots.firstWhere(
|
||||
(e) => e.x >= subjectSpots.map((f) => f.x).reduce(max),
|
||||
orElse: () => const FlSpot(-1, -1)));
|
||||
ghostSpots.removeWhere(
|
||||
(element) => element.x == -1 && element.y == -1); // naplo/#74
|
||||
}
|
||||
|
||||
Grade halfYearGrade = widget.data.lastWhere(
|
||||
(e) => e.type == GradeType.halfYear,
|
||||
orElse: () => Grade.fromJson({}));
|
||||
|
||||
if (halfYearGrade.date.year != 0 && data.isNotEmpty) {
|
||||
final maxX = ghostSpots.isNotEmpty ? ghostSpots.first.x : 0;
|
||||
final x = halfYearGrade.writeDate.month +
|
||||
(halfYearGrade.writeDate.day / 31) +
|
||||
((halfYearGrade.writeDate.year - data.last.writeDate.year) * 12);
|
||||
if (x <= maxX) {
|
||||
extraLinesV.add(
|
||||
VerticalLine(
|
||||
x: x,
|
||||
strokeWidth: 3.0,
|
||||
color: AppColors.of(context).red.withOpacity(.75),
|
||||
label: VerticalLineLabel(
|
||||
labelResolver: (_) => " ${"mid".i18n} ", // <- zwsp for padding
|
||||
show: true,
|
||||
alignment: Alignment.topLeft,
|
||||
style: TextStyle(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
color: AppColors.of(context).text,
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal line displaying the class average
|
||||
if (widget.classAvg != null &&
|
||||
widget.classAvg! > 0.0 &&
|
||||
settings.graphClassAvg) {
|
||||
extraLinesH.add(HorizontalLine(
|
||||
y: widget.classAvg!,
|
||||
color: AppColors.of(context).text.withOpacity(.75),
|
||||
));
|
||||
}
|
||||
|
||||
// LineChart is really cute because it tries to render it's contents outside of it's rect.
|
||||
return widget.data.length <= 2
|
||||
? SizedBox(
|
||||
height: 150,
|
||||
child: Center(
|
||||
child: Text(
|
||||
"not_enough_grades".i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
)
|
||||
: ClipRect(
|
||||
child: SizedBox(
|
||||
height: 158,
|
||||
child: subjectSpots.length > 1
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0, right: 8.0),
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
extraLinesData: ExtraLinesData(
|
||||
verticalLines: extraLinesV,
|
||||
horizontalLines: extraLinesH),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
preventCurveOverShooting: true,
|
||||
spots: subjectSpots,
|
||||
isCurved: true,
|
||||
colors: averageColors,
|
||||
barWidth: 8,
|
||||
isStrokeCapRound: true,
|
||||
dotData: FlDotData(show: false),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
colors: [
|
||||
averageColor.withOpacity(0.7),
|
||||
averageColor.withOpacity(0.3),
|
||||
averageColor.withOpacity(0.2),
|
||||
averageColor.withOpacity(0.1),
|
||||
],
|
||||
gradientColorStops: [0.1, 0.6, 0.8, 1],
|
||||
gradientFrom: const Offset(0, 0),
|
||||
gradientTo: const Offset(0, 1),
|
||||
),
|
||||
),
|
||||
if (ghostData.isNotEmpty && ghostSpots.isNotEmpty)
|
||||
LineChartBarData(
|
||||
preventCurveOverShooting: true,
|
||||
spots: ghostSpots,
|
||||
isCurved: true,
|
||||
colors: [AppColors.of(context).text],
|
||||
barWidth: 8,
|
||||
isStrokeCapRound: true,
|
||||
dotData: FlDotData(show: false),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
colors: [
|
||||
AppColors.of(context).text.withOpacity(0.7),
|
||||
AppColors.of(context).text.withOpacity(0.3),
|
||||
AppColors.of(context).text.withOpacity(0.2),
|
||||
AppColors.of(context).text.withOpacity(0.1),
|
||||
],
|
||||
gradientColorStops: [0.1, 0.6, 0.8, 1],
|
||||
gradientFrom: const Offset(0, 0),
|
||||
gradientTo: const Offset(0, 1),
|
||||
),
|
||||
),
|
||||
],
|
||||
minY: 1,
|
||||
maxY: 5,
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
horizontalInterval: 1,
|
||||
// checkToShowVerticalLine: (_) => false,
|
||||
// getDrawingHorizontalLine: (_) => FlLine(
|
||||
// color: AppColors.of(context).text.withOpacity(.15),
|
||||
// strokeWidth: 2,
|
||||
// ),
|
||||
// getDrawingVerticalLine: (_) => FlLine(
|
||||
// color: AppColors.of(context).text.withOpacity(.25),
|
||||
// strokeWidth: 2,
|
||||
// ),
|
||||
),
|
||||
lineTouchData: LineTouchData(
|
||||
touchTooltipData: LineTouchTooltipData(
|
||||
tooltipBgColor: Colors.grey.shade800,
|
||||
fitInsideVertically: true,
|
||||
fitInsideHorizontally: true,
|
||||
),
|
||||
handleBuiltInTouches: true,
|
||||
touchSpotThreshold: 20.0,
|
||||
getTouchedSpotIndicator: (_, spots) {
|
||||
return List.generate(
|
||||
spots.length,
|
||||
(index) => TouchedSpotIndicatorData(
|
||||
FlLine(
|
||||
color: Colors.grey.shade900,
|
||||
strokeWidth: 3.5,
|
||||
),
|
||||
FlDotData(
|
||||
getDotPainter: (a, b, c, d) =>
|
||||
FlDotCirclePainter(
|
||||
strokeWidth: 0,
|
||||
color: Colors.grey.shade900,
|
||||
radius: 10.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
borderData: FlBorderData(
|
||||
show: false,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
width: 4,
|
||||
),
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
bottomTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 24,
|
||||
getTextStyles: (context, value) => TextStyle(
|
||||
color:
|
||||
AppColors.of(context).text.withOpacity(.75),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
margin: 12.0,
|
||||
getTitles: (value) {
|
||||
var format = DateFormat(
|
||||
"MMM", I18n.of(context).locale.toString());
|
||||
|
||||
String title = format
|
||||
.format(DateTime(0, value.floor() % 12))
|
||||
.replaceAll(".", "");
|
||||
title =
|
||||
title.substring(0, min(title.length, 4));
|
||||
|
||||
return title.toUpperCase();
|
||||
},
|
||||
interval: () {
|
||||
List<Grade> tData =
|
||||
ghostData.isNotEmpty ? ghostData : data;
|
||||
tData.sort((a, b) =>
|
||||
a.writeDate.compareTo(b.writeDate));
|
||||
return tData.first.writeDate
|
||||
.add(const Duration(days: 120))
|
||||
.isBefore(tData.last.writeDate)
|
||||
? 2.0
|
||||
: 1.0;
|
||||
}(),
|
||||
checkToShowTitle: (double minValue,
|
||||
double maxValue,
|
||||
SideTitles sideTitles,
|
||||
double appliedInterval,
|
||||
double value) {
|
||||
if (value == maxValue || value == minValue) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
),
|
||||
leftTitles: SideTitles(
|
||||
showTitles: true,
|
||||
interval: 1.0,
|
||||
getTextStyles: (context, value) => TextStyle(
|
||||
color: AppColors.of(context).text,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18.0,
|
||||
),
|
||||
margin: 16,
|
||||
),
|
||||
rightTitles: SideTitles(showTitles: false),
|
||||
topTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
24
refilc_mobile_ui/lib/pages/grades/graph.i18n.dart
Normal file
24
refilc_mobile_ui/lib/pages/grades/graph.i18n.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"mid": "Mid year",
|
||||
"not_enough_grades": "Not enough data to show a graph here.",
|
||||
},
|
||||
"hu_hu": {
|
||||
"mid": "Félév",
|
||||
"not_enough_grades": "Nem szereztél még elég jegyet grafikon mutatáshoz.",
|
||||
},
|
||||
"de_de": {
|
||||
"mid": "Halbjährlich",
|
||||
"not_enough_grades": "Noch nicht genug Noten, um die Grafik zu zeigen.",
|
||||
},
|
||||
};
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
String fill(List<Object> params) => localizeFill(this, params);
|
||||
String plural(int value) => localizePlural(value, this, _t);
|
||||
String version(Object modifier) => localizeVersion(modifier, this, _t);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SubjectGradesContainer extends InheritedWidget {
|
||||
const SubjectGradesContainer({super.key, required super.child});
|
||||
|
||||
static SubjectGradesContainer? of(BuildContext context) =>
|
||||
context.dependOnInheritedWidgetOfExactType<SubjectGradesContainer>();
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(SubjectGradesContainer oldWidget) => false;
|
||||
}
|
||||
Reference in New Issue
Block a user