diff --git a/packages/uni_app/lib/view/academic_path/exam_page.dart b/packages/uni_app/lib/view/academic_path/exam_page.dart index 221ba299d..bf7539c79 100644 --- a/packages/uni_app/lib/view/academic_path/exam_page.dart +++ b/packages/uni_app/lib/view/academic_path/exam_page.dart @@ -11,6 +11,8 @@ import 'package:uni/view/lazy_consumer.dart'; import 'package:uni/view/locale_notifier.dart'; import 'package:uni_ui/cards/exam_card.dart'; import 'package:uni_ui/cards/timeline_card.dart'; +import 'package:uni_ui/theme.dart'; +import 'package:uni_ui/timeline/timeline.dart'; class ExamsPage extends StatefulWidget { const ExamsPage({super.key}); @@ -30,98 +32,119 @@ class _ExamsPageState extends State { If we want to filters exams again filteredExamTypes[Exam.getExamTypeLong(exam.examType)] ?? */ - - return LazyConsumer>( - builder: (context, exams) => ListView( - children: _examsByMonth(exams) - .entries - .map( - (entry) => Padding( - padding: const EdgeInsets.all(8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - DateTime( - int.parse(entry.key.split('-')[0]), - int.parse(entry.key.split('-')[1]), - ) + return MediaQuery.removePadding( + context: context, + removeBottom: true, + child: LazyConsumer>( + builder: (context, exams) { + final examsByMonth = _examsByMonth(exams); + final allMonths = List.generate(12, (index) => index + 1); + final tabs = allMonths.map((month) { + final date = DateTime(DateTime.now().year, month); + return Column( + children: [ + Text( + date.shortMonth( + Provider.of(context).getLocale(), + ), + ), + Text( + '${date.month}', + ), + ], + ); + }).toList(); + final content = allMonths.map((month) { + final monthKey = '${DateTime.now().year}-$month'; + final exams = examsByMonth[monthKey] ?? []; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (exams.isNotEmpty) + Padding( + padding: const EdgeInsets.all(16), + child: Text( + DateTime(DateTime.now().year, month) .fullMonth( Provider.of(context).getLocale(), ) .capitalize(), - style: Theme.of(context).textTheme.headlineMedium, + style: lightTheme.textTheme.headlineLarge, ), - const SizedBox(height: 8), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: entry.value.length, - prototypeItem: const TimelineItem( - title: '1', - subtitle: 'Jan', + ), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: exams.length, + itemBuilder: (context, index) { + final exam = exams[index]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: TimelineItem( + title: exam.start.day.toString(), + subtitle: exam.start + .shortMonth( + Provider.of(context).getLocale(), + ) + .capitalize(), + // isActive: _nextExam(exams) == exam, //TODO: Emphasize next exam together with the exam card. card: ExamCard( - name: 'Computer Laboratory', - acronym: 'LCOM', - rooms: ['B315', 'B224', 'B207'], - type: 'MT', - startTime: '12:00', + name: exam.subject, + acronym: exam.subjectAcronym, + rooms: exam.rooms, + type: exam.examType, + startTime: exam.formatTime(exam.start), + isInvisible: hiddenExams.contains(exam.id), + iconAction: () { + setState(() { + if (hiddenExams.contains(exam.id)) { + hiddenExams.remove(exam.id); + } else { + hiddenExams.add(exam.id); + } + PreferencesController.saveHiddenExams( + hiddenExams, + ); + }); + }, ), ), - itemBuilder: (context, index) { - final exam = entry.value[index]; - return TimelineItem( - title: exam.start.day.toString(), - subtitle: exam.start - .shortMonth( - Provider.of(context) - .getLocale(), - ) - .capitalize(), - isActive: _nextExam(exams) == exam, - card: ExamCard( - name: exam.subject, - acronym: exam.subjectAcronym, - rooms: exam.rooms, - type: exam.examType, - startTime: exam.formatTime(exam.start), - isInvisible: hiddenExams.contains(exam.id), - iconAction: () { - setState(() { - if (hiddenExams.contains(exam.id)) { - hiddenExams.remove(exam.id); - } else { - hiddenExams.add(exam.id); - } - - PreferencesController.saveHiddenExams( - hiddenExams, - ); - }); - }, - ), - ); - }, - ), - ], + ); + }, ), - ), - ) - .toList(), - ), - hasContent: (exams) => exams.isNotEmpty, - onNullContent: Center( - heightFactor: 1.2, - child: ImageLabel( - imagePath: 'assets/images/vacation.png', - label: S.of(context).no_exams_label, - labelTextStyle: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - color: Theme.of(context).colorScheme.primary, + ], + ); + }).toList(); + return Padding( + padding: const EdgeInsets.all(8), + child: Timeline( + tabs: tabs, + content: content, + initialTab: allMonths.indexWhere((month) { + final monthKey = '${DateTime.now().year}-$month'; + return examsByMonth.containsKey(monthKey); + }), + tabEnabled: allMonths.map((month) { + final monthKey = '${DateTime.now().year}-$month'; + return examsByMonth.containsKey(monthKey); + }).toList(), + ), + ); + }, + hasContent: (exams) => exams.isNotEmpty, + onNullContent: Center( + heightFactor: 1.2, + child: ImageLabel( + imagePath: 'assets/images/vacation.png', + label: S.of(context).no_exams_label, + labelTextStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: Theme.of(context).colorScheme.primary, + ), + sublabel: S.of(context).no_exams, + sublabelTextStyle: const TextStyle(fontSize: 15), ), - sublabel: S.of(context).no_exams, - sublabelTextStyle: const TextStyle(fontSize: 15), ), ), ); @@ -136,12 +159,12 @@ class _ExamsPageState extends State { return months; } - Exam? _nextExam(List exams) { + /*Exam? _nextExam(List exams) { final now = DateTime.now(); final nextExams = exams.where((exam) => exam.start.isAfter(now)).toList() ..sort((a, b) => a.start.compareTo(b.start)); return nextExams.isNotEmpty ? nextExams.first : null; - } + }*/ /* @override diff --git a/packages/uni_ui/lib/cards/timeline_card.dart b/packages/uni_ui/lib/cards/timeline_card.dart index b0da66059..ec392946b 100644 --- a/packages/uni_ui/lib/cards/timeline_card.dart +++ b/packages/uni_ui/lib/cards/timeline_card.dart @@ -17,15 +17,11 @@ class TimelineItem extends StatelessWidget { Widget build(BuildContext context) { return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - width: 60, + width: 50, child: Column( children: [ - Text(title, - style: Theme.of(context).textTheme.bodyLarge, - textAlign: TextAlign.center), - Text(subtitle, - style: Theme.of(context).textTheme.labelLarge, - textAlign: TextAlign.center), + Text(title, style: Theme.of(context).textTheme.bodyLarge), + Text(subtitle, style: Theme.of(context).textTheme.labelLarge) ], ), ), @@ -54,7 +50,7 @@ class TimelineItem extends StatelessWidget { : null), Container( margin: EdgeInsets.only(bottom: 5, left: 10, right: 10), - height: 55, + height: isActive ? 75 : 55, width: 3, decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(10)), @@ -72,6 +68,8 @@ class CardTimeline extends StatelessWidget { @override Widget build(BuildContext context) { return ListView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), itemCount: items.length, itemBuilder: (context, index) => items[index], ); diff --git a/packages/uni_ui/lib/timeline/timeline.dart b/packages/uni_ui/lib/timeline/timeline.dart index 8ac5b68ed..58c7adbbc 100644 --- a/packages/uni_ui/lib/timeline/timeline.dart +++ b/packages/uni_ui/lib/timeline/timeline.dart @@ -6,18 +6,22 @@ class Timeline extends StatefulWidget { const Timeline({ required this.tabs, required this.content, + required this.initialTab, + required this.tabEnabled, super.key, }); final List tabs; final List content; + final int initialTab; + final List tabEnabled; @override State createState() => _TimelineState(); } class _TimelineState extends State { - int _currentIndex = 0; + late int _currentIndex; final ItemScrollController _itemScrollController = ItemScrollController(); final ItemPositionsListener _itemPositionsListener = ItemPositionsListener.create(); @@ -27,6 +31,7 @@ class _TimelineState extends State { @override void initState() { super.initState(); + _currentIndex = widget.initialTab; _tabKeys.addAll(List.generate(widget.tabs.length, (index) => GlobalKey())); @@ -57,6 +62,7 @@ class _TimelineState extends State { } void _onTabTapped(int index) { + if (!widget.tabEnabled[index]) return; _itemScrollController.scrollTo( index: index, duration: const Duration(milliseconds: 300), @@ -98,10 +104,13 @@ class _TimelineState extends State { children: widget.tabs.asMap().entries.map((entry) { int index = entry.key; Widget tab = entry.value; + bool isSelected = _currentIndex == index; + TextStyle textStyle = Theme.of(context).textTheme.bodySmall!; return GestureDetector( onTap: () => _onTabTapped(index), child: Padding( - padding: const EdgeInsets.all(7.0), + padding: const EdgeInsets.symmetric( + vertical: 10.0, horizontal: 5.0), child: ClipSmoothRect( radius: SmoothBorderRadius( cornerRadius: 10, @@ -110,14 +119,23 @@ class _TimelineState extends State { child: Container( key: _tabKeys[index], padding: const EdgeInsets.symmetric( - vertical: 10.0, horizontal: 15.0), - color: _currentIndex == index + vertical: 9.0, horizontal: 8.0), + color: isSelected ? Theme.of(context) .colorScheme .tertiary .withOpacity(0.25) : Colors.transparent, - child: tab, + child: DefaultTextStyle( + style: textStyle.copyWith( + color: widget.tabEnabled[index] + ? (isSelected + ? Theme.of(context).colorScheme.primary + : Colors.black) + : Colors.grey, + ), + child: tab, + ), ), ), ), @@ -127,9 +145,11 @@ class _TimelineState extends State { ), Expanded( child: ScrollablePositionedList.builder( + padding: const EdgeInsets.only(bottom: 88), itemCount: widget.content.length, itemScrollController: _itemScrollController, itemPositionsListener: _itemPositionsListener, + initialScrollIndex: _currentIndex, itemBuilder: (context, index) { return widget.content[index]; },