Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Some Questions & Requests #14

Closed
Skquark opened this issue Jul 23, 2018 · 7 comments
Closed

Some Questions & Requests #14

Skquark opened this issue Jul 23, 2018 · 7 comments
Labels
question Further information is requested

Comments

@Skquark
Copy link

Skquark commented Jul 23, 2018

First off, big thanks for this package, much needed, well implemented and very capable. I got it integrated into my app easily enough, just a few minor hickups when coding it in, but got past most of the tricky bits. Just a few things I'm trying to work out now... When embedding an image, it's saved there with {"embed":{"type":"image","source":"file:///... but naturally I needed to take the local file and upload it to server (I'm personally using Firebase Cloud Storage, but may also push to an ftp with Dio or something) to make it shared. Thought there would of been a built in way to do it, but I ended up writing a method to search through the Delta List for the embed attribute, look for the source that starts with file:/// then clean the path, upload the file, wait for complete, get the new image url, make the source attribute = url as an https://...jpg and I'd be good to go. After going through all that and getting the updated delta in place correctly, I end up with this exception:

I/flutter ( 3628): The following UnsupportedError was thrown building RawZefyrLine(state: _RawZefyrLineState#6e77e):
I/flutter ( 3628): Unsupported operation: Cannot extract a file path from a https URI
I/flutter ( 3628): When the exception was thrown, this was the stack:
I/flutter ( 3628): #0      _Uri.toFilePath (dart:core/uri.dart:2606:7)
I/flutter ( 3628): #1      new File.fromUri (dart:io/file.dart:263:49)

So either I'm missing a trick in the way to do a network embed source, or the ability is not in there yet and it's only meant for local files.. Suggestions?

Next thing was, I've got a place where I was stacking up a list of dynamic Delta descriptions from the database for viewing only, didn't need the extra controllers and focusnode stuff but got it in there anyways because I had to. So I got it mostly working, but a few problems I could use help with.. One is I wanted to height to be Flexible/Expaded to fit like I have the Text component, but I could only get it working with a fixed height in the parent container. No matter how I experimented with it, I couldn't make it sized dynamic. I hope there's a way to do it, just couldn't find it.
I also wanted to change the line-spacing, the fontFamily, fontSize, color like we have in the standard Text components, but didn't see any method to change the text style. I expected to see those options as part of the ZefyrThemeData class or ZefyrEditor properties, but wasn't there, I could live with those limitations in the editor, but where I'm displaying it, just doesn't look right..
What I would propose is a separate ZefyrViewer component without the extras in the Editor, but with more options for displaying and a flexible height without the ScrollView in it. I'm pretty sure most people using this would find that useful... There were more little things I was gonna bring up, but probably asking too much already. Thanks guys, much appreciated, looking great so far...

@pulyaevskiy
Copy link
Contributor

For the first issue, I don't have good documentation yet but you kind find an example in the example app. Specifically this bit here:

class CustomImageDelegate extends ZefyrDefaultImageDelegate {
@override
ImageProvider createImageProvider(String imageSource) {
// We use custom "asset" scheme to distinguish asset images from other files.
if (imageSource.startsWith('asset://')) {
return new AssetImage(imageSource.replaceFirst('asset://', ''));
} else {
return super.createImageProvider(imageSource);
}
}
}

This API is still experimental but it should be flexible enough to handle custom workflows for managing image assets.

For Firebase you'd probably need a delegate class similar to this:

class FirebaseStorageImageDelegate extends ZefyrDefaultImageDelegate {
  final FirebaseStorage storage;

  FirebaseStorageImageDelegate(this.storage);

  @override
  Future<String> pickImage(ImageSource source) async {
    // Default delegate uses standard image_picker plugin to choose images
    final imagePath = await super.pickImage(source);
    // Create file and upload to Firestore
    final file = new File.fromUri(Uri.parse(imagePath));
    final ref = storage.ref().child('unique-filename.png');

    await ref.putFile(file);

    // Return path to the Firestore Storage object. This value will be passed
    // to createImageProvider function below which is responsible for
    // resolving this value into an instance of ImageProvider: FileImage,
    // NetworkImage, etc.
    return ref.path;

    /// You can also define a custom scheme if you expect to use multiple
    /// mechanisms to store images. So for Firestore it could be:
    ///
    ///    return "firestore://${ref.path}";
  }

  @override
  ImageProvider createImageProvider(String imageSource) {
    // Convert imageSource to StorageReference
    final ref = storage.ref().child(imageSource);
    return createFirestoreImageProvider(ref);
  }

  ImageProvider createFirestoreImageProvider(StorageReference ref) {
    // We can't use standard NetworkImage provider here because
    // ref.getDownloadUrl() is asynchronous.
    // TODO: implement
  }
}

I have not used FirebaseStorage yet so above example is very much pseudo-code, but I hope it provides enough details on how this can be handled.


One is I wanted to height to be Flexible/Expaded to fit like I have the Text component

It should normally expand if inside of an Expanded widget, unless there is no scrollable around as well. I might need to see example code to help you more with this.

I also wanted to change the line-spacing, the fontFamily, fontSize

This all should be configurable with ZefyrThemeData, default TextStyle for paragraphs in Zefyr is defined in ZefyrThemeData.paragraphTheme. Note that you might also want to modify ZefyrThemeData.headingTheme and ZefyrThemeData.blockTheme to match your custom paragraph styles.

Theming is not very well document yet and I have this on my list, just need to find time for it.

What I would propose is a separate ZefyrViewer component without the extras in the Editor, but with more options for displaying and a flexible height without the ScrollView in it. I'm pretty sure most people using this would find that useful...

This makes sense to me. Curious about your use case for not having a ScrollView there. I suspect you'd still have it inside a scrollable anyway?

Note that you can make the editor read-only by setting enabled to false. This would essentially remove all the extras like toolbar and selection controls.

P.S: If you have more questions, please create separate issue for each question or suggestion. It would be easier to keep unrelated conversations separate and will allow me to help you more effectively.

@pulyaevskiy pulyaevskiy added the question Further information is requested label Jul 24, 2018
@pulyaevskiy
Copy link
Contributor

Closing this issue. Feel free to submit a new one if you have more questions,

@Skquark
Copy link
Author

Skquark commented Oct 24, 2018

I had this implemented well, was working until the 0.2.0 update that broke the createImageProvider line, and I had to replace it with the buildImage method that returns a widget instead of ImageProvider. So here's the updated code replacement that should work, in case anybody else needs it:

class FirebaseStorageImageDelegate extends ZefyrDefaultImageDelegate {
  final FirebaseStorage storage;
  FirebaseStorageImageDelegate(this.storage);

  @override
  Future<String> pickImage(ImageSource source) async {
    // Default delegate uses standard image_picker plugin to choose images
    final imagePath = await super.pickImage(source);
    // Create file and upload to Firestore
    Uri fileUri = Uri.parse(imagePath);
    final file = new File.fromUri(fileUri);
    String filename = path.basename(fileUri.path);

    final ref = FirebaseStorage.instance.ref().child(filename);

    StorageUploadTask fileUpload = ref.putFile(file);
    fileUpload.onComplete.then((upload) {
      print("Uploaded: " + ref.getDownloadURL().toString());
    });
    return "firestore://${ref.path}";
  }

  Widget createFirestoreImageWidget(StorageReference ref) {
    return Image.network(ref.getDownloadURL().toString());
  }
  @override
  Widget buildImage(BuildContext context, String imageSource) {
    if (imageSource.startsWith('asset://')) {
      return Image.asset(imageSource.replaceFirst('asset://', ''));
    } else if (imageSource.startsWith('firestore://')) {
      imageSource = imageSource.replaceFirst('firestore://', '');
      final ref = FirebaseStorage.instance.ref().child(imageSource);
      return createFirestoreImageWidget(ref);
    } else {
      return super.buildImage(context, imageSource);
    }
  }
}

@pulyaevskiy
Copy link
Contributor

Yes, there is a breaking change in 0.2.0. Make sure to always check changelog for updates, all breaking changes are announced there.

There is also a documentation page describing embedded images which should help migrating or implementing.

@aniketsongara
Copy link

aniketsongara commented Sep 10, 2020

@Skquark i try above code for Firebase Storage image but getting this error :

Image provider: NetworkImage("Instance of 'Future'", scale: 1.0)
Image key: NetworkImage("Instance of 'Future'", scale: 1.0)

This error has gone when i change below code :

class CustomImageDelegate extends ZefyrImageDelegate {
@OverRide
ImageSource get cameraSource => ImageSource.camera;

@OverRide
ImageSource get gallerySource => ImageSource.gallery;

@OverRide
Future pickImage(ImageSource source) async {
final image = await ImagePicker.pickImage(source: source);
if (image == null) return null;

String filename = DateTime.now().millisecondsSinceEpoch.toString();

final ref = FirebaseStorage.instance.ref().child(filename);

StorageUploadTask uploadTask =
    ref.putFile(image , StorageMetadata(contentType: 'image/jpeg'));

StorageTaskSnapshot storageTaskSnapshot = await uploadTask.onComplete;
return getImageUrl(storageTaskSnapshot);

}

Future getImageUrl(StorageTaskSnapshot snapshot) {
return snapshot.ref.getDownloadURL().then((value) => value);
}

@OverRide
Widget buildImage(BuildContext context, String imageSource) {
if (imageSource.startsWith('asset://')) {
return Image.asset(imageSource.replaceFirst('asset://', ''));
} else if (imageSource.startsWith('https://firebasestorage')) {
return Image.network(imageSource);
} else {
return buildImage(context, imageSource);
}
}
}

@madhavam12
Copy link

madhavam12 commented Jan 12, 2021

@Skquark Hi, hope you're doing okay. I used your code. It's awesome. But, how can I upload the photos after sometime, not exactly when i selected the pictures. I mean, It would cause unnecessarary uploads, as the user can also choose the wrong picture. An example would be uploading on a button click

@Skquark
Copy link
Author

Skquark commented Jan 15, 2021

That's a good point, what I did in my app was whenever I deleted a post/item or cancel, I would search the description text for the firestore image embedded then delete the refs from the server. Here's the code that worked for me:

void deleteImages(Delta delta) {
  for (var t in delta.toList()) {
    if (t.data.startsWith('firestore://')) {
      String refPath = t.data.replaceFirst('firestore://', '');
      final ref = FirebaseStorage.instance.ref().child(refPath);
      ref.delete();
    }
  }
}
void deleteImagesNotus(NotusDocument document) => deleteImages(document.toDelta());

That does the trick, but you make a point where when you're editing the rich text and upload an image, then delete it in the editor without saving, that junk image still gets uploaded without cleanup. I'm not actually sure how I'd get a callback from the ZepherEditor when a media block gets removed so we can delete upload.
I might as well share my full updated version of the CustomImageDeligate since the breaking changes in FilePicker and FirebaseFirestore:

class CustomImageDelegate implements ZefyrImageDelegate<ImageSource> {

  @override
  Future<String> pickImage(ImageSource source) async {
    final picker = ImagePicker();
    final file = await picker.getImage(source: source);
    if (file == null) return null;
    final ref = FirebaseStorage.instance.ref().child("images").child(file.path);
    UploadTask fileUpload = ref.putFile(File(file.path));
    await fileUpload.then((upload) async {
      String url = await upload.ref.getDownloadURL();
      print("Uploaded: " + url);
    });
    return "firestore://${ref.fullPath}";
  }

  @override
  Widget buildImage(BuildContext context, String key) {
    if (key.startsWith('asset://')) {
      return Image.asset(key.replaceFirst('asset://', ''));
    } else if (key.startsWith('firestore://')) {
      key = key.replaceFirst('firestore://', '');
      final ref = FirebaseStorage.instance.ref().child(key);
      return FutureBuilder<Widget>(
        future: createFirestoreImageWidget(context, ref),
        initialData: Container(),
        builder: (BuildContext context, AsyncSnapshot<Widget> snapshot) {
          if (snapshot == null) return Container();
          if (!snapshot.hasData) return Container();
          return snapshot.data;
        },
      );
    } else {
      print("returning createImageProvider which is now with buildImage " + key);
      if (key.startsWith('http://') || key.startsWith('https://')) return cachedNetworkImage(key);
      return null;
    }
  }

  @override
  ImageSource get cameraSource => ImageSource.camera;

  @override
  ImageSource get gallerySource => ImageSource.gallery;

  Future<Widget> createFirestoreImageWidget(BuildContext context, Reference ref) async {
    String downloadUrl = await ref.getDownloadURL();
    return GestureDetector(
      child: cachedNetworkImage(downloadUrl),
      onTap: () {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => FullScreenWrapper(
              imageProvider: NetworkImage(downloadUrl),
              maxScale: PhotoViewComputedScale.covered * 8,
              minScale: 0.3,
              loadingChild: CircularProgressIndicator(),
            ),
          ),
        );
      },
    );
  }
}

Hope that works and is helpful. Note that in my version I'm using CachedNetworkImage for efficiency and made the image expand to full screen when tapped, but adapt it to your own needs.
If someone comes up with a way to know when an image is deleted, let us know. I might tinker with it more later, but at the moment it wasn't a high priority since it works well enough.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

4 participants