igen
This commit is contained in:
@@ -1,79 +1,79 @@
|
||||
import 'package:filcnaplo/helpers/subject.dart';
|
||||
import 'package:filcnaplo/theme/colors/colors.dart';
|
||||
import 'package:filcnaplo/ui/date_widget.dart';
|
||||
import 'package:filcnaplo/utils/reverse_search.dart';
|
||||
import 'package:filcnaplo_kreta_api/models/absence.dart';
|
||||
import 'package:filcnaplo_kreta_api/models/subject.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/custom_snack_bar.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_viewable.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/hero_scrollview.dart';
|
||||
import 'package:filcnaplo_mobile_ui/pages/absences/absence_subject_view_container.dart';
|
||||
import 'package:filcnaplo_mobile_ui/pages/timetable/timetable_page.dart';
|
||||
import 'package:filcnaplo/ui/filter/sort.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:filcnaplo/utils/format.dart';
|
||||
|
||||
import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_view.i18n.dart';
|
||||
|
||||
class AbsenceSubjectView extends StatelessWidget {
|
||||
const AbsenceSubjectView(this.subject, {Key? key, this.absences = const []}) : super(key: key);
|
||||
|
||||
final Subject subject;
|
||||
final List<Absence> absences;
|
||||
|
||||
static void show(Subject subject, List<Absence> absences, {required BuildContext context}) {
|
||||
Navigator.of(context, rootNavigator: true)
|
||||
.push<Absence>(CupertinoPageRoute(builder: (context) => AbsenceSubjectView(subject, absences: absences)))
|
||||
.then((value) {
|
||||
if (value == null) return;
|
||||
|
||||
Future.delayed(const Duration(milliseconds: 250)).then((_) {
|
||||
ReverseSearch.getLessonByAbsence(value, context).then((lesson) {
|
||||
if (lesson != null) {
|
||||
TimetablePage.jump(context, lesson: lesson);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(
|
||||
content: Text("Cannot find lesson".i18n, style: const TextStyle(color: Colors.white)),
|
||||
backgroundColor: AppColors.of(context).red,
|
||||
context: context,
|
||||
));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dateWidgets = absences
|
||||
.map((a) => DateWidget(
|
||||
widget: AbsenceViewable(a, padding: EdgeInsets.zero),
|
||||
date: a.date,
|
||||
))
|
||||
.toList();
|
||||
List<Widget> absenceTiles = sortDateWidgets(context, dateWidgets: dateWidgets, padding: EdgeInsets.zero, hasShadow: true);
|
||||
|
||||
return Scaffold(
|
||||
body: HeroScrollView(
|
||||
title: subject.renamedTo ?? subject.name.capital(),
|
||||
italic: subject.isRenamed,
|
||||
icon: SubjectIcon.resolveVariant(subject: subject, context: context),
|
||||
child: AbsenceSubjectViewContainer(
|
||||
child: CupertinoScrollbar(
|
||||
child: ListView.builder(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: absenceTiles[index],
|
||||
),
|
||||
itemCount: absenceTiles.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:filcnaplo/helpers/subject.dart';
|
||||
import 'package:filcnaplo/theme/colors/colors.dart';
|
||||
import 'package:filcnaplo/ui/date_widget.dart';
|
||||
import 'package:filcnaplo/utils/reverse_search.dart';
|
||||
import 'package:filcnaplo_kreta_api/models/absence.dart';
|
||||
import 'package:filcnaplo_kreta_api/models/subject.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/custom_snack_bar.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_viewable.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/hero_scrollview.dart';
|
||||
import 'package:filcnaplo_mobile_ui/pages/absences/absence_subject_view_container.dart';
|
||||
import 'package:filcnaplo_mobile_ui/pages/timetable/timetable_page.dart';
|
||||
import 'package:filcnaplo/ui/filter/sort.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:filcnaplo/utils/format.dart';
|
||||
|
||||
import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_view.i18n.dart';
|
||||
|
||||
class AbsenceSubjectView extends StatelessWidget {
|
||||
const AbsenceSubjectView(this.subject, {Key? key, this.absences = const []}) : super(key: key);
|
||||
|
||||
final Subject subject;
|
||||
final List<Absence> absences;
|
||||
|
||||
static void show(Subject subject, List<Absence> absences, {required BuildContext context}) {
|
||||
Navigator.of(context, rootNavigator: true)
|
||||
.push<Absence>(CupertinoPageRoute(builder: (context) => AbsenceSubjectView(subject, absences: absences)))
|
||||
.then((value) {
|
||||
if (value == null) return;
|
||||
|
||||
Future.delayed(const Duration(milliseconds: 250)).then((_) {
|
||||
ReverseSearch.getLessonByAbsence(value, context).then((lesson) {
|
||||
if (lesson != null) {
|
||||
TimetablePage.jump(context, lesson: lesson);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(CustomSnackBar(
|
||||
content: Text("Cannot find lesson".i18n, style: const TextStyle(color: Colors.white)),
|
||||
backgroundColor: AppColors.of(context).red,
|
||||
context: context,
|
||||
));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dateWidgets = absences
|
||||
.map((a) => DateWidget(
|
||||
widget: AbsenceViewable(a, padding: EdgeInsets.zero),
|
||||
date: a.date,
|
||||
))
|
||||
.toList();
|
||||
List<Widget> absenceTiles = sortDateWidgets(context, dateWidgets: dateWidgets, padding: EdgeInsets.zero, hasShadow: true);
|
||||
|
||||
return Scaffold(
|
||||
body: HeroScrollView(
|
||||
title: subject.renamedTo ?? subject.name.capital(),
|
||||
italic: subject.isRenamed,
|
||||
icon: SubjectIcon.resolveVariant(subject: subject, context: context),
|
||||
child: AbsenceSubjectViewContainer(
|
||||
child: CupertinoScrollbar(
|
||||
child: ListView.builder(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: absenceTiles[index],
|
||||
),
|
||||
itemCount: absenceTiles.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AbsenceSubjectViewContainer extends InheritedWidget {
|
||||
const AbsenceSubjectViewContainer({Key? key, required Widget child}) : super(key: key, child: child);
|
||||
|
||||
static AbsenceSubjectViewContainer? of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<AbsenceSubjectViewContainer>();
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(AbsenceSubjectViewContainer oldWidget) => false;
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AbsenceSubjectViewContainer extends InheritedWidget {
|
||||
const AbsenceSubjectViewContainer({Key? key, required Widget child}) : super(key: key, child: child);
|
||||
|
||||
static AbsenceSubjectViewContainer? of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<AbsenceSubjectViewContainer>();
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(AbsenceSubjectViewContainer oldWidget) => false;
|
||||
}
|
||||
|
||||
@@ -1,382 +1,382 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:filcnaplo/api/providers/update_provider.dart';
|
||||
import 'package:filcnaplo/ui/date_widget.dart';
|
||||
import 'package:filcnaplo_kreta_api/models/absence.dart';
|
||||
import 'package:filcnaplo_kreta_api/models/lesson.dart';
|
||||
import 'package:filcnaplo_kreta_api/models/subject.dart';
|
||||
import 'package:filcnaplo_kreta_api/models/week.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/absence_provider.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/note_provider.dart';
|
||||
import 'package:filcnaplo/api/providers/user_provider.dart';
|
||||
import 'package:filcnaplo/theme/colors/colors.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/action_button.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/empty.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/filter_bar.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/absence/absence_subject_tile.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_viewable.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/widgets/statistics_tile.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/widgets/miss_tile.dart';
|
||||
import 'package:filcnaplo_mobile_ui/pages/absences/absence_subject_view.dart';
|
||||
import 'package:filcnaplo/ui/filter/sort.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:filcnaplo/utils/color.dart';
|
||||
import 'absences_page.i18n.dart';
|
||||
|
||||
enum AbsenceFilter { absences, delays, misses }
|
||||
|
||||
class SubjectAbsence {
|
||||
Subject 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<Subject, 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
void buildSubjectAbsences() {
|
||||
Map<Subject, 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,
|
||||
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: ColorUtils.stringToColor(user.displayName ?? "?"),
|
||||
badge: updateProvider.available,
|
||||
role: user.role,
|
||||
profilePictureString: user.picture,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
automaticallyImplyLeading: false,
|
||||
shadowColor: Theme.of(context).shadowColor,
|
||||
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);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:filcnaplo/api/providers/update_provider.dart';
|
||||
import 'package:filcnaplo/ui/date_widget.dart';
|
||||
import 'package:filcnaplo_kreta_api/models/absence.dart';
|
||||
import 'package:filcnaplo_kreta_api/models/lesson.dart';
|
||||
import 'package:filcnaplo_kreta_api/models/subject.dart';
|
||||
import 'package:filcnaplo_kreta_api/models/week.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/absence_provider.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/note_provider.dart';
|
||||
import 'package:filcnaplo/api/providers/user_provider.dart';
|
||||
import 'package:filcnaplo/theme/colors/colors.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/action_button.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/empty.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/filter_bar.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/absence/absence_subject_tile.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_viewable.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/widgets/statistics_tile.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/widgets/miss_tile.dart';
|
||||
import 'package:filcnaplo_mobile_ui/pages/absences/absence_subject_view.dart';
|
||||
import 'package:filcnaplo/ui/filter/sort.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:filcnaplo/utils/color.dart';
|
||||
import 'absences_page.i18n.dart';
|
||||
|
||||
enum AbsenceFilter { absences, delays, misses }
|
||||
|
||||
class SubjectAbsence {
|
||||
Subject 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<Subject, 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
void buildSubjectAbsences() {
|
||||
Map<Subject, 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,
|
||||
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: ColorUtils.stringToColor(user.displayName ?? "?"),
|
||||
badge: updateProvider.available,
|
||||
role: user.role,
|
||||
profilePictureString: user.picture,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
automaticallyImplyLeading: false,
|
||||
shadowColor: Theme.of(context).shadowColor,
|
||||
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);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,167 +1,167 @@
|
||||
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_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 >= 30) {
|
||||
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: "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();
|
||||
}
|
||||
}
|
||||
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_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 >= 30) {
|
||||
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: "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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,53 +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 = [];
|
||||
}
|
||||
}
|
||||
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 = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +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])),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
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])),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,283 +1,283 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:filcnaplo/utils/format.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/models/premium_scopes.dart';
|
||||
import 'package:filcnaplo_premium/providers/premium_provider.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/goalplanner/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 double average;
|
||||
late Widget gradeGraph;
|
||||
|
||||
bool gradeCalcMode = false;
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
gradeProvider = Provider.of<GradeProvider>(context);
|
||||
calculatorProvider = Provider.of<GradeCalculatorProvider>(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);
|
||||
}
|
||||
|
||||
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(
|
||||
child: const Icon(FeatherIcons.plus),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
onPressed: () {
|
||||
gradeCalc(context);
|
||||
},
|
||||
),
|
||||
FloatingActionButton.small(
|
||||
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) => 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,
|
||||
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;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:filcnaplo/utils/format.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/models/premium_scopes.dart';
|
||||
import 'package:filcnaplo_premium/providers/premium_provider.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/goalplanner/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 double average;
|
||||
late Widget gradeGraph;
|
||||
|
||||
bool gradeCalcMode = false;
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
gradeProvider = Provider.of<GradeProvider>(context);
|
||||
calculatorProvider = Provider.of<GradeCalculatorProvider>(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);
|
||||
}
|
||||
|
||||
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(
|
||||
child: const Icon(FeatherIcons.plus),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
onPressed: () {
|
||||
gradeCalc(context);
|
||||
},
|
||||
),
|
||||
FloatingActionButton.small(
|
||||
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) => 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,
|
||||
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;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,294 +1,294 @@
|
||||
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/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/utils/color.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,
|
||||
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: 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();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
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/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/utils/color.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,
|
||||
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: 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();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,295 +1,295 @@
|
||||
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;
|
||||
}(),
|
||||
),
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}(),
|
||||
),
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,10 +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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,357 +1,357 @@
|
||||
// ignore_for_file: dead_code
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:filcnaplo/api/providers/live_card_provider.dart';
|
||||
import 'package:filcnaplo/ui/date_widget.dart';
|
||||
import 'package:filcnaplo_premium/providers/premium_provider.dart';
|
||||
import 'package:animated_list_plus/animated_list_plus.dart';
|
||||
import 'package:filcnaplo/api/providers/update_provider.dart';
|
||||
import 'package:filcnaplo/api/providers/sync.dart';
|
||||
import 'package:confetti/confetti.dart';
|
||||
import 'package:filcnaplo/models/settings.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/absence_provider.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/event_provider.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/exam_provider.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/homework_provider.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/message_provider.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/note_provider.dart';
|
||||
import 'package:filcnaplo/api/providers/user_provider.dart';
|
||||
import 'package:filcnaplo/api/providers/status_provider.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/empty.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/filter_bar.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/pages/home/live_card/live_card.dart';
|
||||
import 'package:filcnaplo_mobile_ui/screens/navigation/navigation_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'home_page.i18n.dart';
|
||||
import 'package:filcnaplo/utils/color.dart';
|
||||
import 'package:filcnaplo/ui/filter/widgets.dart';
|
||||
import 'package:filcnaplo/ui/filter/sort.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_HomePageState createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
late UserProvider user;
|
||||
late SettingsProvider settings;
|
||||
late UpdateProvider updateProvider;
|
||||
late StatusProvider statusProvider;
|
||||
late GradeProvider gradeProvider;
|
||||
late TimetableProvider timetableProvider;
|
||||
late MessageProvider messageProvider;
|
||||
late AbsenceProvider absenceProvider;
|
||||
late HomeworkProvider homeworkProvider;
|
||||
late ExamProvider examProvider;
|
||||
late NoteProvider noteProvider;
|
||||
late EventProvider eventProvider;
|
||||
|
||||
late PageController _pageController;
|
||||
ConfettiController? _confettiController;
|
||||
late LiveCardProvider _liveCard;
|
||||
late AnimationController _liveCardAnimation;
|
||||
|
||||
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();
|
||||
user = Provider.of<UserProvider>(context, listen: false);
|
||||
_liveCard = Provider.of<LiveCardProvider>(context, listen: false);
|
||||
_liveCardAnimation = AnimationController(vsync: this, duration: const Duration(milliseconds: 500));
|
||||
|
||||
_liveCardAnimation.animateTo(_liveCard.show ? 1.0 : 0.0, duration: Duration.zero);
|
||||
|
||||
listOrder = List.generate(pageCount, (index) => "$index");
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// _filterController.dispose();
|
||||
_pageController.dispose();
|
||||
_tabController.dispose();
|
||||
_confettiController?.dispose();
|
||||
_liveCardAnimation.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void setGreeting() {
|
||||
DateTime now = DateTime.now();
|
||||
if (now.isBefore(DateTime(now.year, DateTime.august, 31)) && now.isAfter(DateTime(now.year, DateTime.june, 14))) {
|
||||
greeting = "goodrest";
|
||||
|
||||
if (NavigationScreen.of(context)?.init("confetti") ?? false) {
|
||||
_confettiController = ConfettiController(duration: const Duration(seconds: 1));
|
||||
Future.delayed(const Duration(seconds: 1)).then((value) => mounted ? _confettiController?.play() : null);
|
||||
}
|
||||
} else if (now.month == user.student?.birth.month && now.day == user.student?.birth.day) {
|
||||
greeting = "happybirthday";
|
||||
|
||||
if (NavigationScreen.of(context)?.init("confetti") ?? false) {
|
||||
_confettiController = ConfettiController(duration: const Duration(seconds: 3));
|
||||
Future.delayed(const Duration(seconds: 1)).then((value) => mounted ? _confettiController?.play() : null);
|
||||
}
|
||||
} 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);
|
||||
statusProvider = Provider.of<StatusProvider>(context, listen: false);
|
||||
updateProvider = Provider.of<UpdateProvider>(context);
|
||||
_liveCard = Provider.of<LiveCardProvider>(context);
|
||||
gradeProvider = Provider.of<GradeProvider>(context);
|
||||
context.watch<PremiumProvider>();
|
||||
|
||||
_liveCardAnimation.animateTo(_liveCard.show ? 1.0 : 0.0);
|
||||
|
||||
setGreeting();
|
||||
|
||||
List<String> nameParts = user.displayName?.split(" ") ?? ["?"];
|
||||
if (!settings.presentationMode) {
|
||||
firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0];
|
||||
} else {
|
||||
firstName = "Béla";
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12.0),
|
||||
child: NestedScrollView(
|
||||
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
|
||||
headerSliverBuilder: (context, _) => [
|
||||
AnimatedBuilder(
|
||||
animation: _liveCardAnimation,
|
||||
builder: (context, child) {
|
||||
return SliverAppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
centerTitle: false,
|
||||
titleSpacing: 0.0,
|
||||
// Welcome text
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(left: 24.0),
|
||||
child: Text(
|
||||
greeting.i18n.fill([firstName]),
|
||||
overflow: TextOverflow.fade,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18.0,
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
// Profile Icon
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 24.0),
|
||||
child: ProfileButton(
|
||||
child: ProfileImage(
|
||||
heroTag: "profile",
|
||||
name: firstName,
|
||||
backgroundColor: !settings.presentationMode
|
||||
? ColorUtils.stringToColor(user.displayName ?? "?")
|
||||
: Theme.of(context).colorScheme.secondary,
|
||||
badge: updateProvider.available,
|
||||
role: user.role,
|
||||
profilePictureString: user.picture,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
expandedHeight: _liveCardAnimation.value * 234.0,
|
||||
|
||||
// Live Card
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 24.0,
|
||||
right: 24.0,
|
||||
top: 58.0 + MediaQuery.of(context).padding.top,
|
||||
bottom: 52.0,
|
||||
),
|
||||
child: Transform.scale(
|
||||
scale: _liveCardAnimation.value,
|
||||
child: Opacity(
|
||||
opacity: _liveCardAnimation.value,
|
||||
child: const LiveCard(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
shadowColor: Colors.black,
|
||||
|
||||
// Filter Bar
|
||||
bottom: 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;
|
||||
});
|
||||
},
|
||||
),
|
||||
pinned: true,
|
||||
floating: false,
|
||||
snap: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.only(top: 12.0),
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
// from flutter source
|
||||
if (notification is ScrollUpdateNotification && !_tabController.indexIsChanging) {
|
||||
if ((_pageController.page! - _tabController.index).abs() > 1.0) {
|
||||
_tabController.index = _pageController.page!.floor();
|
||||
}
|
||||
_tabController.offset = (_pageController.page! - _tabController.index).clamp(-1.0, 1.0);
|
||||
} else if (notification is ScrollEndNotification) {
|
||||
_tabController.index = _pageController.page!.round();
|
||||
if (!_tabController.indexIsChanging) _tabController.offset = (_pageController.page! - _tabController.index).clamp(-1.0, 1.0);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
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
|
||||
? RefreshIndicator(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
onRefresh: () => syncAll(context),
|
||||
child: ImplicitlyAnimatedList<Widget>(
|
||||
items: [
|
||||
if (index == 0) const SizedBox(key: Key("\$premium")),
|
||||
...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()),
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
|
||||
// confetti 🎊
|
||||
if (_confettiController != null)
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ConfettiWidget(
|
||||
confettiController: _confettiController!,
|
||||
blastDirection: -pi / 2,
|
||||
emissionFrequency: 0.01,
|
||||
numberOfParticles: 80,
|
||||
maxBlastForce: 100,
|
||||
minBlastForce: 90,
|
||||
gravity: 0.3,
|
||||
minimumSize: const Size(5, 5),
|
||||
maximumSize: const Size(20, 20),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Widget> filterViewBuilder(context, int activeData) async {
|
||||
final activeFilter = homeFilters[activeData];
|
||||
|
||||
List<Widget> filterWidgets = sortDateWidgets(
|
||||
context,
|
||||
dateWidgets: await getFilterWidgets(activeFilter, context: context),
|
||||
showDivider: true,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 12.0),
|
||||
child: RefreshIndicator(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
onRefresh: () => syncAll(context),
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
if (filterWidgets.isNotEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: filterWidgets[index],
|
||||
);
|
||||
} else {
|
||||
return Empty(subtitle: "empty".i18n);
|
||||
}
|
||||
},
|
||||
itemCount: max(filterWidgets.length, 1),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// ignore_for_file: dead_code
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:filcnaplo/api/providers/live_card_provider.dart';
|
||||
import 'package:filcnaplo/ui/date_widget.dart';
|
||||
import 'package:filcnaplo_premium/providers/premium_provider.dart';
|
||||
import 'package:animated_list_plus/animated_list_plus.dart';
|
||||
import 'package:filcnaplo/api/providers/update_provider.dart';
|
||||
import 'package:filcnaplo/api/providers/sync.dart';
|
||||
import 'package:confetti/confetti.dart';
|
||||
import 'package:filcnaplo/models/settings.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/absence_provider.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/event_provider.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/exam_provider.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/homework_provider.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/message_provider.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/note_provider.dart';
|
||||
import 'package:filcnaplo/api/providers/user_provider.dart';
|
||||
import 'package:filcnaplo/api/providers/status_provider.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/empty.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/filter_bar.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/pages/home/live_card/live_card.dart';
|
||||
import 'package:filcnaplo_mobile_ui/screens/navigation/navigation_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'home_page.i18n.dart';
|
||||
import 'package:filcnaplo/utils/color.dart';
|
||||
import 'package:filcnaplo/ui/filter/widgets.dart';
|
||||
import 'package:filcnaplo/ui/filter/sort.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_HomePageState createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
late UserProvider user;
|
||||
late SettingsProvider settings;
|
||||
late UpdateProvider updateProvider;
|
||||
late StatusProvider statusProvider;
|
||||
late GradeProvider gradeProvider;
|
||||
late TimetableProvider timetableProvider;
|
||||
late MessageProvider messageProvider;
|
||||
late AbsenceProvider absenceProvider;
|
||||
late HomeworkProvider homeworkProvider;
|
||||
late ExamProvider examProvider;
|
||||
late NoteProvider noteProvider;
|
||||
late EventProvider eventProvider;
|
||||
|
||||
late PageController _pageController;
|
||||
ConfettiController? _confettiController;
|
||||
late LiveCardProvider _liveCard;
|
||||
late AnimationController _liveCardAnimation;
|
||||
|
||||
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();
|
||||
user = Provider.of<UserProvider>(context, listen: false);
|
||||
_liveCard = Provider.of<LiveCardProvider>(context, listen: false);
|
||||
_liveCardAnimation = AnimationController(vsync: this, duration: const Duration(milliseconds: 500));
|
||||
|
||||
_liveCardAnimation.animateTo(_liveCard.show ? 1.0 : 0.0, duration: Duration.zero);
|
||||
|
||||
listOrder = List.generate(pageCount, (index) => "$index");
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// _filterController.dispose();
|
||||
_pageController.dispose();
|
||||
_tabController.dispose();
|
||||
_confettiController?.dispose();
|
||||
_liveCardAnimation.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void setGreeting() {
|
||||
DateTime now = DateTime.now();
|
||||
if (now.isBefore(DateTime(now.year, DateTime.august, 31)) && now.isAfter(DateTime(now.year, DateTime.june, 14))) {
|
||||
greeting = "goodrest";
|
||||
|
||||
if (NavigationScreen.of(context)?.init("confetti") ?? false) {
|
||||
_confettiController = ConfettiController(duration: const Duration(seconds: 1));
|
||||
Future.delayed(const Duration(seconds: 1)).then((value) => mounted ? _confettiController?.play() : null);
|
||||
}
|
||||
} else if (now.month == user.student?.birth.month && now.day == user.student?.birth.day) {
|
||||
greeting = "happybirthday";
|
||||
|
||||
if (NavigationScreen.of(context)?.init("confetti") ?? false) {
|
||||
_confettiController = ConfettiController(duration: const Duration(seconds: 3));
|
||||
Future.delayed(const Duration(seconds: 1)).then((value) => mounted ? _confettiController?.play() : null);
|
||||
}
|
||||
} 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);
|
||||
statusProvider = Provider.of<StatusProvider>(context, listen: false);
|
||||
updateProvider = Provider.of<UpdateProvider>(context);
|
||||
_liveCard = Provider.of<LiveCardProvider>(context);
|
||||
gradeProvider = Provider.of<GradeProvider>(context);
|
||||
context.watch<PremiumProvider>();
|
||||
|
||||
_liveCardAnimation.animateTo(_liveCard.show ? 1.0 : 0.0);
|
||||
|
||||
setGreeting();
|
||||
|
||||
List<String> nameParts = user.displayName?.split(" ") ?? ["?"];
|
||||
if (!settings.presentationMode) {
|
||||
firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0];
|
||||
} else {
|
||||
firstName = "Béla";
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12.0),
|
||||
child: NestedScrollView(
|
||||
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
|
||||
headerSliverBuilder: (context, _) => [
|
||||
AnimatedBuilder(
|
||||
animation: _liveCardAnimation,
|
||||
builder: (context, child) {
|
||||
return SliverAppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
centerTitle: false,
|
||||
titleSpacing: 0.0,
|
||||
// Welcome text
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(left: 24.0),
|
||||
child: Text(
|
||||
greeting.i18n.fill([firstName]),
|
||||
overflow: TextOverflow.fade,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18.0,
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
// Profile Icon
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 24.0),
|
||||
child: ProfileButton(
|
||||
child: ProfileImage(
|
||||
heroTag: "profile",
|
||||
name: firstName,
|
||||
backgroundColor: !settings.presentationMode
|
||||
? ColorUtils.stringToColor(user.displayName ?? "?")
|
||||
: Theme.of(context).colorScheme.secondary,
|
||||
badge: updateProvider.available,
|
||||
role: user.role,
|
||||
profilePictureString: user.picture,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
expandedHeight: _liveCardAnimation.value * 234.0,
|
||||
|
||||
// Live Card
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 24.0,
|
||||
right: 24.0,
|
||||
top: 58.0 + MediaQuery.of(context).padding.top,
|
||||
bottom: 52.0,
|
||||
),
|
||||
child: Transform.scale(
|
||||
scale: _liveCardAnimation.value,
|
||||
child: Opacity(
|
||||
opacity: _liveCardAnimation.value,
|
||||
child: const LiveCard(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
shadowColor: Colors.black,
|
||||
|
||||
// Filter Bar
|
||||
bottom: 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;
|
||||
});
|
||||
},
|
||||
),
|
||||
pinned: true,
|
||||
floating: false,
|
||||
snap: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.only(top: 12.0),
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
// from flutter source
|
||||
if (notification is ScrollUpdateNotification && !_tabController.indexIsChanging) {
|
||||
if ((_pageController.page! - _tabController.index).abs() > 1.0) {
|
||||
_tabController.index = _pageController.page!.floor();
|
||||
}
|
||||
_tabController.offset = (_pageController.page! - _tabController.index).clamp(-1.0, 1.0);
|
||||
} else if (notification is ScrollEndNotification) {
|
||||
_tabController.index = _pageController.page!.round();
|
||||
if (!_tabController.indexIsChanging) _tabController.offset = (_pageController.page! - _tabController.index).clamp(-1.0, 1.0);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
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
|
||||
? RefreshIndicator(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
onRefresh: () => syncAll(context),
|
||||
child: ImplicitlyAnimatedList<Widget>(
|
||||
items: [
|
||||
if (index == 0) const SizedBox(key: Key("\$premium")),
|
||||
...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()),
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
|
||||
// confetti 🎊
|
||||
if (_confettiController != null)
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ConfettiWidget(
|
||||
confettiController: _confettiController!,
|
||||
blastDirection: -pi / 2,
|
||||
emissionFrequency: 0.01,
|
||||
numberOfParticles: 80,
|
||||
maxBlastForce: 100,
|
||||
minBlastForce: 90,
|
||||
gravity: 0.3,
|
||||
minimumSize: const Size(5, 5),
|
||||
maximumSize: const Size(20, 20),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Widget> filterViewBuilder(context, int activeData) async {
|
||||
final activeFilter = homeFilters[activeData];
|
||||
|
||||
List<Widget> filterWidgets = sortDateWidgets(
|
||||
context,
|
||||
dateWidgets: await getFilterWidgets(activeFilter, context: context),
|
||||
showDivider: true,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 12.0),
|
||||
child: RefreshIndicator(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
onRefresh: () => syncAll(context),
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
if (filterWidgets.isNotEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: filterWidgets[index],
|
||||
);
|
||||
} else {
|
||||
return Empty(subtitle: "empty".i18n);
|
||||
}
|
||||
},
|
||||
itemCount: max(filterWidgets.length, 1),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,102 +1,102 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:animated_flip_counter/animated_flip_counter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
|
||||
class HeadsUpCountdown extends StatefulWidget {
|
||||
const HeadsUpCountdown({Key? key, required this.maxTime, required this.elapsedTime}) : super(key: key);
|
||||
|
||||
final double maxTime;
|
||||
final double elapsedTime;
|
||||
|
||||
@override
|
||||
State<HeadsUpCountdown> createState() => _HeadsUpCountdownState();
|
||||
}
|
||||
|
||||
class _HeadsUpCountdownState extends State<HeadsUpCountdown> {
|
||||
static const _style = TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 70.0,
|
||||
letterSpacing: -.5,
|
||||
);
|
||||
|
||||
late final Timer _timer;
|
||||
late double elapsed;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
elapsed = widget.elapsedTime;
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (elapsed <= widget.maxTime) elapsed += 1;
|
||||
setState(() {});
|
||||
|
||||
if (elapsed >= widget.maxTime) {
|
||||
Future.delayed(const Duration(seconds: 5), () {
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dur = Duration(seconds: (widget.maxTime - elapsed).round());
|
||||
return Center(
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
AnimatedOpacity(
|
||||
opacity: dur.inSeconds > 0 ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if ((dur.inHours % 24) > 0) ...[
|
||||
AnimatedFlipCounter(
|
||||
value: dur.inHours % 24,
|
||||
curve: Curves.fastLinearToSlowEaseIn,
|
||||
textStyle: _style,
|
||||
),
|
||||
const Text(":", style: _style),
|
||||
],
|
||||
AnimatedFlipCounter(
|
||||
duration: const Duration(seconds: 2),
|
||||
value: dur.inMinutes % 60,
|
||||
curve: Curves.fastLinearToSlowEaseIn,
|
||||
wholeDigits: (dur.inHours % 24) > 0 ? 2 : 1,
|
||||
textStyle: _style,
|
||||
),
|
||||
const Text(":", style: _style),
|
||||
AnimatedFlipCounter(
|
||||
duration: const Duration(seconds: 1),
|
||||
value: dur.inSeconds % 60,
|
||||
curve: Curves.fastLinearToSlowEaseIn,
|
||||
wholeDigits: 2,
|
||||
textStyle: _style,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (dur.inSeconds < 0)
|
||||
AnimatedOpacity(
|
||||
opacity: dur.inSeconds > 0 ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
child: Lottie.asset("assets/animations/bell-alert.json", width: 400),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:animated_flip_counter/animated_flip_counter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
|
||||
class HeadsUpCountdown extends StatefulWidget {
|
||||
const HeadsUpCountdown({Key? key, required this.maxTime, required this.elapsedTime}) : super(key: key);
|
||||
|
||||
final double maxTime;
|
||||
final double elapsedTime;
|
||||
|
||||
@override
|
||||
State<HeadsUpCountdown> createState() => _HeadsUpCountdownState();
|
||||
}
|
||||
|
||||
class _HeadsUpCountdownState extends State<HeadsUpCountdown> {
|
||||
static const _style = TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 70.0,
|
||||
letterSpacing: -.5,
|
||||
);
|
||||
|
||||
late final Timer _timer;
|
||||
late double elapsed;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
elapsed = widget.elapsedTime;
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (elapsed <= widget.maxTime) elapsed += 1;
|
||||
setState(() {});
|
||||
|
||||
if (elapsed >= widget.maxTime) {
|
||||
Future.delayed(const Duration(seconds: 5), () {
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dur = Duration(seconds: (widget.maxTime - elapsed).round());
|
||||
return Center(
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
AnimatedOpacity(
|
||||
opacity: dur.inSeconds > 0 ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if ((dur.inHours % 24) > 0) ...[
|
||||
AnimatedFlipCounter(
|
||||
value: dur.inHours % 24,
|
||||
curve: Curves.fastLinearToSlowEaseIn,
|
||||
textStyle: _style,
|
||||
),
|
||||
const Text(":", style: _style),
|
||||
],
|
||||
AnimatedFlipCounter(
|
||||
duration: const Duration(seconds: 2),
|
||||
value: dur.inMinutes % 60,
|
||||
curve: Curves.fastLinearToSlowEaseIn,
|
||||
wholeDigits: (dur.inHours % 24) > 0 ? 2 : 1,
|
||||
textStyle: _style,
|
||||
),
|
||||
const Text(":", style: _style),
|
||||
AnimatedFlipCounter(
|
||||
duration: const Duration(seconds: 1),
|
||||
value: dur.inSeconds % 60,
|
||||
curve: Curves.fastLinearToSlowEaseIn,
|
||||
wholeDigits: 2,
|
||||
textStyle: _style,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (dur.inSeconds < 0)
|
||||
AnimatedOpacity(
|
||||
opacity: dur.inSeconds > 0 ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
child: Lottie.asset("assets/animations/bell-alert.json", width: 400),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,197 +1,197 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:filcnaplo/api/providers/user_provider.dart';
|
||||
import 'package:filcnaplo/helpers/subject.dart';
|
||||
import 'package:filcnaplo/icons/filc_icons.dart';
|
||||
import 'package:filcnaplo_mobile_ui/pages/home/live_card/heads_up_countdown.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:filcnaplo/utils/format.dart';
|
||||
import 'package:filcnaplo/api/providers/live_card_provider.dart';
|
||||
import 'package:filcnaplo_mobile_ui/pages/home/live_card/live_card_widget.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:i18n_extension/i18n_widget.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'live_card.i18n.dart';
|
||||
|
||||
class LiveCard extends StatefulWidget {
|
||||
const LiveCard({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_LiveCardState createState() => _LiveCardState();
|
||||
}
|
||||
|
||||
class _LiveCardState extends State<LiveCard> {
|
||||
late void Function() listener;
|
||||
late UserProvider _userProvider;
|
||||
late LiveCardProvider liveCard;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
listener = () => setState(() {});
|
||||
_userProvider = Provider.of<UserProvider>(context, listen: false);
|
||||
liveCard = Provider.of<LiveCardProvider>(context, listen: false);
|
||||
_userProvider.addListener(liveCard.update);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_userProvider.removeListener(liveCard.update);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
liveCard = Provider.of<LiveCardProvider>(context);
|
||||
|
||||
if (!liveCard.show) return Container();
|
||||
|
||||
Widget child;
|
||||
Duration bellDelay = liveCard.delay;
|
||||
|
||||
switch (liveCard.currentState) {
|
||||
case LiveCardState.morning:
|
||||
child = LiveCardWidget(
|
||||
key: const Key('livecard.morning'),
|
||||
title: DateFormat("EEEE", I18n.of(context).locale.toString()).format(DateTime.now()).capital(),
|
||||
icon: FeatherIcons.sun,
|
||||
description: liveCard.nextLesson != null
|
||||
? Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(text: "first_lesson_1".i18n),
|
||||
TextSpan(
|
||||
text: liveCard.nextLesson!.subject.renamedTo ?? liveCard.nextLesson!.subject.name.capital(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.secondary.withOpacity(.85),
|
||||
fontStyle: liveCard.nextLesson!.subject.isRenamed ? FontStyle.italic : null),
|
||||
),
|
||||
TextSpan(text: "first_lesson_2".i18n),
|
||||
TextSpan(
|
||||
text: liveCard.nextLesson!.room.capital(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.secondary.withOpacity(.85),
|
||||
),
|
||||
),
|
||||
TextSpan(text: "first_lesson_3".i18n),
|
||||
TextSpan(
|
||||
text: DateFormat('H:mm').format(liveCard.nextLesson!.start),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.secondary.withOpacity(.85),
|
||||
),
|
||||
),
|
||||
TextSpan(text: "first_lesson_4".i18n),
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
break;
|
||||
case LiveCardState.duringLesson:
|
||||
final elapsedTime = DateTime.now().difference(liveCard.currentLesson!.start).inSeconds.toDouble() + bellDelay.inSeconds;
|
||||
final maxTime = liveCard.currentLesson!.end.difference(liveCard.currentLesson!.start).inSeconds.toDouble();
|
||||
|
||||
final showMinutes = maxTime - elapsedTime > 60;
|
||||
|
||||
child = LiveCardWidget(
|
||||
key: const Key('livecard.duringLesson'),
|
||||
leading: liveCard.currentLesson!.lessonIndex + (RegExp(r'\d').hasMatch(liveCard.currentLesson!.lessonIndex) ? "." : ""),
|
||||
title: liveCard.currentLesson!.subject.renamedTo ?? liveCard.currentLesson!.subject.name.capital(),
|
||||
titleItalic: liveCard.currentLesson!.subject.isRenamed,
|
||||
subtitle: liveCard.currentLesson!.room,
|
||||
icon: SubjectIcon.resolveVariant(subject: liveCard.currentLesson!.subject, context: context),
|
||||
description: liveCard.currentLesson!.description != "" ? Text(liveCard.currentLesson!.description) : null,
|
||||
nextSubject: liveCard.nextLesson?.subject.renamedTo ?? liveCard.nextLesson?.subject.name.capital(),
|
||||
nextSubjectItalic: liveCard.nextLesson?.subject.isRenamed ?? false,
|
||||
nextRoom: liveCard.nextLesson?.room,
|
||||
progressMax: showMinutes ? maxTime / 60 : maxTime,
|
||||
progressCurrent: showMinutes ? elapsedTime / 60 : elapsedTime,
|
||||
progressAccuracy: showMinutes ? ProgressAccuracy.minutes : ProgressAccuracy.seconds,
|
||||
onProgressTap: () {
|
||||
showDialog(
|
||||
barrierColor: Colors.black,
|
||||
context: context,
|
||||
builder: (context) => HeadsUpCountdown(maxTime: maxTime, elapsedTime: elapsedTime),
|
||||
);
|
||||
},
|
||||
);
|
||||
break;
|
||||
case LiveCardState.duringBreak:
|
||||
final iconFloorMap = {
|
||||
"to room": FeatherIcons.chevronsRight,
|
||||
"up floor": FilcIcons.upstairs,
|
||||
"down floor": FilcIcons.downstairs,
|
||||
"ground floor": FilcIcons.downstairs,
|
||||
};
|
||||
|
||||
final diff = liveCard.getFloorDifference();
|
||||
|
||||
final maxTime = liveCard.nextLesson!.start.difference(liveCard.prevLesson!.end).inSeconds.toDouble();
|
||||
final elapsedTime = DateTime.now().difference(liveCard.prevLesson!.end).inSeconds.toDouble() + bellDelay.inSeconds.toDouble();
|
||||
|
||||
final showMinutes = maxTime - elapsedTime > 60;
|
||||
|
||||
child = LiveCardWidget(
|
||||
key: const Key('livecard.duringBreak'),
|
||||
title: "break".i18n,
|
||||
icon: iconFloorMap[diff],
|
||||
description: liveCard.nextLesson!.room != liveCard.prevLesson!.room
|
||||
? Text("go $diff".i18n.fill([diff != "to room" ? (liveCard.nextLesson!.getFloor() ?? 0) : liveCard.nextLesson!.room]))
|
||||
: Text("stay".i18n),
|
||||
nextSubject: liveCard.nextLesson?.subject.renamedTo ?? liveCard.nextLesson?.subject.name.capital(),
|
||||
nextSubjectItalic: liveCard.nextLesson?.subject.isRenamed ?? false,
|
||||
nextRoom: diff != "to room" ? liveCard.nextLesson?.room : null,
|
||||
progressMax: showMinutes ? maxTime / 60 : maxTime,
|
||||
progressCurrent: showMinutes ? elapsedTime / 60 : elapsedTime,
|
||||
progressAccuracy: showMinutes ? ProgressAccuracy.minutes : ProgressAccuracy.seconds,
|
||||
onProgressTap: () {
|
||||
showDialog(
|
||||
barrierColor: Colors.black,
|
||||
context: context,
|
||||
builder: (context) => HeadsUpCountdown(
|
||||
maxTime: maxTime,
|
||||
elapsedTime: elapsedTime,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
break;
|
||||
case LiveCardState.afternoon:
|
||||
child = LiveCardWidget(
|
||||
key: const Key('livecard.afternoon'),
|
||||
title: DateFormat("EEEE", I18n.of(context).locale.toString()).format(DateTime.now()).capital(),
|
||||
icon: FeatherIcons.coffee,
|
||||
);
|
||||
break;
|
||||
case LiveCardState.night:
|
||||
child = LiveCardWidget(
|
||||
key: const Key('livecard.night'),
|
||||
title: DateFormat("EEEE", I18n.of(context).locale.toString()).format(DateTime.now()).capital(),
|
||||
icon: FeatherIcons.moon,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
child = Container();
|
||||
}
|
||||
|
||||
return PageTransitionSwitcher(
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
child: child,
|
||||
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:filcnaplo/api/providers/user_provider.dart';
|
||||
import 'package:filcnaplo/helpers/subject.dart';
|
||||
import 'package:filcnaplo/icons/filc_icons.dart';
|
||||
import 'package:filcnaplo_mobile_ui/pages/home/live_card/heads_up_countdown.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:filcnaplo/utils/format.dart';
|
||||
import 'package:filcnaplo/api/providers/live_card_provider.dart';
|
||||
import 'package:filcnaplo_mobile_ui/pages/home/live_card/live_card_widget.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:i18n_extension/i18n_widget.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'live_card.i18n.dart';
|
||||
|
||||
class LiveCard extends StatefulWidget {
|
||||
const LiveCard({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_LiveCardState createState() => _LiveCardState();
|
||||
}
|
||||
|
||||
class _LiveCardState extends State<LiveCard> {
|
||||
late void Function() listener;
|
||||
late UserProvider _userProvider;
|
||||
late LiveCardProvider liveCard;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
listener = () => setState(() {});
|
||||
_userProvider = Provider.of<UserProvider>(context, listen: false);
|
||||
liveCard = Provider.of<LiveCardProvider>(context, listen: false);
|
||||
_userProvider.addListener(liveCard.update);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_userProvider.removeListener(liveCard.update);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
liveCard = Provider.of<LiveCardProvider>(context);
|
||||
|
||||
if (!liveCard.show) return Container();
|
||||
|
||||
Widget child;
|
||||
Duration bellDelay = liveCard.delay;
|
||||
|
||||
switch (liveCard.currentState) {
|
||||
case LiveCardState.morning:
|
||||
child = LiveCardWidget(
|
||||
key: const Key('livecard.morning'),
|
||||
title: DateFormat("EEEE", I18n.of(context).locale.toString()).format(DateTime.now()).capital(),
|
||||
icon: FeatherIcons.sun,
|
||||
description: liveCard.nextLesson != null
|
||||
? Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(text: "first_lesson_1".i18n),
|
||||
TextSpan(
|
||||
text: liveCard.nextLesson!.subject.renamedTo ?? liveCard.nextLesson!.subject.name.capital(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.secondary.withOpacity(.85),
|
||||
fontStyle: liveCard.nextLesson!.subject.isRenamed ? FontStyle.italic : null),
|
||||
),
|
||||
TextSpan(text: "first_lesson_2".i18n),
|
||||
TextSpan(
|
||||
text: liveCard.nextLesson!.room.capital(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.secondary.withOpacity(.85),
|
||||
),
|
||||
),
|
||||
TextSpan(text: "first_lesson_3".i18n),
|
||||
TextSpan(
|
||||
text: DateFormat('H:mm').format(liveCard.nextLesson!.start),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.secondary.withOpacity(.85),
|
||||
),
|
||||
),
|
||||
TextSpan(text: "first_lesson_4".i18n),
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
break;
|
||||
case LiveCardState.duringLesson:
|
||||
final elapsedTime = DateTime.now().difference(liveCard.currentLesson!.start).inSeconds.toDouble() + bellDelay.inSeconds;
|
||||
final maxTime = liveCard.currentLesson!.end.difference(liveCard.currentLesson!.start).inSeconds.toDouble();
|
||||
|
||||
final showMinutes = maxTime - elapsedTime > 60;
|
||||
|
||||
child = LiveCardWidget(
|
||||
key: const Key('livecard.duringLesson'),
|
||||
leading: liveCard.currentLesson!.lessonIndex + (RegExp(r'\d').hasMatch(liveCard.currentLesson!.lessonIndex) ? "." : ""),
|
||||
title: liveCard.currentLesson!.subject.renamedTo ?? liveCard.currentLesson!.subject.name.capital(),
|
||||
titleItalic: liveCard.currentLesson!.subject.isRenamed,
|
||||
subtitle: liveCard.currentLesson!.room,
|
||||
icon: SubjectIcon.resolveVariant(subject: liveCard.currentLesson!.subject, context: context),
|
||||
description: liveCard.currentLesson!.description != "" ? Text(liveCard.currentLesson!.description) : null,
|
||||
nextSubject: liveCard.nextLesson?.subject.renamedTo ?? liveCard.nextLesson?.subject.name.capital(),
|
||||
nextSubjectItalic: liveCard.nextLesson?.subject.isRenamed ?? false,
|
||||
nextRoom: liveCard.nextLesson?.room,
|
||||
progressMax: showMinutes ? maxTime / 60 : maxTime,
|
||||
progressCurrent: showMinutes ? elapsedTime / 60 : elapsedTime,
|
||||
progressAccuracy: showMinutes ? ProgressAccuracy.minutes : ProgressAccuracy.seconds,
|
||||
onProgressTap: () {
|
||||
showDialog(
|
||||
barrierColor: Colors.black,
|
||||
context: context,
|
||||
builder: (context) => HeadsUpCountdown(maxTime: maxTime, elapsedTime: elapsedTime),
|
||||
);
|
||||
},
|
||||
);
|
||||
break;
|
||||
case LiveCardState.duringBreak:
|
||||
final iconFloorMap = {
|
||||
"to room": FeatherIcons.chevronsRight,
|
||||
"up floor": FilcIcons.upstairs,
|
||||
"down floor": FilcIcons.downstairs,
|
||||
"ground floor": FilcIcons.downstairs,
|
||||
};
|
||||
|
||||
final diff = liveCard.getFloorDifference();
|
||||
|
||||
final maxTime = liveCard.nextLesson!.start.difference(liveCard.prevLesson!.end).inSeconds.toDouble();
|
||||
final elapsedTime = DateTime.now().difference(liveCard.prevLesson!.end).inSeconds.toDouble() + bellDelay.inSeconds.toDouble();
|
||||
|
||||
final showMinutes = maxTime - elapsedTime > 60;
|
||||
|
||||
child = LiveCardWidget(
|
||||
key: const Key('livecard.duringBreak'),
|
||||
title: "break".i18n,
|
||||
icon: iconFloorMap[diff],
|
||||
description: liveCard.nextLesson!.room != liveCard.prevLesson!.room
|
||||
? Text("go $diff".i18n.fill([diff != "to room" ? (liveCard.nextLesson!.getFloor() ?? 0) : liveCard.nextLesson!.room]))
|
||||
: Text("stay".i18n),
|
||||
nextSubject: liveCard.nextLesson?.subject.renamedTo ?? liveCard.nextLesson?.subject.name.capital(),
|
||||
nextSubjectItalic: liveCard.nextLesson?.subject.isRenamed ?? false,
|
||||
nextRoom: diff != "to room" ? liveCard.nextLesson?.room : null,
|
||||
progressMax: showMinutes ? maxTime / 60 : maxTime,
|
||||
progressCurrent: showMinutes ? elapsedTime / 60 : elapsedTime,
|
||||
progressAccuracy: showMinutes ? ProgressAccuracy.minutes : ProgressAccuracy.seconds,
|
||||
onProgressTap: () {
|
||||
showDialog(
|
||||
barrierColor: Colors.black,
|
||||
context: context,
|
||||
builder: (context) => HeadsUpCountdown(
|
||||
maxTime: maxTime,
|
||||
elapsedTime: elapsedTime,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
break;
|
||||
case LiveCardState.afternoon:
|
||||
child = LiveCardWidget(
|
||||
key: const Key('livecard.afternoon'),
|
||||
title: DateFormat("EEEE", I18n.of(context).locale.toString()).format(DateTime.now()).capital(),
|
||||
icon: FeatherIcons.coffee,
|
||||
);
|
||||
break;
|
||||
case LiveCardState.night:
|
||||
child = LiveCardWidget(
|
||||
key: const Key('livecard.night'),
|
||||
title: DateFormat("EEEE", I18n.of(context).locale.toString()).format(DateTime.now()).capital(),
|
||||
icon: FeatherIcons.moon,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
child = Container();
|
||||
}
|
||||
|
||||
return PageTransitionSwitcher(
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
child: child,
|
||||
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"next": "Next",
|
||||
"remaining min": "%d mins".one("%d min"),
|
||||
"remaining sec": "%d secs".one("%d sec"),
|
||||
"break": "Break",
|
||||
"go to room": "Go to room %s.",
|
||||
"go ground floor": "Go to the ground floor.",
|
||||
"go up floor": "Go upstairs, to floor %d.",
|
||||
"go down floor": "Go downstaris, to floor %d.",
|
||||
"stay": "Stay in this room.",
|
||||
"first_lesson_1": "Your first lesson will be ",
|
||||
"first_lesson_2": " in room ",
|
||||
"first_lesson_3": ", at ",
|
||||
"first_lesson_4": ".",
|
||||
},
|
||||
"hu_hu": {
|
||||
"next": "Következő",
|
||||
"remaining min": "%d perc".one("%d perc"),
|
||||
"remaining sec": "%d másodperc".one("%d másodperc"),
|
||||
"break": "Szünet",
|
||||
"go to room": "Menj a(z) %s terembe.",
|
||||
"go ground floor": "Menj a földszintre.",
|
||||
"go up floor": "Menj fel a(z) %d. emeletre.",
|
||||
"go down floor": "Menj le a(z) %d. emeletre.",
|
||||
"stay": "Maradj ebben a teremben.",
|
||||
"first_lesson_1": "Az első órád ",
|
||||
"first_lesson_2": " lesz, a ",
|
||||
"first_lesson_3": " teremben, ",
|
||||
"first_lesson_4": "-kor.",
|
||||
},
|
||||
"de_de": {
|
||||
"next": "Nächste",
|
||||
"remaining min": "%d Minuten".one("%d Minute"),
|
||||
"remaining sec": "%d Sekunden".one("%d Sekunden"),
|
||||
"break": "Pause",
|
||||
"go to room": "Geh in den Raum %s.",
|
||||
"go ground floor": "Geh dir Treppe hinunter.",
|
||||
"go up floor": "Geh in die %d. Stock hinauf.",
|
||||
"go down floor": "Geh runter in den %d. Stock.",
|
||||
"stay": "Im Zimmer bleiben.",
|
||||
"first_lesson_1": "Ihre erste Stunde ist ",
|
||||
"first_lesson_2": ", in Raum ",
|
||||
"first_lesson_3": ", um ",
|
||||
"first_lesson_4": " Uhr.",
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
import 'package:i18n_extension/i18n_extension.dart';
|
||||
|
||||
extension Localization on String {
|
||||
static final _t = Translations.byLocale("hu_hu") +
|
||||
{
|
||||
"en_en": {
|
||||
"next": "Next",
|
||||
"remaining min": "%d mins".one("%d min"),
|
||||
"remaining sec": "%d secs".one("%d sec"),
|
||||
"break": "Break",
|
||||
"go to room": "Go to room %s.",
|
||||
"go ground floor": "Go to the ground floor.",
|
||||
"go up floor": "Go upstairs, to floor %d.",
|
||||
"go down floor": "Go downstaris, to floor %d.",
|
||||
"stay": "Stay in this room.",
|
||||
"first_lesson_1": "Your first lesson will be ",
|
||||
"first_lesson_2": " in room ",
|
||||
"first_lesson_3": ", at ",
|
||||
"first_lesson_4": ".",
|
||||
},
|
||||
"hu_hu": {
|
||||
"next": "Következő",
|
||||
"remaining min": "%d perc".one("%d perc"),
|
||||
"remaining sec": "%d másodperc".one("%d másodperc"),
|
||||
"break": "Szünet",
|
||||
"go to room": "Menj a(z) %s terembe.",
|
||||
"go ground floor": "Menj a földszintre.",
|
||||
"go up floor": "Menj fel a(z) %d. emeletre.",
|
||||
"go down floor": "Menj le a(z) %d. emeletre.",
|
||||
"stay": "Maradj ebben a teremben.",
|
||||
"first_lesson_1": "Az első órád ",
|
||||
"first_lesson_2": " lesz, a ",
|
||||
"first_lesson_3": " teremben, ",
|
||||
"first_lesson_4": "-kor.",
|
||||
},
|
||||
"de_de": {
|
||||
"next": "Nächste",
|
||||
"remaining min": "%d Minuten".one("%d Minute"),
|
||||
"remaining sec": "%d Sekunden".one("%d Sekunden"),
|
||||
"break": "Pause",
|
||||
"go to room": "Geh in den Raum %s.",
|
||||
"go ground floor": "Geh dir Treppe hinunter.",
|
||||
"go up floor": "Geh in die %d. Stock hinauf.",
|
||||
"go down floor": "Geh runter in den %d. Stock.",
|
||||
"stay": "Im Zimmer bleiben.",
|
||||
"first_lesson_1": "Ihre erste Stunde ist ",
|
||||
"first_lesson_2": ", in Raum ",
|
||||
"first_lesson_3": ", um ",
|
||||
"first_lesson_4": " Uhr.",
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,247 +1,247 @@
|
||||
import 'package:filcnaplo/theme/colors/colors.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/progress_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'live_card.i18n.dart';
|
||||
|
||||
enum ProgressAccuracy { minutes, seconds }
|
||||
|
||||
class LiveCardWidget extends StatefulWidget {
|
||||
const LiveCardWidget({
|
||||
Key? key,
|
||||
this.leading,
|
||||
this.title,
|
||||
this.titleItalic = false,
|
||||
this.subtitle,
|
||||
this.icon,
|
||||
this.description,
|
||||
this.nextRoom,
|
||||
this.nextSubject,
|
||||
this.nextSubjectItalic = false,
|
||||
this.progressCurrent,
|
||||
this.progressMax,
|
||||
this.progressAccuracy = ProgressAccuracy.minutes,
|
||||
this.onProgressTap,
|
||||
}) : super(key: key);
|
||||
|
||||
final String? leading;
|
||||
final String? title;
|
||||
final bool titleItalic;
|
||||
final String? subtitle;
|
||||
final IconData? icon;
|
||||
final Widget? description;
|
||||
final String? nextSubject;
|
||||
final bool nextSubjectItalic;
|
||||
final String? nextRoom;
|
||||
final double? progressCurrent;
|
||||
final double? progressMax;
|
||||
final ProgressAccuracy? progressAccuracy;
|
||||
final Function()? onProgressTap;
|
||||
|
||||
@override
|
||||
State<LiveCardWidget> createState() => _LiveCardWidgetState();
|
||||
}
|
||||
|
||||
class _LiveCardWidgetState extends State<LiveCardWidget> {
|
||||
bool hold = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onLongPressDown: (_) => setState(() => hold = true),
|
||||
onLongPressEnd: (_) => setState(() => hold = false),
|
||||
onLongPressCancel: () => setState(() => hold = false),
|
||||
child: AnimatedScale(
|
||||
scale: hold ? 1.03 : 1.0,
|
||||
curve: Curves.easeInOutBack,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
offset: const Offset(0, 21),
|
||||
blurRadius: 23.0,
|
||||
color: Theme.of(context).shadowColor,
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
child: OverflowBox(
|
||||
maxHeight: 96.0,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.leading != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0, top: 8.0),
|
||||
child: Text(
|
||||
widget.leading!,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (widget.title != null)
|
||||
Expanded(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(text: widget.title!, style: TextStyle(fontStyle: widget.titleItalic ? FontStyle.italic : null)),
|
||||
if (widget.subtitle != null)
|
||||
WidgetSpan(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 6.0, bottom: 3.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 2.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondary.withOpacity(.3),
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
),
|
||||
child: Text(
|
||||
widget.subtitle!,
|
||||
style: TextStyle(
|
||||
height: 1.2,
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 22.0),
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (widget.title != null) const SizedBox(width: 6.0),
|
||||
if (widget.icon != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Icon(
|
||||
widget.icon,
|
||||
size: 26.0,
|
||||
color: AppColors.of(context).text.withOpacity(.75),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.description != null)
|
||||
DefaultTextStyle(
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16.0,
|
||||
height: 1.0,
|
||||
color: AppColors.of(context).text.withOpacity(.75),
|
||||
),
|
||||
maxLines: !(widget.nextSubject == null && widget.progressCurrent == null && widget.progressMax == null) ? 1 : 2,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: widget.description!,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!(widget.nextSubject == null && widget.progressCurrent == null && widget.progressMax == null))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.nextSubject != null) const Icon(FeatherIcons.arrowRight, size: 12.0),
|
||||
if (widget.nextSubject != null) const SizedBox(width: 4.0),
|
||||
if (widget.nextSubject != null)
|
||||
Expanded(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: widget.nextSubject!, style: TextStyle(fontStyle: widget.nextSubjectItalic ? FontStyle.italic : null)),
|
||||
if (widget.nextRoom != null)
|
||||
WidgetSpan(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 4.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3.0, vertical: 1.5),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondary.withOpacity(.25),
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
),
|
||||
child: Text(
|
||||
widget.nextRoom!,
|
||||
style: TextStyle(
|
||||
height: 1.1,
|
||||
fontSize: 11.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.secondary.withOpacity(.9),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
style: TextStyle(
|
||||
color: AppColors.of(context).text.withOpacity(.8),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: false,
|
||||
),
|
||||
),
|
||||
if (widget.nextRoom == null && widget.nextSubject == null) const Spacer(),
|
||||
if (widget.progressCurrent != null && widget.progressMax != null)
|
||||
GestureDetector(
|
||||
onTap: widget.onProgressTap,
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: Text(
|
||||
"remaining ${widget.progressAccuracy == ProgressAccuracy.minutes ? 'min' : 'sec'}"
|
||||
.plural((widget.progressMax! - widget.progressCurrent!).round()),
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.of(context).text.withOpacity(.75),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.progressCurrent != null && widget.progressMax != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: ProgressBar(value: widget.progressCurrent! / widget.progressMax!),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:filcnaplo/theme/colors/colors.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/progress_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'live_card.i18n.dart';
|
||||
|
||||
enum ProgressAccuracy { minutes, seconds }
|
||||
|
||||
class LiveCardWidget extends StatefulWidget {
|
||||
const LiveCardWidget({
|
||||
Key? key,
|
||||
this.leading,
|
||||
this.title,
|
||||
this.titleItalic = false,
|
||||
this.subtitle,
|
||||
this.icon,
|
||||
this.description,
|
||||
this.nextRoom,
|
||||
this.nextSubject,
|
||||
this.nextSubjectItalic = false,
|
||||
this.progressCurrent,
|
||||
this.progressMax,
|
||||
this.progressAccuracy = ProgressAccuracy.minutes,
|
||||
this.onProgressTap,
|
||||
}) : super(key: key);
|
||||
|
||||
final String? leading;
|
||||
final String? title;
|
||||
final bool titleItalic;
|
||||
final String? subtitle;
|
||||
final IconData? icon;
|
||||
final Widget? description;
|
||||
final String? nextSubject;
|
||||
final bool nextSubjectItalic;
|
||||
final String? nextRoom;
|
||||
final double? progressCurrent;
|
||||
final double? progressMax;
|
||||
final ProgressAccuracy? progressAccuracy;
|
||||
final Function()? onProgressTap;
|
||||
|
||||
@override
|
||||
State<LiveCardWidget> createState() => _LiveCardWidgetState();
|
||||
}
|
||||
|
||||
class _LiveCardWidgetState extends State<LiveCardWidget> {
|
||||
bool hold = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onLongPressDown: (_) => setState(() => hold = true),
|
||||
onLongPressEnd: (_) => setState(() => hold = false),
|
||||
onLongPressCancel: () => setState(() => hold = false),
|
||||
child: AnimatedScale(
|
||||
scale: hold ? 1.03 : 1.0,
|
||||
curve: Curves.easeInOutBack,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
offset: const Offset(0, 21),
|
||||
blurRadius: 23.0,
|
||||
color: Theme.of(context).shadowColor,
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
child: OverflowBox(
|
||||
maxHeight: 96.0,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.leading != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0, top: 8.0),
|
||||
child: Text(
|
||||
widget.leading!,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (widget.title != null)
|
||||
Expanded(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(text: widget.title!, style: TextStyle(fontStyle: widget.titleItalic ? FontStyle.italic : null)),
|
||||
if (widget.subtitle != null)
|
||||
WidgetSpan(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 6.0, bottom: 3.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 2.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondary.withOpacity(.3),
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
),
|
||||
child: Text(
|
||||
widget.subtitle!,
|
||||
style: TextStyle(
|
||||
height: 1.2,
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 22.0),
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (widget.title != null) const SizedBox(width: 6.0),
|
||||
if (widget.icon != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Icon(
|
||||
widget.icon,
|
||||
size: 26.0,
|
||||
color: AppColors.of(context).text.withOpacity(.75),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.description != null)
|
||||
DefaultTextStyle(
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16.0,
|
||||
height: 1.0,
|
||||
color: AppColors.of(context).text.withOpacity(.75),
|
||||
),
|
||||
maxLines: !(widget.nextSubject == null && widget.progressCurrent == null && widget.progressMax == null) ? 1 : 2,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: widget.description!,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!(widget.nextSubject == null && widget.progressCurrent == null && widget.progressMax == null))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.nextSubject != null) const Icon(FeatherIcons.arrowRight, size: 12.0),
|
||||
if (widget.nextSubject != null) const SizedBox(width: 4.0),
|
||||
if (widget.nextSubject != null)
|
||||
Expanded(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: widget.nextSubject!, style: TextStyle(fontStyle: widget.nextSubjectItalic ? FontStyle.italic : null)),
|
||||
if (widget.nextRoom != null)
|
||||
WidgetSpan(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 4.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3.0, vertical: 1.5),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondary.withOpacity(.25),
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
),
|
||||
child: Text(
|
||||
widget.nextRoom!,
|
||||
style: TextStyle(
|
||||
height: 1.1,
|
||||
fontSize: 11.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.secondary.withOpacity(.9),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
style: TextStyle(
|
||||
color: AppColors.of(context).text.withOpacity(.8),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: false,
|
||||
),
|
||||
),
|
||||
if (widget.nextRoom == null && widget.nextSubject == null) const Spacer(),
|
||||
if (widget.progressCurrent != null && widget.progressMax != null)
|
||||
GestureDetector(
|
||||
onTap: widget.onProgressTap,
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: Text(
|
||||
"remaining ${widget.progressAccuracy == ProgressAccuracy.minutes ? 'min' : 'sec'}"
|
||||
.plural((widget.progressMax! - widget.progressCurrent!).round()),
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.of(context).text.withOpacity(.75),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.progressCurrent != null && widget.progressMax != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: ProgressBar(value: widget.progressCurrent! / widget.progressMax!),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,438 +1,438 @@
|
||||
// MIT License
|
||||
|
||||
// Copyright (c) 2018 Norbert Kozsir
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
// ignore_for_file: invalid_use_of_protected_member
|
||||
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
typedef ParticleBuilder = Particle Function(int index);
|
||||
|
||||
abstract class Particle {
|
||||
void paint(Canvas canvas, Size size, double progress, int seed);
|
||||
}
|
||||
|
||||
class FourRandomSlotParticle extends Particle {
|
||||
final List<Particle> children;
|
||||
|
||||
final double relativeDistanceToMiddle;
|
||||
|
||||
FourRandomSlotParticle({required this.children, this.relativeDistanceToMiddle = 2.0});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, int seed) {
|
||||
Random random = Random(seed);
|
||||
int side = 0;
|
||||
for (Particle particle in children) {
|
||||
PositionedParticle(
|
||||
position: sideToOffset(side, size, random) * relativeDistanceToMiddle,
|
||||
child: particle,
|
||||
).paint(canvas, size, progress, seed);
|
||||
side++;
|
||||
}
|
||||
}
|
||||
|
||||
Offset sideToOffset(int side, Size size, Random random) {
|
||||
if (side == 0) {
|
||||
return Offset(-random.nextDouble() * (size.width / 2), -random.nextDouble() * (size.height / 2));
|
||||
} else if (side == 1) {
|
||||
return Offset(random.nextDouble() * (size.width / 2), -random.nextDouble() * (size.height / 2));
|
||||
} else if (side == 2) {
|
||||
return Offset(random.nextDouble() * (size.width / 2), random.nextDouble() * (size.height / 2));
|
||||
} else if (side == 3) {
|
||||
return Offset(-random.nextDouble() * (size.width / 2), random.nextDouble() * (size.height / 2));
|
||||
} else {
|
||||
throw Exception();
|
||||
}
|
||||
}
|
||||
|
||||
double randomOffset(Random random, int range) {
|
||||
return range / 2 - random.nextInt(range);
|
||||
}
|
||||
}
|
||||
|
||||
class PoppingCircle extends Particle {
|
||||
final Color color;
|
||||
|
||||
PoppingCircle({required this.color});
|
||||
|
||||
final double radius = 3.0;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
if (progress < 0.5) {
|
||||
canvas.drawCircle(
|
||||
Offset.zero,
|
||||
radius + (progress * 8),
|
||||
Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 5.0 - progress * 2);
|
||||
} else {
|
||||
CircleMirror(
|
||||
numberOfParticles: 4,
|
||||
child: AnimatedPositionedParticle(
|
||||
begin: const Offset(0.0, 5.0),
|
||||
end: const Offset(0.0, 15.0),
|
||||
child: FadingRect(
|
||||
color: color,
|
||||
height: 7.0,
|
||||
width: 2.0,
|
||||
)),
|
||||
initialRotation: pi / 4,
|
||||
).paint(canvas, size, progress, seed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Firework extends Particle {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, int seed) {
|
||||
FourRandomSlotParticle(children: [
|
||||
IntervalParticle(
|
||||
interval: const Interval(0.0, 0.5, curve: Curves.easeIn),
|
||||
child: PoppingCircle(
|
||||
color: Colors.deepOrangeAccent,
|
||||
),
|
||||
),
|
||||
IntervalParticle(
|
||||
interval: const Interval(0.2, 0.5, curve: Curves.easeIn),
|
||||
child: PoppingCircle(
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
IntervalParticle(
|
||||
interval: const Interval(0.4, 0.8, curve: Curves.easeIn),
|
||||
child: PoppingCircle(
|
||||
color: Colors.indigo,
|
||||
),
|
||||
),
|
||||
IntervalParticle(
|
||||
interval: const Interval(0.5, 1.0, curve: Curves.easeIn),
|
||||
child: PoppingCircle(
|
||||
color: Colors.teal,
|
||||
),
|
||||
),
|
||||
]).paint(canvas, size, progress, seed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirrors a given particle around a circle.
|
||||
///
|
||||
/// When using the default constructor you specify one [Particle], this particle
|
||||
/// is going to be used on its own, this implies that
|
||||
/// all mirrored particles are identical (expect for the rotation around the circle)
|
||||
class CircleMirror extends Particle {
|
||||
final ParticleBuilder particleBuilder;
|
||||
|
||||
final double initialRotation;
|
||||
|
||||
final int numberOfParticles;
|
||||
|
||||
CircleMirror.builder({required this.particleBuilder, required this.initialRotation, required this.numberOfParticles});
|
||||
|
||||
CircleMirror({required Particle child, required this.initialRotation, required this.numberOfParticles}) : particleBuilder = ((index) => child);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
canvas.save();
|
||||
canvas.rotate(initialRotation);
|
||||
for (int i = 0; i < numberOfParticles; i++) {
|
||||
particleBuilder(i).paint(canvas, size, progress, seed);
|
||||
canvas.rotate(pi / (numberOfParticles / 2));
|
||||
}
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirrors a given particle around a circle.
|
||||
///
|
||||
/// When using the default constructor you specify one [Particle], this particle
|
||||
/// is going to be used on its own, this implies that
|
||||
/// all mirrored particles are identical (expect for the rotation around the circle)
|
||||
class RectangleMirror extends Particle {
|
||||
final ParticleBuilder particleBuilder;
|
||||
|
||||
/// Position of the first particle on the rect
|
||||
final double initialDistance;
|
||||
|
||||
final int numberOfParticles;
|
||||
|
||||
RectangleMirror.builder({required this.particleBuilder, required this.initialDistance, required this.numberOfParticles});
|
||||
|
||||
RectangleMirror({required Particle child, required this.initialDistance, required this.numberOfParticles})
|
||||
: particleBuilder = ((index) => child);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
canvas.save();
|
||||
double totalLength = size.width * 2 + size.height * 2;
|
||||
double distanceBetweenParticles = totalLength / numberOfParticles;
|
||||
|
||||
bool onHorizontalAxis = true;
|
||||
int side = 0;
|
||||
|
||||
assert((distanceBetweenParticles * numberOfParticles).round() == totalLength.round());
|
||||
|
||||
canvas.translate(-size.width / 2, -size.height / 2);
|
||||
|
||||
double currentDistance = initialDistance;
|
||||
for (int i = 0; i < numberOfParticles; i++) {
|
||||
while (true) {
|
||||
if (onHorizontalAxis ? currentDistance > size.width : currentDistance > size.height) {
|
||||
currentDistance -= onHorizontalAxis ? size.width : size.height;
|
||||
onHorizontalAxis = !onHorizontalAxis;
|
||||
side = (++side) % 4;
|
||||
} else {
|
||||
if (side == 0) {
|
||||
assert(onHorizontalAxis);
|
||||
moveTo(canvas, size, 0, currentDistance, 0.0, () {
|
||||
particleBuilder(i).paint(canvas, size, progress, seed);
|
||||
});
|
||||
} else if (side == 1) {
|
||||
assert(!onHorizontalAxis);
|
||||
moveTo(canvas, size, 1, size.width, currentDistance, () {
|
||||
particleBuilder(i).paint(canvas, size, progress, seed);
|
||||
});
|
||||
} else if (side == 2) {
|
||||
assert(onHorizontalAxis);
|
||||
moveTo(canvas, size, 2, size.width - currentDistance, size.height, () {
|
||||
particleBuilder(i).paint(canvas, size, progress, seed);
|
||||
});
|
||||
} else if (side == 3) {
|
||||
assert(!onHorizontalAxis);
|
||||
moveTo(canvas, size, 3, 0.0, size.height - currentDistance, () {
|
||||
particleBuilder(i).paint(canvas, size, progress, seed);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
currentDistance += distanceBetweenParticles;
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
void moveTo(Canvas canvas, Size size, int side, double x, double y, VoidCallback painter) {
|
||||
canvas.save();
|
||||
canvas.translate(x, y);
|
||||
canvas.rotate(-atan2(size.width / 2 - x, size.height / 2 - y));
|
||||
painter();
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets a child by a given [Offset]
|
||||
class PositionedParticle extends Particle {
|
||||
PositionedParticle({required this.position, required this.child});
|
||||
|
||||
final Particle child;
|
||||
|
||||
final Offset position;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
canvas.save();
|
||||
canvas.translate(position.dx, position.dy);
|
||||
child.paint(canvas, size, progress, seed);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/// Animates a childs position based on a Tween<Offset>
|
||||
class AnimatedPositionedParticle extends Particle {
|
||||
AnimatedPositionedParticle({required Offset begin, required Offset end, required this.child}) : offsetTween = Tween<Offset>(begin: begin, end: end);
|
||||
|
||||
final Particle child;
|
||||
|
||||
final Tween<Offset> offsetTween;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
canvas.save();
|
||||
canvas.translate(offsetTween.lerp(progress).dx, offsetTween.lerp(progress).dy);
|
||||
child.paint(canvas, size, progress, seed);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/// Specifies an [Interval] for its child.
|
||||
///
|
||||
/// Instead of applying a curve the the input parameters of the paint method,
|
||||
/// apply it with this Particle.
|
||||
///
|
||||
/// If you want you child to only animate from 0.0 - 0.5 (relative), specify an [Interval] with those values.
|
||||
class IntervalParticle extends Particle {
|
||||
final Interval interval;
|
||||
|
||||
final Particle child;
|
||||
|
||||
IntervalParticle({required this.child, required this.interval});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
if (progress < interval.begin || progress > interval.end) return;
|
||||
child.paint(canvas, size, interval.transform(progress), seed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Does nothing else than holding a list of particles and painting them in that order
|
||||
class CompositeParticle extends Particle {
|
||||
final List<Particle> children;
|
||||
|
||||
CompositeParticle({required this.children});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
for (Particle particle in children) {
|
||||
particle.paint(canvas, size, progress, seed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A particle which rotates the child.
|
||||
///
|
||||
/// Does not animate.
|
||||
class RotationParticle extends Particle {
|
||||
final Particle child;
|
||||
|
||||
final double rotation;
|
||||
|
||||
RotationParticle({required this.child, required this.rotation});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, int seed) {
|
||||
canvas.save();
|
||||
canvas.rotate(rotation);
|
||||
child.paint(canvas, size, progress, seed);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/// A particle which rotates a child along a given [Tween]
|
||||
class AnimatedRotationParticle extends Particle {
|
||||
final Particle child;
|
||||
|
||||
final Tween<double> rotation;
|
||||
|
||||
AnimatedRotationParticle({required this.child, required double begin, required double end}) : rotation = Tween<double>(begin: begin, end: end);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, int seed) {
|
||||
canvas.save();
|
||||
canvas.rotate(rotation.lerp(progress));
|
||||
child.paint(canvas, size, progress, seed);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/// Geometry
|
||||
///
|
||||
/// These are some basic geometric classes which also fade out as time goes on.
|
||||
/// Each primitive should draw itself at the origin. If the orientation matters it should be directed to the top
|
||||
/// (negative y)
|
||||
///
|
||||
/// A rectangle which also fades out over time.
|
||||
class FadingRect extends Particle {
|
||||
final Color color;
|
||||
final double width;
|
||||
final double height;
|
||||
|
||||
FadingRect({required this.color, required this.width, required this.height});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
canvas.drawRect(Rect.fromLTWH(0.0, 0.0, width, height), Paint()..color = color.withOpacity(1 - progress));
|
||||
}
|
||||
}
|
||||
|
||||
/// A circle which fades out over time
|
||||
class FadingCircle extends Particle {
|
||||
final Color color;
|
||||
final double radius;
|
||||
|
||||
FadingCircle({required this.color, required this.radius});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
canvas.drawCircle(Offset.zero, radius, Paint()..color = color.withOpacity(1 - progress));
|
||||
}
|
||||
}
|
||||
|
||||
/// A triangle which also fades out over time
|
||||
class FadingTriangle extends Particle {
|
||||
/// This controls the shape of the triangle.
|
||||
///
|
||||
/// Value between 0 and 1
|
||||
final double variation;
|
||||
|
||||
final Color color;
|
||||
|
||||
/// The size of the base side of the triangle.
|
||||
final double baseSize;
|
||||
|
||||
/// This is the factor of how much bigger then length than the width is
|
||||
final double heightToBaseFactor;
|
||||
|
||||
FadingTriangle({required this.variation, required this.color, required this.baseSize, required this.heightToBaseFactor});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, int seed) {
|
||||
Path path = Path();
|
||||
path.moveTo(0.0, 0.0);
|
||||
path.lineTo(baseSize * variation, baseSize * heightToBaseFactor);
|
||||
path.lineTo(baseSize, 0.0);
|
||||
path.close();
|
||||
canvas.drawPath(path, Paint()..color = color.withOpacity(1 - progress));
|
||||
}
|
||||
}
|
||||
|
||||
/// An ugly looking "snake"
|
||||
///
|
||||
/// See for yourself
|
||||
class FadingSnake extends Particle {
|
||||
final double width;
|
||||
final double segmentLength;
|
||||
final int segments;
|
||||
final double curvyness;
|
||||
|
||||
final Color color;
|
||||
|
||||
FadingSnake({required this.width, required this.segmentLength, required this.segments, required this.curvyness, required this.color});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, int seed) {
|
||||
canvas.save();
|
||||
canvas.rotate(pi / 6);
|
||||
Path path = Path();
|
||||
for (int i = 0; i < segments; i++) {
|
||||
path.quadraticBezierTo(curvyness * i, segmentLength * (i + 1), curvyness * (i + 1), segmentLength * (i + 1));
|
||||
}
|
||||
for (int i = segments - 1; i >= 0; i--) {
|
||||
path.quadraticBezierTo(curvyness * (i + 1), segmentLength * i - curvyness, curvyness * i, segmentLength * i - curvyness);
|
||||
}
|
||||
path.close();
|
||||
canvas.drawPath(path, Paint()..color = color);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
// MIT License
|
||||
|
||||
// Copyright (c) 2018 Norbert Kozsir
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
// ignore_for_file: invalid_use_of_protected_member
|
||||
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
typedef ParticleBuilder = Particle Function(int index);
|
||||
|
||||
abstract class Particle {
|
||||
void paint(Canvas canvas, Size size, double progress, int seed);
|
||||
}
|
||||
|
||||
class FourRandomSlotParticle extends Particle {
|
||||
final List<Particle> children;
|
||||
|
||||
final double relativeDistanceToMiddle;
|
||||
|
||||
FourRandomSlotParticle({required this.children, this.relativeDistanceToMiddle = 2.0});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, int seed) {
|
||||
Random random = Random(seed);
|
||||
int side = 0;
|
||||
for (Particle particle in children) {
|
||||
PositionedParticle(
|
||||
position: sideToOffset(side, size, random) * relativeDistanceToMiddle,
|
||||
child: particle,
|
||||
).paint(canvas, size, progress, seed);
|
||||
side++;
|
||||
}
|
||||
}
|
||||
|
||||
Offset sideToOffset(int side, Size size, Random random) {
|
||||
if (side == 0) {
|
||||
return Offset(-random.nextDouble() * (size.width / 2), -random.nextDouble() * (size.height / 2));
|
||||
} else if (side == 1) {
|
||||
return Offset(random.nextDouble() * (size.width / 2), -random.nextDouble() * (size.height / 2));
|
||||
} else if (side == 2) {
|
||||
return Offset(random.nextDouble() * (size.width / 2), random.nextDouble() * (size.height / 2));
|
||||
} else if (side == 3) {
|
||||
return Offset(-random.nextDouble() * (size.width / 2), random.nextDouble() * (size.height / 2));
|
||||
} else {
|
||||
throw Exception();
|
||||
}
|
||||
}
|
||||
|
||||
double randomOffset(Random random, int range) {
|
||||
return range / 2 - random.nextInt(range);
|
||||
}
|
||||
}
|
||||
|
||||
class PoppingCircle extends Particle {
|
||||
final Color color;
|
||||
|
||||
PoppingCircle({required this.color});
|
||||
|
||||
final double radius = 3.0;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
if (progress < 0.5) {
|
||||
canvas.drawCircle(
|
||||
Offset.zero,
|
||||
radius + (progress * 8),
|
||||
Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 5.0 - progress * 2);
|
||||
} else {
|
||||
CircleMirror(
|
||||
numberOfParticles: 4,
|
||||
child: AnimatedPositionedParticle(
|
||||
begin: const Offset(0.0, 5.0),
|
||||
end: const Offset(0.0, 15.0),
|
||||
child: FadingRect(
|
||||
color: color,
|
||||
height: 7.0,
|
||||
width: 2.0,
|
||||
)),
|
||||
initialRotation: pi / 4,
|
||||
).paint(canvas, size, progress, seed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Firework extends Particle {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, int seed) {
|
||||
FourRandomSlotParticle(children: [
|
||||
IntervalParticle(
|
||||
interval: const Interval(0.0, 0.5, curve: Curves.easeIn),
|
||||
child: PoppingCircle(
|
||||
color: Colors.deepOrangeAccent,
|
||||
),
|
||||
),
|
||||
IntervalParticle(
|
||||
interval: const Interval(0.2, 0.5, curve: Curves.easeIn),
|
||||
child: PoppingCircle(
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
IntervalParticle(
|
||||
interval: const Interval(0.4, 0.8, curve: Curves.easeIn),
|
||||
child: PoppingCircle(
|
||||
color: Colors.indigo,
|
||||
),
|
||||
),
|
||||
IntervalParticle(
|
||||
interval: const Interval(0.5, 1.0, curve: Curves.easeIn),
|
||||
child: PoppingCircle(
|
||||
color: Colors.teal,
|
||||
),
|
||||
),
|
||||
]).paint(canvas, size, progress, seed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirrors a given particle around a circle.
|
||||
///
|
||||
/// When using the default constructor you specify one [Particle], this particle
|
||||
/// is going to be used on its own, this implies that
|
||||
/// all mirrored particles are identical (expect for the rotation around the circle)
|
||||
class CircleMirror extends Particle {
|
||||
final ParticleBuilder particleBuilder;
|
||||
|
||||
final double initialRotation;
|
||||
|
||||
final int numberOfParticles;
|
||||
|
||||
CircleMirror.builder({required this.particleBuilder, required this.initialRotation, required this.numberOfParticles});
|
||||
|
||||
CircleMirror({required Particle child, required this.initialRotation, required this.numberOfParticles}) : particleBuilder = ((index) => child);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
canvas.save();
|
||||
canvas.rotate(initialRotation);
|
||||
for (int i = 0; i < numberOfParticles; i++) {
|
||||
particleBuilder(i).paint(canvas, size, progress, seed);
|
||||
canvas.rotate(pi / (numberOfParticles / 2));
|
||||
}
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirrors a given particle around a circle.
|
||||
///
|
||||
/// When using the default constructor you specify one [Particle], this particle
|
||||
/// is going to be used on its own, this implies that
|
||||
/// all mirrored particles are identical (expect for the rotation around the circle)
|
||||
class RectangleMirror extends Particle {
|
||||
final ParticleBuilder particleBuilder;
|
||||
|
||||
/// Position of the first particle on the rect
|
||||
final double initialDistance;
|
||||
|
||||
final int numberOfParticles;
|
||||
|
||||
RectangleMirror.builder({required this.particleBuilder, required this.initialDistance, required this.numberOfParticles});
|
||||
|
||||
RectangleMirror({required Particle child, required this.initialDistance, required this.numberOfParticles})
|
||||
: particleBuilder = ((index) => child);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
canvas.save();
|
||||
double totalLength = size.width * 2 + size.height * 2;
|
||||
double distanceBetweenParticles = totalLength / numberOfParticles;
|
||||
|
||||
bool onHorizontalAxis = true;
|
||||
int side = 0;
|
||||
|
||||
assert((distanceBetweenParticles * numberOfParticles).round() == totalLength.round());
|
||||
|
||||
canvas.translate(-size.width / 2, -size.height / 2);
|
||||
|
||||
double currentDistance = initialDistance;
|
||||
for (int i = 0; i < numberOfParticles; i++) {
|
||||
while (true) {
|
||||
if (onHorizontalAxis ? currentDistance > size.width : currentDistance > size.height) {
|
||||
currentDistance -= onHorizontalAxis ? size.width : size.height;
|
||||
onHorizontalAxis = !onHorizontalAxis;
|
||||
side = (++side) % 4;
|
||||
} else {
|
||||
if (side == 0) {
|
||||
assert(onHorizontalAxis);
|
||||
moveTo(canvas, size, 0, currentDistance, 0.0, () {
|
||||
particleBuilder(i).paint(canvas, size, progress, seed);
|
||||
});
|
||||
} else if (side == 1) {
|
||||
assert(!onHorizontalAxis);
|
||||
moveTo(canvas, size, 1, size.width, currentDistance, () {
|
||||
particleBuilder(i).paint(canvas, size, progress, seed);
|
||||
});
|
||||
} else if (side == 2) {
|
||||
assert(onHorizontalAxis);
|
||||
moveTo(canvas, size, 2, size.width - currentDistance, size.height, () {
|
||||
particleBuilder(i).paint(canvas, size, progress, seed);
|
||||
});
|
||||
} else if (side == 3) {
|
||||
assert(!onHorizontalAxis);
|
||||
moveTo(canvas, size, 3, 0.0, size.height - currentDistance, () {
|
||||
particleBuilder(i).paint(canvas, size, progress, seed);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
currentDistance += distanceBetweenParticles;
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
void moveTo(Canvas canvas, Size size, int side, double x, double y, VoidCallback painter) {
|
||||
canvas.save();
|
||||
canvas.translate(x, y);
|
||||
canvas.rotate(-atan2(size.width / 2 - x, size.height / 2 - y));
|
||||
painter();
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets a child by a given [Offset]
|
||||
class PositionedParticle extends Particle {
|
||||
PositionedParticle({required this.position, required this.child});
|
||||
|
||||
final Particle child;
|
||||
|
||||
final Offset position;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
canvas.save();
|
||||
canvas.translate(position.dx, position.dy);
|
||||
child.paint(canvas, size, progress, seed);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/// Animates a childs position based on a Tween<Offset>
|
||||
class AnimatedPositionedParticle extends Particle {
|
||||
AnimatedPositionedParticle({required Offset begin, required Offset end, required this.child}) : offsetTween = Tween<Offset>(begin: begin, end: end);
|
||||
|
||||
final Particle child;
|
||||
|
||||
final Tween<Offset> offsetTween;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
canvas.save();
|
||||
canvas.translate(offsetTween.lerp(progress).dx, offsetTween.lerp(progress).dy);
|
||||
child.paint(canvas, size, progress, seed);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/// Specifies an [Interval] for its child.
|
||||
///
|
||||
/// Instead of applying a curve the the input parameters of the paint method,
|
||||
/// apply it with this Particle.
|
||||
///
|
||||
/// If you want you child to only animate from 0.0 - 0.5 (relative), specify an [Interval] with those values.
|
||||
class IntervalParticle extends Particle {
|
||||
final Interval interval;
|
||||
|
||||
final Particle child;
|
||||
|
||||
IntervalParticle({required this.child, required this.interval});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
if (progress < interval.begin || progress > interval.end) return;
|
||||
child.paint(canvas, size, interval.transform(progress), seed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Does nothing else than holding a list of particles and painting them in that order
|
||||
class CompositeParticle extends Particle {
|
||||
final List<Particle> children;
|
||||
|
||||
CompositeParticle({required this.children});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
for (Particle particle in children) {
|
||||
particle.paint(canvas, size, progress, seed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A particle which rotates the child.
|
||||
///
|
||||
/// Does not animate.
|
||||
class RotationParticle extends Particle {
|
||||
final Particle child;
|
||||
|
||||
final double rotation;
|
||||
|
||||
RotationParticle({required this.child, required this.rotation});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, int seed) {
|
||||
canvas.save();
|
||||
canvas.rotate(rotation);
|
||||
child.paint(canvas, size, progress, seed);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/// A particle which rotates a child along a given [Tween]
|
||||
class AnimatedRotationParticle extends Particle {
|
||||
final Particle child;
|
||||
|
||||
final Tween<double> rotation;
|
||||
|
||||
AnimatedRotationParticle({required this.child, required double begin, required double end}) : rotation = Tween<double>(begin: begin, end: end);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, int seed) {
|
||||
canvas.save();
|
||||
canvas.rotate(rotation.lerp(progress));
|
||||
child.paint(canvas, size, progress, seed);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/// Geometry
|
||||
///
|
||||
/// These are some basic geometric classes which also fade out as time goes on.
|
||||
/// Each primitive should draw itself at the origin. If the orientation matters it should be directed to the top
|
||||
/// (negative y)
|
||||
///
|
||||
/// A rectangle which also fades out over time.
|
||||
class FadingRect extends Particle {
|
||||
final Color color;
|
||||
final double width;
|
||||
final double height;
|
||||
|
||||
FadingRect({required this.color, required this.width, required this.height});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
canvas.drawRect(Rect.fromLTWH(0.0, 0.0, width, height), Paint()..color = color.withOpacity(1 - progress));
|
||||
}
|
||||
}
|
||||
|
||||
/// A circle which fades out over time
|
||||
class FadingCircle extends Particle {
|
||||
final Color color;
|
||||
final double radius;
|
||||
|
||||
FadingCircle({required this.color, required this.radius});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, seed) {
|
||||
canvas.drawCircle(Offset.zero, radius, Paint()..color = color.withOpacity(1 - progress));
|
||||
}
|
||||
}
|
||||
|
||||
/// A triangle which also fades out over time
|
||||
class FadingTriangle extends Particle {
|
||||
/// This controls the shape of the triangle.
|
||||
///
|
||||
/// Value between 0 and 1
|
||||
final double variation;
|
||||
|
||||
final Color color;
|
||||
|
||||
/// The size of the base side of the triangle.
|
||||
final double baseSize;
|
||||
|
||||
/// This is the factor of how much bigger then length than the width is
|
||||
final double heightToBaseFactor;
|
||||
|
||||
FadingTriangle({required this.variation, required this.color, required this.baseSize, required this.heightToBaseFactor});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, int seed) {
|
||||
Path path = Path();
|
||||
path.moveTo(0.0, 0.0);
|
||||
path.lineTo(baseSize * variation, baseSize * heightToBaseFactor);
|
||||
path.lineTo(baseSize, 0.0);
|
||||
path.close();
|
||||
canvas.drawPath(path, Paint()..color = color.withOpacity(1 - progress));
|
||||
}
|
||||
}
|
||||
|
||||
/// An ugly looking "snake"
|
||||
///
|
||||
/// See for yourself
|
||||
class FadingSnake extends Particle {
|
||||
final double width;
|
||||
final double segmentLength;
|
||||
final int segments;
|
||||
final double curvyness;
|
||||
|
||||
final Color color;
|
||||
|
||||
FadingSnake({required this.width, required this.segmentLength, required this.segments, required this.curvyness, required this.color});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size, double progress, int seed) {
|
||||
canvas.save();
|
||||
canvas.rotate(pi / 6);
|
||||
Path path = Path();
|
||||
for (int i = 0; i < segments; i++) {
|
||||
path.quadraticBezierTo(curvyness * i, segmentLength * (i + 1), curvyness * (i + 1), segmentLength * (i + 1));
|
||||
}
|
||||
for (int i = segments - 1; i >= 0; i--) {
|
||||
path.quadraticBezierTo(curvyness * (i + 1), segmentLength * i - curvyness, curvyness * i, segmentLength * i - curvyness);
|
||||
}
|
||||
path.close();
|
||||
canvas.drawPath(path, Paint()..color = color);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,179 +1,179 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:filcnaplo/api/providers/update_provider.dart';
|
||||
import 'package:filcnaplo/ui/date_widget.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/message_provider.dart';
|
||||
import 'package:filcnaplo/api/providers/user_provider.dart';
|
||||
import 'package:filcnaplo/theme/colors/colors.dart';
|
||||
import 'package:filcnaplo_kreta_api/models/message.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/empty.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/filter_bar.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/ui/filter/sort.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/widgets/message/message_viewable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:filcnaplo/utils/color.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,
|
||||
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: ColorUtils.stringToColor(user.displayName ?? "?"),
|
||||
badge: updateProvider.available,
|
||||
role: user.role,
|
||||
profilePictureString: user.picture,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
automaticallyImplyLeading: false,
|
||||
shadowColor: Theme.of(context).shadowColor,
|
||||
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),
|
||||
),
|
||||
],
|
||||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:filcnaplo/api/providers/update_provider.dart';
|
||||
import 'package:filcnaplo/ui/date_widget.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/message_provider.dart';
|
||||
import 'package:filcnaplo/api/providers/user_provider.dart';
|
||||
import 'package:filcnaplo/theme/colors/colors.dart';
|
||||
import 'package:filcnaplo_kreta_api/models/message.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/empty.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/filter_bar.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/ui/filter/sort.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/widgets/message/message_viewable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:filcnaplo/utils/color.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,
|
||||
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: ColorUtils.stringToColor(user.displayName ?? "?"),
|
||||
badge: updateProvider.available,
|
||||
role: user.role,
|
||||
profilePictureString: user.picture,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
automaticallyImplyLeading: false,
|
||||
shadowColor: Theme.of(context).shadowColor,
|
||||
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),
|
||||
),
|
||||
],
|
||||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,62 +1,62 @@
|
||||
import 'package:filcnaplo/theme/colors/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:filcnaplo/utils/format.dart';
|
||||
|
||||
class DayTitle extends StatefulWidget {
|
||||
const DayTitle({Key? key, required this.dayTitle, required this.controller}) : super(key: key);
|
||||
|
||||
final String Function(int) dayTitle;
|
||||
final TabController controller;
|
||||
|
||||
@override
|
||||
State<DayTitle> createState() => _DayTitleState();
|
||||
}
|
||||
|
||||
class _DayTitleState extends State<DayTitle> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.addListener(listener);
|
||||
}
|
||||
|
||||
void listener() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(listener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double width = MediaQuery.of(context).size.width;
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: widget.controller.animation!,
|
||||
builder: (context, _) {
|
||||
double value = widget.controller.animation!.value;
|
||||
|
||||
return Transform.translate(
|
||||
offset: Offset(-value * width / 1.5, 0),
|
||||
child: Row(
|
||||
children: List.generate(
|
||||
widget.controller.length,
|
||||
(index) {
|
||||
double opacity = (value - index + 1).clamp(0, 1);
|
||||
|
||||
return SizedBox(
|
||||
width: MediaQuery.of(context).size.width / 1.5,
|
||||
child: Text(
|
||||
widget.dayTitle(index).capital(),
|
||||
style: TextStyle(color: AppColors.of(context).text.withOpacity(opacity), fontSize: 32.0, fontWeight: FontWeight.bold),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
import 'package:filcnaplo/theme/colors/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:filcnaplo/utils/format.dart';
|
||||
|
||||
class DayTitle extends StatefulWidget {
|
||||
const DayTitle({Key? key, required this.dayTitle, required this.controller}) : super(key: key);
|
||||
|
||||
final String Function(int) dayTitle;
|
||||
final TabController controller;
|
||||
|
||||
@override
|
||||
State<DayTitle> createState() => _DayTitleState();
|
||||
}
|
||||
|
||||
class _DayTitleState extends State<DayTitle> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.addListener(listener);
|
||||
}
|
||||
|
||||
void listener() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(listener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double width = MediaQuery.of(context).size.width;
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: widget.controller.animation!,
|
||||
builder: (context, _) {
|
||||
double value = widget.controller.animation!.value;
|
||||
|
||||
return Transform.translate(
|
||||
offset: Offset(-value * width / 1.5, 0),
|
||||
child: Row(
|
||||
children: List.generate(
|
||||
widget.controller.length,
|
||||
(index) {
|
||||
double opacity = (value - index + 1).clamp(0, 1);
|
||||
|
||||
return SizedBox(
|
||||
width: MediaQuery.of(context).size.width / 1.5,
|
||||
child: Text(
|
||||
widget.dayTitle(index).capital(),
|
||||
style: TextStyle(color: AppColors.of(context).text.withOpacity(opacity), fontSize: 32.0, fontWeight: FontWeight.bold),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,472 +1,472 @@
|
||||
import 'dart:math';
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:filcnaplo/api/providers/update_provider.dart';
|
||||
import 'package:filcnaplo_kreta_api/client/client.dart';
|
||||
import 'package:filcnaplo_kreta_api/models/week.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
|
||||
import 'package:filcnaplo/api/providers/user_provider.dart';
|
||||
import 'package:filcnaplo/theme/colors/colors.dart';
|
||||
import 'package:filcnaplo_kreta_api/models/lesson.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/dot.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/lesson/lesson_view.dart';
|
||||
import 'package:filcnaplo_kreta_api/controllers/timetable_controller.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/widgets/lesson/lesson_viewable.dart';
|
||||
import 'package:filcnaplo_mobile_ui/pages/timetable/day_title.dart';
|
||||
import 'package:filcnaplo_mobile_ui/screens/navigation/navigation_route_handler.dart';
|
||||
import 'package:filcnaplo_mobile_ui/screens/navigation/navigation_screen.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:filcnaplo/utils/color.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:i18n_extension/i18n_widget.dart';
|
||||
import 'package:filcnaplo_premium/ui/mobile/timetable/fs_timetable_button.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, WidgetsBindingObserver {
|
||||
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);
|
||||
}
|
||||
|
||||
// When the app comes to foreground, refresh the timetable
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
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);
|
||||
|
||||
// Register listening for app state changes to refresh the timetable
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
_controller.dispose();
|
||||
user.removeListener(_userListener);
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
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.displayName?.split(" ") ?? ["?"];
|
||||
firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0];
|
||||
|
||||
return Scaffold(
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.only(top: 9.0),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => mounted ? _controller.jump(_controller.currentWeek, context: context, loader: false) : Future.value(null),
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
edgeOffset: 132.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: [
|
||||
PremiumFSTimetableButton(controller: _controller),
|
||||
|
||||
// Profile Icon
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 24.0),
|
||||
child: ProfileButton(
|
||||
child: ProfileImage(
|
||||
heroTag: "profile",
|
||||
name: firstName,
|
||||
backgroundColor: ColorUtils.stringToColor(user.displayName ?? "?"),
|
||||
badge: updateProvider.available,
|
||||
role: user.role,
|
||||
profilePictureString: user.picture,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
automaticallyImplyLeading: false,
|
||||
// Current day text
|
||||
title: PageTransitionSwitcher(
|
||||
reverse: _controller.currentWeekId < _controller.previousWeekId,
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
child: child,
|
||||
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
);
|
||||
},
|
||||
layoutBuilder: (List<Widget> entries) {
|
||||
return Stack(
|
||||
children: entries,
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
() {
|
||||
final show =
|
||||
_controller.days == null || (_controller.loadType != LoadType.offline && _controller.loadType != LoadType.online);
|
||||
const duration = Duration(milliseconds: 150);
|
||||
return AnimatedOpacity(
|
||||
opacity: show ? 1.0 : 0.0,
|
||||
duration: duration,
|
||||
curve: Curves.easeInOut,
|
||||
child: AnimatedContainer(
|
||||
duration: duration,
|
||||
width: show ? 24.0 : 0.0,
|
||||
curve: Curves.easeInOut,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(right: 12.0),
|
||||
child: CupertinoActivityIndicator(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}(),
|
||||
() {
|
||||
if ((_controller.days?.length ?? 0) > 0) {
|
||||
return DayTitle(controller: _tabController, dayTitle: dayTitle);
|
||||
} else {
|
||||
return Text(
|
||||
"timetable".i18n,
|
||||
style: TextStyle(
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.of(context).text,
|
||||
),
|
||||
);
|
||||
}
|
||||
}(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
shadowColor: Theme.of(context).shadowColor,
|
||||
bottom: PreferredSize(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.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,
|
||||
);
|
||||
}
|
||||
_tabController.animateTo(_getDayIndex(DateTime.now()));
|
||||
}),
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
preferredSize: const Size.fromHeight(50.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
body: 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
|
||||
? Column(
|
||||
key: Key(_controller.currentWeek.toString()),
|
||||
children: [
|
||||
// Week view
|
||||
_tabController.length > 0
|
||||
? Expanded(
|
||||
child: TabBarView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
controller: _tabController,
|
||||
// days
|
||||
children: List.generate(
|
||||
_controller.days!.length,
|
||||
(tab) => RefreshIndicator(
|
||||
onRefresh: () =>
|
||||
mounted ? _controller.jump(_controller.currentWeek, context: context, loader: false) : Future.value(null),
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
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),
|
||||
),
|
||||
|
||||
// Day selector
|
||||
TabBar(
|
||||
dividerColor: Colors.transparent,
|
||||
controller: _tabController,
|
||||
// Label
|
||||
labelPadding: EdgeInsets.zero,
|
||||
labelColor: Theme.of(context).colorScheme.secondary,
|
||||
unselectedLabelColor: AppColors.of(context).text.withOpacity(0.9),
|
||||
// Indicator
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
indicatorPadding: EdgeInsets.zero,
|
||||
indicator: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondary.withOpacity(0.25),
|
||||
borderRadius: BorderRadius.circular(45.0),
|
||||
),
|
||||
overlayColor: MaterialStateProperty.all(const Color(0x00000000)),
|
||||
// Tabs
|
||||
padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 8.0),
|
||||
tabs: List.generate(_tabController.length, (index) {
|
||||
String label = DateFormat("E", I18n.of(context).locale.languageCode).format(_controller.days![index].first.date);
|
||||
return Tab(
|
||||
height: 46.0,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (_sameDate(_controller.days![index].first.date, DateTime.now()))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: Dot(size: 4.0, color: Theme.of(context).colorScheme.secondary),
|
||||
),
|
||||
Text(
|
||||
label.substring(0, min(2, label.length)),
|
||||
style: const TextStyle(fontSize: 26.0, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// difference.inDays is not reliable
|
||||
bool _sameDate(DateTime a, DateTime b) => (a.year == b.year && a.month == b.month && a.day == b.day);
|
||||
import 'dart:math';
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:filcnaplo/api/providers/update_provider.dart';
|
||||
import 'package:filcnaplo_kreta_api/client/client.dart';
|
||||
import 'package:filcnaplo_kreta_api/models/week.dart';
|
||||
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
|
||||
import 'package:filcnaplo/api/providers/user_provider.dart';
|
||||
import 'package:filcnaplo/theme/colors/colors.dart';
|
||||
import 'package:filcnaplo_kreta_api/models/lesson.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/dot.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/lesson/lesson_view.dart';
|
||||
import 'package:filcnaplo_kreta_api/controllers/timetable_controller.dart';
|
||||
import 'package:filcnaplo_mobile_ui/common/widgets/lesson/lesson_viewable.dart';
|
||||
import 'package:filcnaplo_mobile_ui/pages/timetable/day_title.dart';
|
||||
import 'package:filcnaplo_mobile_ui/screens/navigation/navigation_route_handler.dart';
|
||||
import 'package:filcnaplo_mobile_ui/screens/navigation/navigation_screen.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:filcnaplo/utils/color.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:i18n_extension/i18n_widget.dart';
|
||||
import 'package:filcnaplo_premium/ui/mobile/timetable/fs_timetable_button.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, WidgetsBindingObserver {
|
||||
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);
|
||||
}
|
||||
|
||||
// When the app comes to foreground, refresh the timetable
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
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);
|
||||
|
||||
// Register listening for app state changes to refresh the timetable
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
_controller.dispose();
|
||||
user.removeListener(_userListener);
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
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.displayName?.split(" ") ?? ["?"];
|
||||
firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0];
|
||||
|
||||
return Scaffold(
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.only(top: 9.0),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => mounted ? _controller.jump(_controller.currentWeek, context: context, loader: false) : Future.value(null),
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
edgeOffset: 132.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: [
|
||||
PremiumFSTimetableButton(controller: _controller),
|
||||
|
||||
// Profile Icon
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 24.0),
|
||||
child: ProfileButton(
|
||||
child: ProfileImage(
|
||||
heroTag: "profile",
|
||||
name: firstName,
|
||||
backgroundColor: ColorUtils.stringToColor(user.displayName ?? "?"),
|
||||
badge: updateProvider.available,
|
||||
role: user.role,
|
||||
profilePictureString: user.picture,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
automaticallyImplyLeading: false,
|
||||
// Current day text
|
||||
title: PageTransitionSwitcher(
|
||||
reverse: _controller.currentWeekId < _controller.previousWeekId,
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
child: child,
|
||||
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
);
|
||||
},
|
||||
layoutBuilder: (List<Widget> entries) {
|
||||
return Stack(
|
||||
children: entries,
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
() {
|
||||
final show =
|
||||
_controller.days == null || (_controller.loadType != LoadType.offline && _controller.loadType != LoadType.online);
|
||||
const duration = Duration(milliseconds: 150);
|
||||
return AnimatedOpacity(
|
||||
opacity: show ? 1.0 : 0.0,
|
||||
duration: duration,
|
||||
curve: Curves.easeInOut,
|
||||
child: AnimatedContainer(
|
||||
duration: duration,
|
||||
width: show ? 24.0 : 0.0,
|
||||
curve: Curves.easeInOut,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(right: 12.0),
|
||||
child: CupertinoActivityIndicator(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}(),
|
||||
() {
|
||||
if ((_controller.days?.length ?? 0) > 0) {
|
||||
return DayTitle(controller: _tabController, dayTitle: dayTitle);
|
||||
} else {
|
||||
return Text(
|
||||
"timetable".i18n,
|
||||
style: TextStyle(
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.of(context).text,
|
||||
),
|
||||
);
|
||||
}
|
||||
}(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
shadowColor: Theme.of(context).shadowColor,
|
||||
bottom: PreferredSize(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.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,
|
||||
);
|
||||
}
|
||||
_tabController.animateTo(_getDayIndex(DateTime.now()));
|
||||
}),
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
preferredSize: const Size.fromHeight(50.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
body: 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
|
||||
? Column(
|
||||
key: Key(_controller.currentWeek.toString()),
|
||||
children: [
|
||||
// Week view
|
||||
_tabController.length > 0
|
||||
? Expanded(
|
||||
child: TabBarView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
controller: _tabController,
|
||||
// days
|
||||
children: List.generate(
|
||||
_controller.days!.length,
|
||||
(tab) => RefreshIndicator(
|
||||
onRefresh: () =>
|
||||
mounted ? _controller.jump(_controller.currentWeek, context: context, loader: false) : Future.value(null),
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
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),
|
||||
),
|
||||
|
||||
// Day selector
|
||||
TabBar(
|
||||
dividerColor: Colors.transparent,
|
||||
controller: _tabController,
|
||||
// Label
|
||||
labelPadding: EdgeInsets.zero,
|
||||
labelColor: Theme.of(context).colorScheme.secondary,
|
||||
unselectedLabelColor: AppColors.of(context).text.withOpacity(0.9),
|
||||
// Indicator
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
indicatorPadding: EdgeInsets.zero,
|
||||
indicator: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondary.withOpacity(0.25),
|
||||
borderRadius: BorderRadius.circular(45.0),
|
||||
),
|
||||
overlayColor: MaterialStateProperty.all(const Color(0x00000000)),
|
||||
// Tabs
|
||||
padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 8.0),
|
||||
tabs: List.generate(_tabController.length, (index) {
|
||||
String label = DateFormat("E", I18n.of(context).locale.languageCode).format(_controller.days![index].first.date);
|
||||
return Tab(
|
||||
height: 46.0,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (_sameDate(_controller.days![index].first.date, DateTime.now()))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: Dot(size: 4.0, color: Theme.of(context).colorScheme.secondary),
|
||||
),
|
||||
Text(
|
||||
label.substring(0, min(2, label.length)),
|
||||
style: const TextStyle(fontSize: 26.0, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// difference.inDays is not reliable
|
||||
bool _sameDate(DateTime a, DateTime b) => (a.year == b.year && a.month == b.month && a.day == b.day);
|
||||
|
||||
@@ -1,30 +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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user