Revert "Rename everything filcnaplo-related to refilc"

This reverts commit d1a9625d93.
This commit is contained in:
Pearoo
2023-09-19 18:16:57 +02:00
parent d1a9625d93
commit 7b517b333a
669 changed files with 39481 additions and 39799 deletions

View File

@@ -0,0 +1,178 @@
import 'dart:math';
import 'package:filcnaplo_kreta_api/models/category.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_kreta_api/models/subject.dart';
import 'package:filcnaplo_kreta_api/models/teacher.dart';
import 'package:filcnaplo_mobile_ui/common/custom_snack_bar.dart';
import 'package:filcnaplo_mobile_ui/common/material_action_button.dart';
import 'package:filcnaplo/ui/widgets/grade/grade_tile.dart';
import 'package:filcnaplo_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, {Key? key}) : super(key: key);
final Subject 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)
.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,
mode: Category.fromJson({}),
seenDate: DateTime(0),
groupId: "",
));
},
),
),
],
),
);
}
String randomId() {
var rng = Random();
return rng.nextInt(1000000000).toString();
}
}

View File

@@ -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);
}

View File

@@ -0,0 +1,53 @@
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
class GradeCalculatorProvider extends GradeProvider {
GradeCalculatorProvider({
List<Grade> initialGrades = const [],
required SettingsProvider settings,
required UserProvider user,
required DatabaseProvider database,
required KretaClient kreta,
}) : super(
initialGrades: initialGrades,
settings: settings,
database: database,
kreta: kreta,
user: user,
);
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 = [];
}
}

View File

@@ -0,0 +1,39 @@
import 'package:filcnaplo_kreta_api/models/subject.dart';
import 'package:filcnaplo_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({Key? key, required this.subjectAvgs}) : super(key: key);
final Map<Subject, 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])),
],
),
),
);
}
}

View File

@@ -0,0 +1,370 @@
import 'dart:math';
import 'package:animations/animations.dart';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/utils/format.dart';
// import 'package:filcnaplo_kreta_api/client/api.dart';
// import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:filcnaplo/helpers/average_helper.dart';
import 'package:filcnaplo/helpers/subject.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_kreta_api/models/subject.dart';
import 'package:filcnaplo_mobile_ui/common/average_display.dart';
import 'package:filcnaplo_mobile_ui/common/bottom_sheet_menu/rounded_bottom_sheet.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel.dart';
import 'package:filcnaplo_mobile_ui/common/trend_display.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/cretification/certification_tile.dart';
import 'package:filcnaplo/ui/widgets/grade/grade_tile.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/grade/grade_viewable.dart';
import 'package:filcnaplo_mobile_ui/common/hero_scrollview.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/calculator/grade_calculator.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/calculator/grade_calculator_provider.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/grades_count.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/graph.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/subject_grades_container.dart';
import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_planner_screen.dart';
import 'package:filcnaplo_premium/models/premium_scopes.dart';
import 'package:filcnaplo_premium/providers/premium_provider.dart';
import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_state_screen.dart';
import 'package:filcnaplo_premium/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:filcnaplo_premium/ui/mobile/goal_planner/new_goal.dart';
class GradeSubjectView extends StatefulWidget {
const GradeSubjectView(this.subject, {Key? key, this.groupAverage = 0.0})
: super(key: key);
final Subject 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(Subject 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()),
),
));
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,
child: child,
fillColor: Colors.transparent,
);
},
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(
backgroundColor: Theme.of(context).colorScheme.secondary,
type: ExpandableFabType.up,
distance: 50,
closeButtonStyle: ExpandableFabCloseButtonStyle(
backgroundColor: Theme.of(context).colorScheme.secondary,
),
children: [
FloatingActionButton.small(
heroTag: "btn_ghost_grades",
child: const Icon(FeatherIcons.plus),
backgroundColor: Theme.of(context).colorScheme.secondary,
onPressed: () {
gradeCalc(context);
},
),
FloatingActionButton.small(
heroTag: "btn_goal_planner",
child: const Icon(FeatherIcons.flag, size: 20.0),
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)));
},
),
],
),
),
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(
child: GradeCalculator(widget.subject), borderRadius: 14.0),
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;
});
}
});
}
}

View File

@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/grades_count_item.dart';
import 'package:collection/collection.dart';
class GradesCount extends StatelessWidget {
const GradesCount({Key? key, required this.grades}) : super(key: key);
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.only(bottom: 6.0, top: 6.0, left: 12.0, right: 6.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: gradesCount.mapIndexed((index, e) => GradesCountItem(count: e, value: index + 1)).toList(),
),
);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:filcnaplo/ui/widgets/grade/grade_tile.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:flutter/material.dart';
class GradesCountItem extends StatelessWidget {
const GradesCountItem({Key? key, required this.count, required this.value}) : super(key: key);
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: 5.0),
GradeValueWidget(GradeValue(value, "Value", "Value", 100), size: 19.0, fill: true, shadow: false),
],
);
}
}

View File

@@ -0,0 +1,357 @@
import 'dart:math';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:filcnaplo/api/providers/update_provider.dart';
// import 'package:filcnaplo_kreta_api/client/api.dart';
// import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_kreta_api/models/subject.dart';
import 'package:filcnaplo_kreta_api/models/group_average.dart';
import 'package:filcnaplo_mobile_ui/common/average_display.dart';
import 'package:filcnaplo_mobile_ui/common/empty.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel.dart';
import 'package:filcnaplo_mobile_ui/common/profile_image/profile_button.dart';
import 'package:filcnaplo_mobile_ui/common/profile_image/profile_image.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/statistics_tile.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/grade/grade_subject_tile.dart';
import 'package:filcnaplo_mobile_ui/common/trend_display.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/fail_warning.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/grades_count.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/graph.dart';
import 'package:filcnaplo_mobile_ui/pages/grades/grade_subject_view.dart';
import 'package:filcnaplo_premium/providers/premium_provider.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:filcnaplo/helpers/average_helper.dart';
import 'package:filcnaplo_premium/ui/mobile/grades/average_selector.dart';
import 'package:filcnaplo_premium/ui/mobile/premium/premium_inline.dart';
import 'grades_page.i18n.dart';
class GradesPage extends StatefulWidget {
const GradesPage({Key? key}) : super(key: key);
@override
_GradesPageState createState() => _GradesPageState();
}
class _GradesPageState extends State<GradesPage> {
late UserProvider user;
late GradeProvider gradeProvider;
late UpdateProvider updateProvider;
late String firstName;
late Widget yearlyGraph;
late Widget gradesCount;
List<Widget> subjectTiles = [];
int avgDropValue = 0;
List<Grade> getSubjectGrades(Subject subject, {int days = 0}) => gradeProvider
.grades
.where((e) =>
e.subject == subject &&
e.type == GradeType.midYear &&
(days == 0 ||
e.date.isBefore(DateTime.now().subtract(Duration(days: days)))))
.toList();
void generateTiles() {
List<Subject> subjects = gradeProvider.grades
.map((e) => e.subject)
.toSet()
.toList()
..sort((a, b) => a.name.compareTo(b.name));
List<Widget> tiles = [];
Map<Subject, double> subjectAvgs = {};
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;
return GradeSubjectTile(
subject,
averageBefore: averageBefore,
average: avg,
groupAverage: avgDropValue == 0 ? groupAverage : 0.0,
onTap: () {
GradeSubjectView(subject, groupAverage: groupAverage)
.push(context, root: true);
},
);
}));
if (tiles.isNotEmpty) {
tiles.insert(0, yearlyGraph);
tiles.insert(1, gradesCount);
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(const Padding(padding: EdgeInsets.only(bottom: 24.0)));
} else {
tiles.insert(
0,
// TextButton(
// onPressed: () async {
// var url = KretaAPI.averages(
// user.instituteCode!,
// user.id!,
// );
// print(url);
// var res = await Provider.of<KretaClient>(context, listen: false)
// .getAPI(url);
// print(res);
// },
// child: Text('test')),
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) {
tiles.add(Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: StatisticsTile(
fill: true,
title: AutoSizeText(
"subjectavg".i18n,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
value: subjectAvg,
),
),
const SizedBox(width: 24.0),
Expanded(
child: StatisticsTile(
outline: true,
title: AutoSizeText(
"classavg".i18n,
textAlign: TextAlign.center,
maxLines: 2,
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);
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(gradeProvider.grades
.where((e) => e.type == GradeType.midYear)
.toList());
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 = gradeProvider.grades
.where((e) =>
e.type == GradeType.midYear &&
(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: [
PremiumAverageSelector(
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(
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: [
// 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(
child: subjectTiles[index],
padding: const EdgeInsets.symmetric(horizontal: 8.0),
));
} else {
return Padding(
padding: panelPadding, child: subjectTiles[index]);
}
} else {
return Container();
}
},
),
),
),
),
);
}
}

View File

@@ -0,0 +1,60 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"Grades": "Grades",
"Ghost Grades": "Grades",
"Subjects": "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": "Faliure warning",
"fail_warning_description": "You are failing %d subject(s)",
},
"hu_hu": {
"Grades": "Jegyek",
"Ghost Grades": "Szellem jegyek",
"Subjects": "Tantárgyak",
"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",
},
"de_de": {
"Grades": "Noten",
"Ghost Grades": "Geist Noten",
"Subjects": "Fächer",
"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",
},
};
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);
}

View File

@@ -0,0 +1,296 @@
import 'dart:math';
import 'package:filcnaplo/helpers/average_helper.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo_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, {Key? key, this.dayThreshold = 7, this.classAvg}) : super(key: key);
final List<Grade> data;
final int dayThreshold;
final double? classAvg;
@override
_GradeGraphState createState() => _GradeGraphState();
}
class _GradeGraphState extends State<GradeGraph> {
late SettingsProvider settings;
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;
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(
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: [averageColor],
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,
height: 158,
),
);
}
}

View 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);
}

View File

@@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class SubjectGradesContainer extends InheritedWidget {
const SubjectGradesContainer({Key? key, required Widget child}) : super(key: key, child: child);
static SubjectGradesContainer? of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<SubjectGradesContainer>();
@override
bool updateShouldNotify(SubjectGradesContainer oldWidget) => false;
}