diff --git a/filcnaplo/lib/ui/date_widget.dart b/filcnaplo/lib/ui/date_widget.dart new file mode 100644 index 0000000..65cb12c --- /dev/null +++ b/filcnaplo/lib/ui/date_widget.dart @@ -0,0 +1,8 @@ +import 'package:flutter/widgets.dart'; + +class DateWidget { + final DateTime date; + final Widget widget; + final String? key; + const DateWidget({required this.date, required this.widget, this.key}); +} diff --git a/filcnaplo/lib/ui/filter/sort.dart b/filcnaplo/lib/ui/filter/sort.dart new file mode 100644 index 0000000..4364257 --- /dev/null +++ b/filcnaplo/lib/ui/filter/sort.dart @@ -0,0 +1,159 @@ +import 'package:filcnaplo/theme/colors/colors.dart'; +import 'package:filcnaplo/ui/date_widget.dart'; +import 'package:filcnaplo/ui/filter/widgets.dart'; +import 'package:filcnaplo/ui/widgets/message/message_tile.dart'; +import 'package:filcnaplo_kreta_api/models/message.dart'; +import 'package:filcnaplo_mobile_ui/common/panel/panel.dart'; +import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_viewable.dart'; +import 'package:filcnaplo_mobile_ui/common/widgets/absence_group/absence_group_tile.dart'; +import 'package:filcnaplo_mobile_ui/common/widgets/timetable/changed_lesson_tile.dart'; +import 'package:flutter/material.dart'; +import 'package:implicitly_animated_reorderable_list/implicitly_animated_reorderable_list.dart'; +import 'package:filcnaplo/utils/format.dart'; + +// difference.inDays is not reliable +bool _sameDate(DateTime a, DateTime b) => (a.year == b.year && a.month == b.month && a.day == b.day); + +List sortDateWidgets( + BuildContext context, { + required List dateWidgets, + bool showTitle = true, + bool showDivider = false, + bool hasShadow = false, + EdgeInsetsGeometry? padding, +}) { + dateWidgets.sort((a, b) => -a.date.compareTo(b.date)); + + List conversations = []; + List convMessages = []; + + // Group messages into conversations + for (var w in dateWidgets) { + if (w.widget.runtimeType == MessageTile) { + Message message = (w.widget as MessageTile).message; + + if (message.conversationId != null) { + convMessages.add(w); + + Conversation conv = conversations.firstWhere((e) => e.id == message.conversationId, orElse: () => Conversation(id: message.conversationId!)); + conv.add(message); + if (conv.messages.length == 1) conversations.add(conv); + } + + if (conversations.any((c) => c.id == message.messageId)) { + Conversation conv = conversations.firstWhere((e) => e.id == message.messageId); + convMessages.add(w); + conv.add(message); + } + } + } + + // remove individual messages + for (var e in convMessages) { + dateWidgets.remove(e); + } + + // Add conversations + for (var conv in conversations) { + conv.sort(); + + dateWidgets.add(DateWidget( + key: "${conv.newest.date.millisecondsSinceEpoch}-msg", + date: conv.newest.date, + widget: MessageTile( + conv.newest, + ), + )); + } + + dateWidgets.sort((a, b) => -a.date.compareTo(b.date)); + + List> groupedDateWidgets = [[]]; + for (var element in dateWidgets) { + if (groupedDateWidgets.last.isNotEmpty) { + if (!_sameDate(element.date, groupedDateWidgets.last.last.date)) { + groupedDateWidgets.add([element]); + continue; + } + } + groupedDateWidgets.last.add(element); + } + + List items = []; + + if (groupedDateWidgets.first.isNotEmpty) { + for (var elements in groupedDateWidgets) { + bool cst = showTitle; + + // Group Absence Tiles + List absenceTileWidgets = elements.where((element) { + return element.widget is AbsenceViewable && (element.widget as AbsenceViewable).absence.delay == 0; + }).toList(); + List absenceTiles = absenceTileWidgets.map((e) => e.widget as AbsenceViewable).toList(); + if (absenceTiles.length > 1) { + elements.removeWhere((element) => element.widget.runtimeType == AbsenceViewable && (element.widget as AbsenceViewable).absence.delay == 0); + if (elements.isEmpty) { + cst = false; + } + elements.add(DateWidget( + widget: AbsenceGroupTile(absenceTiles, showDate: !cst), + date: absenceTileWidgets.first.date, + key: "${absenceTileWidgets.first.date.millisecondsSinceEpoch}-absence-group")); + } + + // Bring Lesson Tiles to front & sort by index asc + List lessonTiles = elements.where((element) { + return element.widget.runtimeType == ChangedLessonTile; + }).toList(); + lessonTiles.sort((a, b) => (a.widget as ChangedLessonTile).lesson.lessonIndex.compareTo((b.widget as ChangedLessonTile).lesson.lessonIndex)); + elements.removeWhere((element) => element.widget.runtimeType == ChangedLessonTile); + elements.insertAll(0, lessonTiles); + + final date = (elements + absenceTileWidgets).first.date; + items.add(DateWidget( + date: date, + widget: Panel( + key: ValueKey(date), + padding: padding ?? const EdgeInsets.symmetric(vertical: 6.0), + title: cst ? Text(date.format(context, forceToday: true)) : null, + hasShadow: hasShadow, + child: ImplicitlyAnimatedList( + areItemsTheSame: (a, b) => a.key == b.key, + spawnIsolate: false, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, animation, item, index) => filterItemBuilder(context, animation, item.widget, index), + items: elements, + ), + ), + )); + } + } + + final nh = DateTime.now(); + final now = DateTime(nh.year, nh.month, nh.day).subtract(const Duration(seconds: 1)); + + if (showDivider && items.any((i) => i.date.isBefore(now)) && items.any((i) => i.date.isAfter(now))) { + items.add( + DateWidget( + date: now, + widget: Center( + child: Container( + margin: const EdgeInsets.symmetric(vertical: 12.0), + height: 3.0, + width: 150.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.0), + color: AppColors.of(context).text.withOpacity(.25), + ), + ), + ), + ), + ); + } + + // Sort future dates asc, past dates desc + items.sort((a, b) => (a.date.isAfter(now) && b.date.isAfter(now) ? 1 : -1) * a.date.compareTo(b.date)); + + return items.map((e) => e.widget).toList(); +} diff --git a/filcnaplo/lib/ui/filter/widgets.dart b/filcnaplo/lib/ui/filter/widgets.dart new file mode 100644 index 0000000..8dfbbce --- /dev/null +++ b/filcnaplo/lib/ui/filter/widgets.dart @@ -0,0 +1,160 @@ +import 'package:filcnaplo/api/providers/update_provider.dart'; +import 'package:filcnaplo/theme/colors/colors.dart'; +import 'package:filcnaplo/ui/date_widget.dart'; +import 'package:filcnaplo/ui/filter/widgets/grades.dart' as grade_filter; +import 'package:filcnaplo/ui/filter/widgets/certifications.dart' as certification_filter; +import 'package:filcnaplo/ui/filter/widgets/messages.dart' as message_filter; +import 'package:filcnaplo/ui/filter/widgets/absences.dart' as absence_filter; +import 'package:filcnaplo/ui/filter/widgets/homework.dart' as homework_filter; +import 'package:filcnaplo/ui/filter/widgets/exams.dart' as exam_filter; +import 'package:filcnaplo/ui/filter/widgets/notes.dart' as note_filter; +import 'package:filcnaplo/ui/filter/widgets/events.dart' as event_filter; +import 'package:filcnaplo/ui/filter/widgets/lessons.dart' as lesson_filter; +import 'package:filcnaplo/ui/filter/widgets/update.dart' as update_filter; +import 'package:filcnaplo/ui/filter/widgets/missed_exams.dart' as missed_exam_filter; +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_kreta_api/providers/timetable_provider.dart'; +import 'package:filcnaplo_mobile_ui/common/panel/panel.dart'; +import 'package:flutter/material.dart'; +import 'package:implicitly_animated_reorderable_list/transitions.dart'; +import 'package:provider/provider.dart'; + +const List homeFilters = [FilterType.all, FilterType.grades, FilterType.messages, FilterType.absences]; + +enum FilterType { all, grades, messages, absences, homework, exams, notes, events, lessons, updates, certifications, missedExams } + +Future> getFilterWidgets(FilterType activeData, {bool absencesNoExcused = false, required BuildContext context}) async { + final gradeProvider = Provider.of(context); + final timetableProvider = Provider.of(context); + final messageProvider = Provider.of(context); + final absenceProvider = Provider.of(context); + final homeworkProvider = Provider.of(context); + final examProvider = Provider.of(context); + final noteProvider = Provider.of(context); + final eventProvider = Provider.of(context); + final updateProvider = Provider.of(context); + + List items = []; + + switch (activeData) { + // All + case FilterType.all: + final all = await Future.wait>([ + getFilterWidgets(FilterType.grades, context: context), + getFilterWidgets(FilterType.lessons, context: context), + getFilterWidgets(FilterType.messages, context: context), + getFilterWidgets(FilterType.absences, context: context, absencesNoExcused: true), + getFilterWidgets(FilterType.homework, context: context), + getFilterWidgets(FilterType.exams, context: context), + getFilterWidgets(FilterType.updates, context: context), + getFilterWidgets(FilterType.certifications, context: context), + getFilterWidgets(FilterType.missedExams, context: context), + ]); + items = all.expand((x) => x).toList(); + + break; + + // Grades + case FilterType.grades: + items = grade_filter.getWidgets(gradeProvider.grades); + break; + + // Certifications + case FilterType.certifications: + items = certification_filter.getWidgets(gradeProvider.grades); + break; + + // Messages + case FilterType.messages: + items = message_filter.getWidgets( + messageProvider.messages, + noteProvider.notes, + eventProvider.events, + ); + break; + + // Absences + case FilterType.absences: + items = absence_filter.getWidgets(absenceProvider.absences, noExcused: absencesNoExcused); + break; + + // Homework + case FilterType.homework: + items = homework_filter.getWidgets(homeworkProvider.homework); + break; + + // Exams + case FilterType.exams: + items = exam_filter.getWidgets(examProvider.exams); + break; + + // Notes + case FilterType.notes: + items = note_filter.getWidgets(noteProvider.notes); + break; + + // Events + case FilterType.events: + items = event_filter.getWidgets(eventProvider.events); + break; + + // Changed Lessons + case FilterType.lessons: + items = lesson_filter.getWidgets(timetableProvider.lessons); + break; + + // Updates + case FilterType.updates: + if (updateProvider.releases.isNotEmpty) items = [update_filter.getWidget(updateProvider.releases.first)]; + break; + + // Missed Exams + case FilterType.missedExams: + items = missed_exam_filter.getWidgets(timetableProvider.lessons); + break; + } + return items; +} + +Widget filterItemBuilder(BuildContext context, Animation animation, Widget item, int index) { + final wrappedItem = SizeFadeTransition( + curve: Curves.easeInOutCubic, + animation: animation, + child: item, + ); + return item is Panel + // Re-add & animate shadow + ? AnimatedBuilder( + animation: animation, + child: wrappedItem, + builder: (context, child) { + return Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: DecoratedBox( + decoration: BoxDecoration( + boxShadow: [ + if (Theme.of(context).brightness == Brightness.light) + BoxShadow( + offset: const Offset(0, 21), + blurRadius: 23.0, + color: AppColors.of(context).shadow.withOpacity( + CurvedAnimation( + parent: CurvedAnimation(parent: animation, curve: Curves.easeInOutCubic), + curve: const Interval(2 / 3, 1.0), + ).value, + ), + ), + ], + ), + child: child, + ), + ); + }) + : wrappedItem; +} diff --git a/filcnaplo/lib/ui/filter/widgets/absences.dart b/filcnaplo/lib/ui/filter/widgets/absences.dart new file mode 100644 index 0000000..75ec70b --- /dev/null +++ b/filcnaplo/lib/ui/filter/widgets/absences.dart @@ -0,0 +1,15 @@ +import 'package:filcnaplo/ui/date_widget.dart'; +import 'package:filcnaplo_kreta_api/models/absence.dart'; +import 'package:filcnaplo_mobile_ui/common/widgets/absence/absence_viewable.dart' as mobile; + +List getWidgets(List providerAbsences, {bool noExcused = false}) { + List items = []; + providerAbsences.where((a) => !noExcused || a.state != Justification.excused).forEach((absence) { + items.add(DateWidget( + key: absence.id, + date: absence.date, + widget: mobile.AbsenceViewable(absence), + )); + }); + return items; +} diff --git a/filcnaplo/lib/ui/filter/widgets/certifications.dart b/filcnaplo/lib/ui/filter/widgets/certifications.dart new file mode 100644 index 0000000..75dff55 --- /dev/null +++ b/filcnaplo/lib/ui/filter/widgets/certifications.dart @@ -0,0 +1,24 @@ +import 'package:filcnaplo/ui/date_widget.dart'; +import 'package:filcnaplo_kreta_api/models/grade.dart'; +import 'package:filcnaplo_mobile_ui/common/widgets/cretification/certification_card.dart' as mobile; + +List getWidgets(List providerGrades) { + List items = []; + for (var gradeType in GradeType.values) { + if ([GradeType.midYear, GradeType.unknown, GradeType.levelExam].contains(gradeType)) continue; + + List grades = providerGrades.where((grade) => grade.type == gradeType).toList(); + if (grades.isNotEmpty) { + grades.sort((a, b) => -a.date.compareTo(b.date)); + + items.add(DateWidget( + date: grades.first.date, + widget: mobile.CertificationCard( + grades, + gradeType: gradeType, + ), + )); + } + } + return items; +} diff --git a/filcnaplo/lib/ui/filter/widgets/events.dart b/filcnaplo/lib/ui/filter/widgets/events.dart new file mode 100644 index 0000000..89d3ad2 --- /dev/null +++ b/filcnaplo/lib/ui/filter/widgets/events.dart @@ -0,0 +1,15 @@ +import 'package:filcnaplo/ui/date_widget.dart'; +import 'package:filcnaplo_kreta_api/models/event.dart'; +import 'package:filcnaplo_mobile_ui/common/widgets/event/event_viewable.dart' as mobile; + +List getWidgets(List providerEvents) { + List items = []; + for (var event in providerEvents) { + items.add(DateWidget( + key: event.id, + date: event.start, + widget: mobile.EventViewable(event), + )); + } + return items; +} diff --git a/filcnaplo/lib/ui/filter/widgets/exams.dart b/filcnaplo/lib/ui/filter/widgets/exams.dart new file mode 100644 index 0000000..e651542 --- /dev/null +++ b/filcnaplo/lib/ui/filter/widgets/exams.dart @@ -0,0 +1,15 @@ +import 'package:filcnaplo/ui/date_widget.dart'; +import 'package:filcnaplo_kreta_api/models/exam.dart'; +import 'package:filcnaplo_mobile_ui/common/widgets/exam/exam_viewable.dart' as mobile; + +List getWidgets(List providerExams) { + List items = []; + for (var exam in providerExams) { + items.add(DateWidget( + key: exam.id, + date: exam.writeDate.year != 0 ? exam.writeDate : exam.date, + widget: mobile.ExamViewable(exam), + )); + } + return items; +} diff --git a/filcnaplo/lib/ui/filter/widgets/grades.dart b/filcnaplo/lib/ui/filter/widgets/grades.dart new file mode 100644 index 0000000..4092607 --- /dev/null +++ b/filcnaplo/lib/ui/filter/widgets/grades.dart @@ -0,0 +1,19 @@ +import 'package:filcnaplo/ui/date_widget.dart'; +import 'package:filcnaplo/utils/platform.dart'; +import 'package:filcnaplo_kreta_api/models/grade.dart'; +import 'package:filcnaplo_mobile_ui/common/widgets/grade/grade_viewable.dart' as mobile; +import 'package:filcnaplo_desktop_ui/common/widgets/grade/grade_viewable.dart' as desktop; + +List getWidgets(List providerGrades) { + List items = []; + for (var grade in providerGrades) { + if (grade.type == GradeType.midYear) { + items.add(DateWidget( + key: grade.id, + date: grade.date, + widget: PlatformUtils.isMobile ? mobile.GradeViewable(grade) : desktop.GradeViewable(grade), + )); + } + } + return items; +} diff --git a/filcnaplo/lib/ui/filter/widgets/homework.dart b/filcnaplo/lib/ui/filter/widgets/homework.dart new file mode 100644 index 0000000..f89d41c --- /dev/null +++ b/filcnaplo/lib/ui/filter/widgets/homework.dart @@ -0,0 +1,18 @@ +import 'package:filcnaplo/ui/date_widget.dart'; +import 'package:filcnaplo_kreta_api/models/homework.dart'; +import 'package:filcnaplo_mobile_ui/common/widgets/homework/homework_viewable.dart' as mobile; + +List getWidgets(List providerHomework) { + List items = []; + final now = DateTime.now(); + providerHomework.where((h) => h.deadline.hour == 0 ? _sameDate(h.deadline, now) : h.deadline.isAfter(now)).forEach((homework) { + items.add(DateWidget( + key: homework.id, + date: homework.deadline.year != 0 ? homework.deadline : homework.date, + widget: mobile.HomeworkViewable(homework), + )); + }); + return items; +} + +bool _sameDate(DateTime a, DateTime b) => (a.year == b.year && a.month == b.month && a.day == b.day); diff --git a/filcnaplo/lib/ui/filter/widgets/lessons.dart b/filcnaplo/lib/ui/filter/widgets/lessons.dart new file mode 100644 index 0000000..4eb030f --- /dev/null +++ b/filcnaplo/lib/ui/filter/widgets/lessons.dart @@ -0,0 +1,15 @@ +import 'package:filcnaplo/ui/date_widget.dart'; +import 'package:filcnaplo_kreta_api/models/lesson.dart'; +import 'package:filcnaplo_mobile_ui/common/widgets/timetable/changed_lesson_viewable.dart' as mobile; + +List getWidgets(List providerLessons) { + List items = []; + providerLessons.where((l) => l.isChanged && l.start.isAfter(DateTime.now())).forEach((lesson) { + items.add(DateWidget( + key: lesson.id, + date: lesson.date, + widget: mobile.ChangedLessonViewable(lesson), + )); + }); + return items; +} diff --git a/filcnaplo/lib/ui/filter/widgets/messages.dart b/filcnaplo/lib/ui/filter/widgets/messages.dart new file mode 100644 index 0000000..7ad08e0 --- /dev/null +++ b/filcnaplo/lib/ui/filter/widgets/messages.dart @@ -0,0 +1,23 @@ +import 'package:filcnaplo/ui/date_widget.dart'; +import 'package:filcnaplo/ui/filter/widgets/notes.dart' as note_filter; +import 'package:filcnaplo/ui/filter/widgets/events.dart' as event_filter; +import 'package:filcnaplo_kreta_api/models/event.dart'; +import 'package:filcnaplo_kreta_api/models/message.dart'; +import 'package:filcnaplo_kreta_api/models/note.dart'; +import 'package:filcnaplo_mobile_ui/common/widgets/message/message_viewable.dart' as mobile; + +List getWidgets(List providerMessages, List providerNotes, List providerEvents) { + List items = []; + for (var message in providerMessages) { + if (message.type == MessageType.inbox) { + items.add(DateWidget( + key: "${message.id}", + date: message.date, + widget: mobile.MessageViewable(message), + )); + } + } + items.addAll(note_filter.getWidgets(providerNotes)); + items.addAll(event_filter.getWidgets(providerEvents)); + return items; +} diff --git a/filcnaplo/lib/ui/filter/widgets/missed_exams.dart b/filcnaplo/lib/ui/filter/widgets/missed_exams.dart new file mode 100644 index 0000000..c2ebf59 --- /dev/null +++ b/filcnaplo/lib/ui/filter/widgets/missed_exams.dart @@ -0,0 +1,35 @@ +import 'package:filcnaplo/utils/format.dart'; +import 'package:filcnaplo/ui/date_widget.dart'; +import 'package:filcnaplo_kreta_api/models/lesson.dart'; +import 'package:filcnaplo_mobile_ui/common/widgets/missed_exam/missed_exam_viewable.dart'; + +List getWidgets(List providerLessons) { + List items = []; + List missedExams = []; + + for (var lesson in providerLessons) { + final desc = lesson.description.toLowerCase().specialChars(); + // Check if lesson description includes hints for an exam written during the lesson + if (!lesson.studentPresence && + (lesson.exam != "" || + desc.contains("dolgozat") || + desc.contains("feleles") || + desc.contains("temazaro") || + desc.contains("szamonkeres") || + desc == "tz") && + !(desc.contains("felkeszules") || desc.contains("gyakorlas"))) { + missedExams.add(lesson); + } + } + + if (missedExams.isNotEmpty) { + missedExams.sort((a, b) => -a.date.compareTo(b.date)); + + items.add(DateWidget( + date: missedExams.first.date, + widget: MissedExamViewable(missedExams), + )); + } + + return items; +} diff --git a/filcnaplo/lib/ui/filter/widgets/notes.dart b/filcnaplo/lib/ui/filter/widgets/notes.dart new file mode 100644 index 0000000..8521160 --- /dev/null +++ b/filcnaplo/lib/ui/filter/widgets/notes.dart @@ -0,0 +1,15 @@ +import 'package:filcnaplo/ui/date_widget.dart'; +import 'package:filcnaplo_kreta_api/models/note.dart'; +import 'package:filcnaplo_mobile_ui/common/widgets/note/note_viewable.dart' as mobile; + +List getWidgets(List providerNotes) { + List items = []; + for (var note in providerNotes) { + items.add(DateWidget( + key: note.id, + date: note.date, + widget: mobile.NoteViewable(note), + )); + } + return items; +} diff --git a/filcnaplo/lib/ui/filter/widgets/update.dart b/filcnaplo/lib/ui/filter/widgets/update.dart new file mode 100644 index 0000000..904c683 --- /dev/null +++ b/filcnaplo/lib/ui/filter/widgets/update.dart @@ -0,0 +1,10 @@ +import 'package:filcnaplo/models/release.dart'; +import 'package:filcnaplo/ui/date_widget.dart'; +import 'package:filcnaplo_mobile_ui/common/widgets/update/update_viewable.dart' as mobile; + +DateWidget getWidget(Release providerRelease) { + return DateWidget( + date: DateTime.now(), + widget: mobile.UpdateViewable(providerRelease), + ); +} diff --git a/filcnaplo/lib/ui/widgets/grade/grade_tile.dart b/filcnaplo/lib/ui/widgets/grade/grade_tile.dart new file mode 100644 index 0000000..0c3af26 --- /dev/null +++ b/filcnaplo/lib/ui/widgets/grade/grade_tile.dart @@ -0,0 +1,229 @@ +import 'package:filcnaplo/models/settings.dart'; +import 'package:filcnaplo/theme/colors/colors.dart'; +import 'package:filcnaplo_kreta_api/models/grade.dart'; +import 'package:filcnaplo/helpers/subject_icon.dart'; +import 'package:filcnaplo/utils/format.dart'; +import 'package:filcnaplo_mobile_ui/pages/grades/calculator/grade_calculator_provider.dart'; +import 'package:filcnaplo_mobile_ui/pages/grades/subject_grades_container.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:provider/provider.dart'; + +class GradeTile extends StatelessWidget { + const GradeTile(this.grade, {Key? key, this.onTap, this.padding}) : super(key: key); + + final Grade grade; + final void Function()? onTap; + final EdgeInsetsGeometry? padding; + + @override + Widget build(BuildContext context) { + String title; + String subtitle; + EdgeInsets leadingPadding = EdgeInsets.zero; + bool isSubjectView = SubjectGradesContainer.of(context) != null; + String subjectName = grade.subject.name.capital(); + String modeDescription = grade.mode.description.capital(); + String description = grade.description.capital(); + + GradeCalculatorProvider calculatorProvider = Provider.of(context, listen: false); + + // Test order: + // description + // mode + // value name + if (grade.type == GradeType.midYear || grade.type == GradeType.ghost) { + if (grade.description != "") { + title = description; + } else { + title = modeDescription != "" ? modeDescription : grade.value.valueName.split("(")[0]; + } + } else { + title = subjectName; + } + + // Test order: + // subject name + // mode + weight != 100 + if (grade.type == GradeType.midYear) { + subtitle = isSubjectView + ? description != "" + ? modeDescription + : "" + : subjectName; + } else { + subtitle = grade.value.valueName.split("(")[0]; + } + + if (subtitle != "") leadingPadding = const EdgeInsets.only(top: 2.0); + + return Material( + type: MaterialType.transparency, + child: Padding( + padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0), + child: ListTile( + visualDensity: VisualDensity.compact, + contentPadding: isSubjectView + ? grade.type != GradeType.ghost + ? const EdgeInsets.symmetric(horizontal: 12.0) + : const EdgeInsets.only(left: 12.0, right: 4.0) + : const EdgeInsets.only(left: 8.0, right: 12.0), + onTap: onTap, + // onLongPress: kDebugMode ? () => log(jsonEncode(grade.json)) : null, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)), + leading: isSubjectView + ? GradeValueWidget(grade.value) + : SizedBox( + width: 44, + height: 44, + child: Center( + child: Padding( + padding: leadingPadding, + child: Icon(SubjectIcon.lookup(subject: grade.subject), size: 28.0, color: AppColors.of(context).text.withOpacity(.75)), + ), + ), + ), + title: Text( + title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontWeight: FontWeight.w600), + ), + subtitle: subtitle != "" + ? Text( + subtitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontWeight: FontWeight.w500), + ) + : null, + trailing: isSubjectView + ? grade.type != GradeType.ghost + ? Text(grade.date.format(context), style: const TextStyle(fontWeight: FontWeight.w500)) + : IconButton( + splashRadius: 24.0, + icon: Icon(FeatherIcons.trash2, color: AppColors.of(context).red), + onPressed: () { + calculatorProvider.removeGrade(grade); + }, + ) + : GradeValueWidget(grade.value), + minLeadingWidth: isSubjectView ? 32.0 : 0, + ), + ), + ); + } +} + +class GradeValueWidget extends StatelessWidget { + const GradeValueWidget(this.value, {Key? key, this.size = 38.0, this.fill = false, this.complemented = false}) : super(key: key); + + final GradeValue value; + final double size; + final bool fill; + final bool complemented; + + @override + Widget build(BuildContext context) { + bool isSubjectView = SubjectGradesContainer.of(context) != null; + + Color color = gradeColor(context: context, value: value.value); + Widget valueText; + final percentage = value.percentage; + + if (percentage) { + double multiplier = 1.0; + + if (isSubjectView) multiplier = 0.75; + + valueText = Text.rich( + TextSpan( + text: value.value.toString(), + children: [ + TextSpan( + text: "\n%", + style: TextStyle(fontWeight: FontWeight.w700, fontSize: size / 2.5 * multiplier, height: 0.7), + ), + ], + style: TextStyle(fontWeight: FontWeight.w700, fontSize: size / 1 * multiplier, height: 1), + ), + textAlign: TextAlign.center, + ); + } else if (value.value != 0) { + valueText = Stack(alignment: Alignment.topRight, children: [ + Transform.translate( + offset: (value.weight >= 200) ? const Offset(2, 1.5) : Offset.zero, + child: Text( + value.value.toString(), + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: value.weight == 50 ? FontWeight.w600 : FontWeight.bold, + fontSize: size, + color: color, + shadows: [ + if (value.weight >= 200) + Shadow( + color: color.withOpacity(.4), + offset: const Offset(-4, -3), + ) + ], + ), + ), + ), + if (complemented) + Transform.translate( + offset: const Offset(9, 1), + child: Text( + "*", + style: TextStyle(fontSize: size / 1.6, fontWeight: FontWeight.bold), + ), + ), + ]); + } else if (value.valueName.toLowerCase().specialChars() == 'nem irt') { + valueText = const Icon(FeatherIcons.slash); + } else { + valueText = const Icon(FeatherIcons.type); + } + + return fill + ? Container( + width: size * 1.4, + height: size * 1.4, + decoration: BoxDecoration( + color: color.withOpacity(.25), + shape: BoxShape.circle, + ), + child: Center(child: valueText), + ) + : valueText; + } +} + +Color gradeColor({required BuildContext context, required num value}) { + int valueInt = 0; + + var settings = Provider.of(context, listen: false); + + try { + if (value > value.floor() + settings.rounding / 10) { + valueInt = value.ceil(); + } else { + valueInt = value.floor(); + } + } catch (_) {} + + switch (valueInt) { + case 5: + return settings.gradeColors[4]; + case 4: + return settings.gradeColors[3]; + case 3: + return settings.gradeColors[2]; + case 2: + return settings.gradeColors[1]; + case 1: + return settings.gradeColors[0]; + default: + return AppColors.of(context).text; + } +} diff --git a/filcnaplo/lib/ui/widgets/message/message_tile.dart b/filcnaplo/lib/ui/widgets/message/message_tile.dart new file mode 100644 index 0000000..4798344 --- /dev/null +++ b/filcnaplo/lib/ui/widgets/message/message_tile.dart @@ -0,0 +1,79 @@ +import 'package:animations/animations.dart'; +import 'package:filcnaplo/models/settings.dart'; +import 'package:filcnaplo/theme/colors/colors.dart'; +import 'package:filcnaplo/utils/color.dart'; +import 'package:filcnaplo/utils/format.dart'; +import 'package:filcnaplo_kreta_api/models/message.dart'; +import 'package:filcnaplo_mobile_ui/common/profile_image/profile_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:provider/provider.dart'; + +class MessageTile extends StatelessWidget { + const MessageTile( + this.message, { + Key? key, + this.messages, + this.padding, + this.onTap, + }) : super(key: key); + + final Message message; + final List? messages; + final EdgeInsetsGeometry? padding; + final Function()? onTap; + + @override + Widget build(BuildContext context) { + return Material( + type: MaterialType.transparency, + child: Padding( + padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0), + child: ListTile( + onTap: onTap, + visualDensity: VisualDensity.compact, + contentPadding: const EdgeInsets.only(left: 8.0, right: 4.0), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)), + leading: !Provider.of(context, listen: false).presentationMode + ? ProfileImage( + name: message.author, + radius: 22.0, + backgroundColor: ColorUtils.stringToColor(message.author), + ) + : ProfileImage( + name: "Béla", + radius: 22.0, + backgroundColor: Theme.of(context).colorScheme.secondary, + ), + title: Row( + children: [ + Expanded( + child: Text( + !Provider.of(context, listen: false).presentationMode ? message.author : "Béla", + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15.5), + ), + ), + if (message.attachments.isNotEmpty) const Icon(FeatherIcons.paperclip, size: 16.0) + ], + ), + subtitle: Text( + message.subject, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14.0), + ), + trailing: Text( + message.date.format(context), + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14.0, + color: AppColors.of(context).text.withOpacity(.75), + ), + ), + ), + ), + ); + } +} diff --git a/filcnaplo/lib/utils/platform.dart b/filcnaplo/lib/utils/platform.dart new file mode 100644 index 0000000..0594ad3 --- /dev/null +++ b/filcnaplo/lib/utils/platform.dart @@ -0,0 +1,6 @@ +import 'dart:io'; + +class PlatformUtils { + static bool get isDesktop => Platform.isWindows || Platform.isMacOS || Platform.isLinux; + static bool get isMobile => !isDesktop; +} diff --git a/filcnaplo/pubspec.yaml b/filcnaplo/pubspec.yaml index 94e811c..7b254ef 100644 --- a/filcnaplo/pubspec.yaml +++ b/filcnaplo/pubspec.yaml @@ -45,6 +45,7 @@ dependencies: material_color_utilities: ^0.1.3 crypto: ^3.0.2 elegant_notification: ^1.6.1 + flutter_feather_icons: ^2.0.0+1 dev_dependencies: flutter_test: diff --git a/filcnaplo_desktop_ui b/filcnaplo_desktop_ui index 351c241..98ae9b4 160000 --- a/filcnaplo_desktop_ui +++ b/filcnaplo_desktop_ui @@ -1 +1 @@ -Subproject commit 351c241efb3aad1ba4f7384ee63475ab74399f5d +Subproject commit 98ae9b4139ca795ac9b39bcbb1d9ef2713d98a63 diff --git a/filcnaplo_mobile_ui b/filcnaplo_mobile_ui index 2ed17a6..5600330 160000 --- a/filcnaplo_mobile_ui +++ b/filcnaplo_mobile_ui @@ -1 +1 @@ -Subproject commit 2ed17a62b3d463c95452a9fa5bd406d87856668f +Subproject commit 5600330a166e274c06cbbc5678ca2a40db919eb1