From f6f27fd974710628ee438d3645695b19cc43a648 Mon Sep 17 00:00:00 2001 From: Saina Amiri Moghadam Date: Sun, 28 Jan 2024 16:33:14 +0100 Subject: [PATCH 1/8] use enum for section kind --- lib/utils/section_kind.dart | 1 + .../components/course_section.dart | 18 ++++++---- lib/views/course_view/courses_overview.dart | 14 ++++---- .../public_courses_view.dart | 1 + .../pinned_courses_view/pinned_card.dart | 36 ------------------- 5 files changed, 21 insertions(+), 49 deletions(-) create mode 100644 lib/utils/section_kind.dart delete mode 100644 lib/views/course_view/pinned_courses_view/pinned_card.dart diff --git a/lib/utils/section_kind.dart b/lib/utils/section_kind.dart new file mode 100644 index 00000000..61edbb44 --- /dev/null +++ b/lib/utils/section_kind.dart @@ -0,0 +1 @@ +enum SectionKind { livestreams, myCourses, publicCourses } diff --git a/lib/views/course_view/components/course_section.dart b/lib/views/course_view/components/course_section.dart index 66afb338..b0071a96 100644 --- a/lib/views/course_view/components/course_section.dart +++ b/lib/views/course_view/components/course_section.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gocast_mobile/base/networking/api/gocast/api_v2.pb.dart'; import 'package:gocast_mobile/providers.dart'; import 'package:gocast_mobile/utils/constants.dart'; +import 'package:gocast_mobile/utils/section_kind.dart'; import 'package:gocast_mobile/views/components/view_all_button.dart'; import 'package:gocast_mobile/views/course_view/components/course_card.dart'; import 'package:gocast_mobile/views/course_view/components/pulse_background.dart'; @@ -28,7 +29,7 @@ import 'package:gocast_mobile/views/video_view/video_player.dart'; /// different titles, courses and onViewAll actions. class CourseSection extends StatelessWidget { final String sectionTitle; - final int + final SectionKind sectionKind; //0 for livestreams, 1 cor mycourses, 2 for puliccourses final List courses; final List streams; @@ -69,7 +70,7 @@ class CourseSection extends StatelessWidget { required WidgetRef ref, required BuildContext context, required String title, - required int sectionKind, + required SectionKind sectionKind, VoidCallback? onViewAll, required List courses, required List streams, @@ -94,24 +95,27 @@ class CourseSection extends StatelessWidget { VoidCallback? onViewAll, required List courses, required List streams, - required int sectionKind, + required SectionKind sectionKind, }) { return Padding( padding: const EdgeInsets.all(10.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - sectionKind == 0 + sectionKind == SectionKind.livestreams ? const PulsingBackground() : _buildSectionTitle( context, title, - sectionKind == 1 ? Icons.school : Icons.public, + sectionKind == SectionKind.myCourses + ? Icons.school + : Icons.public, onViewAll, ), - if (sectionKind == 1 || sectionKind == 2) + if (sectionKind == SectionKind.myCourses || + sectionKind == SectionKind.publicCourses) _buildCourseList(context) - else if (sectionKind == 0) + else if (sectionKind == SectionKind.livestreams) _buildStreamList(context), ], ), diff --git a/lib/views/course_view/courses_overview.dart b/lib/views/course_view/courses_overview.dart index 629addff..cf3807e8 100644 --- a/lib/views/course_view/courses_overview.dart +++ b/lib/views/course_view/courses_overview.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gocast_mobile/providers.dart'; +import 'package:gocast_mobile/utils/section_kind.dart'; import 'package:gocast_mobile/views/components/base_view.dart'; import 'package:gocast_mobile/views/course_view/components/course_section.dart'; import 'package:gocast_mobile/views/course_view/list_courses_view/my_courses_view.dart'; @@ -65,7 +66,7 @@ class CourseOverviewState extends ConsumerState { Center( child: _buildSection( "Live Now", - 0, + SectionKind.livestreams, (userCourses ?? []) + (publicCourses ?? []), liveStreams, ), @@ -77,7 +78,7 @@ class CourseOverviewState extends ConsumerState { Expanded( child: _buildSection( "My Courses", - 1, + SectionKind.myCourses, userCourses, liveStreams, ), @@ -85,7 +86,7 @@ class CourseOverviewState extends ConsumerState { Expanded( child: _buildSection( "Public Courses", - 2, + SectionKind.publicCourses, publicCourses, liveStreams, ), @@ -95,13 +96,13 @@ class CourseOverviewState extends ConsumerState { else ...[ _buildSection( "My Courses", - 1, + SectionKind.myCourses, userCourses, liveStreams, ), _buildSection( "Public Courses", - 2, + SectionKind.publicCourses, publicCourses, liveStreams, ), @@ -113,7 +114,8 @@ class CourseOverviewState extends ConsumerState { ); } - Widget _buildSection(String title, int sectionKind, courses, streams) { + Widget _buildSection( + String title, SectionKind sectionKind, courses, streams) { return CourseSection( ref: ref, sectionTitle: title, diff --git a/lib/views/course_view/list_courses_view/public_courses_view.dart b/lib/views/course_view/list_courses_view/public_courses_view.dart index 5aa0c89c..c9893ade 100644 --- a/lib/views/course_view/list_courses_view/public_courses_view.dart +++ b/lib/views/course_view/list_courses_view/public_courses_view.dart @@ -30,6 +30,7 @@ class PublicCoursesState extends ConsumerState { final userViewModelNotifier = ref.read(userViewModelProvider.notifier); Future.microtask(() async { await userViewModelNotifier.fetchPublicCourses(); + await userViewModelNotifier.fetchSemesters(); }); } diff --git a/lib/views/course_view/pinned_courses_view/pinned_card.dart b/lib/views/course_view/pinned_courses_view/pinned_card.dart deleted file mode 100644 index 2985b10c..00000000 --- a/lib/views/course_view/pinned_courses_view/pinned_card.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:gocast_mobile/base/networking/api/gocast/api_v2.pb.dart'; -import 'package:gocast_mobile/views/course_view/components/base_card.dart'; -import 'package:gocast_mobile/views/course_view/components/pin_button.dart'; - -class PinnedCourseCard extends BaseCard { - final Course course; - final bool isPinned; - final VoidCallback onPinToggle; - - const PinnedCourseCard({ - super.key, - required super.imageName, - required super.onTap, - required this.course, - required this.isPinned, - required this.onPinToggle, - }); - - @override - List buildCardContent() { - return [ - buildHeader( - title: '${course.name} - ${course.tUMOnlineIdentifier}', - subtitle: "${course.semester.year} ${course.semester.teachingTerm}", - trailing: PinButton( - // Use PinButton - courseId: course.id, - isInitiallyPinned: isPinned, - onPinStatusChanged: onPinToggle, - ), - ), - buildImage(), - ]; - } -} From de38aca4648933df48865e656a1765de3b2bf868 Mon Sep 17 00:00:00 2001 From: Saina Amiri Moghadam Date: Sun, 28 Jan 2024 18:35:37 +0100 Subject: [PATCH 2/8] fix the no live stream bug --- .../course_view/components/course_card.dart | 6 +- .../components/course_section.dart | 166 ++++------------- .../components/live_stream_section.dart | 173 ++++++++++++++++++ lib/views/course_view/courses_overview.dart | 37 ++-- .../list_courses_view/courses_list_view.dart | 2 - 5 files changed, 230 insertions(+), 154 deletions(-) create mode 100644 lib/views/course_view/components/live_stream_section.dart diff --git a/lib/views/course_view/components/course_card.dart b/lib/views/course_view/components/course_card.dart index 062d0bde..d2efd5b7 100644 --- a/lib/views/course_view/components/course_card.dart +++ b/lib/views/course_view/components/course_card.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:gocast_mobile/base/networking/api/gocast/api_v2.pb.dart'; import 'package:gocast_mobile/views/components/view_all_button.dart'; @@ -12,11 +11,10 @@ class CourseCard extends StatelessWidget { final int courseId; final bool isCourse; //true: course, false: stream - final WidgetRef? ref; //for displaying courses final bool? live; - final String? semester; + final Course? course; final Function(Course)? onPinUnpin; final bool? isPinned; @@ -30,7 +28,6 @@ class CourseCard extends StatelessWidget { const CourseCard({ super.key, - this.ref, required this.isCourse, required this.title, this.subtitle, @@ -42,7 +39,6 @@ class CourseCard extends StatelessWidget { required this.courseId, required this.onTap, this.live, - this.semester, this.course, this.onPinUnpin, this.isPinned, diff --git a/lib/views/course_view/components/course_section.dart b/lib/views/course_view/components/course_section.dart index b0071a96..82af3961 100644 --- a/lib/views/course_view/components/course_section.dart +++ b/lib/views/course_view/components/course_section.dart @@ -35,6 +35,7 @@ class CourseSection extends StatelessWidget { final List streams; final VoidCallback? onViewAll; final WidgetRef ref; + final String baseUrl = 'https://live.rbg.tum.de'; const CourseSection({ super.key, @@ -52,51 +53,13 @@ class CourseSection extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildCourseSectionOrMessage( - ref: ref, - context: context, - title: sectionTitle, - sectionKind: sectionKind, - onViewAll: onViewAll, - courses: courses, - streams: streams, - ), + _buildCourseSection(context), ], ), ); } - Widget _buildCourseSectionOrMessage({ - required WidgetRef ref, - required BuildContext context, - required String title, - required SectionKind sectionKind, - VoidCallback? onViewAll, - required List courses, - required List streams, - }) { - return (streams.isNotEmpty - ? _buildCourseSection( - ref: ref, - context: context, - title: title, - onViewAll: onViewAll, - courses: courses, - streams: streams, - sectionKind: sectionKind, - ) - : _buildNoCoursesMessage(context, title)); - } - - Widget _buildCourseSection({ - required WidgetRef ref, - required BuildContext context, - required String title, - VoidCallback? onViewAll, - required List courses, - required List streams, - required SectionKind sectionKind, - }) { + Widget _buildCourseSection(BuildContext context) { return Padding( padding: const EdgeInsets.all(10.0), child: Column( @@ -104,14 +67,7 @@ class CourseSection extends StatelessWidget { children: [ sectionKind == SectionKind.livestreams ? const PulsingBackground() - : _buildSectionTitle( - context, - title, - sectionKind == SectionKind.myCourses - ? Icons.school - : Icons.public, - onViewAll, - ), + : _buildSectionTitle(context), if (sectionKind == SectionKind.myCourses || sectionKind == SectionKind.publicCourses) _buildCourseList(context) @@ -133,15 +89,7 @@ class CourseSection extends StatelessWidget { scrollDirection: Axis.vertical, itemCount: courses.length, itemBuilder: (BuildContext context, int index) { - /// Those are temporary values until we get the real data from the API - final Random random = Random(); final course = courses[index]; - String imagePath; - List imagePaths = [ - AppImages.course1, - AppImages.course2, - ]; - imagePath = imagePaths[random.nextInt(imagePaths.length)]; final userPinned = ref.watch(userViewModelProvider).userPinned ?? []; final isPinned = userPinned.contains(course); return CourseCard( @@ -149,13 +97,9 @@ class CourseSection extends StatelessWidget { isPinned: isPinned, onPinUnpin: (course) => _togglePin(course, isPinned), isCourse: true, - ref: ref, title: course.name, tumID: course.tUMOnlineIdentifier, - path: imagePath, live: streams.any((stream) => stream.courseID == course.id), - semester: - course.semester.teachingTerm + course.semester.year.toString(), courseId: course.id, onTap: () { Navigator.push( @@ -206,7 +150,6 @@ class CourseSection extends StatelessWidget { return CourseCard( isCourse: false, - ref: ref, title: stream.name, subtitle: course.name, tumID: course.tUMOnlineIdentifier, @@ -231,86 +174,49 @@ class CourseSection extends StatelessWidget { ); } - Row _buildSectionTitle( - BuildContext context, - String title, - IconData? icon, - VoidCallback? onViewAll, - ) { + Widget _buildLivestreamsTitle(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - onViewAll != null - ? Row( - children: [ - icon != null - ? Row( - children: [ - Icon(icon), - const SizedBox(width: 10), - ], - ) - : const SizedBox(), - Text( - title, - style: Theme.of(context).textTheme.titleMedium, - ), - ], - ) - : const PulsingBackground(), - onViewAll != null - ? ViewAllButton(onViewAll: onViewAll) - : const SizedBox(), + Text( + sectionTitle, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(), ], ); } - Widget _buildNoCoursesMessage(BuildContext context, String title) { - return Padding( - padding: const EdgeInsets.all(10.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: Theme.of(context).textTheme.titleMedium, - ), - const Spacer(), - ], - ), - const SizedBox(height: 20), - const Center( - child: Icon(Icons.folder_open, size: 50, color: Colors.grey), - ), - const SizedBox(height: 8), - Center( - child: Text( - 'No $title found', - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.grey, - ), - ), - ), - const SizedBox(height: 12), - Center( - child: ElevatedButton( - onPressed: () {}, - child: Text( - _buttonText(title), - ), + Widget _buildCoursesTitle(BuildContext context) { + IconData icon = + sectionKind == SectionKind.myCourses ? Icons.school : Icons.public; + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon(icon), + const SizedBox(width: 10), + Text( + sectionTitle, + style: Theme.of(context).textTheme.titleMedium, ), - ), - ], - ), + ], + ), + ViewAllButton(onViewAll: onViewAll), + ], ); } + Widget _buildSectionTitle(BuildContext context) { + if (sectionKind == SectionKind.livestreams) { + return _buildLivestreamsTitle(context); + } else { + return _buildCoursesTitle(context); + } + } + String _buttonText(String title) { switch (title) { case 'My courses': diff --git a/lib/views/course_view/components/live_stream_section.dart b/lib/views/course_view/components/live_stream_section.dart new file mode 100644 index 00000000..0b0158ab --- /dev/null +++ b/lib/views/course_view/components/live_stream_section.dart @@ -0,0 +1,173 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:gocast_mobile/base/networking/api/gocast/api_v2.pb.dart'; +import 'package:gocast_mobile/utils/constants.dart'; +import 'package:gocast_mobile/views/course_view/components/course_card.dart'; +import 'package:gocast_mobile/views/course_view/components/pulse_background.dart'; +import 'package:gocast_mobile/views/video_view/video_player.dart'; + +/// CourseSection +/// +/// A reusable stateless widget to display a specific course section. +/// +/// It takes a [sectionTitle] to display the title of the section and +/// dynamically generates a horizontal list of courses. This widget can be +/// reused for various course sections by providing different titles and +/// course lists. +/// +/// This widget also takes an [onViewAll] action to define the action to be +/// performed when the user taps on the View All button. +/// This widget also takes a [courses] list to display the list of courses. +/// If no courses are provided, it will display a default list of courses. +/// This widget can be reused for various course sections by providing +/// different titles, courses and onViewAll actions. +class LiveStreamSection extends StatelessWidget { + final String sectionTitle; + final List courses; + final List streams; + final VoidCallback? onViewAll; + final WidgetRef ref; + final String baseUrl = 'https://live.rbg.tum.de'; + + const LiveStreamSection({ + super.key, + required this.ref, + required this.sectionTitle, + required this.streams, + this.onViewAll, + required this.courses, + }); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildCourseSection(context), + ], + ), + ); + } + + Widget _buildCourseSection(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const PulsingBackground(), + // Use the ternary conditional operator for inline conditions + streams.isEmpty + ? _buildNoCoursesMessage( + context, + ) // If streams are empty, show no courses message + : _buildStreamList(context), // Otherwise, show the stream list + ], + ), + ); + } + + Widget _buildStreamList(BuildContext context) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: streams.map((stream) { + final Random random = Random(); + String imagePath; + List imagePaths = [ + AppImages.course1, + AppImages.course2, + ]; + imagePath = imagePaths[random.nextInt(imagePaths.length)]; + + final course = + courses.where((course) => course.id == stream.courseID).first; + + return CourseCard( + isCourse: false, + title: stream.name, + subtitle: course.name, + tumID: course.tUMOnlineIdentifier, + roomName: stream.roomName, + roomNumber: stream.roomCode, + viewerCount: stream.vodViews.toString(), + path: imagePath, + courseId: course.id, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => VideoPlayerPage( + stream: stream, + ), + ), + ); + }, + ); + }).toList(), + ), + ); + } + + Widget _buildNoCoursesMessage(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + sectionTitle, + style: Theme.of(context).textTheme.titleMedium, + ), + const Spacer(), + ], + ), + const SizedBox(height: 20), + const Center( + child: Icon(Icons.folder_open, size: 50, color: Colors.grey), + ), + const SizedBox(height: 8), + Center( + child: Text( + 'No $sectionTitle found', + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.grey, + ), + ), + ), + const SizedBox(height: 12), + Center( + child: ElevatedButton( + onPressed: () {}, + child: Text( + _buttonText(sectionTitle), + ), + ), + ), + ], + ), + ); + } + + String _buttonText(String title) { + switch (title) { + case 'My courses': + return 'Enroll in a Course'; + case 'Live Now': + return 'No courses currently live'; + case 'Public courses': + return 'No public courses found'; + default: + return 'Discover Courses'; + } + } +} diff --git a/lib/views/course_view/courses_overview.dart b/lib/views/course_view/courses_overview.dart index cf3807e8..276cc3b8 100644 --- a/lib/views/course_view/courses_overview.dart +++ b/lib/views/course_view/courses_overview.dart @@ -4,6 +4,7 @@ import 'package:gocast_mobile/providers.dart'; import 'package:gocast_mobile/utils/section_kind.dart'; import 'package:gocast_mobile/views/components/base_view.dart'; import 'package:gocast_mobile/views/course_view/components/course_section.dart'; +import 'package:gocast_mobile/views/course_view/components/live_stream_section.dart'; import 'package:gocast_mobile/views/course_view/list_courses_view/my_courses_view.dart'; import 'package:gocast_mobile/views/course_view/list_courses_view/public_courses_view.dart'; import 'package:gocast_mobile/views/settings_view/settings_screen_view.dart'; @@ -64,13 +65,12 @@ class CourseOverviewState extends ConsumerState { children: [ if (isLoggedIn) Center( - child: _buildSection( - "Live Now", - SectionKind.livestreams, - (userCourses ?? []) + (publicCourses ?? []), - liveStreams, - ), - ), + child: LiveStreamSection( + ref: ref, + sectionTitle: "Live Now", + courses: (userCourses ?? []) + (publicCourses ?? []), + streams: liveStreams ?? [], + )), if (isTablet) Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -120,21 +120,24 @@ class CourseOverviewState extends ConsumerState { ref: ref, sectionTitle: title, sectionKind: sectionKind, - onViewAll: () { - switch (title) { - case "My Courses": - _navigateTo(const MyCourses()); - break; - case "Public Courses": - _navigateTo(const PublicCourses()); - break; - } - }, + onViewAll: () => _onViewAllPressed(title), courses: courses ?? [], streams: streams ?? [], ); } + void _onViewAllPressed(String title) { + switch (title) { + case "My Courses": + _navigateTo(const MyCourses()); + break; + case "Public Courses": + _navigateTo(const PublicCourses()); + break; + // Add more cases as needed + } + } + void _navigateTo(Widget page) { Navigator.push( context, diff --git a/lib/views/course_view/list_courses_view/courses_list_view.dart b/lib/views/course_view/list_courses_view/courses_list_view.dart index 52a4e1f5..41f014e3 100644 --- a/lib/views/course_view/list_courses_view/courses_list_view.dart +++ b/lib/views/course_view/list_courses_view/courses_list_view.dart @@ -77,8 +77,6 @@ class CoursesList extends ConsumerWidget { path: 'assets/images/course2.png', live: liveCourses.contains(course), courseId: course.id, - semester: - course.semester.teachingTerm + course.semester.year.toString(), isCourse: true, onTap: () { Navigator.push( From 57cea83b0bb117d155f277d3c058a90c09ffab16 Mon Sep 17 00:00:00 2001 From: Saina Amiri Moghadam Date: Sun, 28 Jan 2024 19:11:15 +0100 Subject: [PATCH 3/8] separate course section and live stream section --- .../components/course_section.dart | 118 +++++------------- .../components/live_stream_section.dart | 33 +---- 2 files changed, 33 insertions(+), 118 deletions(-) diff --git a/lib/views/course_view/components/course_section.dart b/lib/views/course_view/components/course_section.dart index 82af3961..6ba466b0 100644 --- a/lib/views/course_view/components/course_section.dart +++ b/lib/views/course_view/components/course_section.dart @@ -1,16 +1,12 @@ -import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gocast_mobile/base/networking/api/gocast/api_v2.pb.dart'; import 'package:gocast_mobile/providers.dart'; -import 'package:gocast_mobile/utils/constants.dart'; import 'package:gocast_mobile/utils/section_kind.dart'; import 'package:gocast_mobile/views/components/view_all_button.dart'; import 'package:gocast_mobile/views/course_view/components/course_card.dart'; -import 'package:gocast_mobile/views/course_view/components/pulse_background.dart'; import 'package:gocast_mobile/views/course_view/course_detail_view/course_detail_view.dart'; -import 'package:gocast_mobile/views/video_view/video_player.dart'; /// CourseSection /// @@ -65,14 +61,12 @@ class CourseSection extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - sectionKind == SectionKind.livestreams - ? const PulsingBackground() - : _buildSectionTitle(context), - if (sectionKind == SectionKind.myCourses || - sectionKind == SectionKind.publicCourses) - _buildCourseList(context) - else if (sectionKind == SectionKind.livestreams) - _buildStreamList(context), + _buildCoursesTitle(context), + courses.isEmpty + ? _buildNoCoursesMessage( + context, + ) // If streams are empty, show no courses message + : _buildCourseList(context), ], ), ); @@ -132,61 +126,6 @@ class CourseSection extends StatelessWidget { await ref.read(userViewModelProvider.notifier).fetchUserPinned(); } - Widget _buildStreamList(BuildContext context) { - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: streams.map((stream) { - final Random random = Random(); - String imagePath; - List imagePaths = [ - AppImages.course1, - AppImages.course2, - ]; - imagePath = imagePaths[random.nextInt(imagePaths.length)]; - - final course = - courses.where((course) => course.id == stream.courseID).first; - - return CourseCard( - isCourse: false, - title: stream.name, - subtitle: course.name, - tumID: course.tUMOnlineIdentifier, - roomName: stream.roomName, - roomNumber: stream.roomCode, - viewerCount: stream.vodViews.toString(), - path: imagePath, - courseId: course.id, - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => VideoPlayerPage( - stream: stream, - ), - ), - ); - }, - ); - }).toList(), - ), - ); - } - - Widget _buildLivestreamsTitle(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - sectionTitle, - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(), - ], - ); - } - Widget _buildCoursesTitle(BuildContext context) { IconData icon = sectionKind == SectionKind.myCourses ? Icons.school : Icons.public; @@ -209,24 +148,31 @@ class CourseSection extends StatelessWidget { ); } - Widget _buildSectionTitle(BuildContext context) { - if (sectionKind == SectionKind.livestreams) { - return _buildLivestreamsTitle(context); - } else { - return _buildCoursesTitle(context); - } - } - - String _buttonText(String title) { - switch (title) { - case 'My courses': - return 'Enroll in a Course'; - case 'Live Now': - return 'No courses currently live'; - case 'Public courses': - return 'No public courses found'; - default: - return 'Discover Courses'; - } + Widget _buildNoCoursesMessage(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + const Center( + child: Icon(Icons.not_interested, size: 50, color: Colors.grey), + ), + const SizedBox(height: 8), + Center( + child: Text( + 'No $sectionTitle found', + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.grey, + ), + ), + ), + const SizedBox(height: 12), + ], + ), + ); } } diff --git a/lib/views/course_view/components/live_stream_section.dart b/lib/views/course_view/components/live_stream_section.dart index 0b0158ab..090c7261 100644 --- a/lib/views/course_view/components/live_stream_section.dart +++ b/lib/views/course_view/components/live_stream_section.dart @@ -118,19 +118,9 @@ class LiveStreamSection extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - sectionTitle, - style: Theme.of(context).textTheme.titleMedium, - ), - const Spacer(), - ], - ), const SizedBox(height: 20), const Center( - child: Icon(Icons.folder_open, size: 50, color: Colors.grey), + child: Icon(Icons.not_interested, size: 50, color: Colors.grey), ), const SizedBox(height: 8), Center( @@ -145,29 +135,8 @@ class LiveStreamSection extends StatelessWidget { ), ), const SizedBox(height: 12), - Center( - child: ElevatedButton( - onPressed: () {}, - child: Text( - _buttonText(sectionTitle), - ), - ), - ), ], ), ); } - - String _buttonText(String title) { - switch (title) { - case 'My courses': - return 'Enroll in a Course'; - case 'Live Now': - return 'No courses currently live'; - case 'Public courses': - return 'No public courses found'; - default: - return 'Discover Courses'; - } - } } From 6e5ab6fbf989c9587e1d1824b4bd846c2c023593 Mon Sep 17 00:00:00 2001 From: Saina Amiri Moghadam Date: Sun, 28 Jan 2024 20:27:03 +0100 Subject: [PATCH 4/8] separate course card and small stream card --- .../course_view/components/course_card.dart | 201 +++------------ .../components/course_section.dart | 1 - .../components/live_stream_section.dart | 6 +- .../components/small_stream_card.dart | 232 ++++++++++++++++++ .../list_courses_view/courses_list_view.dart | 2 - .../pinned_courses_view.dart | 1 - 6 files changed, 267 insertions(+), 176 deletions(-) create mode 100644 lib/views/course_view/components/small_stream_card.dart diff --git a/lib/views/course_view/components/course_card.dart b/lib/views/course_view/components/course_card.dart index d2efd5b7..1239abef 100644 --- a/lib/views/course_view/components/course_card.dart +++ b/lib/views/course_view/components/course_card.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:gocast_mobile/base/networking/api/gocast/api_v2.pb.dart'; -import 'package:gocast_mobile/views/components/view_all_button.dart'; -import 'package:url_launcher/url_launcher.dart'; class CourseCard extends StatelessWidget { final String title; @@ -10,7 +8,6 @@ class CourseCard extends StatelessWidget { final VoidCallback onTap; final int courseId; - final bool isCourse; //true: course, false: stream //for displaying courses final bool? live; @@ -21,21 +18,12 @@ class CourseCard extends StatelessWidget { //for displaying livestreams final String? subtitle; - final String? roomName; - final String? roomNumber; - final String? viewerCount; - final String? path; const CourseCard({ super.key, - required this.isCourse, required this.title, this.subtitle, required this.tumID, - this.roomName, - this.roomNumber, - this.viewerCount, - this.path, required this.courseId, required this.onTap, this.live, @@ -70,66 +58,19 @@ class CourseCard extends StatelessWidget { ), child: ClipRRect( borderRadius: BorderRadius.circular(8.0), - child: isCourse - ? _buildCourseCard( - themeData, - cardWidth, - context, - course!, + child: _buildCourseCard( + themeData, + cardWidth, + context, + course!, onPinUnpin!, isPinned!, ) - : _buildStreamCard( - themeData, - cardWidth, - ), ), ), ); } - Widget _buildStreamCard(ThemeData themeData, double cardWidth) { - return Container( - width: cardWidth, - padding: const EdgeInsets.all(8.0), - color: themeData.cardColor, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildCourseTumID(), - _buildCourseViewerCount(themeData), - ], - ), - const SizedBox(height: 2), - Row( - children: [ - SizedBox( - width: 100, - child: _buildCourseImage(), - ), - const SizedBox(width: 15), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 10), - _buildCourseTitle(themeData.textTheme), - _buildCourseSubtitle(themeData.textTheme), - const SizedBox(height: 15), - _buildLocation(), - ], - ), - ), - ], - ), - ], - ), - ); - } - Widget _buildCourseCard( ThemeData themeData, double cardWidth, @@ -224,114 +165,9 @@ class CourseCard extends StatelessWidget { false; } - Widget _buildCourseImage() { - if (path == null) return const SizedBox(); - return Stack( - children: [ - AspectRatio( - aspectRatio: 16 / 12, - child: ClipRRect( - borderRadius: BorderRadius.circular(8.0), - child: Image.asset( - path!, - fit: BoxFit.cover, - ), - ), - ), - ], - ); - } - - Widget _buildLocation() { - if (roomNumber == null) return const SizedBox(); - - final Uri url = Uri.parse('https://nav.tum.de/room/$roomNumber'); - - return InkWell( - onTap: () async { - if (await canLaunchUrl(url)) { - await launchUrl(url); - } else { - throw 'Could not launch $url'; - } - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const Icon( - Icons.room, - size: 20, - ), - Text(roomName ?? "Location"), - Transform.scale( - scale: 0.6, // Adjust the scale factor as needed - child: ViewAllButton(onViewAll: () {}), - ), - ], - ), - ); - } - - Widget _buildCourseTumID() { - return Text( - tumID, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 14, - color: Colors.grey, - height: 0.9, - ), - ); - } - - Widget _buildCourseTitle(TextTheme textTheme) { - return Text( - title, - overflow: TextOverflow.ellipsis, - maxLines: 2, - softWrap: true, - style: textTheme.titleMedium?.copyWith( - fontSize: 16, - fontWeight: FontWeight.w700, - height: 1.2, - ) ?? - const TextStyle(), - ); - } - Widget _buildCourseSubtitle(TextTheme textTheme) { - if (subtitle == null) return const SizedBox(); - return Text( - subtitle!, //nullcheck already passed - overflow: TextOverflow.ellipsis, - style: textTheme.labelSmall?.copyWith( - fontSize: 14, - fontWeight: FontWeight.w400, - height: 1, - ) ?? - const TextStyle(), - ); - } - Widget _buildCourseViewerCount(ThemeData themeData) { - if (viewerCount == null) return const SizedBox(); - return Container( - decoration: BoxDecoration( - color: themeData.shadowColor.withOpacity(0.15), - borderRadius: BorderRadius.circular(4), - ), - padding: const EdgeInsets.all(3), - child: Text( - "${viewerCount!} viewers", - style: themeData.textTheme.labelSmall?.copyWith( - fontSize: 12, - height: 1, - ) ?? - const TextStyle(), - ), - ); - } Widget _buildCourseIsLive() { if (live == null) return const SizedBox(); @@ -382,4 +218,31 @@ class CourseCard extends StatelessWidget { return Colors.grey; } } + + Widget _buildCourseTitle(TextTheme textTheme) { + return Text( + title, + overflow: TextOverflow.ellipsis, + maxLines: 2, + softWrap: true, + style: textTheme.titleMedium?.copyWith( + fontSize: 16, + fontWeight: FontWeight.w700, + height: 1.2, + ) ?? + const TextStyle(), + ); + } + + Widget _buildCourseTumID() { + return Text( + tumID, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + height: 0.9, + ), + ); + } } diff --git a/lib/views/course_view/components/course_section.dart b/lib/views/course_view/components/course_section.dart index 6ba466b0..1ae8bdff 100644 --- a/lib/views/course_view/components/course_section.dart +++ b/lib/views/course_view/components/course_section.dart @@ -90,7 +90,6 @@ class CourseSection extends StatelessWidget { course: course, isPinned: isPinned, onPinUnpin: (course) => _togglePin(course, isPinned), - isCourse: true, title: course.name, tumID: course.tUMOnlineIdentifier, live: streams.any((stream) => stream.courseID == course.id), diff --git a/lib/views/course_view/components/live_stream_section.dart b/lib/views/course_view/components/live_stream_section.dart index 090c7261..0921d306 100644 --- a/lib/views/course_view/components/live_stream_section.dart +++ b/lib/views/course_view/components/live_stream_section.dart @@ -4,8 +4,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gocast_mobile/base/networking/api/gocast/api_v2.pb.dart'; import 'package:gocast_mobile/utils/constants.dart'; -import 'package:gocast_mobile/views/course_view/components/course_card.dart'; + import 'package:gocast_mobile/views/course_view/components/pulse_background.dart'; +import 'package:gocast_mobile/views/course_view/components/small_stream_card.dart'; import 'package:gocast_mobile/views/video_view/video_player.dart'; /// CourseSection @@ -86,8 +87,7 @@ class LiveStreamSection extends StatelessWidget { final course = courses.where((course) => course.id == stream.courseID).first; - return CourseCard( - isCourse: false, + return SmallStreamCard( title: stream.name, subtitle: course.name, tumID: course.tUMOnlineIdentifier, diff --git a/lib/views/course_view/components/small_stream_card.dart b/lib/views/course_view/components/small_stream_card.dart new file mode 100644 index 00000000..7110947f --- /dev/null +++ b/lib/views/course_view/components/small_stream_card.dart @@ -0,0 +1,232 @@ +import 'package:flutter/material.dart'; +import 'package:gocast_mobile/base/networking/api/gocast/api_v2.pb.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class SmallStreamCard extends StatelessWidget { + final String title; + final String tumID; + final VoidCallback onTap; + final int courseId; + + //for displaying courses + final bool? live; + + final Course? course; + + //for displaying livestreams + final String? subtitle; + final String? roomName; + final String? roomNumber; + final String? viewerCount; + final String? path; + + const SmallStreamCard({ + super.key, + required this.title, + this.subtitle, + required this.tumID, + this.roomName, + this.roomNumber, + this.viewerCount, + this.path, + required this.courseId, + required this.onTap, + this.live, + this.course, + }); + + @override + Widget build(BuildContext context) { + ThemeData themeData = Theme.of(context); + + double cardWidth = MediaQuery.of(context).size.width >= 600 + ? MediaQuery.of(context).size.width * 0.4 + : MediaQuery.of(context).size.width * 0.9; + + return InkWell( + onTap: onTap, + child: Card( + elevation: 1, + shadowColor: themeData.shadowColor, + color: themeData.cardTheme.color, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + side: BorderSide( + color: themeData + .inputDecorationTheme.enabledBorder?.borderSide.color + .withOpacity(0.2) ?? + Colors.grey.withOpacity(0.2), + width: 1.0, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: _buildStreamCard( + themeData, + cardWidth, + ), + ), + ), + ); + } + + Widget _buildStreamCard(ThemeData themeData, double cardWidth) { + return Container( + width: cardWidth, + padding: const EdgeInsets.all(8.0), + color: themeData.cardColor, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildCourseTumID(), + _buildCourseViewerCount(themeData), + ], + ), + const SizedBox(height: 2), + Row( + children: [ + SizedBox( + width: 100, + child: _buildCourseImage(), + ), + const SizedBox(width: 15), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + _buildCourseTitle(themeData.textTheme), + _buildCourseSubtitle(themeData.textTheme), + const SizedBox(height: 15), + _buildLocation(themeData), + ], + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildCourseImage() { + return Stack( + children: [ + AspectRatio( + aspectRatio: 16 / 12, + child: ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: Image.asset( + path!, + fit: BoxFit.cover, + ), + ), + ), + ], + ); + } + + Widget _buildLocation(ThemeData themeData) { + if (roomNumber == null) return const SizedBox(); + + final Uri url = Uri.parse('https://nav.tum.de/room/$roomNumber'); + + return Align( + alignment: Alignment.centerRight, // Align to the right + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + border: Border.all( + color: themeData + .inputDecorationTheme.enabledBorder?.borderSide.color + .withOpacity(0.4) ?? + Colors.grey.withOpacity(0.4), + ), + borderRadius: BorderRadius.circular(5), + ), + child: InkWell( + onTap: () async { + if (await canLaunchUrl(url)) { + await launchUrl(url); + } else { + throw 'Could not launch $url'; + } + }, + child: const Row( + mainAxisSize: MainAxisSize.min, + // Constrain row size to its children + children: [ + Icon(Icons.room, size: 24), + SizedBox(width: 8), // Spacing before the arrow icon + Icon(Icons.arrow_forward_ios, size: 16), + ], + ), + ), + ), + ); + } + + Widget _buildCourseTumID() { + return Text( + tumID, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + height: 0.9, + ), + ); + } + + Widget _buildCourseTitle(TextTheme textTheme) { + return Text( + title, + overflow: TextOverflow.ellipsis, + maxLines: 2, + softWrap: true, + style: textTheme.titleMedium?.copyWith( + fontSize: 16, + fontWeight: FontWeight.w700, + height: 1.2, + ) ?? + const TextStyle(), + ); + } + + Widget _buildCourseSubtitle(TextTheme textTheme) { + if (subtitle == null) return const SizedBox(); + + return Text( + subtitle!, //nullcheck already passed + overflow: TextOverflow.ellipsis, + style: textTheme.labelSmall?.copyWith( + fontSize: 14, + fontWeight: FontWeight.w400, + height: 1, + ) ?? + const TextStyle(), + ); + } + + Widget _buildCourseViewerCount(ThemeData themeData) { + if (viewerCount == null) return const SizedBox(); + return Container( + decoration: BoxDecoration( + color: themeData.shadowColor.withOpacity(0.15), + borderRadius: BorderRadius.circular(4), + ), + padding: const EdgeInsets.all(3), + child: Text( + "${viewerCount!} viewers", + style: themeData.textTheme.labelSmall?.copyWith( + fontSize: 12, + height: 1, + ) ?? + const TextStyle(), + ), + ); + } +} diff --git a/lib/views/course_view/list_courses_view/courses_list_view.dart b/lib/views/course_view/list_courses_view/courses_list_view.dart index 41f014e3..f6a706fe 100644 --- a/lib/views/course_view/list_courses_view/courses_list_view.dart +++ b/lib/views/course_view/list_courses_view/courses_list_view.dart @@ -74,10 +74,8 @@ class CoursesList extends ConsumerWidget { }, title: course.name, tumID: course.tUMOnlineIdentifier, - path: 'assets/images/course2.png', live: liveCourses.contains(course), courseId: course.id, - isCourse: true, onTap: () { Navigator.push( context, diff --git a/lib/views/course_view/pinned_courses_view/pinned_courses_view.dart b/lib/views/course_view/pinned_courses_view/pinned_courses_view.dart index a016c2d2..4b07d883 100644 --- a/lib/views/course_view/pinned_courses_view/pinned_courses_view.dart +++ b/lib/views/course_view/pinned_courses_view/pinned_courses_view.dart @@ -95,7 +95,6 @@ class PinnedCoursesState extends ConsumerState { isPinned: isPinned, onPinUnpin: (course) => _togglePin(course, isPinned), live: liveCourses.contains(course), - isCourse: true, title: course.name, courseId: course.id, subtitle: course.tUMOnlineIdentifier, From 696235b09687223392e8ee5586f2fe2273ef883b Mon Sep 17 00:00:00 2001 From: Saina Amiri Moghadam Date: Sun, 28 Jan 2024 20:52:52 +0100 Subject: [PATCH 5/8] reduce logged in --- .../course_view/components/live_stream_section.dart | 2 +- .../course_view/components/small_stream_card.dart | 7 +++---- lib/views/course_view/courses_overview.dart | 12 +++++++----- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/views/course_view/components/live_stream_section.dart b/lib/views/course_view/components/live_stream_section.dart index 0921d306..a7b5a00a 100644 --- a/lib/views/course_view/components/live_stream_section.dart +++ b/lib/views/course_view/components/live_stream_section.dart @@ -93,7 +93,7 @@ class LiveStreamSection extends StatelessWidget { tumID: course.tUMOnlineIdentifier, roomName: stream.roomName, roomNumber: stream.roomCode, - viewerCount: stream.vodViews.toString(), + viewerCount: stream.vodViews, path: imagePath, courseId: course.id, onTap: () { diff --git a/lib/views/course_view/components/small_stream_card.dart b/lib/views/course_view/components/small_stream_card.dart index 7110947f..1f945255 100644 --- a/lib/views/course_view/components/small_stream_card.dart +++ b/lib/views/course_view/components/small_stream_card.dart @@ -17,7 +17,7 @@ class SmallStreamCard extends StatelessWidget { final String? subtitle; final String? roomName; final String? roomNumber; - final String? viewerCount; + final int viewerCount; final String? path; const SmallStreamCard({ @@ -27,7 +27,7 @@ class SmallStreamCard extends StatelessWidget { required this.tumID, this.roomName, this.roomNumber, - this.viewerCount, + required this.viewerCount, this.path, required this.courseId, required this.onTap, @@ -212,7 +212,6 @@ class SmallStreamCard extends StatelessWidget { } Widget _buildCourseViewerCount(ThemeData themeData) { - if (viewerCount == null) return const SizedBox(); return Container( decoration: BoxDecoration( color: themeData.shadowColor.withOpacity(0.15), @@ -220,7 +219,7 @@ class SmallStreamCard extends StatelessWidget { ), padding: const EdgeInsets.all(3), child: Text( - "${viewerCount!} viewers", + "$viewerCount viewers", style: themeData.textTheme.labelSmall?.copyWith( fontSize: 12, height: 1, diff --git a/lib/views/course_view/courses_overview.dart b/lib/views/course_view/courses_overview.dart index 276cc3b8..4ec73df7 100644 --- a/lib/views/course_view/courses_overview.dart +++ b/lib/views/course_view/courses_overview.dart @@ -39,7 +39,6 @@ class CourseOverviewState extends ConsumerState { @override Widget build(BuildContext context) { - final isLoggedIn = ref.watch(userViewModelProvider).user != null; final userCourses = ref.watch(userViewModelProvider).userCourses; final publicCourses = ref.watch(userViewModelProvider).publicCourses; final liveStreams = ref.watch(videoViewModelProvider).liveStreams; @@ -63,14 +62,14 @@ class CourseOverviewState extends ConsumerState { onRefresh: _refreshData, child: ListView( children: [ - if (isLoggedIn) Center( child: LiveStreamSection( ref: ref, sectionTitle: "Live Now", courses: (userCourses ?? []) + (publicCourses ?? []), streams: liveStreams ?? [], - )), + ), + ), if (isTablet) Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -114,8 +113,10 @@ class CourseOverviewState extends ConsumerState { ); } - Widget _buildSection( - String title, SectionKind sectionKind, courses, streams) { + Widget _buildSection(String title, + SectionKind sectionKind, + courses, + streams,) { return CourseSection( ref: ref, sectionTitle: title, @@ -156,5 +157,6 @@ class CourseOverviewState extends ConsumerState { final userViewModelNotifier = ref.read(userViewModelProvider.notifier); await userViewModelNotifier.fetchUserCourses(); await userViewModelNotifier.fetchPublicCourses(); + await ref.read(videoViewModelProvider.notifier).fetchLiveNowStreams(); } } From 7eb8076fda1b05c12afab6d8b19802ff4f5a026d Mon Sep 17 00:00:00 2001 From: Saina Amiri Moghadam Date: Sun, 28 Jan 2024 22:15:36 +0100 Subject: [PATCH 6/8] fetch live now thumbnails --- lib/models/video/stream_state_model.dart | 11 ++-- lib/view_models/stream_view_model.dart | 58 +++++++++++++------ .../components/live_stream_section.dart | 35 +++++------ .../components/small_stream_card.dart | 54 +++++++++-------- .../course_view/components/stream_card.dart | 3 +- lib/views/course_view/courses_overview.dart | 47 ++++++++++----- 6 files changed, 127 insertions(+), 81 deletions(-) diff --git a/lib/models/video/stream_state_model.dart b/lib/models/video/stream_state_model.dart index 61195212..b4626af0 100644 --- a/lib/models/video/stream_state_model.dart +++ b/lib/models/video/stream_state_model.dart @@ -8,12 +8,12 @@ class StreamState { final bool isLoading; final List? streams; final List? liveStreams; - final List? thumbnails; final AppError? error; final Progress? progress; final bool isWatched; final String? videoSource; final List>? streamsWithThumb; + final List>? liveStreamsWithThumb; final List>? displayedStreams; final String selectedFilterOption; @@ -21,12 +21,12 @@ class StreamState { this.isLoading = false, this.streams, this.liveStreams, - this.thumbnails, this.error, this.progress, this.isWatched = false, this.videoSource, this.streamsWithThumb, + this.liveStreamsWithThumb, this.displayedStreams, this.selectedFilterOption = 'Oldest First', }); @@ -42,6 +42,7 @@ class StreamState { String? videoSource, Map? downloadedVideos, List>? streamsWithThumb, + List>? liveStreamsWithThumb, List>? displayedStreams, String? selectedFilterOption, }) { @@ -49,12 +50,12 @@ class StreamState { isLoading: isLoading ?? this.isLoading, streams: streams ?? this.streams, liveStreams: liveStreams ?? this.liveStreams, - thumbnails: thumbnails ?? this.thumbnails, error: error ?? this.error, progress: progress ?? this.progress, isWatched: isWatched ?? this.isWatched, videoSource: videoSource ?? this.videoSource, streamsWithThumb: streamsWithThumb ?? this.streamsWithThumb, + liveStreamsWithThumb: liveStreamsWithThumb ?? this.liveStreamsWithThumb, displayedStreams: displayedStreams ?? this.displayedStreams, selectedFilterOption: selectedFilterOption ?? this.selectedFilterOption, ); @@ -65,11 +66,11 @@ class StreamState { isLoading: isLoading, streams: streams, liveStreams: liveStreams, - thumbnails: thumbnails, progress: progress, isWatched: isWatched, videoSource: videoSource, streamsWithThumb: streamsWithThumb, + liveStreamsWithThumb: liveStreamsWithThumb, displayedStreams: displayedStreams, error: null, ); @@ -80,11 +81,11 @@ class StreamState { isLoading: isLoading, streams: streams, liveStreams: liveStreams, - thumbnails: thumbnails, progress: progress, isWatched: isWatched, videoSource: videoSource, streamsWithThumb: streamsWithThumb, + liveStreamsWithThumb: liveStreamsWithThumb, displayedStreams: displayedStreams, error: null, ); diff --git a/lib/view_models/stream_view_model.dart b/lib/view_models/stream_view_model.dart index 820ad2ae..e09f507b 100644 --- a/lib/view_models/stream_view_model.dart +++ b/lib/view_models/stream_view_model.dart @@ -28,6 +28,28 @@ class StreamViewModel extends StateNotifier { } } + + + void updatedDisplayedStreams(List> allStreams) { + state = state.copyWith(displayedStreams: allStreams); + } + + void setUpDisplayedCourses(List> allStreams) { + updatedDisplayedStreams( + CourseUtils.sortStreams(allStreams, state.selectedFilterOption), + ); + } + + void updateSelectedFilterOption( + String option, + List> allStreams, + ) { + state = state.copyWith(selectedFilterOption: option); + updatedDisplayedStreams( + CourseUtils.sortStreams(allStreams, state.selectedFilterOption), + ); + } + /// This asynchronous function fetches thumbnails for all available streams in the current state. /// only if there are streams in the current state. /// It initiates fetching of thumbnails for each stream by invoking `fetchThumbnailForStream`. @@ -51,23 +73,25 @@ class StreamViewModel extends StateNotifier { setUpDisplayedCourses(fetchedStreamsWithThumbnails); } - void updatedDisplayedStreams(List> allStreams) { - state = state.copyWith(displayedStreams: allStreams); - } - - void setUpDisplayedCourses(List> allStreams) { - updatedDisplayedStreams( - CourseUtils.sortStreams(allStreams, state.selectedFilterOption), - ); - } - - void updateSelectedFilterOption( - String option, - List> allStreams, - ) { - state = state.copyWith(selectedFilterOption: option); - updatedDisplayedStreams( - CourseUtils.sortStreams(allStreams, state.selectedFilterOption), + /// This asynchronous function fetches thumbnails for all available live streams in the current state. + /// only if there are live streams in the current state. + /// It initiates fetching of thumbnails for each live stream by invoking `fetchThumbnailForStream`. + Future fetchLiveThumbnails() async { + if (state.liveStreams == null) { + return; + } + var fetchLiveThumbnailTasks = >>[]; + for (var stream in state.liveStreams!) { + fetchLiveThumbnailTasks.add( + fetchStreamThumbnail(stream.id) + .then((thumbnail) => Tuple2(stream, thumbnail)), + ); + } + var fetchedStreamsWithThumbnails = + await Future.wait(fetchLiveThumbnailTasks); + state = state.copyWith( + liveStreamsWithThumb: fetchedStreamsWithThumbnails, + isLoading: false, ); } diff --git a/lib/views/course_view/components/live_stream_section.dart b/lib/views/course_view/components/live_stream_section.dart index a7b5a00a..7195934a 100644 --- a/lib/views/course_view/components/live_stream_section.dart +++ b/lib/views/course_view/components/live_stream_section.dart @@ -1,13 +1,11 @@ -import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gocast_mobile/base/networking/api/gocast/api_v2.pb.dart'; -import 'package:gocast_mobile/utils/constants.dart'; import 'package:gocast_mobile/views/course_view/components/pulse_background.dart'; import 'package:gocast_mobile/views/course_view/components/small_stream_card.dart'; import 'package:gocast_mobile/views/video_view/video_player.dart'; +import 'package:tuple/tuple.dart'; /// CourseSection /// @@ -27,7 +25,7 @@ import 'package:gocast_mobile/views/video_view/video_player.dart'; class LiveStreamSection extends StatelessWidget { final String sectionTitle; final List courses; - final List streams; + final List> streams; final VoidCallback? onViewAll; final WidgetRef ref; final String baseUrl = 'https://live.rbg.tum.de'; @@ -76,24 +74,20 @@ class LiveStreamSection extends StatelessWidget { scrollDirection: Axis.horizontal, child: Row( children: streams.map((stream) { - final Random random = Random(); String imagePath; - List imagePaths = [ - AppImages.course1, - AppImages.course2, - ]; - imagePath = imagePaths[random.nextInt(imagePaths.length)]; - final course = - courses.where((course) => course.id == stream.courseID).first; + imagePath = _getThumbnailUrl(stream.item2); + + final course = courses + .where((course) => course.id == stream.item1.courseID) + .first; return SmallStreamCard( - title: stream.name, + title: stream.item1.name, subtitle: course.name, tumID: course.tUMOnlineIdentifier, - roomName: stream.roomName, - roomNumber: stream.roomCode, - viewerCount: stream.vodViews, + roomName: stream.item1.roomName, + roomNumber: stream.item1.roomCode, path: imagePath, courseId: course.id, onTap: () { @@ -101,7 +95,7 @@ class LiveStreamSection extends StatelessWidget { context, MaterialPageRoute( builder: (context) => VideoPlayerPage( - stream: stream, + stream: stream.item1, ), ), ); @@ -139,4 +133,11 @@ class LiveStreamSection extends StatelessWidget { ), ); } + + String _getThumbnailUrl(String thumbnail) { + if (!thumbnail.startsWith('http')) { + thumbnail = '$baseUrl$thumbnail'; + } + return thumbnail; + } } diff --git a/lib/views/course_view/components/small_stream_card.dart b/lib/views/course_view/components/small_stream_card.dart index 1f945255..e894a034 100644 --- a/lib/views/course_view/components/small_stream_card.dart +++ b/lib/views/course_view/components/small_stream_card.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:gocast_mobile/base/networking/api/gocast/api_v2.pb.dart'; +import 'package:gocast_mobile/utils/constants.dart'; import 'package:url_launcher/url_launcher.dart'; class SmallStreamCard extends StatelessWidget { @@ -17,7 +18,7 @@ class SmallStreamCard extends StatelessWidget { final String? subtitle; final String? roomName; final String? roomNumber; - final int viewerCount; + final String? path; const SmallStreamCard({ @@ -27,7 +28,6 @@ class SmallStreamCard extends StatelessWidget { required this.tumID, this.roomName, this.roomNumber, - required this.viewerCount, this.path, required this.courseId, required this.onTap, @@ -82,7 +82,6 @@ class SmallStreamCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildCourseTumID(), - _buildCourseViewerCount(themeData), ], ), const SizedBox(height: 2), @@ -113,18 +112,41 @@ class SmallStreamCard extends StatelessWidget { } Widget _buildCourseImage() { + // Assuming `path` is now a URL string return Stack( children: [ AspectRatio( - aspectRatio: 16 / 12, + aspectRatio: 16 / 12, // Maintain the same aspect ratio child: ClipRRect( borderRadius: BorderRadius.circular(8.0), - child: Image.asset( - path!, - fit: BoxFit.cover, + // Keep the rounded corners + child: Image.network( + path!, // Use the image URL + fit: BoxFit.cover, // Maintain the cover fit + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) + return child; // Image is fully loaded + return Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + (loadingProgress.expectedTotalBytes ?? 1) + : null, + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + // Provide a fallback asset image in case of error + return Image.asset( + AppImages.course1, + // Path to your default/fallback image asset + fit: BoxFit.cover, + ); + }, ), ), ), + // If you have additional overlays like in the thumbnail widget, add them here ], ); } @@ -210,22 +232,4 @@ class SmallStreamCard extends StatelessWidget { const TextStyle(), ); } - - Widget _buildCourseViewerCount(ThemeData themeData) { - return Container( - decoration: BoxDecoration( - color: themeData.shadowColor.withOpacity(0.15), - borderRadius: BorderRadius.circular(4), - ), - padding: const EdgeInsets.all(3), - child: Text( - "$viewerCount viewers", - style: themeData.textTheme.labelSmall?.copyWith( - fontSize: 12, - height: 1, - ) ?? - const TextStyle(), - ), - ); - } } diff --git a/lib/views/course_view/components/stream_card.dart b/lib/views/course_view/components/stream_card.dart index b23d93f3..21868822 100644 --- a/lib/views/course_view/components/stream_card.dart +++ b/lib/views/course_view/components/stream_card.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gocast_mobile/base/networking/api/gocast/api_v2.pb.dart'; import 'package:gocast_mobile/providers.dart'; +import 'package:gocast_mobile/utils/constants.dart'; import 'package:gocast_mobile/views/video_view/video_player.dart'; import 'package:intl/intl.dart'; @@ -119,7 +120,7 @@ class StreamCardState extends ConsumerState { }, errorBuilder: (context, error, stackTrace) { return Image.asset( - 'assets/images/default_image.png', + AppImages.course1, fit: BoxFit.cover, ); }, diff --git a/lib/views/course_view/courses_overview.dart b/lib/views/course_view/courses_overview.dart index 4ec73df7..75f988d7 100644 --- a/lib/views/course_view/courses_overview.dart +++ b/lib/views/course_view/courses_overview.dart @@ -18,30 +18,38 @@ class CourseOverview extends ConsumerStatefulWidget { } class CourseOverviewState extends ConsumerState { + bool isLoading = true; @override void initState() { super.initState(); final userViewModelNotifier = ref.read(userViewModelProvider.notifier); final videoViewModelNotifier = ref.read(videoViewModelProvider.notifier); - Future.microtask(() { - // Fetch user courses if the user is logged in - if (ref.read(userViewModelProvider).user != null) { - userViewModelNotifier.fetchUserCourses(); - videoViewModelNotifier.fetchLiveNowStreams(); - userViewModelNotifier.fetchUserPinned(); - } - // Fetch public courses regardless of user's login status - userViewModelNotifier.fetchPublicCourses(); - userViewModelNotifier.fetchSemesters(); + Future.microtask(() async { + await userViewModelNotifier.fetchUserCourses(); + await videoViewModelNotifier.fetchLiveNowStreams(); + await videoViewModelNotifier.fetchLiveThumbnails(); + await userViewModelNotifier.fetchUserPinned(); + await userViewModelNotifier.fetchPublicCourses(); + await userViewModelNotifier.fetchSemesters(); + + setState(() { + isLoading = false; // Set loading to false once data is fetched + }); }); } @override Widget build(BuildContext context) { - final userCourses = ref.watch(userViewModelProvider).userCourses; - final publicCourses = ref.watch(userViewModelProvider).publicCourses; + if (isLoading) { + // Show a loading spinner when data is being fetched + return Center(child: CircularProgressIndicator()); + } + final userCourses = ref.watch(userViewModelProvider).userCourses ?? []; + final publicCourses = ref.watch(userViewModelProvider).publicCourses ?? []; final liveStreams = ref.watch(videoViewModelProvider).liveStreams; + final liveStreamWithThumb = + ref.watch(videoViewModelProvider).liveStreamsWithThumb ?? []; bool isTablet = MediaQuery.of(context).size.width >= 600 ? true : false; return PopScope( @@ -66,8 +74,8 @@ class CourseOverviewState extends ConsumerState { child: LiveStreamSection( ref: ref, sectionTitle: "Live Now", - courses: (userCourses ?? []) + (publicCourses ?? []), - streams: liveStreams ?? [], + courses: (userCourses) + (publicCourses), + streams: liveStreamWithThumb, ), ), if (isTablet) @@ -122,8 +130,8 @@ class CourseOverviewState extends ConsumerState { sectionTitle: title, sectionKind: sectionKind, onViewAll: () => _onViewAllPressed(title), - courses: courses ?? [], - streams: streams ?? [], + courses: courses, + streams: streams, ); } @@ -154,9 +162,16 @@ class CourseOverviewState extends ConsumerState { } Future _refreshData() async { + setState( + () => isLoading = true); // Set loading to true at the start of refresh + final userViewModelNotifier = ref.read(userViewModelProvider.notifier); await userViewModelNotifier.fetchUserCourses(); await userViewModelNotifier.fetchPublicCourses(); await ref.read(videoViewModelProvider.notifier).fetchLiveNowStreams(); + await ref.read(videoViewModelProvider.notifier).fetchLiveThumbnails(); + + setState(() => + isLoading = false); // Set loading to false once refresh is complete } } From d756956c185fab1da2dd56c0ba893c801b1f4d50 Mon Sep 17 00:00:00 2001 From: Saina Amiri Moghadam Date: Sun, 28 Jan 2024 22:35:14 +0100 Subject: [PATCH 7/8] fetch live now thumbnails --- lib/views/course_view/courses_overview.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/course_view/courses_overview.dart b/lib/views/course_view/courses_overview.dart index bb5d6b0b..27796cba 100644 --- a/lib/views/course_view/courses_overview.dart +++ b/lib/views/course_view/courses_overview.dart @@ -31,7 +31,7 @@ class CourseOverviewState extends ConsumerState { await userViewModelNotifier.fetchUserCourses(); await videoViewModelNotifier.fetchLiveNowStreams(); await videoViewModelNotifier.fetchLiveThumbnails(); - await userViewModelNotifier.fetchUserPinned(); + await pinnedViewModelNotifier.fetchUserPinned(); await userViewModelNotifier.fetchPublicCourses(); await userViewModelNotifier.fetchSemesters(); From 7e19f495a7f818d422bd980350d8a863137604ef Mon Sep 17 00:00:00 2001 From: Saina Amiri Moghadam Date: Sun, 28 Jan 2024 22:37:00 +0100 Subject: [PATCH 8/8] fetch live now thumbnails --- lib/views/course_view/components/course_section.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/views/course_view/components/course_section.dart b/lib/views/course_view/components/course_section.dart index aa874bb2..a383298f 100644 --- a/lib/views/course_view/components/course_section.dart +++ b/lib/views/course_view/components/course_section.dart @@ -84,7 +84,8 @@ class CourseSection extends StatelessWidget { itemCount: courses.length, itemBuilder: (BuildContext context, int index) { final course = courses[index]; - final userPinned = ref.watch(userViewModelProvider).userPinned ?? []; + final userPinned = + ref.watch(pinnedCourseViewModelProvider).userPinned ?? []; final isPinned = userPinned.contains(course); return CourseCard(