I want to get data from Firestore, convert it into csv format, then share it using the user's method of choice (e.g. email). I'm using the csv package to convert the data to csv, the path_provider to get the directory to write the file to the phone, and the share_plus package to share the file.
However, when I tap a share method (e.g. Gmail, Outlook, Whatsapp), it opens the right app but then I get a message on the phone like "Unable to attach file" (but there is no error in my app). The file is definitely being written as I can read it and it comes back with the expected string. The ShareResultStatus that is returned by the share is 'successful' Can anyone figure it out what the problem is and how to fix it?
Here is my code:
Future exportData() async {
// Dummy data (in reality, this will come from Firestore)
List<List> dummyData = [
['Date', 'Piece', 'Rating'],
[DateTime.utc(2022, 05, 01), 'Sonata', 4],
[DateTime.utc(2022, 05, 02), 'Sonata', 2],
];
// Turn into CSV
String csvData = _convertToCsv(dummyData);
// Write to local disk
await _writeData(csvData);
// Share
String filePath = await _filePath;
final result =
await Share.shareFilesWithResult([filePath], mimeTypes: ['text/csv']);
print(result.status);
}
And here are the functions used in above code:
Future<String> get _filePath async {
final directory = await getApplicationDocumentsDirectory();
return '${directory.path}/my_practice_diary_data.csv';
}
Future<File> _writeData(String csvData) async {
String filePath = await _filePath;
final file = File(filePath);
return file.writeAsString(csvData);
}
String _convertToCsv(List<List> data) {
String result = const ListToCsvConverter().convert(data);
return result;
}
Note: I've tried doing it both as txt and csv files, got same result
*** EDIT (06/06/2022): After a lot of reading and watching youtube videos, I have come to realise that the problem is that the directory getApplicationDocumentsDirectory() is only accessible by my app (and hence the app that is is being shared to, like gmail, cannot read it).
For now I have worked around it by using the package mailer and using user's google credentials to send emails (I followed these helpful youtube tutorials: Flutter Tutorial - How To Send Email In Background [2021] Without Backend and Flutter Tutorial - Google SignIn [2021] With Firebase Auth - Android, iOS, Flutter Web.
However, it would still be nice to know a nice way to generate a file and share it using share_plus, as per my original question. I believe that this can be achieved via one of two ways:
Find a way to allow other apps to access this specific file in the app directory; or
Find a way to download the file into an external storage (like downloads folder), then share that. (I cannot find a way to do this on both Android and iOS).
Anyone who knows how to do these or any other solution to the problem, please share!
Related
In my app, I already integrated with google login and I successfully accessed my google drive folder and video files(mp4). But better player can play public video by modified url like this https://drive.google.com/uc?id=my_video_file_id.
I want to develop an app like Nplayer or Kudoplayer.
Can anyone guide me some scenario of Nplayer or Kudoplayer? and guide me how to play private drive's video using better player. Thanks.
Finally I got solution after research for a long time :-)
Summary answer for this question
Don't forget to add headers in BetterPlayerDataSource
Don't use this format "https://drive.google.com/uc?export=view&id=${fileId}" and
Use this format "https://www.googleapis.com/drive/v3/files/${fileId}?alt=media"
For guys who face issue like me, please follow these step
Enable Drive API
Integrate google login in your app with appropriate Scpoe, for me driveReadonlyScope is enough.
Retrieve fileId
Next step that I stuck is
Don't forget to add headers in BetterPlayerDataSource
Don't use this format "https://drive.google.com/uc?export=view&id=${fileId}" and
Use this format "https://www.googleapis.com/drive/v3/files/${fileId}?alt=media"
Like this
BetterPlayerDataSource betterPlayerDataSource = BetterPlayerDataSource(
BetterPlayerDataSourceType.network,
"https://www.googleapis.com/drive/v3/files/$fileId?alt=media",
videoExtension: ".mp4",
headers: getDataFromCache(CacheManagerKey.authHeader),
);
authHeader can get when you call signWithGoogle function,
I save it and retrieve later. In signWithGoogle function, you can get authHeader using this
Map<String, String> authHeader = await googleSignIn.currentUser!.authHeaders;
Bonus - Queries For You.
Get Share With Me Data = > q: "'sharedWithMe = true and (mimeType = 'video/mp4' or mimeType = 'application/vnd.google-apps.folder')"
Get Share Drive Data => q: "'shared_drive_id' in parents and trashed=false and (mimeType = 'video/mp4' or mimeType = 'application/vnd.google-apps.folder')",
Get My Drive Data => q: "'root' in parents and trashed=false and (mimeType = 'video/mp4' or mimeType = 'application/vnd.google-apps.folder')",
that's tricky.
I would propably try to attach approprieate headers so google drive would allow me to reach file using BetterPlayerDataSource class.
but being curious and checking how google would want it to be made... I got into spiral of google docs and I found this
https://cloud.google.com/blog/products/identity-security/enhancing-security-controls-for-google-drive-third-party-apps
Keep in mind that downloading file and streaming also differ. So if you want for your player to not lag you should stream.
Here's my issue :
I am creating a small application based on audio files stored on Google Drive, in Flutter.
I am using the drive api to make my requests, with these scopes in my google sign in :
GoogleSignIn _googleSignIn = GoogleSignIn(
scopes: [
'email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/contacts.readonly',
'https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/docs',
'https://www.googleapis.com/auth/drive.appdata',
],
);
I have an auth element and handle signing in and out. Until then, no issues.
I can also request my files with an implementation looking like this :
var api = widget.api.getAPI();
var files = await api.files.list($fields: '*');
This works perfectly, and so does :
var api = widget.api.getAPI();
var files = await api.files.get("myFileId"); (//does get a file instance)
But since I'd like to retrieve some of the Metadata included in my audio files, and since the drive API doesn't natively support extracting audio metadata and sending it as a google metadata, I thought I'd extract it with a partial download on the file itself.
Here's the catch : I can't seem to get the partial download to work.
Based on the doc, I thought the implementation would look something like this :
import 'package:googleapis/drive/v3.dart' as ga;
(...)
try {
var partiallyDownloadedFile = await api.files.get(
"myFileIdHere",
downloadOptions: ga.PartialDownloadOptions(ga.ByteRange(0, 10))); //should get a ga.Media instance
print("partial download succeeded");
print(partiallyDownloadedFile);
//(...do stuff...)
return;
} catch (err) {
print('Error occured : ');
print(err);
return;
}
But this always throws this error :
ApiRequestError(message: Attempting partial download but got invalid
'Content-Range' header (was: null, expected: bytes 0-10/).)
I tried using it on Wav files, but also MP4 files. The error is always the same, which leads me to believe it's my implementation that's somehow wrong, but I'm not sure what I'm supposed to do to fix it. Is it my request missing the header ? The response not including it ?
While very clear, that error doesn't help me troubleshoot my issue at all. I can't seem to find any documentation on how to conduct a partial media request. I haven't found any example projects to compare it with.
PartialDownloadOptions does not have much documentation.
I could handmake a partial request through the download links (which is how I can read the music to begin with) but the drive API supposedly allows this. Could anyone familiar with Flutter/the google APIs help me correct my implementation?
EDIT : This was due to an error within the commons library in the Dart google APIs, and was (at the very least superficially) fixed thanks to Kevmoo's efforts : https://github.com/google/googleapis.dart/issues/462
It was a Content-Range error happening due to browser specifications with access-control-expose-header compared to iOS/Android-type requests that typically expose every header.
void getHttp() async
{
print("Got called");
var response = await Dio().download('https://www.google.com/', 'assets/xx.html');
print("DDDDD:");
print(response);
}
The directory assets is present with read and write permissions.
I am calling this on the press of a button. "Got called" DOES get printed.
There are no errors present, still "DDDDD" doesn't get printed.
The xx.html doesn't get saved.
Where am I going wrong?
Based on documentation the syntax for Dio to download a file is
var response = await Dio().download('https://www.google.com/', <<Destination directory from which your app is running. Like internal storage or external storage.>>);
I guess you are providing the file path which is associated with the projects assets directory where the file cannot be downloaded since it is bundled with the app.
Use the following package to access the device file system and provide that path to download your file.
var response = await Dio().download('https://www.google.com/', <<Path from internal storage.>>);
Is there a way to store the path to file which user wants to upload, but doesn't have an internet connection (it's a PWA) and reupload it when a connection is back? Or maybe not store the path, but save the file outside browser storage, somewhere on the user's machine (even if it will require some acceptance from the user to allow the browser to read/write files), but I'm not sure if it's even allowed to do.
Currently, I'm storing the whole file as a base64 in IndexedDB, but it's crashing/slowing down the browser when it comes to reading big files (around 100MB). Also, I don't want to overload browser storage.
There's a couple of things to consider.
Storing the data you need to upload in IndexedDB and then reading that in later will be the most widely supported approach. As you say, though, it means taking up extra browser storage. One thing that might help is to skip the step of encoding the file in Base64 first, as in all modern browsers, IndexedDB will gladly store bytes directly for you as a Blob.
A more modern approach, but one that's not currently supported by non-Chromium browsers, would be to use the File System Access API. As described in this article, once you get the user's permission, you can save a handle to a file in IndexedDB, and then read the file later on (assuming the underlying file hasn't changed in the interim). This has the advantage of not duplicating the file's contents in IndexedDB, saving on storage space. Here's a code snippet, borrowed from the article:
import { get, set } from 'https://unpkg.com/idb-keyval#5.0.2/dist/esm/index.js';
const pre = document.querySelector('pre');
const button = document.querySelector('button');
button.addEventListener('click', async () => {
try {
// Try retrieving the file handle.
const fileHandleOrUndefined = await get('file');
if (fileHandleOrUndefined) {
pre.textContent =
`Retrieved file handle "${fileHandleOrUndefined.name}" from IndexedDB.`;
return;
}
// This always returns an array, but we just need the first entry.
const [fileHandle] = await window.showOpenFilePicker();
// Store the file handle.
await set('file', fileHandle);
pre.textContent =
`Stored file handle for "${fileHandle.name}" in IndexedDB.`;
} catch (error) {
alert(error.name, error.message);
}
});
Regardless of how you store the file, it would be helpful to use the Background Sync API when available (again, currently limited to Chromium browsers) to handle automating the upload once the network is available again.
I don't want to upload my code but I am having problems with retrieving multiple files from firebase storage. So in my app, I want to make users upload different files and while uploading it will show up in a list form.
The thing is I am able to save files into the firebase storage but I am freaking out trying to retrieve them into my application. I really hope if I get some to help me with this.
I don't exactly know what you're trying to achieve but maybe this would help.
When you upload your file to firestore storage, get a download link of that file and store it in the collection. Then you can retrieve the list of files you have uploaded.
Get the download URL of the uploaded file
UploadTask imageUploadTask = storageReference.putFile(file);
await imageUploadTask.whenComplete(() async {
mediaURL = await imageUploadTask.snapshot.ref.getDownloadURL();
print("download url = $mediaURL");
return mediaURL;
}).catchError((err){
print(err.toString());
});
Save URL to the collection
final CollectionReference _filesCollectionReference = FirebaseFirestore.instance.collection("files");
Map<String, String> file = {
"downloadURL" : url,
};
var result = await _filesCollectionReference.add(file);
Retrieve files
await _filesCollectionReference.get();