svg support
This commit is contained in:
parent
38c2207b78
commit
6c8441642c
21 changed files with 218 additions and 95 deletions
|
@ -217,6 +217,7 @@ 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;
|
||||
|
|
|
@ -34,6 +34,7 @@ import java.util.regex.Pattern;
|
|||
|
||||
import deckers.thibault.aves.utils.Constants;
|
||||
import deckers.thibault.aves.utils.MetadataHelper;
|
||||
import deckers.thibault.aves.utils.MimeTypes;
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
|
@ -69,7 +70,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
|||
}
|
||||
|
||||
private boolean isVideo(@Nullable String mimeType) {
|
||||
return mimeType != null && mimeType.startsWith(Constants.MIME_VIDEO);
|
||||
return mimeType != null && mimeType.startsWith(MimeTypes.VIDEO);
|
||||
}
|
||||
|
||||
private InputStream getInputStream(String path, String uri) throws FileNotFoundException {
|
||||
|
@ -171,7 +172,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
|||
Map<String, Object> metadataMap = new HashMap<>();
|
||||
|
||||
try (InputStream is = getInputStream(path, uri)) {
|
||||
if (!Constants.MIME_MP2T.equalsIgnoreCase(mimeType)) {
|
||||
if (!MimeTypes.MP2T.equals(mimeType)) {
|
||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||
|
||||
// EXIF Sub-IFD
|
||||
|
|
|
@ -27,8 +27,8 @@ import java.util.HashMap;
|
|||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import deckers.thibault.aves.utils.Constants;
|
||||
import deckers.thibault.aves.utils.MetadataHelper;
|
||||
import deckers.thibault.aves.utils.MimeTypes;
|
||||
|
||||
import static deckers.thibault.aves.utils.MetadataHelper.getOrientationDegreesForExifCode;
|
||||
|
||||
|
@ -100,11 +100,15 @@ public class ImageEntry {
|
|||
}
|
||||
|
||||
public boolean isImage() {
|
||||
return mimeType.startsWith(Constants.MIME_IMAGE);
|
||||
return mimeType.startsWith(MimeTypes.IMAGE);
|
||||
}
|
||||
|
||||
public boolean isSvg() {
|
||||
return mimeType.equals(MimeTypes.SVG);
|
||||
}
|
||||
|
||||
public boolean isVideo() {
|
||||
return mimeType.startsWith(Constants.MIME_VIDEO);
|
||||
return mimeType.startsWith(MimeTypes.VIDEO);
|
||||
}
|
||||
|
||||
// metadata retrieval
|
||||
|
@ -179,10 +183,12 @@ public class ImageEntry {
|
|||
// expects entry with: uri/path, mimeType
|
||||
// finds: width, height, orientation, date
|
||||
private void fillByMetadataExtractor(Context context) {
|
||||
if (MimeTypes.SVG.equals(mimeType)) return;
|
||||
|
||||
try (InputStream is = getInputStream(context)) {
|
||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||
|
||||
if (Constants.MIME_JPEG.equals(mimeType)) {
|
||||
if (MimeTypes.JPEG.equals(mimeType)) {
|
||||
JpegDirectory jpegDir = metadata.getFirstDirectoryOfType(JpegDirectory.class);
|
||||
if (jpegDir != null) {
|
||||
if (jpegDir.containsTag(JpegDirectory.TAG_IMAGE_WIDTH)) {
|
||||
|
@ -201,7 +207,7 @@ public class ImageEntry {
|
|||
sourceDateTakenMillis = exifDir.getDate(ExifIFD0Directory.TAG_DATETIME, null, TimeZone.getDefault()).getTime();
|
||||
}
|
||||
}
|
||||
} else if (Constants.MIME_MP4.equals(mimeType)) {
|
||||
} else if (MimeTypes.MP4.equals(mimeType)) {
|
||||
Mp4VideoDirectory mp4VideoDir = metadata.getFirstDirectoryOfType(Mp4VideoDirectory.class);
|
||||
if (mp4VideoDir != null) {
|
||||
if (mp4VideoDir.containsTag(Mp4VideoDirectory.TAG_WIDTH)) {
|
||||
|
@ -220,6 +226,8 @@ public class ImageEntry {
|
|||
// expects entry with: uri/path
|
||||
// finds: width, height
|
||||
private void fillByBitmapDecode(Context context) {
|
||||
if (MimeTypes.SVG.equals(mimeType)) return;
|
||||
|
||||
try (InputStream is = getInputStream(context)) {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
|
|
|
@ -15,7 +15,7 @@ class ContentImageProvider extends ImageProvider {
|
|||
entry.mimeType = mimeType;
|
||||
entry.fillPreCatalogMetadata(context);
|
||||
|
||||
if (entry.hasSize()) {
|
||||
if (entry.hasSize() || entry.isSvg()) {
|
||||
callback.onSuccess(entry.toMap());
|
||||
} else {
|
||||
callback.onFailure();
|
||||
|
|
|
@ -38,7 +38,7 @@ class FileImageProvider extends ImageProvider {
|
|||
}
|
||||
entry.fillPreCatalogMetadata(context);
|
||||
|
||||
if (entry.hasSize()) {
|
||||
if (entry.hasSize() || entry.isSvg()) {
|
||||
callback.onSuccess(entry.toMap());
|
||||
} else {
|
||||
callback.onFailure();
|
||||
|
|
|
@ -33,9 +33,9 @@ import java.io.InputStream;
|
|||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import deckers.thibault.aves.utils.Constants;
|
||||
import deckers.thibault.aves.utils.Env;
|
||||
import deckers.thibault.aves.utils.MetadataHelper;
|
||||
import deckers.thibault.aves.utils.MimeTypes;
|
||||
import deckers.thibault.aves.utils.PermissionManager;
|
||||
import deckers.thibault.aves.utils.StorageUtils;
|
||||
import deckers.thibault.aves.utils.Utils;
|
||||
|
@ -144,10 +144,10 @@ public abstract class ImageProvider {
|
|||
// so we retrieve it again from the file metadata
|
||||
String metadataMimeType = getMimeType(activity, uri);
|
||||
switch (metadataMimeType != null ? metadataMimeType : mimeType) {
|
||||
case Constants.MIME_JPEG:
|
||||
case MimeTypes.JPEG:
|
||||
rotateJpeg(activity, path, uri, clockwise, callback);
|
||||
break;
|
||||
case Constants.MIME_PNG:
|
||||
case MimeTypes.PNG:
|
||||
rotatePng(activity, path, uri, clockwise, callback);
|
||||
break;
|
||||
default:
|
||||
|
@ -156,7 +156,7 @@ public abstract class ImageProvider {
|
|||
}
|
||||
|
||||
private void rotateJpeg(final Activity activity, final String path, final Uri uri, boolean clockwise, final ImageOpCallback callback) {
|
||||
final String mimeType = Constants.MIME_JPEG;
|
||||
final String mimeType = MimeTypes.JPEG;
|
||||
String editablePath = path;
|
||||
boolean onSdCard = Env.isOnSdCard(activity, path);
|
||||
if (onSdCard) {
|
||||
|
@ -232,7 +232,7 @@ public abstract class ImageProvider {
|
|||
}
|
||||
|
||||
private void rotatePng(final Activity activity, final String path, final Uri uri, boolean clockwise, final ImageOpCallback callback) {
|
||||
final String mimeType = Constants.MIME_PNG;
|
||||
final String mimeType = MimeTypes.PNG;
|
||||
if (path == null) {
|
||||
callback.onFailure();
|
||||
return;
|
||||
|
|
|
@ -17,8 +17,8 @@ import java.util.Map;
|
|||
import java.util.stream.Stream;
|
||||
|
||||
import deckers.thibault.aves.model.ImageEntry;
|
||||
import deckers.thibault.aves.utils.Constants;
|
||||
import deckers.thibault.aves.utils.Env;
|
||||
import deckers.thibault.aves.utils.MimeTypes;
|
||||
import deckers.thibault.aves.utils.PermissionManager;
|
||||
import deckers.thibault.aves.utils.StorageUtils;
|
||||
import deckers.thibault.aves.utils.Utils;
|
||||
|
@ -71,10 +71,10 @@ public class MediaStoreImageProvider extends ImageProvider {
|
|||
entry.put("uri", uri.toString());
|
||||
callback.onSuccess(entry);
|
||||
};
|
||||
if (mimeType.startsWith(Constants.MIME_IMAGE)) {
|
||||
if (mimeType.startsWith(MimeTypes.IMAGE)) {
|
||||
Uri contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
|
||||
entryCount = fetchFrom(context, onSuccess, contentUri, IMAGE_PROJECTION);
|
||||
} else if (mimeType.startsWith(Constants.MIME_VIDEO)) {
|
||||
} else if (mimeType.startsWith(MimeTypes.VIDEO)) {
|
||||
Uri contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id);
|
||||
entryCount = fetchFrom(context, onSuccess, contentUri, VIDEO_PROJECTION);
|
||||
}
|
||||
|
@ -114,13 +114,14 @@ public class MediaStoreImageProvider extends ImageProvider {
|
|||
// this is fine if `contentUri` does not already contain the ID
|
||||
final Uri itemUri = ContentUris.withAppendedId(contentUri, contentId);
|
||||
final String path = cursor.getString(pathColumn);
|
||||
final String mimeType = cursor.getString(mimeTypeColumn);
|
||||
int width = cursor.getInt(widthColumn);
|
||||
int height = cursor.getInt(heightColumn);
|
||||
|
||||
Map<String, Object> entryMap = new HashMap<String, Object>() {{
|
||||
put("uri", itemUri.toString());
|
||||
put("path", path);
|
||||
put("mimeType", cursor.getString(mimeTypeColumn));
|
||||
put("mimeType", mimeType);
|
||||
put("orientationDegrees", orientationColumn != -1 ? cursor.getInt(orientationColumn) : 0);
|
||||
put("sizeBytes", cursor.getLong(sizeColumn));
|
||||
put("title", cursor.getString(titleColumn));
|
||||
|
@ -134,7 +135,7 @@ public class MediaStoreImageProvider extends ImageProvider {
|
|||
entryMap.put("width", width);
|
||||
entryMap.put("height", height);
|
||||
|
||||
if (width <= 0 || height <= 0) {
|
||||
if ((width <= 0 || height <= 0) && !MimeTypes.SVG.equals(mimeType)) {
|
||||
// some images are incorrectly registered in the Media Store,
|
||||
// they are valid but miss some attributes, such as width, height, orientation
|
||||
ImageEntry entry = new ImageEntry(entryMap).fillPreCatalogMetadata(context);
|
||||
|
@ -143,9 +144,9 @@ public class MediaStoreImageProvider extends ImageProvider {
|
|||
height = entry.height != null ? entry.height : 0;
|
||||
}
|
||||
|
||||
if (width <= 0 || height <= 0) {
|
||||
if ((width <= 0 || height <= 0) && !MimeTypes.SVG.equals(mimeType)) {
|
||||
// this is probably not a real image, like "/storage/emulated/0", so we skip it
|
||||
Log.w(LOG_TAG, "failed to get size for uri=" + itemUri + ", path=" + path);
|
||||
Log.w(LOG_TAG, "failed to get size for uri=" + itemUri + ", path=" + path + ", mimeType=" + mimeType);
|
||||
} else {
|
||||
newEntryHandler.handleEntry(entryMap);
|
||||
entryCount++;
|
||||
|
|
|
@ -8,17 +8,6 @@ import java.util.Map;
|
|||
public class Constants {
|
||||
public static final int SD_CARD_PERMISSION_REQUEST_CODE = 1;
|
||||
|
||||
// mime types
|
||||
|
||||
public static final String MIME_IMAGE = "image";
|
||||
public static final String MIME_GIF = "image/gif";
|
||||
public static final String MIME_JPEG = "image/jpeg";
|
||||
public static final String MIME_PNG = "image/png";
|
||||
|
||||
public static final String MIME_VIDEO = "video";
|
||||
public static final String MIME_MP2T = "video/mp2t"; // .m2ts
|
||||
public static final String MIME_MP4 = "video/mp4";
|
||||
|
||||
// video metadata keys, from android.media.MediaMetadataRetriever
|
||||
|
||||
public static final Map<Integer, String> MEDIA_METADATA_KEYS = new HashMap<Integer, String>() {
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package deckers.thibault.aves.utils;
|
||||
|
||||
public class MimeTypes {
|
||||
public static final String IMAGE = "image";
|
||||
public static final String GIF = "image/gif";
|
||||
public static final String JPEG = "image/jpeg";
|
||||
public static final String PNG = "image/png";
|
||||
public static final String SVG = "image/svg+xml";
|
||||
|
||||
public static final String VIDEO = "video";
|
||||
public static final String MP2T = "video/mp2t"; // .m2ts
|
||||
public static final String MP4 = "video/mp4";
|
||||
}
|
|
@ -99,6 +99,8 @@ class ImageEntry {
|
|||
|
||||
bool get isGif => mimeType == MimeTypes.MIME_GIF;
|
||||
|
||||
bool get isSvg => mimeType == MimeTypes.MIME_SVG;
|
||||
|
||||
bool get isVideo => mimeType.startsWith(MimeTypes.MIME_VIDEO);
|
||||
|
||||
bool get isCatalogued => catalogMetadata != null;
|
||||
|
|
|
@ -8,6 +8,8 @@ class MetadataService {
|
|||
|
||||
// return Map<Map<Key, Value>> (map of directories, each directory being a map of metadata label and value description)
|
||||
static Future<Map> getAllMetadata(ImageEntry entry) async {
|
||||
if (entry.isSvg) return null;
|
||||
|
||||
try {
|
||||
final result = await platform.invokeMethod('getAllMetadata', <String, dynamic>{
|
||||
'mimeType': entry.mimeType,
|
||||
|
@ -22,6 +24,8 @@ class MetadataService {
|
|||
}
|
||||
|
||||
static Future<CatalogMetadata> getCatalogMetadata(ImageEntry entry) async {
|
||||
if (entry.isSvg) return null;
|
||||
|
||||
try {
|
||||
// return map with:
|
||||
// 'dateMillis': date taken in milliseconds since Epoch (long)
|
||||
|
@ -42,6 +46,8 @@ class MetadataService {
|
|||
}
|
||||
|
||||
static Future<OverlayMetadata> getOverlayMetadata(ImageEntry entry) async {
|
||||
if (entry.isSvg) return null;
|
||||
|
||||
try {
|
||||
// return map with string descriptions for: 'aperture' 'exposureTime' 'focalLength' 'iso'
|
||||
final result = await platform.invokeMethod('getOverlayMetadata', <String, dynamic>{
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
class MimeTypes {
|
||||
static const String MIME_VIDEO = 'video';
|
||||
static const String MIME_GIF = 'image/gif';
|
||||
static const String MIME_JPEG = 'image/jpeg';
|
||||
static const String MIME_PNG = 'image/png';
|
||||
static const String MIME_GIF = 'image/gif';
|
||||
static const String MIME_SVG = 'image/svg+xml';
|
||||
static const String MIME_VIDEO = 'video';
|
||||
}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
class Constants {
|
||||
// as of Flutter v1.11.0, overflowing `Text` miscalculates height and some text (e.g. 'Å') is clipped
|
||||
// so we give it a `strutStyle` with a slightly larger height
|
||||
static const overflowStrutStyle = StrutStyle(height: 1.3);
|
||||
|
||||
static const svgBackground = Colors.white;
|
||||
static const svgColorFilter = ColorFilter.mode(svgBackground, BlendMode.dstOver);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/common/image_preview.dart';
|
||||
import 'package:aves/widgets/fullscreen/uri_picture_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class Thumbnail extends StatelessWidget {
|
||||
final ImageEntry entry;
|
||||
|
@ -22,44 +25,6 @@ class Thumbnail extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final image = ImagePreview(
|
||||
entry: entry,
|
||||
// TODO TLAD smarter sizing, but shouldn't only depend on `extent` so that it doesn't reload during gridview scaling
|
||||
width: 50,
|
||||
height: 50,
|
||||
builder: (bytes) {
|
||||
final image = Image.memory(
|
||||
bytes,
|
||||
width: extent,
|
||||
height: extent,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
return heroTag == null
|
||||
? image
|
||||
: Hero(
|
||||
tag: heroTag,
|
||||
flightShuttleBuilder: (
|
||||
BuildContext flightContext,
|
||||
Animation<double> animation,
|
||||
HeroFlightDirection flightDirection,
|
||||
BuildContext fromHeroContext,
|
||||
BuildContext toHeroContext,
|
||||
) {
|
||||
// use LayoutBuilder only during hero animation
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
final dim = min(constraints.maxWidth, constraints.maxHeight);
|
||||
return Image.memory(
|
||||
bytes,
|
||||
width: dim,
|
||||
height: dim,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
});
|
||||
},
|
||||
child: image,
|
||||
);
|
||||
},
|
||||
);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
|
@ -72,7 +37,7 @@ class Thumbnail extends StatelessWidget {
|
|||
child: Stack(
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
children: [
|
||||
image,
|
||||
entry.isSvg ? _buildVectorImage() : _buildRasterImage(),
|
||||
_ThumbnailOverlay(
|
||||
entry: entry,
|
||||
extent: extent,
|
||||
|
@ -81,6 +46,59 @@ class Thumbnail extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRasterImage() {
|
||||
return ImagePreview(
|
||||
entry: entry,
|
||||
// TODO TLAD smarter sizing, but shouldn't only depend on `extent` so that it doesn't reload during gridview scaling
|
||||
width: 50,
|
||||
height: 50,
|
||||
builder: (bytes) {
|
||||
final imageBuilder = (bytes, dim) => Image.memory(
|
||||
bytes,
|
||||
width: dim,
|
||||
height: dim,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
return heroTag == null
|
||||
? imageBuilder(bytes, extent)
|
||||
: Hero(
|
||||
tag: heroTag,
|
||||
flightShuttleBuilder: (flightContext, animation, flightDirection, fromHeroContext, toHeroContext) {
|
||||
// use LayoutBuilder only during hero animation
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
final dim = min(constraints.maxWidth, constraints.maxHeight);
|
||||
return imageBuilder(bytes, dim);
|
||||
});
|
||||
},
|
||||
child: imageBuilder(bytes, extent),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVectorImage() {
|
||||
final child = Container(
|
||||
// center `SvgPicture` inside `Container` with the thumbnail dimensions
|
||||
// so that `SvgPicture` doesn't get aligned by the `Stack` like the overlay icons
|
||||
width: extent,
|
||||
height: extent,
|
||||
child: SvgPicture(
|
||||
UriPicture(
|
||||
entry.uri,
|
||||
colorFilter: Constants.svgColorFilter,
|
||||
),
|
||||
width: extent,
|
||||
height: extent,
|
||||
),
|
||||
);
|
||||
return heroTag == null
|
||||
? child
|
||||
: Hero(
|
||||
tag: heroTag,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ThumbnailOverlay extends StatelessWidget {
|
||||
|
|
|
@ -36,7 +36,7 @@ class ImagePreviewState extends State<ImagePreview> with AfterInitMixin {
|
|||
|
||||
@override
|
||||
void initState() {
|
||||
debugPrint('$runtimeType initState path=${entry.path}');
|
||||
// debugPrint('$runtimeType initState path=${entry.path}');
|
||||
super.initState();
|
||||
_entryChangeNotifier = Listenable.merge([
|
||||
entry.imageChangeNotifier,
|
||||
|
@ -55,14 +55,14 @@ class ImagePreviewState extends State<ImagePreview> with AfterInitMixin {
|
|||
void didUpdateWidget(ImagePreview old) {
|
||||
// debugPrint('$runtimeType didUpdateWidget from=${old.entry.path} to=${entry.path}');
|
||||
super.didUpdateWidget(old);
|
||||
if (widget.width == old.width && widget.height == old.height && uri == old.entry.uri && widget.entry.width == old.entry.width && widget.entry.height == old.entry.height && widget.entry.orientationDegrees == old.entry.orientationDegrees) return;
|
||||
if (widget.width == old.width && widget.height == old.height && uri == old.entry.uri && entry.width == old.entry.width && entry.height == old.entry.height && entry.orientationDegrees == old.entry.orientationDegrees) return;
|
||||
_initByteLoader();
|
||||
}
|
||||
|
||||
void _initByteLoader() {
|
||||
final width = (widget.width * _devicePixelRatio).round();
|
||||
final height = (widget.height * _devicePixelRatio).round();
|
||||
_byteLoader = ImageFileService.getImageBytes(widget.entry, width, height);
|
||||
_byteLoader = ImageFileService.getImageBytes(entry, width, height);
|
||||
}
|
||||
|
||||
void _onEntryChange() => setState(() => _initByteLoader());
|
||||
|
|
|
@ -5,7 +5,7 @@ import 'package:aves/model/collection_lens.dart';
|
|||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/widgets/fullscreen/fullscreen_action_delegate.dart';
|
||||
import 'package:aves/widgets/fullscreen/image_page.dart';
|
||||
import 'package:aves/widgets/fullscreen/image_uri.dart';
|
||||
import 'package:aves/widgets/fullscreen/uri_image_provider.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/info_page.dart';
|
||||
import 'package:aves/widgets/fullscreen/overlay/bottom.dart';
|
||||
import 'package:aves/widgets/fullscreen/overlay/top.dart';
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/widgets/fullscreen/image_uri.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/fullscreen/uri_image_provider.dart';
|
||||
import 'package:aves/widgets/fullscreen/uri_picture_provider.dart';
|
||||
import 'package:aves/widgets/fullscreen/video.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
@ -43,26 +46,47 @@ class ImageView extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
final placeholderBuilder = (context) => const Center(
|
||||
child: SizedBox(
|
||||
width: 64,
|
||||
height: 64,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
final heroAttributes = heroTag != null
|
||||
? PhotoViewHeroAttributes(
|
||||
tag: heroTag,
|
||||
transitionOnUserGestures: true,
|
||||
)
|
||||
: null;
|
||||
|
||||
if (entry.isSvg) {
|
||||
return PhotoView.customChild(
|
||||
child: SvgPicture(
|
||||
UriPicture(
|
||||
entry.uri,
|
||||
colorFilter: Constants.svgColorFilter,
|
||||
),
|
||||
placeholderBuilder: placeholderBuilder,
|
||||
),
|
||||
backgroundDecoration: backgroundDecoration,
|
||||
heroAttributes: heroAttributes,
|
||||
scaleStateChangedCallback: onScaleChanged,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
initialScale: PhotoViewComputedScale.contained,
|
||||
onTapUp: (tapContext, details, value) => onTap?.call(),
|
||||
);
|
||||
}
|
||||
|
||||
return PhotoView(
|
||||
// key includes size and orientation to refresh when the image is rotated
|
||||
key: ValueKey('${entry.orientationDegrees}_${entry.width}_${entry.height}_${entry.path}'),
|
||||
imageProvider: UriImage(entry.uri),
|
||||
loadingBuilder: (context, event) => const Center(
|
||||
child: SizedBox(
|
||||
width: 64,
|
||||
height: 64,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
loadingBuilder: (context, event) => placeholderBuilder(context),
|
||||
backgroundDecoration: backgroundDecoration,
|
||||
heroAttributes: heroTag != null
|
||||
? PhotoViewHeroAttributes(
|
||||
tag: heroTag,
|
||||
transitionOnUserGestures: true,
|
||||
)
|
||||
: null,
|
||||
heroAttributes: heroAttributes,
|
||||
scaleStateChangedCallback: onScaleChanged,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
initialScale: PhotoViewComputedScale.contained,
|
||||
|
|
|
@ -23,7 +23,7 @@ class BasicSection extends StatelessWidget {
|
|||
'Title': entry.title ?? '?',
|
||||
'Date': dateText,
|
||||
if (entry.isVideo) ..._buildVideoRows(),
|
||||
'Resolution': resolutionText,
|
||||
if (!entry.isSvg) 'Resolution': resolutionText,
|
||||
'Size': entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : '?',
|
||||
'URI': entry.uri ?? '?',
|
||||
if (entry.path != null) 'Path': entry.path,
|
||||
|
|
|
@ -237,7 +237,7 @@ class _DateRow extends StatelessWidget {
|
|||
const Icon(OMIcons.calendarToday, size: _iconSize),
|
||||
const SizedBox(width: _iconPadding),
|
||||
Expanded(flex: 3, child: Text(dateText, strutStyle: Constants.overflowStrutStyle)),
|
||||
Expanded(flex: 2, child: Text(resolution, strutStyle: Constants.overflowStrutStyle)),
|
||||
if (!entry.isSvg) Expanded(flex: 2, child: Text(resolution, strutStyle: Constants.overflowStrutStyle)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
55
lib/widgets/fullscreen/uri_picture_provider.dart
Normal file
55
lib/widgets/fullscreen/uri_picture_provider.dart
Normal file
|
@ -0,0 +1,55 @@
|
|||
import 'package:aves/model/image_file_service.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
|
||||
class UriPicture extends PictureProvider<UriPicture> {
|
||||
const UriPicture(this.uri, {this.colorFilter}) : assert(uri != null);
|
||||
|
||||
final String uri;
|
||||
|
||||
/// The [ColorFilter], if any, to use when drawing this picture.
|
||||
final ColorFilter colorFilter;
|
||||
|
||||
@override
|
||||
Future<UriPicture> obtainKey(PictureConfiguration configuration) {
|
||||
return SynchronousFuture<UriPicture>(this);
|
||||
}
|
||||
|
||||
@override
|
||||
PictureStreamCompleter load(UriPicture key, {PictureErrorListener onError}) {
|
||||
return OneFramePictureStreamCompleter(_loadAsync(key, onError: onError), informationCollector: () sync* {
|
||||
yield DiagnosticsProperty<String>('Uri', uri);
|
||||
});
|
||||
}
|
||||
|
||||
Future<PictureInfo> _loadAsync(UriPicture key, {PictureErrorListener onError}) async {
|
||||
assert(key == this);
|
||||
|
||||
final data = await ImageFileService.readAsBytes(uri);
|
||||
if (data == null || data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final decoder = SvgPicture.svgByteDecoder;
|
||||
if (onError != null) {
|
||||
final future = decoder(data, colorFilter, key.toString());
|
||||
unawaited(future.catchError(onError));
|
||||
return future;
|
||||
}
|
||||
return decoder(data, colorFilter, key.toString());
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is UriPicture && other.uri == uri && other.colorFilter == colorFilter;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(uri, colorFilter);
|
||||
|
||||
@override
|
||||
String toString() => '${objectRuntimeType(this, 'UriPicture')}("$uri", colorFilter: $colorFilter)';
|
||||
}
|
Loading…
Reference in a new issue