refactored method/event channels, use ImageEntry instead of Map
This commit is contained in:
parent
b9cc2c076c
commit
d63e560e7d
16 changed files with 536 additions and 361 deletions
|
@ -1,284 +1,28 @@
|
|||
package deckers.thibault.aves;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.Key;
|
||||
import com.bumptech.glide.request.FutureTarget;
|
||||
import com.bumptech.glide.signature.ObjectKey;
|
||||
import com.drew.imaging.ImageMetadataReader;
|
||||
import com.drew.metadata.Metadata;
|
||||
import com.drew.metadata.exif.ExifSubIFDDirectory;
|
||||
import com.karumi.dexter.Dexter;
|
||||
import com.karumi.dexter.PermissionToken;
|
||||
import com.karumi.dexter.listener.PermissionDeniedResponse;
|
||||
import com.karumi.dexter.listener.PermissionGrantedResponse;
|
||||
import com.karumi.dexter.listener.PermissionRequest;
|
||||
import com.karumi.dexter.listener.single.PermissionListener;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import deckers.thibault.aves.model.ImageEntry;
|
||||
import deckers.thibault.aves.model.provider.MediaStoreImageProvider;
|
||||
import deckers.thibault.aves.utils.ShareUtils;
|
||||
import deckers.thibault.aves.utils.Utils;
|
||||
import deckers.thibault.aves.channelhandlers.AppAdapterHandler;
|
||||
import deckers.thibault.aves.channelhandlers.ImageDecodeHandler;
|
||||
import deckers.thibault.aves.channelhandlers.MediaStoreStreamHandler;
|
||||
import io.flutter.app.FlutterActivity;
|
||||
import io.flutter.plugin.common.EventChannel;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
import io.flutter.plugin.common.MethodChannel.Result;
|
||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||
|
||||
class ThumbnailFetcher {
|
||||
private Activity activity;
|
||||
private HashMap<String, AsyncTask> taskMap = new HashMap<>();
|
||||
|
||||
ThumbnailFetcher(Activity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
void fetch(ImageEntry entry, Integer width, Integer height, Result result) {
|
||||
BitmapWorkerTask.MyTaskParams params = new BitmapWorkerTask.MyTaskParams(entry, width, height, result, this::complete);
|
||||
AsyncTask task = new BitmapWorkerTask(activity).execute(params);
|
||||
taskMap.put(entry.getUri().toString(), task);
|
||||
}
|
||||
|
||||
void cancel(String uri) {
|
||||
AsyncTask task = taskMap.get(uri);
|
||||
if (task != null) task.cancel(true);
|
||||
taskMap.remove(uri);
|
||||
}
|
||||
|
||||
private void complete(String uri) {
|
||||
taskMap.remove(uri);
|
||||
}
|
||||
}
|
||||
import io.flutter.view.FlutterView;
|
||||
|
||||
public class MainActivity extends FlutterActivity {
|
||||
private static final String LOG_TAG = Utils.createLogTag(MainActivity.class);
|
||||
private static final String CHANNEL = "deckers.thibault.aves/mediastore";
|
||||
|
||||
private ThumbnailFetcher thumbnailFetcher;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
GeneratedPluginRegistrant.registerWith(this);
|
||||
|
||||
thumbnailFetcher = new ThumbnailFetcher(this);
|
||||
new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
|
||||
(call, result) -> {
|
||||
switch (call.method) {
|
||||
case "getImageEntries":
|
||||
getPermissionResult(result, this);
|
||||
break;
|
||||
case "getOverlayMetadata":
|
||||
String path = call.argument("path");
|
||||
getOverlayMetadata(result, path);
|
||||
break;
|
||||
case "getImageBytes": {
|
||||
Map map = call.argument("entry");
|
||||
Integer width = call.argument("width");
|
||||
Integer height = call.argument("height");
|
||||
ImageEntry entry = new ImageEntry(map);
|
||||
thumbnailFetcher.fetch(entry, width, height, result);
|
||||
break;
|
||||
}
|
||||
case "cancelGetImageBytes": {
|
||||
String uri = call.argument("uri");
|
||||
thumbnailFetcher.cancel(uri);
|
||||
result.success(null);
|
||||
break;
|
||||
}
|
||||
case "share": {
|
||||
String title = call.argument("title");
|
||||
Uri uri = Uri.parse(call.argument("uri"));
|
||||
String mimeType = call.argument("mimeType");
|
||||
ShareUtils.share(this, title, uri, mimeType);
|
||||
result.success(null);
|
||||
}
|
||||
default:
|
||||
result.notImplemented();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
MediaStoreStreamHandler mediaStoreStreamHandler = new MediaStoreStreamHandler();
|
||||
|
||||
public void getPermissionResult(final Result result, final Activity activity) {
|
||||
Dexter.withActivity(activity)
|
||||
.withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.withListener(new PermissionListener() {
|
||||
@Override
|
||||
public void onPermissionGranted(PermissionGrantedResponse response) {
|
||||
result.success(fetchAll(activity));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPermissionDenied(PermissionDeniedResponse response) {
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setMessage("This permission is needed for use this features of the app so please, allow it!");
|
||||
builder.setTitle("We need this permission");
|
||||
builder.setCancelable(false);
|
||||
builder.setPositiveButton("OK", (dialog, id) -> {
|
||||
dialog.cancel();
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
|
||||
intent.setData(uri);
|
||||
activity.startActivity(intent);
|
||||
});
|
||||
builder.setNegativeButton("Cancel", (dialog, id) -> dialog.cancel());
|
||||
AlertDialog alert = builder.create();
|
||||
alert.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPermissionRationaleShouldBeShown(PermissionRequest permission, final PermissionToken token) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setMessage("This permission is needed for use this features of the app so please, allow it!");
|
||||
builder.setTitle("We need this permission");
|
||||
builder.setCancelable(false);
|
||||
builder.setPositiveButton("OK", (dialog, id) -> {
|
||||
dialog.cancel();
|
||||
token.continuePermissionRequest();
|
||||
});
|
||||
builder.setNegativeButton("Cancel", (dialog, id) -> {
|
||||
dialog.cancel();
|
||||
token.cancelPermissionRequest();
|
||||
});
|
||||
AlertDialog alert = builder.create();
|
||||
alert.show();
|
||||
}
|
||||
}).check();
|
||||
}
|
||||
|
||||
List<Map> fetchAll(Activity activity) {
|
||||
return new MediaStoreImageProvider().fetchAll(activity).stream()
|
||||
.map(ImageEntry::toMap)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
void getOverlayMetadata (Result result, String path) {
|
||||
try (InputStream is = new FileInputStream(path)) {
|
||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||
ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
|
||||
Map<String, String> metadataMap = new HashMap<>();
|
||||
if (directory != null) {
|
||||
if (directory.containsTag(ExifSubIFDDirectory.TAG_FNUMBER)) {
|
||||
metadataMap.put("aperture", directory.getDescription(ExifSubIFDDirectory.TAG_FNUMBER));
|
||||
}
|
||||
if (directory.containsTag(ExifSubIFDDirectory.TAG_EXPOSURE_TIME)) {
|
||||
metadataMap.put("exposureTime", directory.getString(ExifSubIFDDirectory.TAG_EXPOSURE_TIME));
|
||||
}
|
||||
if (directory.containsTag(ExifSubIFDDirectory.TAG_FOCAL_LENGTH)) {
|
||||
metadataMap.put("focalLength", directory.getDescription(ExifSubIFDDirectory.TAG_FOCAL_LENGTH));
|
||||
}
|
||||
if (directory.containsTag(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT)) {
|
||||
metadataMap.put("iso", "ISO" + directory.getDescription(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT));
|
||||
}
|
||||
}
|
||||
result.success(metadataMap);
|
||||
} catch (FileNotFoundException e) {
|
||||
result.error("getOverlayMetadata-filenotfound", "failed to get metadata for path=" + path + " (" + e.getMessage() + ")", null);
|
||||
} catch (Exception e) {
|
||||
result.error("getOverlayMetadata-exception", "failed to get metadata for path=" + path, e);
|
||||
}
|
||||
FlutterView messenger = getFlutterView();
|
||||
new MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(new AppAdapterHandler(this));
|
||||
new MethodChannel(messenger, ImageDecodeHandler.CHANNEL).setMethodCallHandler(new ImageDecodeHandler(this, mediaStoreStreamHandler));
|
||||
new EventChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandler(mediaStoreStreamHandler);
|
||||
}
|
||||
}
|
||||
|
||||
class BitmapWorkerTask extends AsyncTask<BitmapWorkerTask.MyTaskParams, Void, BitmapWorkerTask.MyTaskResult> {
|
||||
private static final String LOG_TAG = Utils.createLogTag(BitmapWorkerTask.class);
|
||||
|
||||
static class MyTaskParams {
|
||||
ImageEntry entry;
|
||||
int width, height;
|
||||
Result result;
|
||||
Consumer<String> complete;
|
||||
|
||||
MyTaskParams(ImageEntry entry, int width, int height, Result result, Consumer<String> complete) {
|
||||
this.entry = entry;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.result = result;
|
||||
this.complete = complete;
|
||||
}
|
||||
}
|
||||
|
||||
static class MyTaskResult {
|
||||
MyTaskParams params;
|
||||
byte[] data;
|
||||
|
||||
MyTaskResult(MyTaskParams params, byte[] data) {
|
||||
this.params = params;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private Activity activity;
|
||||
|
||||
BitmapWorkerTask(Activity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MyTaskResult doInBackground(MyTaskParams... params) {
|
||||
MyTaskParams p = params[0];
|
||||
ImageEntry entry = p.entry;
|
||||
byte[] data = null;
|
||||
if (!this.isCancelled()) {
|
||||
// add signature to ignore cache for images which got modified but kept the same URI
|
||||
Key signature = new ObjectKey("" + entry.getDateModifiedSecs() + entry.getWidth() + entry.getOrientationDegrees());
|
||||
FutureTarget<Bitmap> target = Glide.with(activity)
|
||||
.asBitmap()
|
||||
.load(entry.getUri())
|
||||
.signature(signature)
|
||||
.submit(p.width, p.height);
|
||||
try {
|
||||
Bitmap bmp = target.get();
|
||||
if (bmp != null) {
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
bmp.compress(Bitmap.CompressFormat.JPEG, 90, stream);
|
||||
data = stream.toByteArray();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Log.d(LOG_TAG, "getImageBytes with uri=" + entry.getUri() + " interrupted");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
Glide.with(activity).clear(target);
|
||||
} else {
|
||||
Log.d(LOG_TAG, "getImageBytes with uri=" + entry.getUri() + " cancelled");
|
||||
}
|
||||
return new MyTaskResult(p, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(MyTaskResult result) {
|
||||
MethodChannel.Result r = result.params.result;
|
||||
String uri = result.params.entry.getUri().toString();
|
||||
result.params.complete.accept(uri);
|
||||
if (result.data != null) {
|
||||
r.success(result.data);
|
||||
} else {
|
||||
r.error("getImageBytes-null", "failed to get thumbnail for uri=" + uri, null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package deckers.thibault.aves.channelhandlers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class AppAdapterHandler implements MethodChannel.MethodCallHandler {
|
||||
public static final String CHANNEL = "deckers.thibault/aves/app";
|
||||
|
||||
private Context context;
|
||||
|
||||
public AppAdapterHandler(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
switch (call.method) {
|
||||
case "share": {
|
||||
String title = call.argument("title");
|
||||
Uri uri = Uri.parse(call.argument("uri"));
|
||||
String mimeType = call.argument("mimeType");
|
||||
share(context, title, uri, mimeType);
|
||||
result.success(null);
|
||||
}
|
||||
default:
|
||||
result.notImplemented();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void share(Context context, String title, Uri uri, String mimeType) {
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri);
|
||||
intent.setType(mimeType);
|
||||
context.startActivity(Intent.createChooser(intent, title));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
package deckers.thibault.aves.channelhandlers;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.provider.Settings;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.drew.imaging.ImageMetadataReader;
|
||||
import com.drew.metadata.Metadata;
|
||||
import com.drew.metadata.exif.ExifSubIFDDirectory;
|
||||
import com.karumi.dexter.Dexter;
|
||||
import com.karumi.dexter.PermissionToken;
|
||||
import com.karumi.dexter.listener.PermissionDeniedResponse;
|
||||
import com.karumi.dexter.listener.PermissionGrantedResponse;
|
||||
import com.karumi.dexter.listener.PermissionRequest;
|
||||
import com.karumi.dexter.listener.single.PermissionListener;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import deckers.thibault.aves.model.ImageEntry;
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class ImageDecodeHandler implements MethodChannel.MethodCallHandler {
|
||||
public static final String CHANNEL = "deckers.thibault/aves/image";
|
||||
|
||||
private Activity activity;
|
||||
private ImageDecodeTaskManager imageDecodeTaskManager;
|
||||
private MediaStoreStreamHandler mediaStoreStreamHandler;
|
||||
|
||||
public ImageDecodeHandler(Activity activity, MediaStoreStreamHandler mediaStoreStreamHandler) {
|
||||
this.activity = activity;
|
||||
imageDecodeTaskManager = new ImageDecodeTaskManager(activity);
|
||||
this.mediaStoreStreamHandler = mediaStoreStreamHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
switch (call.method) {
|
||||
case "getImageEntries":
|
||||
getPermissionResult(result, activity);
|
||||
break;
|
||||
case "getImageBytes": {
|
||||
Map map = call.argument("entry");
|
||||
Integer width = call.argument("width");
|
||||
Integer height = call.argument("height");
|
||||
if (map == null) {
|
||||
result.error("getImageBytes-args", "failed to get image bytes because 'entry' is null", null);
|
||||
return;
|
||||
}
|
||||
ImageEntry entry = new ImageEntry(map);
|
||||
imageDecodeTaskManager.fetch(result, entry, width, height);
|
||||
break;
|
||||
}
|
||||
case "cancelGetImageBytes": {
|
||||
String uri = call.argument("uri");
|
||||
imageDecodeTaskManager.cancel(uri);
|
||||
result.success(null);
|
||||
break;
|
||||
}
|
||||
case "getOverlayMetadata":
|
||||
String path = call.argument("path");
|
||||
getOverlayMetadata(result, path);
|
||||
break;
|
||||
default:
|
||||
result.notImplemented();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void getPermissionResult(final MethodChannel.Result result, final Activity activity) {
|
||||
Dexter.withActivity(activity)
|
||||
.withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.withListener(new PermissionListener() {
|
||||
@Override
|
||||
public void onPermissionGranted(PermissionGrantedResponse response) {
|
||||
mediaStoreStreamHandler.fetchAll(activity);
|
||||
result.success(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPermissionDenied(PermissionDeniedResponse response) {
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setMessage("This permission is needed for use this features of the app so please, allow it!");
|
||||
builder.setTitle("We need this permission");
|
||||
builder.setCancelable(false);
|
||||
builder.setPositiveButton("OK", (dialog, id) -> {
|
||||
dialog.cancel();
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
|
||||
intent.setData(uri);
|
||||
activity.startActivity(intent);
|
||||
});
|
||||
builder.setNegativeButton("Cancel", (dialog, id) -> dialog.cancel());
|
||||
AlertDialog alert = builder.create();
|
||||
alert.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPermissionRationaleShouldBeShown(PermissionRequest permission, final PermissionToken token) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setMessage("This permission is needed for use this features of the app so please, allow it!");
|
||||
builder.setTitle("We need this permission");
|
||||
builder.setCancelable(false);
|
||||
builder.setPositiveButton("OK", (dialog, id) -> {
|
||||
dialog.cancel();
|
||||
token.continuePermissionRequest();
|
||||
});
|
||||
builder.setNegativeButton("Cancel", (dialog, id) -> {
|
||||
dialog.cancel();
|
||||
token.cancelPermissionRequest();
|
||||
});
|
||||
AlertDialog alert = builder.create();
|
||||
alert.show();
|
||||
}
|
||||
}).check();
|
||||
}
|
||||
|
||||
private void getOverlayMetadata(MethodChannel.Result result, String path) {
|
||||
try (InputStream is = new FileInputStream(path)) {
|
||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||
ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
|
||||
Map<String, String> metadataMap = new HashMap<>();
|
||||
if (directory != null) {
|
||||
if (directory.containsTag(ExifSubIFDDirectory.TAG_FNUMBER)) {
|
||||
metadataMap.put("aperture", directory.getDescription(ExifSubIFDDirectory.TAG_FNUMBER));
|
||||
}
|
||||
if (directory.containsTag(ExifSubIFDDirectory.TAG_EXPOSURE_TIME)) {
|
||||
metadataMap.put("exposureTime", directory.getString(ExifSubIFDDirectory.TAG_EXPOSURE_TIME));
|
||||
}
|
||||
if (directory.containsTag(ExifSubIFDDirectory.TAG_FOCAL_LENGTH)) {
|
||||
metadataMap.put("focalLength", directory.getDescription(ExifSubIFDDirectory.TAG_FOCAL_LENGTH));
|
||||
}
|
||||
if (directory.containsTag(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT)) {
|
||||
metadataMap.put("iso", "ISO" + directory.getDescription(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT));
|
||||
}
|
||||
}
|
||||
result.success(metadataMap);
|
||||
} catch (FileNotFoundException e) {
|
||||
result.error("getOverlayMetadata-filenotfound", "failed to get metadata for path=" + path + " (" + e.getMessage() + ")", null);
|
||||
} catch (Exception e) {
|
||||
result.error("getOverlayMetadata-exception", "failed to get metadata for path=" + path, e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package deckers.thibault.aves.channelhandlers;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.Key;
|
||||
import com.bumptech.glide.request.FutureTarget;
|
||||
import com.bumptech.glide.signature.ObjectKey;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import deckers.thibault.aves.model.ImageEntry;
|
||||
import deckers.thibault.aves.utils.Utils;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, ImageDecodeTask.Result> {
|
||||
private static final String LOG_TAG = Utils.createLogTag(ImageDecodeTask.class);
|
||||
|
||||
static class Params {
|
||||
ImageEntry entry;
|
||||
int width, height;
|
||||
MethodChannel.Result result;
|
||||
Consumer<String> complete;
|
||||
|
||||
Params(ImageEntry entry, int width, int height, MethodChannel.Result result, Consumer<String> complete) {
|
||||
this.entry = entry;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.result = result;
|
||||
this.complete = complete;
|
||||
}
|
||||
}
|
||||
|
||||
static class Result {
|
||||
Params params;
|
||||
byte[] data;
|
||||
|
||||
Result(Params params, byte[] data) {
|
||||
this.params = params;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private Activity activity;
|
||||
|
||||
ImageDecodeTask(Activity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result doInBackground(Params... params) {
|
||||
Params p = params[0];
|
||||
ImageEntry entry = p.entry;
|
||||
byte[] data = null;
|
||||
if (!this.isCancelled()) {
|
||||
// add signature to ignore cache for images which got modified but kept the same URI
|
||||
Key signature = new ObjectKey("" + entry.getDateModifiedSecs() + entry.getWidth() + entry.getOrientationDegrees());
|
||||
FutureTarget<Bitmap> target = Glide.with(activity)
|
||||
.asBitmap()
|
||||
.load(entry.getUri())
|
||||
.signature(signature)
|
||||
.submit(p.width, p.height);
|
||||
try {
|
||||
Bitmap bmp = target.get();
|
||||
if (bmp != null) {
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
bmp.compress(Bitmap.CompressFormat.JPEG, 90, stream);
|
||||
data = stream.toByteArray();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Log.d(LOG_TAG, "getImageBytes with uri=" + entry.getUri() + " interrupted");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
Glide.with(activity).clear(target);
|
||||
} else {
|
||||
Log.d(LOG_TAG, "getImageBytes with uri=" + entry.getUri() + " cancelled");
|
||||
}
|
||||
return new Result(p, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Result result) {
|
||||
MethodChannel.Result r = result.params.result;
|
||||
String uri = result.params.entry.getUri().toString();
|
||||
result.params.complete.accept(uri);
|
||||
if (result.data != null) {
|
||||
r.success(result.data);
|
||||
} else {
|
||||
r.error("getImageBytes-null", "failed to get thumbnail for uri=" + uri, null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package deckers.thibault.aves.channelhandlers;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import deckers.thibault.aves.model.ImageEntry;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class ImageDecodeTaskManager {
|
||||
private Activity activity;
|
||||
private HashMap<String, AsyncTask> taskMap = new HashMap<>();
|
||||
|
||||
ImageDecodeTaskManager(Activity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
void fetch(MethodChannel.Result result, ImageEntry entry, Integer width, Integer height) {
|
||||
ImageDecodeTask.Params params = new ImageDecodeTask.Params(entry, width, height, result, this::complete);
|
||||
AsyncTask task = new ImageDecodeTask(activity).execute(params);
|
||||
taskMap.put(entry.getUri().toString(), task);
|
||||
}
|
||||
|
||||
void cancel(String uri) {
|
||||
AsyncTask task = taskMap.get(uri);
|
||||
if (task != null) task.cancel(true);
|
||||
taskMap.remove(uri);
|
||||
}
|
||||
|
||||
private void complete(String uri) {
|
||||
taskMap.remove(uri);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package deckers.thibault.aves.channelhandlers;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import deckers.thibault.aves.model.ImageEntry;
|
||||
import deckers.thibault.aves.model.provider.MediaStoreImageProvider;
|
||||
import deckers.thibault.aves.utils.Utils;
|
||||
import io.flutter.plugin.common.EventChannel;
|
||||
|
||||
public class MediaStoreStreamHandler implements EventChannel.StreamHandler {
|
||||
public static final String CHANNEL = "deckers.thibault/aves/mediastore";
|
||||
|
||||
private static final String LOG_TAG = Utils.createLogTag(MediaStoreStreamHandler.class);
|
||||
|
||||
private EventChannel.EventSink eventSink;
|
||||
|
||||
@Override
|
||||
public void onListen(Object args, final EventChannel.EventSink events) {
|
||||
Log.w(LOG_TAG, "onListen with args=" + args);
|
||||
eventSink = events;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel(Object args) {
|
||||
Log.w(LOG_TAG, "onCancel with args=" + args);
|
||||
}
|
||||
|
||||
void fetchAll(Activity activity) {
|
||||
Log.d(LOG_TAG, "fetchAll start");
|
||||
Stream<ImageEntry> stream = new MediaStoreImageProvider().fetchAll(activity);
|
||||
stream.map(ImageEntry::toMap)
|
||||
.forEach(entry -> eventSink.success(entry));
|
||||
eventSink.endOfStream();
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import android.util.Log;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import deckers.thibault.aves.model.ImageEntry;
|
||||
import deckers.thibault.aves.utils.Utils;
|
||||
|
@ -39,11 +40,11 @@ public class MediaStoreImageProvider {
|
|||
+ " OR " + MediaStore.Files.FileColumns.MEDIA_TYPE + "=" + MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO;
|
||||
|
||||
|
||||
public List<ImageEntry> fetchAll(Activity activity) {
|
||||
public Stream<ImageEntry> fetchAll(Activity activity) {
|
||||
return fetch(activity, FILES_URI);
|
||||
}
|
||||
|
||||
private List<ImageEntry> fetch(final Activity activity, final Uri queryUri) {
|
||||
private Stream<ImageEntry> fetch(final Activity activity, final Uri queryUri) {
|
||||
ArrayList<ImageEntry> entries = new ArrayList<>();
|
||||
|
||||
// URI should refer to the "files" table, not to the "images" or "videos" one,
|
||||
|
@ -109,6 +110,6 @@ public class MediaStoreImageProvider {
|
|||
} catch (Exception e) {
|
||||
Log.d(LOG_TAG, "failed to get entries", e);
|
||||
}
|
||||
return entries;
|
||||
return entries.stream();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
package deckers.thibault.aves.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
public class ShareUtils {
|
||||
public static void share(Activity activity, String title, Uri uri, String mimeType) {
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri);
|
||||
intent.setType(mimeType);
|
||||
activity.startActivity(Intent.createChooser(intent, title));
|
||||
}
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/model/android_app_service.dart';
|
||||
import 'package:aves/model/image_decode_service.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/image_fetcher.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
|
@ -25,10 +26,10 @@ class Blurred extends StatelessWidget {
|
|||
}
|
||||
|
||||
class FullscreenTopOverlay extends StatelessWidget {
|
||||
final List<Map> entries;
|
||||
final List<ImageEntry> entries;
|
||||
final int index;
|
||||
|
||||
Map get entry => entries[index];
|
||||
ImageEntry get entry => entries[index];
|
||||
|
||||
const FullscreenTopOverlay({Key key, this.entries, this.index}) : super(key: key);
|
||||
|
||||
|
@ -55,12 +56,12 @@ class FullscreenTopOverlay extends StatelessWidget {
|
|||
delete() {}
|
||||
|
||||
share() {
|
||||
ImageFetcher.share(entry['uri'], entry['mimeType']);
|
||||
AndroidAppService.share(entry.uri, entry.mimeType);
|
||||
}
|
||||
}
|
||||
|
||||
class FullscreenBottomOverlay extends StatefulWidget {
|
||||
final List<Map> entries;
|
||||
final List<ImageEntry> entries;
|
||||
final int index;
|
||||
|
||||
const FullscreenBottomOverlay({Key key, this.entries, this.index}) : super(key: key);
|
||||
|
@ -73,7 +74,7 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
|
|||
Future<Map> _detailLoader;
|
||||
Map _lastDetails;
|
||||
|
||||
Map get entry => widget.entries[widget.index];
|
||||
ImageEntry get entry => widget.entries[widget.index];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -88,7 +89,7 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
|
|||
}
|
||||
|
||||
initDetailLoader() {
|
||||
_detailLoader = ImageFetcher.getOverlayMetadata(entry['path']);
|
||||
_detailLoader = ImageDecodeService.getOverlayMetadata(entry.path);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -96,7 +97,7 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
|
|||
var mediaQuery = MediaQuery.of(context);
|
||||
final screenWidth = mediaQuery.size.width;
|
||||
final viewInsets = mediaQuery.viewInsets;
|
||||
final date = ImageEntry.getBestDate(entry);
|
||||
final date = entry.getBestDate();
|
||||
final subRowWidth = min(400.0, screenWidth);
|
||||
return Blurred(
|
||||
child: IgnorePointer(
|
||||
|
@ -121,7 +122,7 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
|
|||
SizedBox(
|
||||
width: screenWidth,
|
||||
child: Text(
|
||||
entry['title'],
|
||||
entry.title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
@ -133,7 +134,7 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
|
|||
Icon(Icons.calendar_today, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Expanded(child: Text('${DateFormat.yMMMd().format(date)} – ${DateFormat.Hm().format(date)}')),
|
||||
Expanded(child: Text('${entry['width']} × ${entry['height']}')),
|
||||
Expanded(child: Text('${entry.width} × ${entry.height}')),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -2,12 +2,13 @@ import 'dart:io';
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/image_fullscreen_overlay.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:photo_view/photo_view_gallery.dart';
|
||||
|
||||
class ImageFullscreenPage extends StatefulWidget {
|
||||
final List<Map> entries;
|
||||
final List<ImageEntry> entries;
|
||||
final String initialUri;
|
||||
|
||||
const ImageFullscreenPage({
|
||||
|
@ -27,12 +28,12 @@ class ImageFullscreenPageState extends State<ImageFullscreenPage> with SingleTic
|
|||
AnimationController _overlayAnimationController;
|
||||
Animation<Offset> _topOverlayOffset, _bottomOverlayOffset;
|
||||
|
||||
List<Map> get entries => widget.entries;
|
||||
List<ImageEntry> get entries => widget.entries;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final index = entries.indexWhere((entry) => entry['uri'] == widget.initialUri);
|
||||
final index = entries.indexWhere((entry) => entry.uri == widget.initialUri);
|
||||
_currentPage = max(0, index);
|
||||
_pageController = PageController(initialPage: _currentPage);
|
||||
_overlayAnimationController = AnimationController(
|
||||
|
@ -61,8 +62,8 @@ class ImageFullscreenPageState extends State<ImageFullscreenPage> with SingleTic
|
|||
builder: (context, index) {
|
||||
final entry = entries[index];
|
||||
return PhotoViewGalleryPageOptions(
|
||||
imageProvider: FileImage(File(entry['path'])),
|
||||
heroTag: entry['uri'],
|
||||
imageProvider: FileImage(File(entry.path)),
|
||||
heroTag: entry.uri,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
initialScale: PhotoViewComputedScale.contained,
|
||||
onTapUp: (tapContext, details, value) => _overlayVisible.value = !_overlayVisible.value,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:aves/common/fake_app_bar.dart';
|
||||
import 'package:aves/model/image_fetcher.dart';
|
||||
import 'package:aves/model/image_decode_service.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/thumbnail_collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
@ -29,13 +30,24 @@ class HomePage extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
Future<List<Map>> _entryListLoader;
|
||||
static const EventChannel eventChannel = EventChannel('deckers.thibault/aves/mediastore');
|
||||
|
||||
List<ImageEntry> entries = List();
|
||||
bool done = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
imageCache.maximumSizeBytes = 100 * 1024 * 1024;
|
||||
_entryListLoader = ImageFetcher.getImageEntries();
|
||||
eventChannel.receiveBroadcastStream().cast<Map>().listen(
|
||||
(entryMap) => setState(() => entries.add(ImageEntry.fromMap(entryMap))),
|
||||
onDone: () {
|
||||
debugPrint('mediastore stream done');
|
||||
setState(() => done = true);
|
||||
},
|
||||
onError: (error) => debugPrint('mediastore stream error=$error'),
|
||||
);
|
||||
ImageDecodeService.getImageEntries();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -43,18 +55,9 @@ class _HomePageState extends State<HomePage> {
|
|||
return Scaffold(
|
||||
// fake app bar so that content is safe from status bar, even though we use a SliverAppBar
|
||||
appBar: FakeAppBar(),
|
||||
body: Container(
|
||||
child: FutureBuilder(
|
||||
future: _entryListLoader,
|
||||
builder: (futureContext, AsyncSnapshot<List<Map>> snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
|
||||
return ThumbnailCollection(entries: snapshot.data);
|
||||
}
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
),
|
||||
body: ThumbnailCollection(
|
||||
entries: entries,
|
||||
done: done,
|
||||
),
|
||||
resizeToAvoidBottomInset: false,
|
||||
);
|
||||
|
|
18
lib/model/android_app_service.dart
Normal file
18
lib/model/android_app_service.dart
Normal file
|
@ -0,0 +1,18 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class AndroidAppService {
|
||||
static const platform = const MethodChannel('deckers.thibault/aves/app');
|
||||
|
||||
static share(String uri, String mimeType) async {
|
||||
try {
|
||||
await platform.invokeMethod('share', <String, dynamic>{
|
||||
'title': 'Share via:',
|
||||
'uri': uri,
|
||||
'mimeType': mimeType,
|
||||
});
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('share failed with exception=${e.message}');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,28 +1,25 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class ImageFetcher {
|
||||
static const platform = const MethodChannel('deckers.thibault.aves/mediastore');
|
||||
class ImageDecodeService {
|
||||
static const platform = const MethodChannel('deckers.thibault/aves/image');
|
||||
|
||||
static Future<List<Map>> getImageEntries() async {
|
||||
static getImageEntries() async {
|
||||
try {
|
||||
final result = await platform.invokeMethod('getImageEntries');
|
||||
final entries = (result as List).cast<Map>();
|
||||
debugPrint('getImageEntries found ${entries.length} entries');
|
||||
return entries;
|
||||
await platform.invokeMethod('getImageEntries');
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getImageEntries failed with exception=${e.message}');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
static Future<Uint8List> getImageBytes(Map entry, int width, int height) async {
|
||||
debugPrint('getImageBytes with uri=${entry['uri']}');
|
||||
static Future<Uint8List> getImageBytes(ImageEntry entry, int width, int height) async {
|
||||
debugPrint('getImageBytes with uri=${entry.uri}');
|
||||
try {
|
||||
final result = await platform.invokeMethod('getImageBytes', <String, dynamic>{
|
||||
'entry': entry,
|
||||
'entry': entry.toMap(),
|
||||
'width': width,
|
||||
'height': height,
|
||||
});
|
||||
|
@ -55,16 +52,4 @@ class ImageFetcher {
|
|||
}
|
||||
return Map();
|
||||
}
|
||||
|
||||
static share(String uri, String mimeType) async {
|
||||
try {
|
||||
await platform.invokeMethod('share', <String, dynamic>{
|
||||
'title': 'Share via:',
|
||||
'uri': uri,
|
||||
'mimeType': mimeType,
|
||||
});
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('share failed with exception=${e.message}');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +1,78 @@
|
|||
class ImageEntry {
|
||||
static DateTime getBestDate(Map entry) {
|
||||
final dateTakenMillis = entry['sourceDateTakenMillis'] as int;
|
||||
if (dateTakenMillis != null && dateTakenMillis > 0) return DateTime.fromMillisecondsSinceEpoch(dateTakenMillis);
|
||||
String uri;
|
||||
String path;
|
||||
int contentId;
|
||||
String mimeType;
|
||||
int width;
|
||||
int height;
|
||||
int orientationDegrees;
|
||||
int sizeBytes;
|
||||
String title;
|
||||
int dateModifiedSecs;
|
||||
int sourceDateTakenMillis;
|
||||
String bucketDisplayName;
|
||||
int durationMillis;
|
||||
|
||||
final dateModifiedSecs = entry['dateModifiedSecs'] as int;
|
||||
ImageEntry({
|
||||
this.uri,
|
||||
this.path,
|
||||
this.contentId,
|
||||
this.mimeType,
|
||||
this.width,
|
||||
this.height,
|
||||
this.orientationDegrees,
|
||||
this.sizeBytes,
|
||||
this.title,
|
||||
this.dateModifiedSecs,
|
||||
this.sourceDateTakenMillis,
|
||||
this.bucketDisplayName,
|
||||
this.durationMillis,
|
||||
});
|
||||
|
||||
factory ImageEntry.fromMap(Map map) {
|
||||
return ImageEntry(
|
||||
uri: map['uri'],
|
||||
path: map['path'],
|
||||
contentId: map['contentId'],
|
||||
mimeType: map['mimeType'],
|
||||
width: map['width'],
|
||||
height: map['height'],
|
||||
orientationDegrees: map['orientationDegrees'],
|
||||
sizeBytes: map['sizeBytes'],
|
||||
title: map['title'],
|
||||
dateModifiedSecs: map['dateModifiedSecs'],
|
||||
sourceDateTakenMillis: map['sourceDateTakenMillis'],
|
||||
bucketDisplayName: map['bucketDisplayName'],
|
||||
durationMillis: map['durationMillis'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'uri': uri,
|
||||
'path': path,
|
||||
'contentId': contentId,
|
||||
'mimeType': mimeType,
|
||||
'width': width,
|
||||
'height': height,
|
||||
'orientationDegrees': orientationDegrees,
|
||||
'sizeBytes': sizeBytes,
|
||||
'title': title,
|
||||
'dateModifiedSecs': dateModifiedSecs,
|
||||
'sourceDateTakenMillis': sourceDateTakenMillis,
|
||||
'bucketDisplayName': bucketDisplayName,
|
||||
'durationMillis': durationMillis,
|
||||
};
|
||||
}
|
||||
|
||||
DateTime getBestDate() {
|
||||
if (sourceDateTakenMillis != null && sourceDateTakenMillis > 0) return DateTime.fromMillisecondsSinceEpoch(sourceDateTakenMillis);
|
||||
if (dateModifiedSecs != null && dateModifiedSecs > 0) return DateTime.fromMillisecondsSinceEpoch(dateModifiedSecs * 1000);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static DateTime getDayTaken(Map entry) {
|
||||
final d = getBestDate(entry);
|
||||
DateTime getDayTaken() {
|
||||
final d = getBestDate();
|
||||
return d == null ? null : DateTime(d.year, d.month, d.day);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:aves/model/image_fetcher.dart';
|
||||
import 'package:aves/model/image_decode_service.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/mime_types.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:transparent_image/transparent_image.dart';
|
||||
|
||||
class Thumbnail extends StatefulWidget {
|
||||
final Map entry;
|
||||
final ImageEntry entry;
|
||||
final double extent;
|
||||
final double devicePixelRatio;
|
||||
|
||||
|
@ -25,34 +26,31 @@ class Thumbnail extends StatefulWidget {
|
|||
class ThumbnailState extends State<Thumbnail> {
|
||||
Future<Uint8List> _byteLoader;
|
||||
|
||||
String get mimeType => widget.entry['mimeType'];
|
||||
String get mimeType => widget.entry.mimeType;
|
||||
|
||||
String get uri => widget.entry['uri'];
|
||||
String get uri => widget.entry.uri;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// debugPrint('initState with uri=$uri entry=${widget.entry['path']}');
|
||||
initByteLoader();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(Thumbnail oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (uri == oldWidget.entry['uri'] && widget.extent == oldWidget.extent) return;
|
||||
// debugPrint('didUpdateWidget FROM uri=${oldWidget.entry['uri']} TO uri=$uri entry=${widget.entry['path']}');
|
||||
if (uri == oldWidget.entry.uri && widget.extent == oldWidget.extent) return;
|
||||
initByteLoader();
|
||||
}
|
||||
|
||||
initByteLoader() {
|
||||
final dim = (widget.extent * widget.devicePixelRatio).round();
|
||||
_byteLoader = ImageFetcher.getImageBytes(widget.entry, dim, dim);
|
||||
_byteLoader = ImageDecodeService.getImageBytes(widget.entry, dim, dim);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// debugPrint('dispose with uri=$uri entry=${widget.entry['path']}');
|
||||
ImageFetcher.cancelGetImageBytes(uri);
|
||||
ImageDecodeService.cancelGetImageBytes(uri);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
|
@ -10,17 +10,26 @@ import 'package:flutter_sticky_header/flutter_sticky_header.dart';
|
|||
import 'package:intl/intl.dart';
|
||||
|
||||
class ThumbnailCollection extends StatelessWidget {
|
||||
final List<Map> entries;
|
||||
final Map<DateTime, List<Map>> sections;
|
||||
final List<ImageEntry> entries;
|
||||
final bool done;
|
||||
final Map<DateTime, List<ImageEntry>> sections;
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
ThumbnailCollection({Key key, this.entries})
|
||||
: sections = groupBy(entries, ImageEntry.getDayTaken),
|
||||
ThumbnailCollection({Key key, this.entries, this.done})
|
||||
: sections = groupBy(entries, (entry) => entry.getDayTaken()),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// debugPrint('$runtimeType build with sections=${sections.length}');
|
||||
if (!done) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'streamed ${entries.length} items',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
);
|
||||
}
|
||||
return DraggableScrollbar.arrows(
|
||||
labelTextBuilder: (double offset) => Text(
|
||||
"${offset ~/ 1}",
|
||||
|
@ -46,8 +55,8 @@ class ThumbnailCollection extends StatelessWidget {
|
|||
}
|
||||
|
||||
class SectionSliver extends StatelessWidget {
|
||||
final List<Map> entries;
|
||||
final Map<DateTime, List<Map>> sections;
|
||||
final List<ImageEntry> entries;
|
||||
final Map<DateTime, List<ImageEntry>> sections;
|
||||
final DateTime sectionKey;
|
||||
|
||||
const SectionSliver({
|
||||
|
@ -88,13 +97,13 @@ class SectionSliver extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Future _showFullscreen(BuildContext context, Map entry) {
|
||||
Future _showFullscreen(BuildContext context, ImageEntry entry) {
|
||||
return Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ImageFullscreenPage(
|
||||
entries: entries,
|
||||
initialUri: entry['uri'],
|
||||
initialUri: entry.uri,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue