380 lines
12 KiB
Dart
380 lines
12 KiB
Dart
//s1 Imports
|
|
//s2 Core Package Imports
|
|
import 'package:flutter/widgets.dart';
|
|
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
//s2 1st-party Package Imports
|
|
//s2 3rd-party Package Imports
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:flutter_svg/flutter_svg.dart';
|
|
import 'package:octo_image/octo_image.dart';
|
|
//s2 Dependancies Imports
|
|
//s3 Routes
|
|
//s3 Services
|
|
//s3 Models
|
|
//s1 Exports
|
|
|
|
enum ImageSizingMaster {
|
|
w,
|
|
h,
|
|
}
|
|
|
|
enum _DeclaredAssetType {
|
|
path,
|
|
url,
|
|
bytes,
|
|
fallback,
|
|
}
|
|
|
|
_DeclaredAssetType _parseAssetType(
|
|
String? path,
|
|
String? url,
|
|
Uint8List? bytes,
|
|
String? fallback,
|
|
) {
|
|
bool fromPath = (path != null && path != '');
|
|
bool fromBytes = (bytes != null && bytes.isNotEmpty);
|
|
bool fromNetwork = (url != null && url.isNotEmpty && Uri.tryParse(url)?.hasAbsolutePath == true && (url.startsWith('http://') || url.startsWith('https://')));
|
|
|
|
//
|
|
return fromPath
|
|
? _DeclaredAssetType.path
|
|
: fromBytes
|
|
? _DeclaredAssetType.bytes
|
|
: fromNetwork
|
|
? _DeclaredAssetType.url
|
|
: _DeclaredAssetType.fallback;
|
|
}
|
|
|
|
class AstromicImage extends StatelessWidget {
|
|
//!SECTION
|
|
//
|
|
AstromicImage({
|
|
super.key,
|
|
//
|
|
this.assetPath,
|
|
this.assetURL,
|
|
this.assetBytes,
|
|
this.assetFallback,
|
|
//
|
|
this.sizingMaster ,
|
|
this.widthSizing,
|
|
this.heightSizing,
|
|
this.fixedWidth,
|
|
this.fixedHeight,
|
|
//
|
|
this.isCircular,
|
|
this.borderWidth,
|
|
this.borderColor,
|
|
this.borderPadding,
|
|
this.radius,
|
|
this.shadow,
|
|
this.overlayColor,
|
|
this.overlayGradient,
|
|
//
|
|
this.fit,
|
|
this.alignment,
|
|
this.blendMode,
|
|
this.fadeInCurve,
|
|
this.fadeInDuration,
|
|
//
|
|
this.svgColor,
|
|
//
|
|
this.loadingWidget,
|
|
this.errorWidget,
|
|
}) :
|
|
// Assert that a source is provided, or provide a fallback source..
|
|
assert(((assetPath?.isNotEmpty ?? false) || (assetURL?.isNotEmpty ?? false) || (assetBytes?.isNotEmpty ?? false)) || (assetFallback?.isNotEmpty ?? false),
|
|
'Please specify a source or provide a fallback.'),
|
|
// Assert that only ONE source is provided...
|
|
assert(
|
|
(assetPath != null && assetBytes == null && assetURL == null) ||
|
|
(assetPath == null && assetBytes != null && assetURL == null) ||
|
|
(assetPath == null && assetBytes == null && assetURL != null),
|
|
'Please specify only ONE Asset Source'),
|
|
// Assert that correct sizing plan is provided...
|
|
assert((sizingMaster == ImageSizingMaster.w && (widthSizing != null || fixedWidth != null)) || (sizingMaster == ImageSizingMaster.h && (heightSizing != null || fixedHeight != null)),
|
|
'Please provide the correct sizing configurations based on the SizingMaster choosen');
|
|
//SECTION - Widget Arguments
|
|
//S1 -- Assets
|
|
final String? assetPath;
|
|
final String? assetURL;
|
|
final Uint8List? assetBytes;
|
|
final String? assetFallback;
|
|
//S1 -- Sizing
|
|
final ImageSizingMaster? sizingMaster;
|
|
final (double factor, double? min, double? max)? widthSizing;
|
|
|
|
/// Used when the width is Master and want to set fixed width OR if height is Master and want to constraint the width
|
|
final double? fixedWidth;
|
|
final (double factor, double? min, double? max)? heightSizing;
|
|
|
|
/// Used when the height is Master and want to set fixed height OR if width is Master and want to constraint the height
|
|
final double? fixedHeight;
|
|
//S1 -- STYLING
|
|
final bool? isCircular;
|
|
final double? borderWidth;
|
|
final Color? borderColor;
|
|
final EdgeInsetsGeometry? borderPadding;
|
|
final BorderRadiusGeometry? radius;
|
|
final List<BoxShadow>? shadow;
|
|
final Color? overlayColor;
|
|
final Gradient? overlayGradient;
|
|
//S1 -- CONFIGURATIONS
|
|
final Alignment? alignment;
|
|
final BoxFit? fit;
|
|
final BlendMode? blendMode;
|
|
final Curve? fadeInCurve;
|
|
final Duration? fadeInDuration;
|
|
//S1 -- SVG FILTERS
|
|
final Color? svgColor;
|
|
//S1 -- STATE WIDGETS
|
|
final Widget Function(int? loadedBytes, int? totalBytesToLoad)? loadingWidget;
|
|
final Widget Function(dynamic error, StackTrace? stackTrace)? errorWidget;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
//SECTION - Build Setup
|
|
//s1 -Values
|
|
_DeclaredAssetType assetType = _parseAssetType(assetPath, assetURL, assetBytes, assetFallback);
|
|
dynamic assetRef = _getAssetRef(assetType);
|
|
bool isSVG = _isSVG(assetType, assetRef);
|
|
//s1 -Values
|
|
//
|
|
//s1 -Widgets
|
|
// Default Loading Widget
|
|
Widget defaultLoadingWidget = const Text('Loading...');
|
|
|
|
// Default Error Widget
|
|
Widget defaultErrorWidget(dynamic error) => Text('An error has happened: $error');
|
|
|
|
// Get final svg widget
|
|
Widget? finalSVGWidget(Size size, Widget? loadingWidget) => assetType == _DeclaredAssetType.path
|
|
? SvgPicture.asset(
|
|
assetRef,
|
|
key: ValueKey<String>(assetRef),
|
|
width: size.width,
|
|
height: size.height,
|
|
fit: fit!,
|
|
placeholderBuilder: (_) => SizedBox(
|
|
width: size.width,
|
|
height: size.height,
|
|
child: loadingWidget,
|
|
),
|
|
alignment: alignment!,
|
|
colorFilter: svgColor != null ? ColorFilter.mode(svgColor!, blendMode ?? BlendMode.srcATop) : null,
|
|
)
|
|
: assetType == _DeclaredAssetType.url
|
|
? SvgPicture.network(
|
|
assetRef,
|
|
key: ValueKey<String>(assetRef),
|
|
width: size.width,
|
|
height: size.height,
|
|
fit: fit!,
|
|
placeholderBuilder: (_) => SizedBox(
|
|
width: size.width,
|
|
height: size.height,
|
|
child: loadingWidget,
|
|
),
|
|
alignment: alignment!,
|
|
colorFilter: svgColor != null ? ColorFilter.mode(svgColor!, blendMode ?? BlendMode.srcATop) : null,
|
|
)
|
|
: null;
|
|
|
|
Widget buildImage(Size size) {
|
|
return Stack(
|
|
children: <Widget>[
|
|
OctoImage(
|
|
key: ValueKey<String>(assetType == _DeclaredAssetType.bytes ? (assetRef as Uint8List).length.toString() : assetRef),
|
|
//
|
|
width: size.width,
|
|
height: size.height == double.infinity? null : size.height,
|
|
fit: fit,
|
|
alignment: alignment,
|
|
filterQuality: FilterQuality.none,
|
|
color: svgColor,
|
|
colorBlendMode: blendMode ?? (isSVG ? BlendMode.srcATop : null),
|
|
//
|
|
errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) {
|
|
return errorWidget != null
|
|
? errorWidget!(error, stackTrace)
|
|
: assetFallback != null
|
|
? Image.asset(assetFallback!)
|
|
: defaultErrorWidget(error);
|
|
},
|
|
//
|
|
progressIndicatorBuilder: (_, ImageChunkEvent? bytes) => SizedBox(
|
|
width: size.width,
|
|
height: size.height == double.infinity? null : size.height,
|
|
child: loadingWidget != null ? loadingWidget!(bytes?.cumulativeBytesLoaded, bytes?.expectedTotalBytes) : defaultLoadingWidget,
|
|
),
|
|
// placeholderBuilder: (BuildContext context) => loadingWidget != null ? loadingWidget!(null, null) : defaultLoadingWidget,
|
|
fadeInCurve: fadeInCurve,
|
|
fadeInDuration: fadeInDuration,
|
|
imageBuilder: (BuildContext context, Widget image) => Container(
|
|
width: size.width,
|
|
height: size.height == double.infinity? null : size.height,
|
|
padding: borderPadding ?? EdgeInsets.zero,
|
|
margin: EdgeInsets.zero,
|
|
decoration: BoxDecoration(
|
|
border: borderWidth != null
|
|
? Border.all(
|
|
width: borderWidth!,
|
|
color: borderColor ?? const Color(0xff000000),
|
|
)
|
|
: null,
|
|
borderRadius: isCircular! ? BorderRadius.circular(10000000) : radius,
|
|
boxShadow: shadow,
|
|
),
|
|
child: isCircular!
|
|
? ClipOval(
|
|
child: isSVG ? finalSVGWidget(size, loadingWidget != null ? loadingWidget!(null, null) : defaultLoadingWidget) : image,
|
|
)
|
|
: ClipRRect(
|
|
borderRadius: isCircular! ? BorderRadius.circular(10000000) : radius!,
|
|
child: isSVG ? finalSVGWidget(size, loadingWidget != null ? loadingWidget!(null, null) : defaultLoadingWidget) : image,
|
|
),
|
|
),
|
|
image: isSVG ? MemoryImage(kTransparentImage) : _imageProvider(assetType, assetRef),
|
|
),
|
|
if (overlayColor != null || overlayGradient != null)
|
|
Container(
|
|
decoration: BoxDecoration(color: overlayGradient != null ? null : overlayColor, gradient: overlayGradient, borderRadius: radius),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
//s1 -Widgets
|
|
//!SECTION
|
|
|
|
//SECTION - Build Return
|
|
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
|
|
Size size = _calculateSize(constraints);
|
|
return SizedBox(width: size.width, height: size.height, child: buildImage(size));
|
|
});
|
|
//!SECTION
|
|
}
|
|
|
|
//SECTION - Helper Functions
|
|
// Get asset reference based on asset type...
|
|
dynamic _getAssetRef(_DeclaredAssetType type) {
|
|
switch (type) {
|
|
case _DeclaredAssetType.path:
|
|
return assetPath;
|
|
case _DeclaredAssetType.bytes:
|
|
return assetBytes;
|
|
case _DeclaredAssetType.url:
|
|
return assetURL;
|
|
case _DeclaredAssetType.fallback:
|
|
return assetFallback;
|
|
}
|
|
}
|
|
|
|
// Detect if asset is an SVG...
|
|
bool _isSVG(_DeclaredAssetType type, dynamic ref) =>
|
|
type == _DeclaredAssetType.bytes ? utf8.decode(ref.sublist(0, ref.length > 10 ? 10 : ref.length), allowMalformed: true).trimLeft().startsWith('<svg') : (ref?.endsWith('.svg') ?? false);
|
|
|
|
// Get image provider based on type
|
|
ImageProvider _imageProvider(_DeclaredAssetType type, dynamic ref) {
|
|
switch (type) {
|
|
case _DeclaredAssetType.fallback:
|
|
return AssetImage(ref);
|
|
case _DeclaredAssetType.path:
|
|
return AssetImage(ref);
|
|
case _DeclaredAssetType.bytes:
|
|
return MemoryImage(ref);
|
|
case _DeclaredAssetType.url:
|
|
return CachedNetworkImageProvider(ref, cacheKey: ref);
|
|
}
|
|
}
|
|
|
|
// Calculate the sizing of the image...
|
|
Size _calculateSize(BoxConstraints constraints) {
|
|
double? maxAvailablewidth = constraints.maxWidth;
|
|
double? maxAvailableheight = constraints.maxHeight;
|
|
Size finalSize;
|
|
switch (sizingMaster!) {
|
|
case ImageSizingMaster.w:
|
|
{
|
|
finalSize = Size(fixedWidth ?? (widthSizing!.$1 * maxAvailablewidth).clamp(widthSizing!.$2 ?? 0, widthSizing!.$3 ?? double.infinity), fixedHeight ?? maxAvailableheight);
|
|
}
|
|
case ImageSizingMaster.h:
|
|
{
|
|
finalSize = Size(fixedWidth ?? maxAvailablewidth, fixedHeight ?? (heightSizing!.$1 * maxAvailableheight).clamp(heightSizing!.$2 ?? 0, heightSizing!.$3 ?? double.infinity));
|
|
}
|
|
}
|
|
return finalSize;
|
|
}
|
|
//!SECTION
|
|
}
|
|
|
|
final Uint8List kTransparentImage = Uint8List.fromList(<int>[
|
|
0x89,
|
|
0x50,
|
|
0x4E,
|
|
0x47,
|
|
0x0D,
|
|
0x0A,
|
|
0x1A,
|
|
0x0A,
|
|
0x00,
|
|
0x00,
|
|
0x00,
|
|
0x0D,
|
|
0x49,
|
|
0x48,
|
|
0x44,
|
|
0x52,
|
|
0x00,
|
|
0x00,
|
|
0x00,
|
|
0x01,
|
|
0x00,
|
|
0x00,
|
|
0x00,
|
|
0x01,
|
|
0x08,
|
|
0x06,
|
|
0x00,
|
|
0x00,
|
|
0x00,
|
|
0x1F,
|
|
0x15,
|
|
0xC4,
|
|
0x89,
|
|
0x00,
|
|
0x00,
|
|
0x00,
|
|
0x0A,
|
|
0x49,
|
|
0x44,
|
|
0x41,
|
|
0x54,
|
|
0x78,
|
|
0x9C,
|
|
0x63,
|
|
0x00,
|
|
0x01,
|
|
0x00,
|
|
0x00,
|
|
0x05,
|
|
0x00,
|
|
0x01,
|
|
0x0D,
|
|
0x0A,
|
|
0x2D,
|
|
0xB4,
|
|
0x00,
|
|
0x00,
|
|
0x00,
|
|
0x00,
|
|
0x49,
|
|
0x45,
|
|
0x4E,
|
|
0x44,
|
|
0xAE,
|
|
0x42,
|
|
0x60,
|
|
0x82,
|
|
]);
|