fullscreen: stream image in chunks instead of reading and sending all at once
This commit is contained in:
parent
6b299f6c86
commit
b228fcf55d
9 changed files with 177 additions and 106 deletions
|
@ -62,4 +62,6 @@ dependencies {
|
||||||
testImplementation 'junit:junit:4.13'
|
testImplementation 'junit:junit:4.13'
|
||||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||||
|
|
||||||
|
compileOnly rootProject.findProject(':streams_channel')
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,11 @@ import android.util.Log;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import app.loup.streams_channel.StreamsChannel;
|
||||||
import deckers.thibault.aves.channelhandlers.AppAdapterHandler;
|
import deckers.thibault.aves.channelhandlers.AppAdapterHandler;
|
||||||
import deckers.thibault.aves.channelhandlers.FileAdapterHandler;
|
import deckers.thibault.aves.channelhandlers.FileAdapterHandler;
|
||||||
import deckers.thibault.aves.channelhandlers.ImageFileHandler;
|
import deckers.thibault.aves.channelhandlers.ImageFileHandler;
|
||||||
|
import deckers.thibault.aves.channelhandlers.ImageStreamHandler;
|
||||||
import deckers.thibault.aves.channelhandlers.MediaStoreStreamHandler;
|
import deckers.thibault.aves.channelhandlers.MediaStoreStreamHandler;
|
||||||
import deckers.thibault.aves.channelhandlers.MetadataHandler;
|
import deckers.thibault.aves.channelhandlers.MetadataHandler;
|
||||||
import deckers.thibault.aves.utils.Constants;
|
import deckers.thibault.aves.utils.Constants;
|
||||||
|
@ -46,6 +48,9 @@ public class MainActivity extends FlutterActivity {
|
||||||
new MethodChannel(messenger, MetadataHandler.CHANNEL).setMethodCallHandler(new MetadataHandler(this));
|
new MethodChannel(messenger, MetadataHandler.CHANNEL).setMethodCallHandler(new MetadataHandler(this));
|
||||||
new EventChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandler(mediaStoreStreamHandler);
|
new EventChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandler(mediaStoreStreamHandler);
|
||||||
|
|
||||||
|
final StreamsChannel imageStreamChannel = new StreamsChannel(messenger, ImageStreamHandler.CHANNEL);
|
||||||
|
imageStreamChannel.setStreamHandlerFactory(arguments -> new ImageStreamHandler(this, arguments));
|
||||||
|
|
||||||
new MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler(
|
new MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler(
|
||||||
(call, result) -> {
|
(call, result) -> {
|
||||||
if (call.method.contentEquals("getSharedEntry")) {
|
if (call.method.contentEquals("getSharedEntry")) {
|
||||||
|
|
|
@ -1,32 +1,17 @@
|
||||||
package deckers.thibault.aves.channelhandlers;
|
package deckers.thibault.aves.channelhandlers;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.ImageDecoder;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
|
||||||
import com.bumptech.glide.request.FutureTarget;
|
|
||||||
import com.bumptech.glide.request.RequestOptions;
|
|
||||||
import com.bumptech.glide.request.target.Target;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import deckers.thibault.aves.decoder.VideoThumbnail;
|
|
||||||
import deckers.thibault.aves.model.ImageEntry;
|
import deckers.thibault.aves.model.ImageEntry;
|
||||||
import deckers.thibault.aves.model.provider.ImageProvider;
|
import deckers.thibault.aves.model.provider.ImageProvider;
|
||||||
import deckers.thibault.aves.model.provider.ImageProviderFactory;
|
import deckers.thibault.aves.model.provider.ImageProviderFactory;
|
||||||
import deckers.thibault.aves.utils.MimeTypes;
|
|
||||||
import io.flutter.plugin.common.MethodCall;
|
import io.flutter.plugin.common.MethodCall;
|
||||||
import io.flutter.plugin.common.MethodChannel;
|
import io.flutter.plugin.common.MethodChannel;
|
||||||
|
|
||||||
|
@ -51,9 +36,6 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
|
||||||
case "getImageEntry":
|
case "getImageEntry":
|
||||||
new Thread(() -> getImageEntry(call, new MethodResultWrapper(result))).start();
|
new Thread(() -> getImageEntry(call, new MethodResultWrapper(result))).start();
|
||||||
break;
|
break;
|
||||||
case "getImage":
|
|
||||||
new Thread(() -> getImage(call, new MethodResultWrapper(result))).start();
|
|
||||||
break;
|
|
||||||
case "getThumbnail":
|
case "getThumbnail":
|
||||||
new Thread(() -> getThumbnail(call, new MethodResultWrapper(result))).start();
|
new Thread(() -> getThumbnail(call, new MethodResultWrapper(result))).start();
|
||||||
break;
|
break;
|
||||||
|
@ -72,71 +54,6 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void getImage(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
|
||||||
String mimeType = call.argument("mimeType");
|
|
||||||
String uriString = call.argument("uri");
|
|
||||||
|
|
||||||
Uri uri = Uri.parse(uriString);
|
|
||||||
|
|
||||||
byte[] data = null;
|
|
||||||
if (mimeType != null && mimeType.startsWith(MimeTypes.VIDEO)) {
|
|
||||||
RequestOptions options = new RequestOptions()
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE);
|
|
||||||
FutureTarget<Bitmap> target = Glide.with(activity)
|
|
||||||
.asBitmap()
|
|
||||||
.apply(options)
|
|
||||||
.load(new VideoThumbnail(activity, uri))
|
|
||||||
.submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL);
|
|
||||||
try {
|
|
||||||
Bitmap bitmap = target.get();
|
|
||||||
if (bitmap != null) {
|
|
||||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
|
||||||
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
|
|
||||||
// Bitmap.CompressFormat.PNG is slower than JPEG
|
|
||||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
|
|
||||||
data = stream.toByteArray();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
result.error("getImage-video-exception", "failed to get image from uri=" + uri, e.getMessage());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Glide.with(activity).clear(target);
|
|
||||||
} else {
|
|
||||||
ContentResolver cr = activity.getContentResolver();
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && (MimeTypes.HEIC.equals(mimeType) || MimeTypes.HEIF.equals(mimeType))) {
|
|
||||||
// as of Flutter v1.15.17, Dart Image.memory cannot decode HEIF/HEIC images
|
|
||||||
// so we convert the image using Android native decoder
|
|
||||||
try {
|
|
||||||
ImageDecoder.Source source = ImageDecoder.createSource(cr, uri);
|
|
||||||
Bitmap bitmap = ImageDecoder.decodeBitmap(source);
|
|
||||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
|
||||||
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
|
|
||||||
// Bitmap.CompressFormat.PNG is slower than JPEG
|
|
||||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
|
|
||||||
data = stream.toByteArray();
|
|
||||||
} catch (IOException e) {
|
|
||||||
result.error("getImage-decode-exception", "failed to decode image from uri=" + uri, e.getMessage());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try (InputStream is = cr.openInputStream(uri)) {
|
|
||||||
if (is != null) {
|
|
||||||
data = getBytes(is);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
result.error("getImage-read-exception", "failed to get image from uri=" + uri, e.getMessage());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data != null) {
|
|
||||||
result.success(data);
|
|
||||||
} else {
|
|
||||||
result.error("getImage-null", "failed to get image from uri=" + uri, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void getThumbnail(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
private void getThumbnail(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||||
Map entryMap = call.argument("entry");
|
Map entryMap = call.argument("entry");
|
||||||
Integer width = call.argument("width");
|
Integer width = call.argument("width");
|
||||||
|
@ -261,19 +178,4 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// convenience methods
|
|
||||||
|
|
||||||
// InputStream.readAllBytes is only available from Java 9+
|
|
||||||
private byte[] getBytes(InputStream inputStream) throws IOException {
|
|
||||||
ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
|
|
||||||
int bufferSize = 1024;
|
|
||||||
byte[] buffer = new byte[bufferSize];
|
|
||||||
|
|
||||||
int len;
|
|
||||||
while ((len = inputStream.read(buffer)) != -1) {
|
|
||||||
byteBuffer.write(buffer, 0, len);
|
|
||||||
}
|
|
||||||
return byteBuffer.toByteArray();
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
package deckers.thibault.aves.channelhandlers;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.ImageDecoder;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
|
import com.bumptech.glide.request.FutureTarget;
|
||||||
|
import com.bumptech.glide.request.RequestOptions;
|
||||||
|
import com.bumptech.glide.request.target.Target;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import deckers.thibault.aves.decoder.VideoThumbnail;
|
||||||
|
import deckers.thibault.aves.utils.MimeTypes;
|
||||||
|
import io.flutter.plugin.common.EventChannel;
|
||||||
|
|
||||||
|
public class ImageStreamHandler implements EventChannel.StreamHandler {
|
||||||
|
public static final String CHANNEL = "deckers.thibault/aves/imagestream";
|
||||||
|
|
||||||
|
private Activity activity;
|
||||||
|
private Uri uri;
|
||||||
|
private String mimeType;
|
||||||
|
private EventChannel.EventSink eventSink;
|
||||||
|
private Handler handler;
|
||||||
|
|
||||||
|
public ImageStreamHandler(Activity activity, Object arguments) {
|
||||||
|
this.activity = activity;
|
||||||
|
if (arguments instanceof Map) {
|
||||||
|
Map argMap = (Map) arguments;
|
||||||
|
this.mimeType = (String) argMap.get("mimeType");
|
||||||
|
this.uri = Uri.parse((String) argMap.get("uri"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onListen(Object o, final EventChannel.EventSink eventSink) {
|
||||||
|
this.eventSink = eventSink;
|
||||||
|
this.handler = new Handler(Looper.getMainLooper());
|
||||||
|
new Thread(this::getImage).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCancel(Object o) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private void success(final byte[] bytes) {
|
||||||
|
handler.post(() -> eventSink.success(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void error(final String errorCode, final String errorMessage, final Object errorDetails) {
|
||||||
|
handler.post(() -> eventSink.error(errorCode, errorMessage, errorDetails));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void endOfStream() {
|
||||||
|
handler.post(() -> eventSink.endOfStream());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getImage() {
|
||||||
|
if (mimeType != null && mimeType.startsWith(MimeTypes.VIDEO)) {
|
||||||
|
RequestOptions options = new RequestOptions()
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.RESOURCE);
|
||||||
|
FutureTarget<Bitmap> target = Glide.with(activity)
|
||||||
|
.asBitmap()
|
||||||
|
.apply(options)
|
||||||
|
.load(new VideoThumbnail(activity, uri))
|
||||||
|
.submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL);
|
||||||
|
try {
|
||||||
|
Bitmap bitmap = target.get();
|
||||||
|
if (bitmap != null) {
|
||||||
|
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||||
|
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
|
||||||
|
// Bitmap.CompressFormat.PNG is slower than JPEG
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
|
||||||
|
success(stream.toByteArray());
|
||||||
|
} else {
|
||||||
|
error("getImage-video-null", "failed to get image from uri=" + uri, null);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
error("getImage-video-exception", "failed to get image from uri=" + uri, e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Glide.with(activity).clear(target);
|
||||||
|
} else {
|
||||||
|
ContentResolver cr = activity.getContentResolver();
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && (MimeTypes.HEIC.equals(mimeType) || MimeTypes.HEIF.equals(mimeType))) {
|
||||||
|
// as of Flutter v1.15.17, Dart Image.memory cannot decode HEIF/HEIC images
|
||||||
|
// so we convert the image using Android native decoder
|
||||||
|
try {
|
||||||
|
ImageDecoder.Source source = ImageDecoder.createSource(cr, uri);
|
||||||
|
Bitmap bitmap = ImageDecoder.decodeBitmap(source);
|
||||||
|
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||||
|
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
|
||||||
|
// Bitmap.CompressFormat.PNG is slower than JPEG
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
|
||||||
|
success(stream.toByteArray());
|
||||||
|
} catch (IOException e) {
|
||||||
|
error("getImage-image-decode-exception", "failed to decode image from uri=" + uri, e.getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try (InputStream is = cr.openInputStream(uri)) {
|
||||||
|
if (is != null) {
|
||||||
|
streamBytes(is);
|
||||||
|
} else {
|
||||||
|
error("getImage-image-read-null", "failed to get image from uri=" + uri, null);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
error("getImage-image-read-exception", "failed to get image from uri=" + uri, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endOfStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void streamBytes(InputStream inputStream) throws IOException {
|
||||||
|
int bufferSize = 2 << 17; // ~250k
|
||||||
|
byte[] buffer = new byte[bufferSize];
|
||||||
|
int len;
|
||||||
|
while ((len = inputStream.read(buffer)) != -1) {
|
||||||
|
// cannot decode image on Flutter side when using `buffer` directly...
|
||||||
|
byte[] sub = new byte[len];
|
||||||
|
System.arraycopy(buffer, 0, sub, 0, len);
|
||||||
|
success(sub);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,16 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/services/service_policy.dart';
|
import 'package:aves/services/service_policy.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:streams_channel/streams_channel.dart';
|
||||||
|
|
||||||
class ImageFileService {
|
class ImageFileService {
|
||||||
static const platform = MethodChannel('deckers.thibault/aves/image');
|
static const platform = MethodChannel('deckers.thibault/aves/image');
|
||||||
|
static final StreamsChannel streamsChannel = StreamsChannel('deckers.thibault/aves/imagestream');
|
||||||
|
|
||||||
static Future<void> getImageEntries() async {
|
static Future<void> getImageEntries() async {
|
||||||
try {
|
try {
|
||||||
|
@ -30,24 +34,30 @@ class ImageFileService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Uint8List> getImage(String uri, String mimeType) async {
|
static Future<Uint8List> getImage(String uri, String mimeType) {
|
||||||
try {
|
try {
|
||||||
final result = await platform.invokeMethod('getImage', <String, dynamic>{
|
final completer = Completer<Uint8List>();
|
||||||
|
final bytesBuilder = BytesBuilder(copy: false);
|
||||||
|
streamsChannel.receiveBroadcastStream(<String, dynamic>{
|
||||||
'uri': uri,
|
'uri': uri,
|
||||||
'mimeType': mimeType,
|
'mimeType': mimeType,
|
||||||
});
|
}).listen(
|
||||||
return result as Uint8List;
|
(data) => bytesBuilder.add(data as Uint8List),
|
||||||
|
onError: completer.completeError,
|
||||||
|
onDone: () => completer.complete(bytesBuilder.takeBytes()),
|
||||||
|
cancelOnError: true,
|
||||||
|
);
|
||||||
|
return completer.future;
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
debugPrint('getImage failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
debugPrint('getImage failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||||
}
|
}
|
||||||
return Uint8List(0);
|
return Future.sync(() => Uint8List(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Uint8List> getThumbnail(ImageEntry entry, int width, int height, {Object cancellationKey}) {
|
static Future<Uint8List> getThumbnail(ImageEntry entry, int width, int height, {Object cancellationKey}) {
|
||||||
return servicePolicy.call(
|
return servicePolicy.call(
|
||||||
() async {
|
() async {
|
||||||
if (width > 0 && height > 0) {
|
if (width > 0 && height > 0) {
|
||||||
// debugPrint('getThumbnail width=$width path=${entry.path}');
|
|
||||||
try {
|
try {
|
||||||
final result = await platform.invokeMethod('getThumbnail', <String, dynamic>{
|
final result = await platform.invokeMethod('getThumbnail', <String, dynamic>{
|
||||||
'entry': entry.toMap(),
|
'entry': entry.toMap(),
|
||||||
|
|
|
@ -4,17 +4,19 @@ import 'package:outline_material_icons/outline_material_icons.dart';
|
||||||
class EmptyContent extends StatelessWidget {
|
class EmptyContent extends StatelessWidget {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String text;
|
final String text;
|
||||||
|
final AlignmentGeometry alignment;
|
||||||
|
|
||||||
const EmptyContent({
|
const EmptyContent({
|
||||||
this.icon = OMIcons.photo,
|
this.icon = OMIcons.photo,
|
||||||
this.text = 'Nothing!',
|
this.text = 'Nothing!',
|
||||||
|
this.alignment = const FractionalOffset(.5, .35),
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
const color = Color(0xFF607D8B);
|
const color = Color(0xFF607D8B);
|
||||||
return Align(
|
return Align(
|
||||||
alignment: const FractionalOffset(.5, .35),
|
alignment: alignment,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/utils/constants.dart';
|
import 'package:aves/utils/constants.dart';
|
||||||
|
import 'package:aves/widgets/album/empty.dart';
|
||||||
import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
|
import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
|
||||||
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
|
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
|
||||||
import 'package:aves/widgets/common/image_providers/uri_picture_provider.dart';
|
import 'package:aves/widgets/common/image_providers/uri_picture_provider.dart';
|
||||||
import 'package:aves/widgets/fullscreen/video_view.dart';
|
import 'package:aves/widgets/fullscreen/video_view.dart';
|
||||||
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:outline_material_icons/outline_material_icons.dart';
|
||||||
import 'package:photo_view/photo_view.dart';
|
import 'package:photo_view/photo_view.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
|
@ -101,6 +103,11 @@ class ImageView extends StatelessWidget {
|
||||||
image: uriImage,
|
image: uriImage,
|
||||||
)
|
)
|
||||||
: thumbnailLoadingBuilder(context),
|
: thumbnailLoadingBuilder(context),
|
||||||
|
loadFailedChild: EmptyContent(
|
||||||
|
icon: OMIcons.errorOutline,
|
||||||
|
text: 'Oops!',
|
||||||
|
alignment: Alignment.center,
|
||||||
|
),
|
||||||
backgroundDecoration: backgroundDecoration,
|
backgroundDecoration: backgroundDecoration,
|
||||||
scaleStateChangedCallback: onScaleChanged,
|
scaleStateChangedCallback: onScaleChanged,
|
||||||
minScale: PhotoViewComputedScale.contained,
|
minScale: PhotoViewComputedScale.contained,
|
||||||
|
|
|
@ -399,6 +399,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
streams_channel:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: streams_channel
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.0"
|
||||||
string_scanner:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -70,6 +70,7 @@ dependencies:
|
||||||
screen:
|
screen:
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
sqflite:
|
sqflite:
|
||||||
|
streams_channel:
|
||||||
tuple:
|
tuple:
|
||||||
uuid:
|
uuid:
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue