From 0b749509730cfab6e46b6a2445f0805f05c6d627 Mon Sep 17 00:00:00 2001 From: ge59dil Date: Fri, 26 Jan 2024 09:56:54 +0100 Subject: [PATCH 1/5] Added video name and duration --- lib/models/download/download_state_model.dart | 31 ++++++++- lib/view_models/download_view_model.dart | 68 ++++++++++++++----- .../downloaded_courses_view.dart | 13 ++-- lib/views/video_view/video_player.dart | 2 +- 4 files changed, 89 insertions(+), 25 deletions(-) diff --git a/lib/models/download/download_state_model.dart b/lib/models/download/download_state_model.dart index cf45740a..11243ca3 100644 --- a/lib/models/download/download_state_model.dart +++ b/lib/models/download/download_state_model.dart @@ -2,17 +2,44 @@ import 'package:flutter/material.dart'; @immutable class DownloadState { - final Map downloadedVideos; + final Map downloadedVideos; const DownloadState({ this.downloadedVideos = const {}, }); DownloadState copyWith({ - Map? downloadedVideos, + Map? downloadedVideos, }) { return DownloadState( downloadedVideos: downloadedVideos ?? this.downloadedVideos, ); } } + +@immutable +class VideoDetails { + final String filePath; + final String name; + final int duration; // Duration in seconds or your preferred unit + + const VideoDetails({ + required this.filePath, + required this.name, + required this.duration, + }); + + VideoDetails copyWith({ + String? filePath, + String? name, + int? duration, + }) { + return VideoDetails( + filePath: filePath ?? this.filePath, + name: name ?? this.name, + duration: duration ?? this.duration, + ); + } + +// Implement toJson and fromJson if you need to serialize/deserialize the object +} diff --git a/lib/view_models/download_view_model.dart b/lib/view_models/download_view_model.dart index dfe86eef..9ef77c51 100644 --- a/lib/view_models/download_view_model.dart +++ b/lib/view_models/download_view_model.dart @@ -20,14 +20,23 @@ class DownloadViewModel extends StateNotifier { final jsonString = prefs.getString('downloadedVideos'); if (jsonString != null) { final downloaded = Map.from(json.decode(jsonString)); - final downloadedVideos = downloaded - .map((key, value) => MapEntry(int.parse(key), value.toString())); + final downloadedVideos = downloaded.map((key, value) { + // Decode the JSON string into a Map + final videoDetailsMap = json.decode(value); + // Create a VideoDetails object from the Map + final videoDetails = VideoDetails( + filePath: videoDetailsMap['filePath'], + name: videoDetailsMap['name'], + duration: videoDetailsMap['duration'], + ); + return MapEntry(int.parse(key), videoDetails); + }).cast(); // Ensure the map has the correct type state = state.copyWith(downloadedVideos: downloadedVideos); } } Future downloadVideo( - String videoUrl, Int64 streamId, String fileName,) async { + String videoUrl, Int64 streamId, String fileName, String streamName, int streamDuration,) async { try { final directory = await getApplicationDocumentsDirectory(); final filePath = '${directory.path}/$fileName'; @@ -37,14 +46,38 @@ class DownloadViewModel extends StateNotifier { final prefs = await SharedPreferences.getInstance(); final int streamIdInt = streamId.toInt(); - final downloadedVideos = Map.from(state.downloadedVideos) - ..[streamIdInt] = filePath; - // Save to SharedPreferences + // Create a map for the video details + final videoDetailsMap = { + 'filePath': filePath, + 'name': streamName, + 'duration': streamDuration, + }; + + // Convert video details map to JSON string + final videoDetailsJson = json.encode(videoDetailsMap); + + // Save the JSON string in your SharedPreferences + final downloadedVideosJson = Map.from(state.downloadedVideos) + ..[streamIdInt] = videoDetailsJson; + await prefs.setString( - 'downloadedVideos', - json.encode(downloadedVideos - .map((key, value) => MapEntry(key.toString(), value)),),); + 'downloadedVideos', + json.encode(downloadedVideosJson.map((key, value) => MapEntry(key.toString(), value))), + ); + + // Convert the JSON strings back to VideoDetails objects for the state + final downloadedVideos = downloadedVideosJson.map((key, value) { + final videoDetailsMap = json.decode(value); + final videoDetails = VideoDetails( + filePath: videoDetailsMap['filePath'], + name: videoDetailsMap['name'], + duration: videoDetailsMap['duration'], + ); + return MapEntry(key, videoDetails); + }).cast(); + + // Update the state state = state.copyWith(downloadedVideos: downloadedVideos); _logger.d('Downloaded videos: ${state.downloadedVideos}'); return filePath; @@ -61,34 +94,35 @@ class DownloadViewModel extends StateNotifier { _logger.e('Error fetching downloaded videos: $e'); } } - Future deleteDownload(int videoId) async { _logger.i('Deleting downloaded video with ID: $videoId'); try { - String? filePath = state.downloadedVideos[videoId]; - if (filePath != null && filePath.isNotEmpty) { + // Get the VideoDetails object from the state + VideoDetails? videoDetails = state.downloadedVideos[videoId]; + if (videoDetails != null) { + final filePath = videoDetails.filePath; final file = File(filePath); if (await file.exists()) { await file.delete(); _logger.d('Deleted video file at: $filePath'); final prefs = await SharedPreferences.getInstance(); - final updatedDownloads = - Map.from(state.downloadedVideos); + final updatedDownloads = Map.from(state.downloadedVideos); updatedDownloads.remove(videoId); // Save updated list to SharedPreferences + // Convert VideoDetails objects to JSON strings before saving await prefs.setString( 'downloadedVideos', - json.encode(updatedDownloads - .map((key, value) => MapEntry(key.toString(), value)),),); + json.encode(updatedDownloads.map((key, value) => MapEntry(key.toString(), json.encode(value)))) + ,); state = state.copyWith(downloadedVideos: updatedDownloads); } else { _logger.w('File not found: $filePath'); } } else { - _logger.w('No file path found for video ID: $videoId'); + _logger.w('No details found for video ID: $videoId'); } } catch (e) { _logger.e('Error deleting video with ID $videoId: $e'); diff --git a/lib/views/course_view/downloaded_courses_view/downloaded_courses_view.dart b/lib/views/course_view/downloaded_courses_view/downloaded_courses_view.dart index 462deb86..6ea78092 100644 --- a/lib/views/course_view/downloaded_courses_view/downloaded_courses_view.dart +++ b/lib/views/course_view/downloaded_courses_view/downloaded_courses_view.dart @@ -1,6 +1,6 @@ -import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:gocast_mobile/models/download/download_state_model.dart'; import 'package:gocast_mobile/providers.dart'; import 'package:gocast_mobile/views/components/custom_search_top_nav_bar.dart'; import 'package:gocast_mobile/views/course_view/downloaded_courses_view/download_card.dart'; @@ -42,14 +42,17 @@ class DownloadedCoursesState extends ConsumerState { ), videoCards: downloadedVideos.entries.map((entry) { final int videoId = entry.key; - final String localPath = entry.value; - + final VideoDetails videoDetails = entry.value; + final String localPath = videoDetails.filePath; + final String videoName = videoDetails.name; + final int durationSeconds = videoDetails.duration; + final String formattedDuration = "${(durationSeconds ~/ 3600).toString().padLeft(2, '0')}:${((durationSeconds % 3600) ~/ 60).toString().padLeft(2, '0')}:${(durationSeconds % 60).toString().padLeft(2, '0')}"; return VideoCard( duration: - "${Random().nextInt(2).toString().padLeft(2, '0')}:${Random().nextInt(59).toString().padLeft(2, '0')}:${Random().nextInt(60).toString().padLeft(2, '0')}", + formattedDuration, imageName: 'assets/images/course1.png', // Update as necessary - title: 'Video $videoId', + title: videoName, // Replace with the appropriate title date: 'Video Date', // Replace with the appropriate date diff --git a/lib/views/video_view/video_player.dart b/lib/views/video_view/video_player.dart index 57d3ce9b..483cfa21 100644 --- a/lib/views/video_view/video_player.dart +++ b/lib/views/video_view/video_player.dart @@ -262,7 +262,7 @@ class VideoPlayerPageState extends ConsumerState { // Call the download function from the StreamViewModel ref .read(downloadViewModelProvider.notifier) - .downloadVideo(downloadUrl, stream.id, fileName) + .downloadVideo(downloadUrl, stream.id, fileName,stream.name,stream.duration) .then((localPath) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Video Downloaded')), From d30193d25cf200fa2d0dcd0a4dba5a8601fc00b3 Mon Sep 17 00:00:00 2001 From: ge59dil Date: Sun, 28 Jan 2024 15:10:32 +0100 Subject: [PATCH 2/5] Adding description in video details --- lib/models/download/download_state_model.dart | 3 +++ lib/view_models/download_view_model.dart | 5 ++++- lib/views/video_view/video_player.dart | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/models/download/download_state_model.dart b/lib/models/download/download_state_model.dart index 11243ca3..043cd8e9 100644 --- a/lib/models/download/download_state_model.dart +++ b/lib/models/download/download_state_model.dart @@ -22,11 +22,13 @@ class VideoDetails { final String filePath; final String name; final int duration; // Duration in seconds or your preferred unit + final String description; const VideoDetails({ required this.filePath, required this.name, required this.duration, + required this.description, }); VideoDetails copyWith({ @@ -38,6 +40,7 @@ class VideoDetails { filePath: filePath ?? this.filePath, name: name ?? this.name, duration: duration ?? this.duration, + description: description, ); } diff --git a/lib/view_models/download_view_model.dart b/lib/view_models/download_view_model.dart index 31512b9b..576fa2af 100644 --- a/lib/view_models/download_view_model.dart +++ b/lib/view_models/download_view_model.dart @@ -33,6 +33,7 @@ class DownloadViewModel extends StateNotifier { filePath: videoDetailsMap['filePath'], name: videoDetailsMap['name'], duration: videoDetailsMap['duration'], + description: videoDetailsMap['description'], ); return MapEntry(int.parse(key), videoDetails); }).cast(); // Ensure the map has the correct type @@ -41,7 +42,7 @@ class DownloadViewModel extends StateNotifier { } Future downloadVideo(String videoUrl, int streamId, String fileName, - String streamName, int streamDuration,) async { + String streamName, int streamDuration, String description,) async { try { final directory = await getApplicationDocumentsDirectory(); final filePath = '${directory.path}/$fileName'; @@ -57,6 +58,7 @@ class DownloadViewModel extends StateNotifier { 'filePath': filePath, 'name': streamName, 'duration': streamDuration, + 'description': description, }; // Convert video details map to JSON string @@ -79,6 +81,7 @@ class DownloadViewModel extends StateNotifier { filePath: videoDetailsMap['filePath'], name: videoDetailsMap['name'], duration: videoDetailsMap['duration'], + description: videoDetailsMap['description'], ); return MapEntry(key, videoDetails); }).cast(); diff --git a/lib/views/video_view/video_player.dart b/lib/views/video_view/video_player.dart index 5f69b453..0e433f92 100644 --- a/lib/views/video_view/video_player.dart +++ b/lib/views/video_view/video_player.dart @@ -279,7 +279,7 @@ class VideoPlayerPageState extends ConsumerState { ref .read(downloadViewModelProvider.notifier) - .downloadVideo(downloadUrl, stream.id, fileName,stream.name,stream.duration) + .downloadVideo(downloadUrl, stream.id, fileName,stream.name,stream.duration,stream.description,) .then((localPath) { if (localPath.isNotEmpty) { // Download successful From 70e21b7f590a3a91eeb73954e6348d0589ef4860 Mon Sep 17 00:00:00 2001 From: ge59dil Date: Mon, 29 Jan 2024 12:03:29 +0100 Subject: [PATCH 3/5] added a dialog box for deletion --- lib/models/download/download_state_model.dart | 2 - .../downloaded_courses_view.dart | 37 ++++++++++++++++--- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/lib/models/download/download_state_model.dart b/lib/models/download/download_state_model.dart index 043cd8e9..f01322f8 100644 --- a/lib/models/download/download_state_model.dart +++ b/lib/models/download/download_state_model.dart @@ -43,6 +43,4 @@ class VideoDetails { description: description, ); } - -// Implement toJson and fromJson if you need to serialize/deserialize the object } diff --git a/lib/views/course_view/downloaded_courses_view/downloaded_courses_view.dart b/lib/views/course_view/downloaded_courses_view/downloaded_courses_view.dart index 6ea78092..75b1315d 100644 --- a/lib/views/course_view/downloaded_courses_view/downloaded_courses_view.dart +++ b/lib/views/course_view/downloaded_courses_view/downloaded_courses_view.dart @@ -16,7 +16,34 @@ class DownloadedCourses extends ConsumerStatefulWidget { class DownloadedCoursesState extends ConsumerState { final TextEditingController searchController = TextEditingController(); - + void _showDeleteConfirmationDialog(int videoId) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Confirm Deletion'), + content: const Text('Are you sure you want to delete this video?'), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context).pop(); // Dismiss the dialog + }, + ), + TextButton( + child: const Text('Delete'), + onPressed: () async { + Navigator.of(context).pop(); // Dismiss the dialog + await ref + .read(downloadViewModelProvider.notifier) + .deleteDownload(videoId); + }, + ), + ], + ); + }, + ); + } //TODO: void _handleSortOptionSelected(String choice) {} @override @@ -54,7 +81,7 @@ class DownloadedCoursesState extends ConsumerState { // Update as necessary title: videoName, // Replace with the appropriate title - date: 'Video Date', + date: "", // Replace with the appropriate date onTap: () { Navigator.of(context).push( @@ -64,10 +91,8 @@ class DownloadedCoursesState extends ConsumerState { ), ); }, - onDelete: () async { - await ref - .read(downloadViewModelProvider.notifier) - .deleteDownload(videoId); + onDelete: () { + _showDeleteConfirmationDialog(videoId); }, ); }).toList(), From 75574157581d34095ff82928f3ec9fcb16ecf4ce Mon Sep 17 00:00:00 2001 From: Achraf Labidi Date: Mon, 29 Jan 2024 17:58:55 +0100 Subject: [PATCH 4/5] Add swipe t delete too `DownloadCard` --- .../course_view/components/course_card.dart | 2 +- .../components/small_stream_card.dart | 49 ++++++++++++++++--- .../download_content_view.dart | 4 +- .../downloaded_courses_view.dart | 43 ++++++++-------- 4 files changed, 68 insertions(+), 30 deletions(-) diff --git a/lib/views/course_view/components/course_card.dart b/lib/views/course_view/components/course_card.dart index 1239abef..edf07aa6 100644 --- a/lib/views/course_view/components/course_card.dart +++ b/lib/views/course_view/components/course_card.dart @@ -65,7 +65,7 @@ class CourseCard extends StatelessWidget { course!, onPinUnpin!, isPinned!, - ) + ), ), ), ); diff --git a/lib/views/course_view/components/small_stream_card.dart b/lib/views/course_view/components/small_stream_card.dart index e894a034..aedd3089 100644 --- a/lib/views/course_view/components/small_stream_card.dart +++ b/lib/views/course_view/components/small_stream_card.dart @@ -1,4 +1,5 @@ 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/utils/constants.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -7,13 +8,13 @@ class SmallStreamCard extends StatelessWidget { final String title; final String tumID; final VoidCallback onTap; - final int courseId; + final int? courseId; //for displaying courses final bool? live; - + final bool? isDownloaded; final Course? course; - + final Function(int)? showDeleteConfirmationDialog; //for displaying livestreams final String? subtitle; final String? roomName; @@ -29,10 +30,12 @@ class SmallStreamCard extends StatelessWidget { this.roomName, this.roomNumber, this.path, - required this.courseId, + this.courseId, required this.onTap, this.live, this.course, + this.isDownloaded, + this.showDeleteConfirmationDialog, }); @override @@ -71,6 +74,32 @@ class SmallStreamCard extends StatelessWidget { } Widget _buildStreamCard(ThemeData themeData, double cardWidth) { + return (isDownloaded!=null && showDeleteConfirmationDialog!=null) ? _buildDownloadedCard(themeData, cardWidth) : _buildLiveCard(themeData, cardWidth); + } + + Widget _buildDownloadedCard (ThemeData themeData, double cardWidth) { + return Slidable( + key: Key(courseId.toString()), + closeOnScroll: true, + endActionPane: ActionPane( + motion: const DrawerMotion(), + dragDismissible: true, + children: [ + SlidableAction( + onPressed: (_) => showDeleteConfirmationDialog!(courseId!), + autoClose: true, + backgroundColor: Colors.red, + foregroundColor: Colors.white, + icon: Icons.delete_rounded, + label: 'Delete', + ), + ], + ), + child: _buildLiveCard(themeData, cardWidth*1.3), + ); + } + + Widget _buildLiveCard(ThemeData themeData, double cardWidth) { return Container( width: cardWidth, padding: const EdgeInsets.all(8.0), @@ -120,12 +149,19 @@ class SmallStreamCard extends StatelessWidget { child: ClipRRect( borderRadius: BorderRadius.circular(8.0), // Keep the rounded corners - child: Image.network( + child: path == null + ? Image.asset( + AppImages.course1, + fit: BoxFit.cover, + ) + : + Image.network( path!, // Use the image URL fit: BoxFit.cover, // Maintain the cover fit loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) + if (loadingProgress == null) { return child; // Image is fully loaded + } return Center( child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null @@ -139,7 +175,6 @@ class SmallStreamCard extends StatelessWidget { // Provide a fallback asset image in case of error return Image.asset( AppImages.course1, - // Path to your default/fallback image asset fit: BoxFit.cover, ); }, diff --git a/lib/views/course_view/downloaded_courses_view/download_content_view.dart b/lib/views/course_view/downloaded_courses_view/download_content_view.dart index cdf715d5..3065c052 100644 --- a/lib/views/course_view/downloaded_courses_view/download_content_view.dart +++ b/lib/views/course_view/downloaded_courses_view/download_content_view.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gocast_mobile/views/components/base_view.dart'; import 'package:gocast_mobile/views/components/custom_search_top_nav_bar.dart'; +import 'package:gocast_mobile/views/course_view/components/small_stream_card.dart'; -import 'package:gocast_mobile/views/course_view/downloaded_courses_view/download_card.dart'; import '../../../utils/constants.dart'; @@ -14,7 +14,7 @@ import '../../../utils/constants.dart'; /// It takes a [title] to display the title of the section and /// dynamically generates a horizontal list of courses. class DownloadCoursesContentView extends ConsumerWidget { - final List videoCards; + final List videoCards; final CustomSearchTopNavBar customAppBar; const DownloadCoursesContentView({ diff --git a/lib/views/course_view/downloaded_courses_view/downloaded_courses_view.dart b/lib/views/course_view/downloaded_courses_view/downloaded_courses_view.dart index 75b1315d..e689063d 100644 --- a/lib/views/course_view/downloaded_courses_view/downloaded_courses_view.dart +++ b/lib/views/course_view/downloaded_courses_view/downloaded_courses_view.dart @@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gocast_mobile/models/download/download_state_model.dart'; import 'package:gocast_mobile/providers.dart'; import 'package:gocast_mobile/views/components/custom_search_top_nav_bar.dart'; -import 'package:gocast_mobile/views/course_view/downloaded_courses_view/download_card.dart'; +import 'package:gocast_mobile/views/course_view/components/small_stream_card.dart'; import 'package:gocast_mobile/views/course_view/downloaded_courses_view/download_content_view.dart'; import 'package:gocast_mobile/views/video_view/offline_video_player/offline_video_player.dart'; @@ -16,6 +16,7 @@ class DownloadedCourses extends ConsumerStatefulWidget { class DownloadedCoursesState extends ConsumerState { final TextEditingController searchController = TextEditingController(); + void _showDeleteConfirmationDialog(int videoId) { showDialog( context: context, @@ -44,6 +45,7 @@ class DownloadedCoursesState extends ConsumerState { }, ); } + //TODO: void _handleSortOptionSelected(String choice) {} @override @@ -74,30 +76,31 @@ class DownloadedCoursesState extends ConsumerState { final String videoName = videoDetails.name; final int durationSeconds = videoDetails.duration; final String formattedDuration = "${(durationSeconds ~/ 3600).toString().padLeft(2, '0')}:${((durationSeconds % 3600) ~/ 60).toString().padLeft(2, '0')}:${(durationSeconds % 60).toString().padLeft(2, '0')}"; - return VideoCard( - duration: - formattedDuration, - imageName: 'assets/images/course1.png', - // Update as necessary - title: videoName, - // Replace with the appropriate title - date: "", - // Replace with the appropriate date - onTap: () { + return SmallStreamCard( + isDownloaded: true, + courseId: videoId, + title: videoName, + subtitle: formattedDuration, + tumID: "TUMID", + showDeleteConfirmationDialog: _showDeleteConfirmationDialog, + onTap: () { Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => - OfflineVideoPlayerPage(localPath: localPath), - ), - ); + MaterialPageRoute( + builder: (context) => + OfflineVideoPlayerPage(localPath: localPath), + ), + ); }, - onDelete: () { - _showDeleteConfirmationDialog(videoId); - }, - ); + ); }).toList(), ), ), ); + } } + + +// onDelete: () { +// _showDeleteConfirmationDialog(videoId); +// }, \ No newline at end of file From a905e2c74491c78e9e8ee453a9339952a996b01a Mon Sep 17 00:00:00 2001 From: Achraf Labidi Date: Mon, 29 Jan 2024 17:59:00 +0100 Subject: [PATCH 5/5] Add swipe t delete too `DownloadCard` --- lib/views/course_view/courses_overview.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/views/course_view/courses_overview.dart b/lib/views/course_view/courses_overview.dart index 27796cba..02993f60 100644 --- a/lib/views/course_view/courses_overview.dart +++ b/lib/views/course_view/courses_overview.dart @@ -45,7 +45,7 @@ class CourseOverviewState extends ConsumerState { Widget build(BuildContext context) { if (isLoading) { // Show a loading spinner when data is being fetched - return Center(child: CircularProgressIndicator()); + return const Center(child: CircularProgressIndicator()); } final userCourses = ref.watch(userViewModelProvider).userCourses ?? []; final publicCourses = ref.watch(userViewModelProvider).publicCourses ?? []; @@ -165,7 +165,7 @@ class CourseOverviewState extends ConsumerState { Future _refreshData() async { setState( - () => isLoading = true); // Set loading to true at the start of refresh + () => isLoading = true,); // Set loading to true at the start of refresh final userViewModelNotifier = ref.read(userViewModelProvider.notifier); await userViewModelNotifier.fetchUserCourses(); @@ -174,6 +174,6 @@ class CourseOverviewState extends ConsumerState { await ref.read(videoViewModelProvider.notifier).fetchLiveThumbnails(); setState(() => - isLoading = false); // Set loading to false once refresh is complete + isLoading = false,); // Set loading to false once refresh is complete } }