changed everything from filcnaplo to refilc finally

This commit is contained in:
Kima
2024-02-24 20:12:25 +01:00
parent 0d1c7b7143
commit 1171e3aaaf
655 changed files with 38728 additions and 44967 deletions

View File

@@ -0,0 +1,405 @@
import 'dart:math';
import 'package:animations/animations.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:refilc/api/providers/update_provider.dart';
import 'package:refilc/ui/date_widget.dart';
import 'package:refilc_kreta_api/models/absence.dart';
import 'package:refilc_kreta_api/models/lesson.dart';
import 'package:refilc_kreta_api/models/subject.dart';
import 'package:refilc_kreta_api/models/week.dart';
import 'package:refilc_kreta_api/providers/absence_provider.dart';
import 'package:refilc_kreta_api/providers/note_provider.dart';
import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc/theme/colors/colors.dart';
import 'package:refilc_kreta_api/providers/timetable_provider.dart';
import 'package:refilc_mobile_ui/common/action_button.dart';
import 'package:refilc_mobile_ui/common/empty.dart';
import 'package:refilc_mobile_ui/common/filter_bar.dart';
import 'package:refilc_mobile_ui/common/panel/panel.dart';
import 'package:refilc_mobile_ui/common/widgets/absence/absence_subject_tile.dart';
import 'package:refilc_mobile_ui/common/widgets/absence/absence_viewable.dart';
import 'package:refilc_mobile_ui/common/widgets/statistics_tile.dart';
import 'package:refilc_mobile_ui/common/widgets/miss_tile.dart';
import 'package:refilc_mobile_ui/pages/absences/absence_subject_view.dart';
import 'package:refilc/ui/filter/sort.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart';
import 'absences_page.i18n.dart';
enum AbsenceFilter { absences, delays, misses }
class SubjectAbsence {
GradeSubject subject;
List<Absence> absences;
double percentage;
SubjectAbsence(
{required this.subject, this.absences = const [], this.percentage = 0.0});
}
class AbsencesPage extends StatefulWidget {
const AbsencesPage({Key? key}) : super(key: key);
@override
_AbsencesPageState createState() => _AbsencesPageState();
}
class _AbsencesPageState extends State<AbsencesPage>
with TickerProviderStateMixin {
late UserProvider user;
late AbsenceProvider absenceProvider;
late TimetableProvider timetableProvider;
late NoteProvider noteProvider;
late UpdateProvider updateProvider;
late String firstName;
late TabController _tabController;
late List<SubjectAbsence> absences = [];
final Map<GradeSubject, Lesson> _lessonCount = {};
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
timetableProvider = Provider.of<TimetableProvider>(context, listen: false);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
for (final lesson in timetableProvider.getWeek(Week.current()) ?? []) {
if (!lesson.isEmpty &&
lesson.subject.id != '' &&
lesson.lessonYearIndex != null) {
_lessonCount.update(
lesson.subject,
(value) {
if (lesson.lessonYearIndex! > value.lessonYearIndex!) {
return lesson;
} else {
return value;
}
},
ifAbsent: () => lesson,
);
}
}
});
}
void buildSubjectAbsences() {
Map<GradeSubject, SubjectAbsence> _absences = {};
for (final absence in absenceProvider.absences) {
if (absence.delay != 0) continue;
if (!_absences.containsKey(absence.subject)) {
_absences[absence.subject] =
SubjectAbsence(subject: absence.subject, absences: [absence]);
} else {
_absences[absence.subject]?.absences.add(absence);
}
}
_absences.forEach((subject, absence) {
final absentLessonsOfSubject = absenceProvider.absences
.where((e) => e.subject == subject && e.delay == 0)
.length;
final totalLessonsOfSubject = _lessonCount[subject]?.lessonYearIndex ?? 0;
double absentLessonsOfSubjectPercentage;
if (absentLessonsOfSubject <= totalLessonsOfSubject) {
absentLessonsOfSubjectPercentage =
absentLessonsOfSubject / totalLessonsOfSubject * 100;
} else {
absentLessonsOfSubjectPercentage = -1;
}
_absences[subject]?.percentage =
absentLessonsOfSubjectPercentage.clamp(-1, 100.0);
});
absences = _absences.values.toList();
absences.sort((a, b) => -a.percentage.compareTo(b.percentage));
}
@override
Widget build(BuildContext context) {
user = Provider.of<UserProvider>(context);
absenceProvider = Provider.of<AbsenceProvider>(context);
noteProvider = Provider.of<NoteProvider>(context);
updateProvider = Provider.of<UpdateProvider>(context);
timetableProvider = Provider.of<TimetableProvider>(context);
List<String> nameParts = user.displayName?.split(" ") ?? ["?"];
firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0];
buildSubjectAbsences();
return Scaffold(
body: Padding(
padding: const EdgeInsets.only(top: 12.0),
child: NestedScrollView(
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics()),
headerSliverBuilder: (context, _) => [
SliverAppBar(
pinned: true,
floating: false,
snap: false,
centerTitle: false,
automaticallyImplyLeading: false,
shadowColor: Theme.of(context).shadowColor,
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
title: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
"Absences".i18n,
style: TextStyle(
color: AppColors.of(context).text,
fontSize: 32.0,
fontWeight: FontWeight.bold),
),
),
bottom: FilterBar(items: [
Tab(text: "Absences".i18n),
Tab(text: "Delays".i18n),
Tab(text: "Misses".i18n),
], controller: _tabController, disableFading: true),
),
],
body: TabBarView(
physics: const BouncingScrollPhysics(),
controller: _tabController,
children: List.generate(
3, (index) => filterViewBuilder(context, index))),
),
),
);
}
List<DateWidget> getFilterWidgets(AbsenceFilter activeData) {
List<DateWidget> items = [];
switch (activeData) {
case AbsenceFilter.absences:
for (var a in absences) {
items.add(DateWidget(
date: DateTime.fromMillisecondsSinceEpoch(0),
widget: AbsenceSubjectTile(
a.subject,
percentage: a.percentage,
excused: a.absences
.where((a) => a.state == Justification.excused)
.length,
unexcused: a.absences
.where((a) => a.state == Justification.unexcused)
.length,
pending: a.absences
.where((a) => a.state == Justification.pending)
.length,
onTap: () => AbsenceSubjectView.show(a.subject, a.absences,
context: context),
),
));
}
break;
case AbsenceFilter.delays:
for (var absence in absenceProvider.absences) {
if (absence.delay != 0) {
items.add(DateWidget(
date: absence.date,
widget: AbsenceViewable(
absence,
padding: EdgeInsets.zero,
)));
}
}
break;
case AbsenceFilter.misses:
for (var note in noteProvider.notes) {
if (note.type?.name == "HaziFeladatHiany" ||
note.type?.name == "Felszereleshiany") {
items.add(DateWidget(
date: note.date,
widget: MissTile(note),
));
}
}
break;
}
return items;
}
Widget filterViewBuilder(context, int activeData) {
List<Widget> filterWidgets = [];
if (activeData > 0) {
filterWidgets = sortDateWidgets(
context,
dateWidgets: getFilterWidgets(AbsenceFilter.values[activeData]),
padding: EdgeInsets.zero,
hasShadow: true,
);
} else {
filterWidgets = [
Padding(
padding: const EdgeInsets.only(bottom: 24.0),
child: Panel(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Subjects".i18n),
Padding(
padding: const EdgeInsets.only(right: 4.0),
child: IconButton(
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0)),
title: Text("attention".i18n),
content: Text("attention_body".i18n),
actions: [
ActionButton(
label: "Ok",
onTap: () => Navigator.of(context).pop())
],
),
);
},
padding: EdgeInsets.zero,
splashRadius: 24.0,
visualDensity: VisualDensity.compact,
constraints: BoxConstraints.tight(const Size(42.0, 42.0)),
icon: const Icon(FeatherIcons.info),
),
),
],
),
child: PageTransitionSwitcher(
transitionBuilder: (
Widget child,
Animation<double> primaryAnimation,
Animation<double> secondaryAnimation,
) {
return FadeThroughTransition(
child: child,
animation: primaryAnimation,
secondaryAnimation: secondaryAnimation,
fillColor: Theme.of(context).colorScheme.background,
);
},
child: Column(
children: getFilterWidgets(AbsenceFilter.values[activeData])
.map((e) => e.widget)
.cast<Widget>()
.toList(),
),
),
),
)
];
}
return Padding(
padding: const EdgeInsets.only(top: 12.0),
child: RefreshIndicator(
color: Theme.of(context).colorScheme.secondary,
onRefresh: () async {
await absenceProvider.fetch();
await noteProvider.fetch();
},
child: ListView.builder(
padding: EdgeInsets.zero,
physics: const BouncingScrollPhysics(),
itemCount: max(filterWidgets.length + (activeData <= 1 ? 1 : 0), 1),
itemBuilder: (context, index) {
if (filterWidgets.isNotEmpty) {
if ((index == 0 && activeData == 1) ||
(index == 0 && activeData == 0)) {
int value1 = 0;
int value2 = 0;
String title1 = "";
String title2 = "";
String suffix = "";
if (activeData == AbsenceFilter.absences.index) {
value1 = absenceProvider.absences
.where((e) =>
e.delay == 0 && e.state == Justification.excused)
.length;
value2 = absenceProvider.absences
.where((e) =>
e.delay == 0 && e.state == Justification.unexcused)
.length;
title1 = "stat_1".i18n;
title2 = "stat_2".i18n;
suffix = " " + "hr".i18n;
} else if (activeData == AbsenceFilter.delays.index) {
value1 = absenceProvider.absences
.where((e) =>
e.delay != 0 && e.state == Justification.excused)
.map((e) => e.delay)
.fold(0, (a, b) => a + b);
value2 = absenceProvider.absences
.where((e) =>
e.delay != 0 && e.state == Justification.unexcused)
.map((e) => e.delay)
.fold(0, (a, b) => a + b);
title1 = "stat_3".i18n;
title2 = "stat_4".i18n;
suffix = " " + "min".i18n;
}
return Padding(
padding: const EdgeInsets.only(
bottom: 24.0, left: 24.0, right: 24.0),
child: Row(children: [
Expanded(
child: StatisticsTile(
title: AutoSizeText(
title1,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
valueSuffix: suffix,
value: value1.toDouble(),
decimal: false,
color: AppColors.of(context).green,
),
),
const SizedBox(width: 24.0),
Expanded(
child: StatisticsTile(
title: AutoSizeText(
title2,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
valueSuffix: suffix,
value: value2.toDouble(),
decimal: false,
color: AppColors.of(context).red,
),
),
]),
);
}
return Padding(
padding:
const EdgeInsets.symmetric(horizontal: 24.0, vertical: 6.0),
child: filterWidgets[index - (activeData <= 1 ? 1 : 0)],
);
} else {
return Empty(subtitle: "empty".i18n);
}
},
),
),
);
}
}

View File

@@ -0,0 +1,57 @@
import 'package:i18n_extension/i18n_extension.dart';
extension ScreensLocalization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"Absences": "Absences",
"Delays": "Delays",
"Misses": "Misses",
"empty": "You have no absences.",
"stat_1": "Excused Absences",
"stat_2": "Unexcused Absences",
"stat_3": "Excused Delay",
"stat_4": "Unexcused Delay",
"min": "min",
"hr": "hrs",
"Subjects": "Subjects",
"attention": "Attention!",
"attention_body": "Percentage calculations are only an approximation so they may not be accurate.",
},
"hu_hu": {
"Absences": "Hiányzások",
"Delays": "Késések",
"Misses": "Hiányok",
"empty": "Nincsenek hiányaid.",
"stat_1": "Igazolt hiányzások",
"stat_2": "Igazolatlan hiányzások",
"stat_3": "Igazolt Késés",
"stat_4": "Igazolatlan Késés",
"min": "perc",
"hr": "óra",
"Subjects": "Tantárgyak",
"attention": "Figyelem!",
"attention_body": "A százalékos számítások csak közelítések, ezért előfordulhat, hogy nem pontosak.",
},
"de_de": {
"Absences": "Fehlen",
"Delays": "Verspätung",
"Misses": "Fehlt",
"empty": "Sie haben keine Fehlen.",
"stat_1": "Entschuldigte Fehlen",
"stat_2": "Unentschuldigte Fehlen",
"stat_3": "Entschuldigte Verspätung",
"stat_4": "Unentschuldigte Verspätung",
"min": "min",
"hr": "hrs",
"Subjects": "Fächer",
"attention": "Achtung!",
"attention_body": "Prozentberechnungen sind nur eine Annäherung und können daher ungenau sein.",
},
};
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,295 @@
import 'dart:math';
import 'package:animations/animations.dart';
import 'package:refilc/models/settings.dart';
import 'package:refilc/utils/format.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_desktop_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: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, {Key? key, this.groupAverage = 0.0})
: super(key: key);
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 double average;
late Widget gradeGraph;
bool gradeCalcMode = false;
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));
}
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
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: 12.0, right: 12.0),
child: Row(
children: [
Expanded(
child: GradeGraph(subjectGrades,
dayThreshold: 5, classAvg: widget.groupAverage)),
Padding(
padding: const EdgeInsets.only(bottom: 24.0),
child: GradesCount(grades: subjectGrades),
),
],
),
),
),
);
if (!gradeCalcMode) {
buildTiles(subjectGrades);
} else {
List<Grade> ghostGrades = calculatorProvider.ghosts
.where((e) => e.subject == widget.subject)
.toList();
buildTiles(ghostGrades);
}
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,
children: [
FloatingActionButton.small(
child: const Icon(FeatherIcons.plus),
onPressed: () {
gradeCalc(context);
},
),
// FloatingActionButton.small(
// child: const Icon(FeatherIcons.flag, size: 20.0),
// onPressed: () {
// Navigator.of(context).push(CupertinoPageRoute(builder: (context) => PremiumGoalplannerNewGoalScreen(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: 12.0),
],
icon: SubjectIcon.resolveVariant(
subject: widget.subject, context: context),
scrollController: _scrollController,
title: widget.subject.renamedTo ?? widget.subject.name.capital(),
italic: widget.subject.isRenamed &&
settingsProvider.renamedSubjectsItalics,
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(75,
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,30 @@
import 'package:flutter/material.dart';
import 'package:refilc_kreta_api/models/grade.dart';
import 'package:refilc_desktop_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 Container(
width: 75,
padding:
const EdgeInsets.only(bottom: 6.0, top: 6.0, left: 12.0, right: 0.0),
margin: const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: gradesCount
.mapIndexed(
(index, e) => GradesCountItem(count: e, value: index + 1))
.toList(),
),
);
}
}

View File

@@ -0,0 +1,39 @@
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({Key? key, required this.count, required this.value})
: super(key: key);
final int count;
final int value;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
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,314 @@
import 'dart:math';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:refilc/api/providers/update_provider.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_mobile_ui/common/average_display.dart';
import 'package:refilc_mobile_ui/common/empty.dart';
import 'package:refilc_mobile_ui/common/panel/panel.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_desktop_ui/pages/grades/grades_count.dart';
import 'package:refilc_mobile_ui/pages/grades/graph.dart';
import 'package:refilc_desktop_ui/pages/grades/grade_subject_view.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:refilc/helpers/average_helper.dart';
import 'package:refilc_mobile_ui/pages/grades/average_selector.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;
List<Widget> subjectTiles = [];
int avgDropValue = 0;
List<Grade> getSubjectGrades(GradeSubject 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<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 = {};
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, FailWarning(subjectAvgs: subjectAvgs));
tiles.insert(
2,
PanelTitle(
title: Text(avgDropValue == 0
? "Subjects".i18n
: "Subjects_changes".i18n)));
tiles.insert(3, 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,
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(
// https://discord.com/channels/1111649116020285532/1153397476578050130
"classavg".i18n,
textAlign: TextAlign.center,
maxLines: 2,
wrapWords: false,
overflow: TextOverflow.ellipsis,
),
value: classAvg,
),
),
],
));
}
// 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);
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: [
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: Row(
children: [
Expanded(
child: GradeGraph(graphGrades,
dayThreshold: 2, classAvg: totalClassAvg)),
Padding(
padding: const EdgeInsets.only(bottom: 24.0),
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,
automaticallyImplyLeading: false,
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
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": "Failure 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,180 @@
import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc/models/settings.dart';
import 'package:refilc/ui/date_widget.dart';
import 'package:refilc_mobile_ui/common/filter_bar.dart';
import 'package:flutter/material.dart';
import 'package:animated_list_plus/animated_list_plus.dart';
import 'package:provider/provider.dart';
import 'package:refilc/ui/filter/widgets.dart';
import 'package:refilc/ui/filter/sort.dart';
import 'home_page.i18n.dart';
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with SingleTickerProviderStateMixin {
late UserProvider user;
late SettingsProvider settings;
late TabController _tabController;
late PageController _pageController;
late String greeting;
late String firstName;
late List<String> listOrder;
static const pageCount = 4;
@override
void initState() {
super.initState();
_tabController = TabController(length: pageCount, vsync: this);
_pageController = PageController();
listOrder = List.generate(pageCount, (index) => "$index");
user = Provider.of<UserProvider>(context, listen: false);
DateTime now = DateTime.now();
if (now.isBefore(DateTime(now.year, DateTime.august, 31)) &&
now.isAfter(DateTime(now.year, DateTime.june, 14))) {
greeting = "goodrest";
} else if (now.month == user.student?.birth.month &&
now.day == user.student?.birth.day) {
greeting = "happybirthday";
} else if (now.month == DateTime.december &&
now.day >= 24 &&
now.day <= 26) {
greeting = "merryxmas";
} else if (now.month == DateTime.january && now.day == 1) {
greeting = "happynewyear";
} else if (now.hour >= 18) {
greeting = "goodevening";
} else if (now.hour >= 10) {
greeting = "goodafternoon";
} else if (now.hour >= 4) {
greeting = "goodmorning";
} else {
greeting = "goodevening";
}
}
@override
Widget build(BuildContext context) {
user = Provider.of<UserProvider>(context);
settings = Provider.of<SettingsProvider>(context);
List<String> nameParts = user.name?.split(" ") ?? ["?"];
if (!settings.presentationMode) {
firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0];
} else {
firstName = "Béla";
}
return SafeArea(
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Greeting
Padding(
padding: const EdgeInsets.only(
left: 32.0, top: 24.0, bottom: 12.0),
child: Text(
greeting.i18n.fill([firstName]),
overflow: TextOverflow.fade,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 24.0,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: FilterBar(
items: [
Tab(text: "All".i18n),
Tab(text: "Grades".i18n),
Tab(text: "Messages".i18n),
Tab(text: "Absences".i18n),
],
controller: _tabController,
onTap: (i) async {
int selectedPage = _pageController.page!.round();
if (i == selectedPage) return;
if (_pageController.page?.roundToDouble() !=
_pageController.page) {
_pageController.animateToPage(i,
curve: Curves.easeIn, duration: kTabScrollDuration);
return;
}
// swap current page with target page
setState(() {
_pageController.jumpToPage(i);
String currentList = listOrder[selectedPage];
listOrder[selectedPage] = listOrder[i];
listOrder[i] = currentList;
});
},
disableFading: true,
),
),
// Data filters
Expanded(
child: PageView.custom(
controller: _pageController,
childrenDelegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return FutureBuilder<List<DateWidget>>(
key: ValueKey<String>(listOrder[index]),
future: getFilterWidgets(homeFilters[index],
context: context),
builder: (context, dateWidgets) => dateWidgets.data !=
null
? ImplicitlyAnimatedList<Widget>(
items: sortDateWidgets(context,
dateWidgets: dateWidgets.data!),
itemBuilder: filterItemBuilder,
spawnIsolate: false,
areItemsTheSame: (a, b) => a.key == b.key,
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics()),
padding: const EdgeInsets.symmetric(
horizontal: 24.0),
)
: Container(),
);
},
childCount: 4,
findChildIndexCallback: (Key key) {
final ValueKey<String> valueKey =
key as ValueKey<String>;
final String data = valueKey.value;
return listOrder.indexOf(data);
},
),
physics: const PageScrollPhysics()
.applyTo(const BouncingScrollPhysics()),
),
),
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,63 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"goodmorning": "Good morning, %s!",
"goodafternoon": "Good afternoon, %s!",
"goodevening": "Good evening, %s!",
"goodrest": "⛱️ Have a nice holiday, %s!",
"happybirthday": "🎂 Happy birthday, %s!",
"merryxmas": "🎄 Merry Christmas, %s!",
"happynewyear": "🎉 Happy New Year, %s!",
"empty": "Nothing to see here.",
"All": "All",
"Grades": "Grades",
"Messages": "Messages",
"Absences": "Absences",
"update_available": "Update Available",
"missed_exams": "You missed %s exams this week.".one("You missed an exam this week."),
"missed_exam_contact": "Contact %s, to resolve it!",
},
"hu_hu": {
"goodmorning": "Jó reggelt, %s!",
"goodafternoon": "Szép napot, %s!",
"goodevening": "Szép estét, %s!",
"goodrest": "⛱️ Jó szünetet, %s!",
"happybirthday": "🎂 Boldog születésnapot, %s!",
"merryxmas": "🎄 Boldog Karácsonyt, %s!",
"happynewyear": "🎉 Boldog új évet, %s!",
"empty": "Nincs itt semmi látnivaló.",
"All": "Összes",
"Grades": "Jegyek",
"Messages": "Üzenetek",
"Absences": "Hiányok",
"update_available": "Frissítés elérhető",
"missed_exams": "Ezen a héten hiányoztál %s dolgozatról.".one("Ezen a héten hiányoztál egy dolgozatról."),
"missed_exam_contact": "Keresd %s-t, ha pótolni szeretnéd!",
},
"de_de": {
"goodmorning": "Guten morgen, %s!",
"goodafternoon": "Guten Tag, %s!",
"goodevening": "Guten Abend, %s!",
"goodrest": "⛱️ Schöne Ferien, %s!",
"happybirthday": "🎂 Alles Gute zum Geburtstag, %s!",
"merryxmas": "🎄 Frohe Weihnachten, %s!",
"happynewyear": "🎉 Frohes neues Jahr, %s!",
"empty": "Hier gibt es nichts zu sehen.",
"All": "Alles",
"Grades": "Noten",
"Messages": "Nachrichten",
"Absences": "Fehlen",
"update_available": "Update verfügbar",
"missed_exams": "Diese Woche haben Sie %s Prüfungen verpasst.".one("Diese Woche haben Sie eine Prüfung verpasst."),
"missed_exam_contact": "Wenden Sie sich an %s, um sie zu erneuern!",
},
};
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,173 @@
import 'dart:math';
import 'package:refilc/api/providers/update_provider.dart';
import 'package:refilc/ui/date_widget.dart';
import 'package:refilc_kreta_api/providers/message_provider.dart';
import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc/theme/colors/colors.dart';
import 'package:refilc_kreta_api/models/message.dart';
import 'package:refilc_mobile_ui/common/empty.dart';
import 'package:refilc_mobile_ui/common/filter_bar.dart';
import 'package:refilc/ui/filter/sort.dart';
import 'package:refilc_mobile_ui/common/widgets/message/message_viewable.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'messages_page.i18n.dart';
class MessagesPage extends StatefulWidget {
const MessagesPage({Key? key}) : super(key: key);
@override
_MessagesPageState createState() => _MessagesPageState();
}
class _MessagesPageState extends State<MessagesPage>
with TickerProviderStateMixin {
late UserProvider user;
late MessageProvider messageProvider;
late UpdateProvider updateProvider;
late String firstName;
late TabController tabController;
@override
void initState() {
super.initState();
tabController = TabController(length: 4, vsync: this);
}
@override
Widget build(BuildContext context) {
user = Provider.of<UserProvider>(context);
messageProvider = Provider.of<MessageProvider>(context);
updateProvider = Provider.of<UpdateProvider>(context);
List<String> nameParts = user.displayName?.split(" ") ?? ["?"];
firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0];
return Scaffold(
body: Padding(
padding: const EdgeInsets.only(top: 12.0),
child: NestedScrollView(
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics()),
headerSliverBuilder: (context, _) => [
SliverAppBar(
pinned: true,
floating: false,
snap: false,
centerTitle: false,
automaticallyImplyLeading: false,
shadowColor: Theme.of(context).shadowColor,
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
title: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
"Messages".i18n,
style: TextStyle(
color: AppColors.of(context).text,
fontSize: 32.0,
fontWeight: FontWeight.bold),
),
),
bottom: FilterBar(
items: [
Tab(text: "Inbox".i18n),
Tab(text: "Sent".i18n),
Tab(text: "Trash".i18n),
Tab(text: "Draft".i18n),
],
controller: tabController,
disableFading: true,
),
),
],
body: TabBarView(
physics: const BouncingScrollPhysics(),
controller: tabController,
children: List.generate(
4, (index) => filterViewBuilder(context, index))),
),
),
);
}
List<DateWidget> getFilterWidgets(MessageType activeData) {
List<DateWidget> items = [];
switch (activeData) {
case MessageType.inbox:
for (var message in messageProvider.messages) {
if (message.type == MessageType.inbox) {
items.add(DateWidget(
date: message.date,
widget: MessageViewable(message),
));
}
}
break;
case MessageType.sent:
for (var message in messageProvider.messages) {
if (message.type == MessageType.sent) {
items.add(DateWidget(
date: message.date,
widget: MessageViewable(message),
));
}
}
break;
case MessageType.trash:
for (var message in messageProvider.messages) {
if (message.type == MessageType.trash) {
items.add(DateWidget(
date: message.date,
widget: MessageViewable(message),
));
}
}
break;
case MessageType.draft:
for (var message in messageProvider.messages) {
if (message.type == MessageType.draft) {
items.add(DateWidget(
date: message.date,
widget: MessageViewable(message),
));
}
}
break;
}
return items;
}
Widget filterViewBuilder(context, int activeData) {
List<Widget> filterWidgets = sortDateWidgets(context,
dateWidgets: getFilterWidgets(MessageType.values[activeData]),
hasShadow: true);
return Padding(
padding: const EdgeInsets.only(top: 12.0),
child: RefreshIndicator(
color: Theme.of(context).colorScheme.secondary,
onRefresh: () {
return Future.wait([
messageProvider.fetch(type: MessageType.inbox),
messageProvider.fetch(type: MessageType.sent),
messageProvider.fetch(type: MessageType.trash),
]);
},
child: ListView.builder(
padding: EdgeInsets.zero,
physics: const BouncingScrollPhysics(),
itemBuilder: (context, index) => filterWidgets.isNotEmpty
? Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 6.0),
child: filterWidgets[index],
)
: Empty(subtitle: "empty".i18n),
itemCount: max(filterWidgets.length, 1),
),
),
);
}
}

View File

@@ -0,0 +1,36 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"Messages": "Messages",
"Inbox": "Inbox",
"Sent": "Sent",
"Trash": "Trash",
"Draft": "Draft",
"empty": "You have no messages.",
},
"hu_hu": {
"Messages": "Üzenetek",
"Inbox": "Beérkezett",
"Sent": "Elküldött",
"Trash": "Kuka",
"Draft": "Piszkozat",
"empty": "Nincsenek üzeneteid.",
},
"de_de": {
"Messages": "Nachrichten",
"Inbox": "Posteingang",
"Sent": "Gesendet",
"Trash": "Müll",
"Draft": "Entwurf",
"empty": "Sie haben keine Nachrichten.",
},
};
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,390 @@
import 'dart:math';
import 'package:animations/animations.dart';
import 'package:refilc/api/providers/update_provider.dart';
import 'package:refilc/theme/colors/colors.dart';
import 'package:refilc_kreta_api/client/client.dart';
import 'package:refilc_kreta_api/models/week.dart';
import 'package:refilc_kreta_api/providers/timetable_provider.dart';
import 'package:refilc/api/providers/user_provider.dart';
import 'package:refilc_kreta_api/models/lesson.dart';
import 'package:refilc_mobile_ui/common/empty.dart';
import 'package:refilc_mobile_ui/common/panel/panel.dart';
import 'package:refilc_kreta_api/controllers/timetable_controller.dart';
import 'package:refilc_desktop_ui/common/widgets/lesson/lesson_viewable.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart';
import 'package:refilc/utils/format.dart';
import 'package:intl/intl.dart';
import 'package:i18n_extension/i18n_widget.dart';
import 'timetable_page.i18n.dart';
// todo: "fix" overflow (priority: -1)
class TimetablePage extends StatefulWidget {
const TimetablePage({Key? key, this.initialDay, this.initialWeek})
: super(key: key);
final DateTime? initialDay;
final Week? initialWeek;
static void jump(BuildContext context,
{Week? week, DateTime? day, Lesson? lesson}) {
// Go to timetable page with arguments
// NavigationScreen.of(context)?.customRoute(navigationPageRoute((context) => TimetablePage(
// initialDay: lesson?.date ?? day,
// initialWeek: lesson?.date != null
// ? Week.fromDate(lesson!.date)
// : day != null
// ? Week.fromDate(day)
// : week,
// )));
// NavigationScreen.of(context)?.setPage("timetable");
// Show initial Lesson
// if (lesson != null) LessonView.show(lesson, context: context);
}
@override
_TimetablePageState createState() => _TimetablePageState();
}
class _TimetablePageState extends State<TimetablePage>
with TickerProviderStateMixin {
late UserProvider user;
late TimetableProvider timetableProvider;
late UpdateProvider updateProvider;
late String firstName;
late TimetableController _controller;
late TabController _tabController;
late Widget empty;
int _getDayIndex(DateTime date) {
int index = 0;
if (_controller.days == null || (_controller.days?.isEmpty ?? true)) {
return index;
}
// find the first day with upcoming lessons
index = _controller.days!.indexWhere((day) => day.last.end.isAfter(date));
if (index == -1) index = 0; // fallback
return index;
}
// Update timetable on user change
Future<void> _userListener() async {
await Provider.of<KretaClient>(context, listen: false).refreshLogin();
if (mounted) _controller.jump(_controller.currentWeek, context: context);
}
@override
void initState() {
super.initState();
// Initalize controllers
_controller = TimetableController();
_tabController = TabController(length: 0, vsync: this, initialIndex: 0);
empty = Empty(subtitle: "empty".i18n);
bool initial = true;
// Only update the TabController on week changes
_controller.addListener(() {
if (_controller.days == null) return;
setState(() {
_tabController = TabController(
length: _controller.days!.length,
vsync: this,
initialIndex:
min(_tabController.index, max(_controller.days!.length - 1, 0)),
);
if (initial ||
_controller.previousWeekId != _controller.currentWeekId) {
_tabController
.animateTo(_getDayIndex(widget.initialDay ?? DateTime.now()));
}
initial = false;
// Empty is updated once every week change
empty = Empty(subtitle: "empty".i18n);
});
});
if (mounted) {
if (widget.initialWeek != null) {
_controller.jump(widget.initialWeek!, context: context, initial: true);
} else {
_controller.jump(_controller.currentWeek,
context: context, initial: true, skip: true);
}
}
// Listen for user changes
user = Provider.of<UserProvider>(context, listen: false);
user.addListener(_userListener);
}
@override
void dispose() {
_tabController.dispose();
_controller.dispose();
user.removeListener(_userListener);
super.dispose();
}
String dayTitle(int index) {
// Sometimes when changing weeks really fast,
// controller.days might be null or won't include index
try {
return DateFormat("EEEE", I18n.of(context).locale.languageCode)
.format(_controller.days![index].first.date);
} catch (e) {
return "timetable".i18n;
}
}
@override
Widget build(BuildContext context) {
user = Provider.of<UserProvider>(context);
timetableProvider = Provider.of<TimetableProvider>(context);
updateProvider = Provider.of<UpdateProvider>(context);
// First name
List<String> nameParts = user.name?.split(" ") ?? ["?"];
firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0];
return Scaffold(
body: Padding(
padding: const EdgeInsets.only(top: 18.0),
child: Column(
children: [
Expanded(
child: PageTransitionSwitcher(
transitionBuilder: (
Widget child,
Animation<double> primaryAnimation,
Animation<double> secondaryAnimation,
) {
return FadeThroughTransition(
child: child,
animation: primaryAnimation,
secondaryAnimation: secondaryAnimation,
fillColor: Theme.of(context).scaffoldBackgroundColor,
);
},
child: _controller.days != null
?
// Week view
_tabController.length > 0
? CupertinoScrollbar(
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _controller.days!.length,
itemBuilder: (context, tab) => SizedBox(
width: 400,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Day Title
Padding(
padding: const EdgeInsets.only(
left: 24.0,
right: 28.0,
top: 18.0,
bottom: 8.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
dayTitle(tab).capital(),
style: const TextStyle(
fontSize: 32.0,
fontWeight: FontWeight.w600,
),
),
Text(
"${_controller.days![tab].first.date.day}"
.padLeft(2, '0') +
".",
style: TextStyle(
color: AppColors.of(context)
.text
.withOpacity(.5),
fontWeight: FontWeight.w500,
),
),
],
),
),
// Lessons
Expanded(
child: ListView.builder(
padding: EdgeInsets.zero,
physics: const BouncingScrollPhysics(),
itemCount:
_controller.days![tab].length + 2,
itemBuilder: (context, index) {
if (_controller.days == null) {
return Container();
}
// Header
if (index == 0) {
return const Padding(
padding: EdgeInsets.only(
top: 8.0,
left: 24.0,
right: 24.0),
child: PanelHeader(
padding: EdgeInsets.only(
top: 12.0)),
);
}
// Footer
if (index ==
_controller.days![tab].length +
1) {
return const Padding(
padding: EdgeInsets.only(
bottom: 8.0,
left: 24.0,
right: 24.0),
child: PanelFooter(
padding: EdgeInsets.only(
top: 12.0)),
);
}
// Body
final Lesson lesson =
_controller.days![tab][index - 1];
final bool swapDescDay = _controller
.days![tab]
.map(
(l) => l.swapDesc ? 1 : 0)
.reduce((a, b) => a + b) >=
_controller.days![tab].length *
.5;
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0),
child: PanelBody(
padding:
const EdgeInsets.symmetric(
horizontal: 10.0),
child: LessonViewable(
lesson,
swapDesc: swapDescDay,
),
),
);
},
),
),
],
),
),
),
)
// Empty week
: Expanded(
child: Center(child: empty),
)
: Center(
child: CircularProgressIndicator(
color: Theme.of(context).colorScheme.secondary,
),
),
),
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Previous week
IconButton(
onPressed: _controller.currentWeekId == 0
? null
: () => setState(() {
_controller.previous(context);
}),
splashRadius: 24.0,
icon: const Icon(FeatherIcons.chevronLeft),
color: Theme.of(context).colorScheme.secondary),
// Week selector
InkWell(
borderRadius: BorderRadius.circular(6.0),
onTap: () => setState(() {
_controller.current();
if (mounted) {
_controller.jump(_controller.currentWeek,
context: context,
loader: _controller.currentWeekId !=
_controller.previousWeekId);
}
}),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"${_controller.currentWeekId + 1}. " +
"week".i18n +
" (" +
// Week start
DateFormat(
(_controller.currentWeek.start.year !=
DateTime.now().year
? "yy. "
: "") +
"MMM d.",
I18n.of(context).locale.languageCode)
.format(_controller.currentWeek.start) +
" - " +
// Week end
DateFormat(
(_controller.currentWeek.start.year !=
DateTime.now().year
? "yy. "
: "") +
"MMM d.",
I18n.of(context).locale.languageCode)
.format(_controller.currentWeek.end) +
")",
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14.0,
),
),
),
),
// Next week
IconButton(
onPressed: _controller.currentWeekId == 51
? null
: () => setState(() {
_controller.next(context);
}),
splashRadius: 24.0,
icon: const Icon(FeatherIcons.chevronRight),
color: Theme.of(context).colorScheme.secondary),
],
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:i18n_extension/i18n_extension.dart';
extension Localization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"timetable": "Timetable",
"empty": "No school this week!",
"week": "Week",
"error": "Failed to fetch timetable!",
},
"hu_hu": {
"timetable": "Órarend",
"empty": "Ezen a héten nincs iskola.",
"week": "Hét",
"error": "Nem sikerült lekérni az órarendet!",
},
"de_de": {
"timetable": "Zeitplan",
"empty": "Keine Schule diese Woche.",
"week": "Woche",
"error": "Der Fahrplan konnte nicht abgerufen werden!",
},
};
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);
}