diff --git a/packages/uni_app/lib/generated/intl/messages_all.dart b/packages/uni_app/lib/generated/intl/messages_all.dart index 6b3ebeae5..b77f94db2 100644 --- a/packages/uni_app/lib/generated/intl/messages_all.dart +++ b/packages/uni_app/lib/generated/intl/messages_all.dart @@ -11,6 +11,7 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; import 'package:intl/src/intl_helpers.dart'; @@ -20,8 +21,8 @@ import 'messages_pt_PT.dart' as messages_pt_pt; typedef Future LibraryLoader(); Map _deferredLibraries = { - 'en': () => new Future.value(null), - 'pt_PT': () => new Future.value(null), + 'en': () => new SynchronousFuture(null), + 'pt_PT': () => new SynchronousFuture(null), }; MessageLookupByLibrary? _findExact(String localeName) { @@ -36,18 +37,18 @@ MessageLookupByLibrary? _findExact(String localeName) { } /// User programs should call this before using [localeName] for messages. -Future initializeMessages(String localeName) async { +Future initializeMessages(String localeName) { var availableLocale = Intl.verifiedLocale( localeName, (locale) => _deferredLibraries[locale] != null, onFailure: (_) => null); if (availableLocale == null) { - return new Future.value(false); + return new SynchronousFuture(false); } var lib = _deferredLibraries[availableLocale]; - await (lib == null ? new Future.value(false) : lib()); + lib == null ? new SynchronousFuture(false) : lib(); initializeInternalMessageLookup(() => new CompositeMessageLookup()); messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); - return new Future.value(true); + return new SynchronousFuture(true); } bool _messagesExistFor(String locale) { diff --git a/packages/uni_app/lib/generated/intl/messages_en.dart b/packages/uni_app/lib/generated/intl/messages_en.dart index 6a741b4cb..9a6f4148e 100644 --- a/packages/uni_app/lib/generated/intl/messages_en.dart +++ b/packages/uni_app/lib/generated/intl/messages_en.dart @@ -7,7 +7,8 @@ // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases -// ignore_for_file:unused_import, file_names +// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes +// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; @@ -19,12 +20,12 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'en'; - static m0(time) => "last refresh at ${time}"; + static String m0(time) => "last refresh at ${time}"; - static m1(time) => + static String m1(time) => "${Intl.plural(time, zero: 'Refreshed ${time} minutes ago', one: 'Refreshed ${time} minute ago', other: 'Refreshed ${time} minutes ago')}"; - static m2(title) => "${Intl.select(title, { + static String m2(title) => "${Intl.select(title, { 'horario': 'Schedule', 'exames': 'Exams', 'area': 'Personal Area', @@ -41,7 +42,7 @@ class MessageLookup extends MessageLookupByLibrary { })}"; final messages = _notInlinedMessages(_notInlinedMessages); - static _notInlinedMessages(_) => { + static Map _notInlinedMessages(_) => { "about": MessageLookupByLibrary.simpleMessage("About us"), "academic_services": MessageLookupByLibrary.simpleMessage("Academic services"), @@ -161,7 +162,6 @@ class MessageLookup extends MessageLookupByLibrary { "language": MessageLookupByLibrary.simpleMessage("Language"), "last_refresh_time": m0, "last_timestamp": m1, - "lectures": MessageLookupByLibrary.simpleMessage("Lectures"), "library_occupation": MessageLookupByLibrary.simpleMessage("Library Occupation"), "load_error": MessageLookupByLibrary.simpleMessage( @@ -270,6 +270,7 @@ class MessageLookup extends MessageLookupByLibrary { "restaurant_main_page": MessageLookupByLibrary.simpleMessage( "Do you want to see your favorite restaurants in the main page?"), "room": MessageLookupByLibrary.simpleMessage("Room"), + "schedule": MessageLookupByLibrary.simpleMessage("Schedule"), "school_calendar": MessageLookupByLibrary.simpleMessage("School Calendar"), "search": MessageLookupByLibrary.simpleMessage("Search"), diff --git a/packages/uni_app/lib/generated/intl/messages_pt_PT.dart b/packages/uni_app/lib/generated/intl/messages_pt_PT.dart index c4ce2595f..aff6a0fc5 100644 --- a/packages/uni_app/lib/generated/intl/messages_pt_PT.dart +++ b/packages/uni_app/lib/generated/intl/messages_pt_PT.dart @@ -7,7 +7,8 @@ // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases -// ignore_for_file:unused_import, file_names +// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes +// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; @@ -19,12 +20,12 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'pt_PT'; - static m0(time) => "última atualização às ${time}"; + static String m0(time) => "última atualização às ${time}"; - static m1(time) => + static String m1(time) => "${Intl.plural(time, zero: 'Atualizado há ${time} minutos', one: 'Atualizado há ${time} minuto', other: 'Atualizado há ${time} minutos')}"; - static m2(title) => "${Intl.select(title, { + static String m2(title) => "${Intl.select(title, { 'horario': 'Horário', 'exames': 'Exames', 'area': 'Área Pessoal', @@ -41,7 +42,7 @@ class MessageLookup extends MessageLookupByLibrary { })}"; final messages = _notInlinedMessages(_notInlinedMessages); - static _notInlinedMessages(_) => { + static Map _notInlinedMessages(_) => { "about": MessageLookupByLibrary.simpleMessage("Sobre nós"), "academic_services": MessageLookupByLibrary.simpleMessage("Serviços académicos"), @@ -160,7 +161,6 @@ class MessageLookup extends MessageLookupByLibrary { "language": MessageLookupByLibrary.simpleMessage("Idioma"), "last_refresh_time": m0, "last_timestamp": m1, - "lectures": MessageLookupByLibrary.simpleMessage("Aulas"), "library_occupation": MessageLookupByLibrary.simpleMessage("Ocupação da Biblioteca"), "load_error": MessageLookupByLibrary.simpleMessage( @@ -271,6 +271,7 @@ class MessageLookup extends MessageLookupByLibrary { "restaurant_main_page": MessageLookupByLibrary.simpleMessage( "Queres ver os teus restaurantes favoritos na página principal?"), "room": MessageLookupByLibrary.simpleMessage("Sala"), + "schedule": MessageLookupByLibrary.simpleMessage("Aulas"), "school_calendar": MessageLookupByLibrary.simpleMessage("Calendário Escolar"), "search": MessageLookupByLibrary.simpleMessage("Pesquisar"), diff --git a/packages/uni_app/lib/generated/l10n.dart b/packages/uni_app/lib/generated/l10n.dart index 339abb28b..b2a759ba2 100644 --- a/packages/uni_app/lib/generated/l10n.dart +++ b/packages/uni_app/lib/generated/l10n.dart @@ -10,7 +10,7 @@ import 'intl/messages_all.dart'; // ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars // ignore_for_file: join_return_with_assignment, prefer_final_in_for_each -// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes class S { S(); @@ -1738,11 +1738,11 @@ class S { ); } - /// `Lectures` - String get lectures { + /// `Schedule` + String get schedule { return Intl.message( - 'Lectures', - name: 'lectures', + 'Schedule', + name: 'schedule', desc: '', args: [], ); diff --git a/packages/uni_app/lib/l10n/intl_en.arb b/packages/uni_app/lib/l10n/intl_en.arb index 941eae1ad..5c679f5ef 100644 --- a/packages/uni_app/lib/l10n/intl_en.arb +++ b/packages/uni_app/lib/l10n/intl_en.arb @@ -342,8 +342,8 @@ "@wrong_credentials_exception": {}, "internet_status_exception": "Check your internet connection", "@internet_status_exception": {}, - "lectures": "Lectures", - "@lectures": {}, + "schedule": "Schedule", + "@schedule": {}, "exams": "Exams", "@exams": {}, "courses": "Courses", diff --git a/packages/uni_app/lib/l10n/intl_pt_PT.arb b/packages/uni_app/lib/l10n/intl_pt_PT.arb index 6089670c7..0c4413155 100644 --- a/packages/uni_app/lib/l10n/intl_pt_PT.arb +++ b/packages/uni_app/lib/l10n/intl_pt_PT.arb @@ -342,8 +342,8 @@ "@wrong_credentials_exception": {}, "internet_status_exception": "Verifique sua conexão com a internet", "@internet_status_exception": {}, - "lectures": "Aulas", - "@lectures": {}, + "schedule": "Aulas", + "@schedule": {}, "exams": "Exames", "@exams": {}, "courses": "Cursos", diff --git a/packages/uni_app/lib/view/academic_path/academic_path.dart b/packages/uni_app/lib/view/academic_path/academic_path.dart index bfac776d1..dd27b8e5c 100644 --- a/packages/uni_app/lib/view/academic_path/academic_path.dart +++ b/packages/uni_app/lib/view/academic_path/academic_path.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:uni/generated/l10n.dart'; import 'package:uni/utils/navigation_items.dart'; import 'package:uni/view/academic_path/exam_page.dart'; +import 'package:uni/view/academic_path/schedule_page.dart'; import 'package:uni/view/common_widgets/pages_layouts/general/general.dart'; import 'package:uni_ui/icons.dart'; import 'package:uni_ui/tabs/tab_icon.dart'; @@ -41,7 +42,7 @@ class AcademicPathPageViewState extends GeneralPageViewState tabs: [ TabIcon( icon: UniIcons.lecture, - text: S.of(context).lectures, + text: S.of(context).schedule, ), TabIcon(icon: UniIcons.exam, text: S.of(context).exams), TabIcon(icon: UniIcons.course, text: S.of(context).courses), @@ -53,12 +54,10 @@ class AcademicPathPageViewState extends GeneralPageViewState Widget getBody(BuildContext context) { return TabBarView( controller: tabController, - children: const [ - Center( - child: Text('To be implemented'), - ), - ExamsPage(), - Center( + children: [ + SchedulePage(), + const ExamsPage(), + const Center( child: Text('To be implemented'), ), ], diff --git a/packages/uni_app/lib/view/academic_path/schedule_page.dart b/packages/uni_app/lib/view/academic_path/schedule_page.dart new file mode 100644 index 000000000..638d39652 --- /dev/null +++ b/packages/uni_app/lib/view/academic_path/schedule_page.dart @@ -0,0 +1,253 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:uni/model/entities/lecture.dart'; +import 'package:uni/model/providers/lazy/lecture_provider.dart'; +import 'package:uni/model/utils/time/week.dart'; +import 'package:uni/view/academic_path/widgets/academic_schedule_card.dart'; +import 'package:uni/view/common_widgets/expanded_image_label.dart'; +import 'package:uni/view/lazy_consumer.dart'; +import 'package:uni/view/locale_notifier.dart'; +import 'package:uni_ui/timeline/schedule_timeline.dart'; + +class SchedulePage extends StatefulWidget { + SchedulePage({super.key, DateTime? now}) : now = now ?? DateTime.now(); + + final DateTime now; + + @override + SchedulePageState createState() => SchedulePageState(); +} + +class SchedulePageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + body: RefreshIndicator( + onRefresh: () async { + await context.read().forceRefresh(context); + }, + child: LazyConsumer>( + builder: (context, lectures) { + /*final mockLectures = + getMockLectures(); */ // since there are no classes, we can use this to test the schedule page populated + return SchedulePageView( + lectures, + now: widget.now, + ); + }, + hasContent: (lectures) => lectures.isNotEmpty, + onNullContent: SchedulePageView(const [], now: widget.now), + ), + ), + ); + } +} + +class SchedulePageView extends StatefulWidget { + SchedulePageView(this.lectures, {required DateTime now, super.key}) + : currentWeek = Week(start: now); + + final List lectures; + final Week currentWeek; + + @override + SchedulePageViewState createState() => SchedulePageViewState(); +} + +class SchedulePageViewState extends State { + late List reorderedDates; + late int initialTabIndex; + + @override + void initState() { + super.initState(); + reorderedDates = _getReorderedWeekDates(widget.currentWeek.start); + final today = widget.currentWeek.start; + + initialTabIndex = reorderedDates.indexWhere( + (date) => + date.year == today.year && + date.month == today.month && + date.day == today.day, + ); + } + + @override + Widget build(BuildContext context) { + final noLectures = + lecturesOfWeek(widget.lectures, widget.currentWeek).isEmpty; + return ScheduleTimeline( + tabs: createTabs(context), + content: + noLectures ? [emptyWeek(context)] : createTabViewBuilder(context), + initialTabIndex: initialTabIndex, + ); + } + + List createTabs(BuildContext context) { + final daysOfTheWeek = + Provider.of(context).getWeekdaysWithLocale(); + + // Reorder the days of the week to start with Sunday + final reorderedDaysOfTheWeek = [ + daysOfTheWeek[6], // Sunday (index 6 in default order) + ...daysOfTheWeek.sublist(0, 6), // Monday to Saturday + ]; + + return List.generate(7, (index) { + return Tab( + key: Key('schedule-page-tab-$index'), + height: 35, + child: SizedBox( + width: 26, + height: 35, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + reorderedDaysOfTheWeek[index].substring(0, 3), + ), + Text( + '${reorderedDates[index].day}', + ), + ], + ), + ), + ); + }); + } + + List _getReorderedWeekDates(DateTime startOfWeek) { + final sunday = + startOfWeek.subtract(Duration(days: startOfWeek.weekday % 7)); + return List.generate(7, (index) => sunday.add(Duration(days: index))); + } + + List createTabViewBuilder(BuildContext context) { + return List.generate(7, (index) { + final day = reorderedDates[index]; + final lectures = lecturesOfDay(widget.lectures, day); + + return ScheduleDayTimeline( + key: Key('schedule-page-day-view-${day.weekday}'), + day: day, + lectures: lectures, + ); + }); + } + + List lecturesOfWeek(List lectures, Week currentWeek) { + final startOfWeek = currentWeek.start; + final endOfWeek = startOfWeek.add(const Duration(days: 7)); + return lectures.where((lecture) { + final startTime = lecture.startTime; + return startTime.isAfter(startOfWeek) && startTime.isBefore(endOfWeek); + }).toList(); + } + + List lecturesOfDay(List lectures, DateTime day) { + return lectures.where((lecture) { + final startTime = lecture.startTime; + return startTime.year == day.year && + startTime.month == day.month && + startTime.day == day.day; + }).toList(); + } + + Widget emptyWeek(BuildContext context) { + return const Center( + child: ImageLabel( + imagePath: 'assets/images/schedule.png', + label: 'You have no classes this week.', + labelTextStyle: TextStyle(fontSize: 15), + ), + ); + } +} + +// since there are no classes, we can use this to test the schedule page populated +List getMockLectures() { + return [ + Lecture( + 'Fundamentos de Segurança Informática', + 'T', + DateTime.now().subtract(const Duration(hours: 2)), + DateTime.now().subtract(const Duration(hours: 1)), + 'B101', + 'Dr. Smith', + 'Class 1', + 1, + ), + Lecture( + 'Fundamentos de Segurança Informática', + 'TP', + DateTime.now().add(Duration.zero), + DateTime.now().add(const Duration(hours: 1)), + 'B102', + 'Dr. Johnson', + 'Class 2', + 2, + ), + Lecture( + 'Interação Pessoa Computador', + 'T', + DateTime.now().add(const Duration(hours: 5)), + DateTime.now().add(const Duration(hours: 6)), + 'B201', + 'Dr. Brown', + 'Class 3', + 3, + ), + Lecture( + 'Interação Pessoa Computador', + 'TP', + DateTime.now().add(const Duration(days: 2, hours: 3)), + DateTime.now().add(const Duration(days: 2, hours: 4)), + '103', + 'Dr. Taylor', + 'Class 4', + 4, + ), + Lecture( + 'Laboratório de Bases de Dados e Aplicações Web', + 'T', + DateTime.now().add(const Duration(days: 3, hours: 4)), + DateTime.now().add(const Duration(days: 3, hours: 5)), + 'B104', + 'Dr. Martinez', + 'Class 5', + 5, + ), + Lecture( + 'Programação Funcional e em Lógica', + 'TP', + DateTime.now().add(const Duration(days: 4, hours: 5)), + DateTime.now().add(const Duration(days: 4, hours: 6)), + 'B105', + 'Dr. Lee', + 'Class 6', + 6, + ), + Lecture( + 'Redes de Computadores', + 'TP', + DateTime.now().subtract(const Duration(days: 1, hours: 1)), + DateTime.now().subtract(const Duration(days: 1)), + 'B106', + 'Dr. Williams', + 'Class 7', + 7, + ), + Lecture( + 'Redes de Computadores', + 'T', + DateTime.now().add(const Duration(days: 4, hours: 7)), + DateTime.now().add(const Duration(days: 4, hours: 8)), + 'B107', + 'Dr. Harris', + 'Class 8', + 8, + ), + ]; +} diff --git a/packages/uni_app/lib/view/academic_path/widgets/academic_schedule_card.dart b/packages/uni_app/lib/view/academic_path/widgets/academic_schedule_card.dart new file mode 100644 index 000000000..cd05a63f5 --- /dev/null +++ b/packages/uni_app/lib/view/academic_path/widgets/academic_schedule_card.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:uni/model/entities/lecture.dart'; +import 'package:uni_ui/cards/schedule_card.dart'; +import 'package:uni_ui/cards/timeline_card.dart'; + +class ScheduleDayTimeline extends StatelessWidget { + const ScheduleDayTimeline({ + super.key, + required this.day, + required this.lectures, + }); + + final List lectures; + final DateTime day; + + @override + Widget build(BuildContext context) { + if (lectures.isEmpty) { + return const SizedBox.shrink(); + } + + return Padding( + padding: const EdgeInsets.fromLTRB(24, 8, 24, 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + DateFormat('EEEE, d MMMM').format(day), + style: Theme.of(context).textTheme.headlineLarge, + ), + const SizedBox(height: 12), + ..._buildTimelineItems(lectures), + ], + ), + ); + } + + List _buildTimelineItems(List lectures) { + return lectures + .map( + (lecture) => Padding( + padding: EdgeInsets.zero, + child: TimelineItem( + isActive: _isLectureActive(lecture), + title: DateFormat('HH:mm').format(lecture.startTime), + subtitle: DateFormat('HH:mm').format(lecture.endTime), + card: ScheduleCard( + isActive: _isLectureActive(lecture), + name: lecture.subject, + acronym: _getAcronym(lecture.subject), + room: lecture.room, + type: lecture.typeClass, + teacherName: lecture.teacher, + ), + ), + ), + ) + .toList(); + } + + String _getAcronym(String subject) { + return subject + .split(' ') + .where((word) => word.length >= 3) + .map((word) => word[0]) + .join() + .toUpperCase(); + } + + bool _isLectureActive(Lecture lecture) { + final now = DateTime.now(); + return now.isAfter(lecture.startTime) && now.isBefore(lecture.endTime); + } +} diff --git a/packages/uni_ui/lib/timeline/schedule_timeline.dart b/packages/uni_ui/lib/timeline/schedule_timeline.dart new file mode 100644 index 000000000..9ea5dc290 --- /dev/null +++ b/packages/uni_ui/lib/timeline/schedule_timeline.dart @@ -0,0 +1,153 @@ +import 'package:figma_squircle/figma_squircle.dart'; +import 'package:flutter/material.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; + +class ScheduleTimeline extends StatefulWidget { + const ScheduleTimeline({ + required this.tabs, + required this.content, + required this.initialTabIndex, + super.key, + }); + + final List tabs; + final List content; + final int initialTabIndex; + + @override + State createState() => _ScheduleTimelineState(); +} + +class _ScheduleTimelineState extends State { + late int _currentIndex; + final ItemScrollController _itemScrollController = ItemScrollController(); + final ItemPositionsListener _itemPositionsListener = + ItemPositionsListener.create(); + final ScrollController _tabScrollController = ScrollController(); + final List _tabKeys = []; + + @override + void initState() { + super.initState(); + _currentIndex = widget.initialTabIndex; + + _tabKeys.addAll(List.generate(widget.tabs.length, (index) => GlobalKey())); + + _itemPositionsListener.itemPositions.addListener(() { + final positions = _itemPositionsListener.itemPositions.value; + if (positions.isNotEmpty) { + final firstVisibleIndex = positions + .where((ItemPosition position) => position.itemLeadingEdge >= 0) + .reduce((ItemPosition current, ItemPosition next) => + current.itemLeadingEdge < next.itemLeadingEdge ? current : next) + .index; + + if (_currentIndex != firstVisibleIndex && + firstVisibleIndex >= widget.initialTabIndex) { + setState(() { + _currentIndex = firstVisibleIndex; + }); + + _scrollToCenterTab(firstVisibleIndex); + } + } + }); + } + + @override + void dispose() { + _tabScrollController.dispose(); + super.dispose(); + } + + void _onTabTapped(int index) { + _itemScrollController.scrollTo( + index: index, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + _scrollToCenterTab(index); + } + + void _scrollToCenterTab(int index) { + final screenWidth = MediaQuery.of(context).size.width; + final RenderBox tabBox = + _tabKeys[index].currentContext!.findRenderObject() as RenderBox; + + final tabWidth = tabBox.size.width; + final offset = (_tabScrollController.offset + + tabBox.localToGlobal(Offset.zero).dx + + (tabWidth / 2) - + (screenWidth / 2)) + .clamp( + 0.0, + _tabScrollController.position.maxScrollExtent, + ); + + _tabScrollController.animateTo( + offset, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SingleChildScrollView( + scrollDirection: Axis.horizontal, + controller: _tabScrollController, + child: Row( + 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(5.0), + child: ClipSmoothRect( + radius: SmoothBorderRadius( + cornerRadius: 10, + cornerSmoothing: 1, + ), + child: Container( + key: _tabKeys[index], + padding: const EdgeInsets.all(10.0), + color: isSelected + ? Theme.of(context) + .colorScheme + .tertiary + .withOpacity(0.25) + : Colors.transparent, + child: DefaultTextStyle( + style: textStyle.copyWith( + color: isSelected + ? Theme.of(context).colorScheme.primary + : Colors.black, + ), + child: tab, + ), + ), + ), + ), + ); + }).toList(), + ), + ), + Expanded( + child: ScrollablePositionedList.builder( + itemCount: widget.content.length, + itemScrollController: _itemScrollController, + itemPositionsListener: _itemPositionsListener, + itemBuilder: (context, index) { + return widget.content[index]; + }, + ), + ), + ], + ); + } +}