Skip to content

Commit

Permalink
Adding download over wifi func (#242)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Achraf Labidi <[email protected]>
  • Loading branch information
3 people authored Jan 28, 2024
1 parent d44ae60 commit 8049419
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 49 deletions.
1 change: 1 addition & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<application
android:label="gocast_mobile"
android:name="${applicationName}"
Expand Down
10 changes: 10 additions & 0 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
PODS:
- connectivity_plus (0.0.1):
- Flutter
- ReachabilitySwift
- Firebase/CoreOnly (10.18.0):
- FirebaseCore (= 10.18.0)
- Firebase/Messaging (10.18.0):
Expand Down Expand Up @@ -74,6 +77,7 @@ PODS:
- Flutter
- FlutterMacOS
- PromisesObjC (2.3.1)
- ReachabilitySwift (5.0.0)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
Expand All @@ -90,6 +94,7 @@ PODS:
- Flutter

DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`)
Expand All @@ -116,8 +121,11 @@ SPEC REPOS:
- nanopb
- OrderedSet
- PromisesObjC
- ReachabilitySwift

EXTERNAL SOURCES:
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
firebase_messaging:
Expand Down Expand Up @@ -146,6 +154,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"

SPEC CHECKSUMS:
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
Firebase: 414ad272f8d02dfbf12662a9d43f4bba9bec2a06
firebase_core: 0af4a2b24f62071f9bf283691c0ee41556dcb3f5
firebase_messaging: 90e8a6db84b6e1e876cebce4f30f01dc495e7014
Expand All @@ -163,6 +172,7 @@ SPEC CHECKSUMS:
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
Expand Down
2 changes: 1 addition & 1 deletion lib/models/settings/setting_state_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class SettingState {
this.isLightMode = false,
this.isSystemDefault = true,
this.isPushNotificationsEnabled = true,
this.isDownloadWithWifiOnly = true,
this.isDownloadWithWifiOnly = false,
});

SettingState copyWith({
Expand Down
53 changes: 26 additions & 27 deletions lib/view_models/download_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:logger/logger.dart';
import 'dart:io';

class DownloadViewModel extends StateNotifier<DownloadState> {

final Logger _logger = Logger();

DownloadViewModel() : super(const DownloadState()) {
Expand All @@ -25,11 +26,10 @@ class DownloadViewModel extends StateNotifier<DownloadState> {
}
}



Future<String> 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';
Expand All @@ -44,11 +44,9 @@ class DownloadViewModel extends StateNotifier<DownloadState> {

// 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;
Expand All @@ -65,35 +63,35 @@ class DownloadViewModel extends StateNotifier<DownloadState> {
_logger.e('Error fetching downloaded videos: $e');
}
}

Future<void> 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<int, String>.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<int, String>.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');
}
Expand All @@ -102,6 +100,7 @@ class DownloadViewModel extends StateNotifier<DownloadState> {
}
}


Future<void> deleteAllDownloads() async {
_logger.i('Deleting all downloaded videos');

Expand All @@ -127,8 +126,8 @@ class DownloadViewModel extends StateNotifier<DownloadState> {
}
}

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);
}
}
141 changes: 120 additions & 21 deletions lib/views/video_view/video_player.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -31,6 +32,7 @@ class VideoPlayerPageState extends ConsumerState<VideoPlayerPage> {
bool _isChatVisible = false;
bool _isChatActive = false;


Widget _buildVideoLayout() {
return Column(
children: <Widget>[
Expand All @@ -44,11 +46,8 @@ class VideoPlayerPageState extends ConsumerState<VideoPlayerPage> {
onDownload: (type) => _downloadVideo(widget.stream, type),
),
Expanded(
child: ChatView(
isActive: _isChatVisible,
streamID: widget.stream.id,
),
),
child:
ChatView(isActive: _isChatVisible, streamID: widget.stream.id),),
],
);
}
Expand All @@ -65,7 +64,9 @@ class VideoPlayerPageState extends ConsumerState<VideoPlayerPage> {
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) {
Expand Down Expand Up @@ -93,7 +94,9 @@ class VideoPlayerPageState extends ConsumerState<VideoPlayerPage> {
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(),
);
Expand Down Expand Up @@ -135,10 +138,12 @@ class VideoPlayerPageState extends ConsumerState<VideoPlayerPage> {
// Seek to the last progress.
Future<void> _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);
Expand Down Expand Up @@ -203,7 +208,9 @@ class VideoPlayerPageState extends ConsumerState<VideoPlayerPage> {
}

void _switchPlaylist(String newPlaylistUrl) async {
if (ref.read(videoViewModelProvider).videoSource == newPlaylistUrl) {
if (ref
.read(videoViewModelProvider)
.videoSource == newPlaylistUrl) {
Logger().i("Already displaying $newPlaylistUrl");
return;
}
Expand Down Expand Up @@ -239,39 +246,131 @@ class VideoPlayerPageState extends ConsumerState<VideoPlayerPage> {
});
}

void _downloadVideo(Stream stream, String type) {
// Extract the "Combined" download URL from the Stream object
Future<void> _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) {
downloadUrl = download.downloadURL;
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<bool> _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: <Widget>[
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: <Widget>[
TextButton(
child: const Text("OK"),
onPressed: () {
Navigator.of(dialogContext).pop(); // Dismiss dialog
},
),
],
);
},
);
}

Future<bool> _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
}

}
Loading

0 comments on commit 8049419

Please sign in to comment.