From 8049419594ab30c97b62f055eb16632e5b73d4ff Mon Sep 17 00:00:00 2001 From: Anishyou <123313052+Anishyou@users.noreply.github.com> Date: Sun, 28 Jan 2024 12:18:07 +0100 Subject: [PATCH] Adding download over wifi func (#242) * Adding download over wifi func * download over wifi func * download func * added error handing * rollbacking Info.plist * Added changes for mobile data. * solved delete bug * solved error --------- Co-authored-by: ge59dil Co-authored-by: Achraf Labidi <101757413+GravityDarkLab@users.noreply.github.com> --- android/app/src/main/AndroidManifest.xml | 1 + ios/Podfile.lock | 10 ++ lib/models/settings/setting_state_model.dart | 2 +- lib/view_models/download_view_model.dart | 53 ++++--- lib/views/video_view/video_player.dart | 141 ++++++++++++++++--- pubspec.lock | 24 ++++ pubspec.yaml | 1 + 7 files changed, 183 insertions(+), 49 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index cbf99b2e..5d939348 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ + { + final Logger _logger = Logger(); DownloadViewModel() : super(const DownloadState()) { @@ -25,11 +26,10 @@ class DownloadViewModel extends StateNotifier { } } + + Future downloadVideo( - String videoUrl, - int streamId, - String fileName, - ) async { + String videoUrl, int streamId, String fileName,) async { try { final directory = await getApplicationDocumentsDirectory(); final filePath = '${directory.path}/$fileName'; @@ -44,11 +44,9 @@ class DownloadViewModel extends StateNotifier { // Save to SharedPreferences await prefs.setString( - 'downloadedVideos', - json.encode( - downloadedVideos.map((key, value) => MapEntry(key.toString(), value)), - ), - ); + 'downloadedVideos', + json.encode(downloadedVideos + .map((key, value) => MapEntry(key.toString(), value)),),); state = state.copyWith(downloadedVideos: downloadedVideos); _logger.d('Downloaded videos: ${state.downloadedVideos}'); return filePath; @@ -65,35 +63,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'); + _logger.d('Current state before deletion: ${state.downloadedVideos}'); try { String? filePath = state.downloadedVideos[videoId]; + _logger.d('File path to delete: $filePath'); + if (filePath != null && filePath.isNotEmpty) { 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); - updatedDownloads.remove(videoId); - - // Save updated list to SharedPreferences - await prefs.setString( - 'downloadedVideos', - json.encode( - updatedDownloads - .map((key, value) => MapEntry(key.toString(), value)), - ), - ); - state = state.copyWith(downloadedVideos: updatedDownloads); } else { _logger.w('File not found: $filePath'); } + + // Update the state and SharedPreferences after deletion + final updatedDownloads = Map.from(state.downloadedVideos); + updatedDownloads.remove(videoId); + + final prefs = await SharedPreferences.getInstance(); + await prefs.setString( + 'downloadedVideos', + json.encode(updatedDownloads.map((key, value) => MapEntry(key.toString(), value))), + ); + + state = state.copyWith(downloadedVideos: updatedDownloads); + _logger.d('Updated state after deletion: ${state.downloadedVideos}'); } else { _logger.w('No file path found for video ID: $videoId'); } @@ -102,6 +100,7 @@ class DownloadViewModel extends StateNotifier { } } + Future deleteAllDownloads() async { _logger.i('Deleting all downloaded videos'); @@ -127,8 +126,8 @@ class DownloadViewModel extends StateNotifier { } } - bool isStreamDownloaded(id) { - final int streamIdInt = id.toInt(); + bool isStreamDownloaded(int id) { + final int streamIdInt = id.toInt(); // Convert Int64 to int return state.downloadedVideos.containsKey(streamIdInt); } } diff --git a/lib/views/video_view/video_player.dart b/lib/views/video_view/video_player.dart index c76870b1..27cca8f2 100644 --- a/lib/views/video_view/video_player.dart +++ b/lib/views/video_view/video_player.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gocast_mobile/base/networking/api/gocast/api_v2.pb.dart'; @@ -31,6 +32,7 @@ class VideoPlayerPageState extends ConsumerState { bool _isChatVisible = false; bool _isChatActive = false; + Widget _buildVideoLayout() { return Column( children: [ @@ -44,11 +46,8 @@ class VideoPlayerPageState extends ConsumerState { onDownload: (type) => _downloadVideo(widget.stream, type), ), Expanded( - child: ChatView( - isActive: _isChatVisible, - streamID: widget.stream.id, - ), - ), + child: + ChatView(isActive: _isChatVisible, streamID: widget.stream.id),), ], ); } @@ -65,7 +64,9 @@ class VideoPlayerPageState extends ConsumerState { await ref .read(courseViewModelProvider.notifier) .getCourseWithID(widget.stream.courseID); - Course? course = ref.read(courseViewModelProvider).course; + Course? course = ref + .read(courseViewModelProvider) + .course; if (course != null) { if ((course.chatEnabled || course.vodChatEnabled) && widget.stream.chatEnabled) { @@ -93,7 +94,9 @@ class VideoPlayerPageState extends ConsumerState { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(widget.stream.name)), - body: ref.read(videoViewModelProvider).isLoading + body: ref + .read(videoViewModelProvider) + .isLoading ? const Center(child: CircularProgressIndicator()) : _buildVideoLayout(), ); @@ -135,10 +138,12 @@ class VideoPlayerPageState extends ConsumerState { // Seek to the last progress. Future _seekToLastProgress() async { Progress progress = - ref.read(videoViewModelProvider).progress ?? Progress(progress: 0.0); + ref + .read(videoViewModelProvider) + .progress ?? Progress(progress: 0.0); final position = Duration( seconds: (progress.progress * - _controllerManager.videoPlayerController.value.duration.inSeconds) + _controllerManager.videoPlayerController.value.duration.inSeconds) .round(), ); await _controllerManager.videoPlayerController.seekTo(position); @@ -203,7 +208,9 @@ class VideoPlayerPageState extends ConsumerState { } void _switchPlaylist(String newPlaylistUrl) async { - if (ref.read(videoViewModelProvider).videoSource == newPlaylistUrl) { + if (ref + .read(videoViewModelProvider) + .videoSource == newPlaylistUrl) { Logger().i("Already displaying $newPlaylistUrl"); return; } @@ -239,8 +246,11 @@ class VideoPlayerPageState extends ConsumerState { }); } - void _downloadVideo(Stream stream, String type) { - // Extract the "Combined" download URL from the Stream object + Future _downloadVideo(Stream stream, String type) async { + // Extract the "Combined" download URL from the Stream object + bool canDownload = await _handleDownloadConnectivity(stream, type); + if (!canDownload) return; // Exit if download should not proceed + String? downloadUrl; for (var download in stream.downloads) { if (download.friendlyName == type) { @@ -248,30 +258,119 @@ class VideoPlayerPageState extends ConsumerState { break; } } - //combinedDownloadUrl="https://file-examples.com/storage/fe5048eb7365a64ba96daa9/2017/04/file_example_MP4_480_1_5MG.mp4"; + //downloadUrl="https://file-examples.com/storage/fed61549c865b2b5c9768b5/2017/04/file_example_MP4_480_1_5MG.mp4"; // Check if the Combined URL is found if (downloadUrl == null) { + if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Download type "$type" not available for this lecture'), - ), + SnackBar(content: Text( + 'Download type "$type" not available for this lecture',),), ); return; } // Use the extracted URL for downloading String fileName = "stream.mp4"; + if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Downloading Video')), + const SnackBar(content: Text('Starting download...')), ); // Call the download function from the StreamViewModel + ref .read(downloadViewModelProvider.notifier) - .downloadVideo(downloadUrl, stream.id, fileName) + .downloadVideo(downloadUrl, stream.id, fileName,) .then((localPath) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Video Downloaded')), - ); + if (localPath.isNotEmpty) { + // Download successful + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Video Downloaded')), + ); + } else { + // Download failed, but not due to Wi-Fi (since it would throw an exception) + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Download failed')), + ); + } }); } + + Future _showDownloadConfirmationDialog() async { + return await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: const Text("Download Video"), + content: const Text( + "You are on mobile data. Would you like to download the video over mobile data?",), + actions: [ + TextButton( + child: const Text("No"), + onPressed: () { + Navigator.of(dialogContext).pop(false); + }, + ), + TextButton( + child: const Text("Yes"), + onPressed: () { + Navigator.of(dialogContext).pop(true); + }, + ), + ], + ); + }, + ) ?? false; // If dialog is dismissed, return false + } + void _showMobileDataNotAllowedDialog() { + showDialog( + context: context, + barrierDismissible: false, // User must tap a button for the dialog to close + builder: (BuildContext dialogContext) { + return AlertDialog( + title: const Text("Download Not Allowed"), + content: const Text( + "You are currently on mobile data. Video cannot be downloaded over mobile data due to your settings.",), + actions: [ + TextButton( + child: const Text("OK"), + onPressed: () { + Navigator.of(dialogContext).pop(); // Dismiss dialog + }, + ), + ], + ); + }, + ); + } + + Future _handleDownloadConnectivity(Stream stream, String type) async { + final isDownloadWithWifiOnly = ref + .watch(settingViewModelProvider) + .isDownloadWithWifiOnly; + + var connectivityResult = await (Connectivity().checkConnectivity()); + + // If 'Download Over Wi-Fi Only' is enabled and connected to mobile, show a dialog + if (connectivityResult == ConnectivityResult.mobile && isDownloadWithWifiOnly) { + if (!mounted) return false; + _showMobileDataNotAllowedDialog(); + return false; + } + + // If on mobile data and 'Download Over Wi-Fi Only' is disabled, ask for confirmation + if (connectivityResult == ConnectivityResult.mobile && !isDownloadWithWifiOnly) { + bool shouldProceed = await _showDownloadConfirmationDialog(); + if (!mounted) return false; + if (!shouldProceed) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Download cancelled')), + ); + return false; + } + } + + return true; // Proceed with download if all checks pass + } + } diff --git a/pubspec.lock b/pubspec.lock index 4d83c30f..29c2264d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -81,6 +81,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a + url: "https://pub.dev" + source: hosted + version: "1.2.4" convert: dependency: transitive description: @@ -432,6 +448,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" package_info_plus: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3a8159ef..b21b954e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,6 +7,7 @@ environment: sdk: '>=3.1.5 <4.0.0' dependencies: + connectivity_plus: ^5.0.2 tuple: ^2.0.1 bloc: ^8.1.2 cookie_jar: ^4.0.8