flutter_cors_image 0.3.0
flutter_cors_image: ^0.3.0 copied to clipboard
A Flutter package that provides image loading solutions for handling CORS issues and problematic images on web platforms.
Flutter CORS Image #
A Flutter package that provides advanced image loading solutions for handling CORS issues, with modern hover icons, clipboard functionality, and image data access.
Features #
This package provides comprehensive image loading solutions:
CustomNetworkImage #
This approach follows this strategy:
- First, try to load the image using Flutter's normal
Image.network
widget - If that fails on web platforms, automatically fall back to using an HTML img tag
- On native platforms, fall back to using
ExtendedImage
for additional compatibility
New in v0.3.0: Hover icons with customizable positioning, image data callbacks, and advanced clipboard/download functionality.
New in v0.2.0: Widget-based error handling with customizable error, reload, and open URL widgets. HTML errors now callback to Flutter for consistent UI across platforms.
Installation #
Add the dependency to your pubspec.yaml
:
dependencies:
flutter_cors_image: ^0.3.0
Usage #
Import the package:
import 'package:flutter_cors_image/flutter_cors_image.dart';
Using CustomNetworkImage with Hover Icons (v0.3.0+): #
CustomNetworkImage(
url: 'https://example.com/image.jpg',
width: 300,
height: 200,
fit: BoxFit.cover,
// ✅ NEW v0.3.0: Hover icons for quick actions
downloadIcon: Icon(Icons.download, color: Colors.white, size: 20),
copyIcon: Icon(Icons.copy, color: Colors.white, size: 20),
hoverIconPosition: HoverIconPosition.topRight,
hoverIconLayout: HoverIconLayout.auto,
hoverIconSpacing: 8.0,
hoverIconPadding: EdgeInsets.all(8),
// ✅ NEW v0.3.0: Get image data when loaded
onImageLoaded: (ImageDataInfo imageData) {
print('Image ready! Size: ${imageData.width}x${imageData.height}');
// imageData.imageBytes contains raw PNG data for copying/saving
},
// ✅ NEW v0.3.0: Custom action callbacks
onDownloadTap: () => print('Custom download action!'),
onCopyTap: () => print('Custom copy action!'),
)
Advanced Hover Icon Styling: #
CustomNetworkImage(
url: 'https://example.com/image.jpg',
width: 400,
height: 300,
// Custom styled download icon
downloadIcon: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue[400]!, Colors.blue[600]!],
),
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.download, color: Colors.white, size: 16),
SizedBox(width: 4),
Text('Download', style: TextStyle(color: Colors.white, fontSize: 12)),
],
),
),
// Custom styled copy icon
copyIcon: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.green[400]!, Colors.green[600]!],
),
borderRadius: BorderRadius.circular(8),
),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.copy, color: Colors.white, size: 16),
SizedBox(width: 4),
Text('Copy', style: TextStyle(color: Colors.white, fontSize: 12)),
],
),
),
hoverIconPosition: HoverIconPosition.bottomRight,
hoverIconLayout: HoverIconLayout.row,
)
Using Image Data for Copy/Download Operations (v0.3.0+): #
class ImageCopyExample extends StatefulWidget {
@override
_ImageCopyExampleState createState() => _ImageCopyExampleState();
}
class _ImageCopyExampleState extends State<ImageCopyExample> {
ImageDataInfo? _imageData;
@override
Widget build(BuildContext context) {
return Column(
children: [
CustomNetworkImage(
url: 'https://example.com/image.jpg',
onImageLoaded: (imageData) {
setState(() => _imageData = imageData);
},
),
if (_imageData != null) ...[
ElevatedButton(
onPressed: () async {
final success = await ImageClipboardHelper.downloadImage(_imageData!);
// Downloads PNG file to computer
},
child: Text('Download Image'),
),
ElevatedButton(
onPressed: () async {
final success = await ImageClipboardHelper.copyImageToClipboard(_imageData!);
// Copies image to clipboard for Ctrl+V pasting
},
child: Text('Copy to Clipboard'),
),
],
],
);
}
}
Using CustomNetworkImage with Error Handling (v0.2.0+): #
CustomNetworkImage(
url: 'https://example.com/image.jpg',
width: 300,
height: 200,
fit: BoxFit.cover,
// NEW v0.2.0: Widget-based error handling
errorWidget: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error, color: Colors.red),
SizedBox(width: 8),
Text('Image failed to load'),
],
),
reloadWidget: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.refresh),
SizedBox(width: 8),
Text('Reload Image'),
],
),
openUrlWidget: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.open_in_new),
SizedBox(width: 8),
Text('Open in New Tab'),
],
),
)
Hover Icons & Positioning (v0.3.0+) #
Available Positions #
enum HoverIconPosition {
topLeft, // Icons in top-left corner
topRight, // Icons in top-right corner (default)
bottomLeft, // Icons in bottom-left corner
bottomRight, // Icons in bottom-right corner
topCenter, // Icons centered at top
bottomCenter, // Icons centered at bottom
}
Layout Options #
enum HoverIconLayout {
auto, // Smart layout: vertical for corners, horizontal for center
row, // Always horizontal layout
column, // Always vertical layout
}
Customization Parameters #
Parameter | Type | Default | Description |
---|---|---|---|
downloadIcon |
Widget? |
null |
Custom download icon widget |
copyIcon |
Widget? |
null |
Custom copy icon widget |
hoverIconPosition |
HoverIconPosition |
topRight |
Position of hover icons |
hoverIconLayout |
HoverIconLayout |
auto |
Layout direction (row/column) |
enableHoverIcons |
bool |
true |
Enable/disable hover functionality |
hoverIconSpacing |
double |
8.0 |
Space between icons |
hoverIconPadding |
EdgeInsetsGeometry |
EdgeInsets.zero |
Padding around icons |
onDownloadTap |
VoidCallback? |
null |
Custom download action |
onCopyTap |
VoidCallback? |
null |
Custom copy action |
Image Data Access (v0.3.0+) #
ImageDataInfo Class #
class ImageDataInfo {
final Uint8List imageBytes; // Raw PNG image data
final int width; // Image width in pixels
final int height; // Image height in pixels
final String url; // Original image URL
}
Clipboard & Download Methods #
// Copy image to system clipboard (for Ctrl+V pasting)
bool success = await ImageClipboardHelper.copyImageToClipboard(imageData);
// Download image as PNG file
bool success = await ImageClipboardHelper.downloadImage(imageData);
Platform Support #
Platform | Clipboard Copy | File Download |
---|---|---|
Web | ✅ Modern Clipboard API | ✅ Blob download |
Mobile | ⚠️ Basic support* | ✅ Temp directory |
Desktop | ⚠️ File path copy | ✅ Temp directory |
*For enhanced mobile clipboard support, consider adding plugins like clipboard_manager
or pasteboard
.
Widget-based Error Handling (v0.2.0+) #
The CustomNetworkImage
widget supports flexible widget-based error handling:
Parameter | Type | Description |
---|---|---|
errorWidget |
Widget? |
Custom widget to show when image fails to load |
reloadWidget |
Widget? |
Custom widget for retry functionality |
openUrlWidget |
Widget? |
Custom widget for opening image URL in new tab |
Examples for Different UI Styles: #
// Material Design Style
CustomNetworkImage(
url: imageUrl,
errorWidget: Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline, color: Colors.red),
SizedBox(width: 8),
Text('Failed to load image', style: TextStyle(color: Colors.red)),
],
),
),
reloadWidget: ElevatedButton.icon(
onPressed: null, // Handled automatically
icon: Icon(Icons.refresh),
label: Text('Retry'),
),
openUrlWidget: TextButton.icon(
onPressed: null, // Handled automatically
icon: Icon(Icons.open_in_new),
label: Text('Open URL'),
),
)
// Icon-only (minimal)
CustomNetworkImage(
url: imageUrl,
errorWidget: Icon(Icons.broken_image, size: 48, color: Colors.grey),
reloadWidget: Icon(Icons.refresh, size: 24),
openUrlWidget: Icon(Icons.open_in_new, size: 24),
)
Migration Guide #
v0.2.x → v0.3.0 #
No Breaking Changes
All v0.2.x code continues to work unchanged. New features are additive and optional.
Gradual Enhancement
Add new features progressively:
// Step 1: Start with basic image loading (existing code works)
CustomNetworkImage(
url: imageUrl,
width: 300,
height: 200,
)
// Step 2: Add image data callback
CustomNetworkImage(
url: imageUrl,
width: 300,
height: 200,
onImageLoaded: (imageData) {
// Now you have access to image bytes, dimensions, etc.
},
)
// Step 3: Add hover icons
CustomNetworkImage(
url: imageUrl,
width: 300,
height: 200,
onImageLoaded: (imageData) => _imageData = imageData,
downloadIcon: Icon(Icons.download, color: Colors.white),
copyIcon: Icon(Icons.copy, color: Colors.white),
)
// Step 4: Customize positioning and styling
CustomNetworkImage(
url: imageUrl,
width: 300,
height: 200,
onImageLoaded: (imageData) => _imageData = imageData,
downloadIcon: _buildStyledDownloadIcon(),
copyIcon: _buildStyledCopyIcon(),
hoverIconPosition: HoverIconPosition.bottomRight,
hoverIconLayout: HoverIconLayout.row,
hoverIconSpacing: 12.0,
hoverIconPadding: EdgeInsets.all(8),
)
v0.1.x → v0.3.0 #
Update your dependency and replace deprecated parameters:
// OLD (deprecated but still works)
CustomNetworkImage(
url: imageUrl,
errorText: 'Image failed to load',
reloadText: 'Reload Image',
openUrlText: 'Open in New Tab',
)
// NEW (recommended)
CustomNetworkImage(
url: imageUrl,
errorWidget: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error, color: Colors.red),
SizedBox(width: 8),
Text('Image failed to load'),
],
),
reloadWidget: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.refresh),
SizedBox(width: 8),
Text('Reload Image'),
],
),
openUrlWidget: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.open_in_new),
SizedBox(width: 8),
Text('Open in New Tab'),
],
),
// ✅ Add new features
downloadIcon: Icon(Icons.download, color: Colors.white),
copyIcon: Icon(Icons.copy, color: Colors.white),
onImageLoaded: (imageData) {
// Access to image data for copy/download operations
},
)
Complete Example #
Here's a comprehensive example showing all v0.3.0 features:
import 'package:flutter/material.dart';
import 'package:flutter_cors_image/flutter_cors_image.dart';
class AdvancedImageExample extends StatefulWidget {
@override
_AdvancedImageExampleState createState() => _AdvancedImageExampleState();
}
class _AdvancedImageExampleState extends State<AdvancedImageExample> {
ImageDataInfo? _imageData;
HoverIconPosition _position = HoverIconPosition.topRight;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Flutter CORS Image v0.3.0')),
body: Column(
children: [
// Position selector
Wrap(
children: HoverIconPosition.values.map((pos) {
return ChoiceChip(
label: Text(pos.name),
selected: _position == pos,
onSelected: (selected) {
if (selected) setState(() => _position = pos);
},
);
}).toList(),
),
// Main image with hover icons
Expanded(
child: Center(
child: CustomNetworkImage(
url: 'https://example.com/image.jpg',
width: 400,
height: 300,
fit: BoxFit.cover,
// Styled hover icons
downloadIcon: _buildDownloadIcon(),
copyIcon: _buildCopyIcon(),
hoverIconPosition: _position,
hoverIconLayout: HoverIconLayout.auto,
hoverIconSpacing: 8.0,
hoverIconPadding: EdgeInsets.all(8),
// Image data callback
onImageLoaded: (imageData) {
setState(() => _imageData = imageData);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Image loaded! Hover to see icons')),
);
},
// Custom actions
onDownloadTap: () => _handleDownload(),
onCopyTap: () => _handleCopy(),
// Error handling
errorWidget: Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error, color: Colors.red, size: 48),
Text('Failed to load image'),
],
),
),
// Loading state
customLoadingBuilder: (context, child, progress) {
return Container(
width: 400,
height: 300,
color: Colors.grey[200],
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(value: progress?.progress),
if (progress?.progress != null)
Text('${(progress!.progress! * 100).toInt()}%'),
],
),
),
);
},
),
),
),
// Manual action buttons
if (_imageData != null) ...[
Padding(
padding: EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
onPressed: _handleDownload,
icon: Icon(Icons.download),
label: Text('Download'),
),
ElevatedButton.icon(
onPressed: _handleCopy,
icon: Icon(Icons.copy),
label: Text('Copy'),
),
],
),
),
],
],
),
);
}
Widget _buildDownloadIcon() {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.blue, Colors.blueAccent]),
borderRadius: BorderRadius.circular(6),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 3)],
),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.download, color: Colors.white, size: 16),
SizedBox(width: 4),
Text('Download', style: TextStyle(color: Colors.white, fontSize: 12)),
],
),
);
}
Widget _buildCopyIcon() {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.green, Colors.greenAccent]),
borderRadius: BorderRadius.circular(6),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 3)],
),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.copy, color: Colors.white, size: 16),
SizedBox(width: 4),
Text('Copy', style: TextStyle(color: Colors.white, fontSize: 12)),
],
),
);
}
Future<void> _handleDownload() async {
if (_imageData == null) return;
final success = await ImageClipboardHelper.downloadImage(_imageData!);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success ? 'Image downloaded!' : 'Download failed'),
backgroundColor: success ? Colors.green : Colors.red,
),
);
}
Future<void> _handleCopy() async {
if (_imageData == null) return;
final success = await ImageClipboardHelper.copyImageToClipboard(_imageData!);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success
? 'Image copied! Press Ctrl+V to paste.'
: 'Copy failed'),
backgroundColor: success ? Colors.green : Colors.red,
),
);
}
}
License #
MIT