Skip to content

Commit

Permalink
Merge pull request #222 from humhub/46-internal-links-to-humhub-files…
Browse files Browse the repository at this point in the history
…-are-not-clickable

Implement download functionality
  • Loading branch information
luke- committed Sep 9, 2024
2 parents 1c0a414 + f8efa35 commit 0bc3c0d
Show file tree
Hide file tree
Showing 11 changed files with 270 additions and 13 deletions.
2 changes: 1 addition & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ android {

defaultConfig {
applicationId "com.humhub.app"
minSdkVersion 19
minSdkVersion 21
targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
Expand Down
10 changes: 10 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@
<uses-permission
android:name="android.permission.USE_FULL_SCREEN_INTENT"
tools:node="remove" />
<!-- For media access -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

<!-- For older versions (For Android 12 (API 31) and Below) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:label="HumHub"
android:name="${applicationName}"
Expand Down Expand Up @@ -56,5 +63,8 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />

</application>


</manifest>
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.6.10'
ext.kotlin_version = '1.8.22'
repositories {
google()
mavenCentral()
Expand Down
13 changes: 13 additions & 0 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,14 @@ PODS:
- nanopb/encode (= 2.30909.0)
- nanopb/decode (2.30909.0)
- nanopb/encode (2.30909.0)
- open_file_plus (1.0.0):
- Flutter
- OrderedSet (5.0.0)
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- PromisesObjC (2.3.1)
Expand All @@ -106,7 +111,9 @@ DEPENDENCIES:
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_timezone (from `.symlinks/plugins/flutter_native_timezone/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- open_file_plus (from `.symlinks/plugins/open_file_plus/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
Expand Down Expand Up @@ -148,8 +155,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_native_timezone/ios"
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
open_file_plus:
:path: ".symlinks/plugins/open_file_plus/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
receive_sharing_intent:
Expand Down Expand Up @@ -180,8 +191,10 @@ SPEC CHECKSUMS:
GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2
GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084
nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431
open_file_plus: 05737718530c14cf02868e3b7754d7fe4df76d8b
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
Expand Down
2 changes: 2 additions & 0 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,7 @@
</dict>
<key>LSMinimumSystemVersion</key>
<string>14.4</string>
<key>FLTEnableImpeller</key>
<false />
</dict>
</plist>
36 changes: 35 additions & 1 deletion lib/flavored/web_view.f.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import 'package:loggy/loggy.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:humhub/util/file_handler.dart';
import 'package:open_file_plus/open_file_plus.dart';

class WebViewF extends ConsumerStatefulWidget {
static const String path = '/web_view_f';
Expand Down Expand Up @@ -85,6 +87,7 @@ class FlavoredWebViewState extends ConsumerState<WebViewF> {
onLoadStart: _onLoadStart,
onReceivedError: _onLoadError,
onProgressChanged: _onProgressChanged,
onDownloadStartRequest: _onDownloadStartRequest,
),
),
),
Expand Down Expand Up @@ -155,10 +158,14 @@ class FlavoredWebViewState extends ConsumerState<WebViewF> {
return request;
}

Future<bool> _onCreateWindow(inAppWebViewController, createWindowAction) async {
Future<bool> _onCreateWindow(InAppWebViewController controller, CreateWindowAction createWindowAction) async {
logDebug("onCreateWindow");
final urlToOpen = createWindowAction.request.url;
if (urlToOpen == null) return Future.value(false);
if (urlToOpen.rawValue.contains('file/download')) {
controller.loadUrl(urlRequest: createWindowAction.request);
return Future.value(false);
}
if (await canLaunchUrl(urlToOpen)) {
await launchUrl(urlToOpen, mode: LaunchMode.externalApplication);
} else {
Expand Down Expand Up @@ -267,6 +274,33 @@ class FlavoredWebViewState extends ConsumerState<WebViewF> {
}
}

void _onDownloadStartRequest(InAppWebViewController controller, DownloadStartRequest downloadStartRequest) async {
FileHandler(
downloadStartRequest: downloadStartRequest,
controller: controller,
onSuccess: (File file, String filename) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('File downloaded: $filename'),
action: SnackBarAction(
label: 'Open',
onPressed: () {
// Open the downloaded file
OpenFile.open(file.path);
},
),
),
);
},
onError: (er) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Something went wrong'),
),
);
}).download();
}

@override
void dispose() {
super.dispose();
Expand Down
47 changes: 41 additions & 6 deletions lib/pages/web_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:humhub/models/manifest.dart';
import 'package:humhub/pages/opener.dart';
import 'package:humhub/util/connectivity_plugin.dart';
import 'package:humhub/util/extensions.dart';
import 'package:humhub/util/file_handler.dart';
import 'package:humhub/util/loading_provider.dart';
import 'package:humhub/util/notifications/init_from_push.dart';
import 'package:humhub/util/notifications/plugin.dart';
Expand All @@ -21,6 +22,7 @@ import 'package:humhub/util/openers/universal_opener_controller.dart';
import 'package:humhub/util/push/provider.dart';
import 'package:humhub/util/router.dart';
import 'package:loggy/loggy.dart';
import 'package:open_file_plus/open_file_plus.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:humhub/util/router.dart' as m;
import 'package:url_launcher/url_launcher.dart';
Expand Down Expand Up @@ -102,6 +104,7 @@ class WebViewAppState extends ConsumerState<WebView> {
onLoadStart: _onLoadStart,
onProgressChanged: _onProgressChanged,
onReceivedError: _onReceivedError,
onDownloadStartRequest: _onDownloadStartRequest,
),
),
),
Expand Down Expand Up @@ -135,8 +138,9 @@ class WebViewAppState extends ConsumerState<WebView> {

Future<NavigationActionPolicy?> _shouldOverrideUrlLoading(
InAppWebViewController controller, NavigationAction action) async {
// 1st check if url is not def. app url and open it in a browser or inApp.
WebViewGlobalController.ajaxSetHeaders(headers: ref.read(humHubProvider).customHeaders);

//Open in external browser
final url = action.request.url!.origin;
if (!url.startsWith(manifest.baseUrl) && action.isForMainFrame) {
authBrowser.launchUrl(action.request);
Expand Down Expand Up @@ -178,18 +182,22 @@ class WebViewAppState extends ConsumerState<WebView> {
return request;
}

Future<bool?> _onCreateWindow(controller, createWindowAction) async {
final urlToOpen = createWindowAction.request.url;
Future<bool?> _onCreateWindow(InAppWebViewController controller, CreateWindowAction createWindowAction) async {
WebUri? urlToOpen = createWindowAction.request.url;

if (urlToOpen == null) return Future.value(false); // Don't create a new window.
if (urlToOpen == null) return Future.value(false);
if (urlToOpen.rawValue.contains('file/download')) {
controller.loadUrl(urlRequest: createWindowAction.request);
return Future.value(false);
}

if (await canLaunchUrl(urlToOpen)) {
await launchUrl(urlToOpen, mode: LaunchMode.externalApplication); // Open the URL in the default browser.
await launchUrl(urlToOpen, mode: LaunchMode.externalApplication);
} else {
logError('Could not launch $urlToOpen');
}

return Future.value(true); // Allow creating a new window.
return Future.value(true);
}

_onLoadStop(InAppWebViewController controller, Uri? url) {
Expand Down Expand Up @@ -303,6 +311,33 @@ class WebViewAppState extends ConsumerState<WebView> {
}
}

void _onDownloadStartRequest(InAppWebViewController controller, DownloadStartRequest downloadStartRequest) async {
FileHandler(
downloadStartRequest: downloadStartRequest,
controller: controller,
onSuccess: (File file, String filename) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('File downloaded: $filename'),
action: SnackBarAction(
label: 'Open',
onPressed: () {
// Open the downloaded file
OpenFile.open(file.path);
},
),
),
);
},
onError: (er) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Something went wrong'),
),
);
}).download();
}

@override
void dispose() {
super.dispose();
Expand Down
127 changes: 127 additions & 0 deletions lib/util/file_handler.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import 'dart:convert';
import 'dart:io';

import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';

class FileHandler {
final InAppWebViewController controller;
final DownloadStartRequest downloadStartRequest;
final String? filename;
final Function(Exception ex)? onError;
final Function(File file, String filename) onSuccess;
static const String _jsCode = """
function downloadFile(url) {
fetch(url, {
headers: {
// Include necessary headers for authentication if needed
'Authorization': 'Bearer <your_token>'
}
})
.then(response => response.blob())
.then(blob => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function() {
const base64data = reader.result;
// Send the base64 content to Flutter
window.flutter_inappwebview.callHandler('downloadFile', base64data);
}
})
.catch(error => console.error('Error downloading file:', error));
}
""";

const FileHandler({
required this.controller,
required this.downloadStartRequest,
required this.onSuccess,
this.filename,
this.onError,

});

download() async {
try {
await controller.evaluateJavascript(source: _jsCode);
await controller.evaluateJavascript(source: "downloadFile('${downloadStartRequest.url.toString()}');");
controller.addJavaScriptHandler(
handlerName: 'downloadFile',
callback: (args) async {
String base64Data = args[0];
var (file, filename) = await _saveFile(base64Data);
onSuccess(file, filename);
});
} catch (er) {
if (er is Exception && onError != null) {
onError!(er);
}
//rethrow;
}
}

Future<(File file, String filename)> _saveFile(String base64Data) async {
// Decode base64 string to binary data
final decodedBytes = base64Decode(base64Data.split(",").last); // In case there's a base64 header

// Get the directory for storing files
Directory? directory = await _getDownloadDirectory();
if (directory == null) throw Exception("no_download_folder_found");
// Set a default filename if not provided
String endFilename = filename ?? downloadStartRequest.suggestedFilename ?? await _generateFilename();

// Generate a unique file path
String filePath = await _generateUniqueFilePath(directory, endFilename);

// Write the decoded data to the file
File file = File(filePath);
await file.writeAsBytes(decodedBytes);
if (!await file.exists()) throw Exception("file_was_not_created");
return (file, endFilename);
}

Future<String> _generateFilename() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
String appName = packageInfo.appName;
String filename = '${appName}file';

// Replace spaces with underscores
filename = filename.replaceAll(' ', '_');

return filename;
}

Future<String> _generateUniqueFilePath(Directory directory, String baseFilename) async {
String filePath = '${directory.path}/$baseFilename';
int counter = 1;

while (File(filePath).existsSync()) {
// Append a number in parentheses to the baseFilename
String newFilename = '($counter)$baseFilename';
filePath = '${directory.path}/$newFilename';
counter++;
}

return filePath;
}

Future<Directory?> _getDownloadDirectory() async {
Directory? directory;

if (Platform.isIOS) {
directory = await getApplicationDocumentsDirectory();
} else {
String check = "/storage/emulated/0/Download/";

bool dirDownloadExists = await Directory(check).exists();
if (dirDownloadExists) {
directory = Directory(check);
} else {
directory = await getExternalStorageDirectory();
}
}

return directory;
}
}
Loading

0 comments on commit 0bc3c0d

Please sign in to comment.