From 7eb8076fda1b05c12afab6d8b19802ff4f5a026d Mon Sep 17 00:00:00 2001 From: Saina Amiri Moghadam Date: Sun, 28 Jan 2024 22:15:36 +0100 Subject: [PATCH] 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 6119521..b4626af 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 820ad2a..e09f507 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 a7b5a00..7195934 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 1f94525..e894a03 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 b23d93f..2186882 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 4ec73df..75f988d 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 } }