flutter_cors_image 0.3.5
flutter_cors_image: ^0.3.5 copied to clipboard
A Flutter package that provides image loading solutions for handling CORS issues and problematic images on web platforms.
Changelog #
0.3.5 - Platform Compatibility Fix Release #
๐ง Platform Compatibility Improvements #
- Fixed Platform Support: Resolved critical platform compatibility issues that caused 0/6 platform support score
- Conditional Imports: Implemented conditional imports pattern to properly handle platform-specific libraries
- Enhanced Image Loading: Integrated ExtendedImage for better caching, error handling, and retry functionality
- Web/IO Separation: Created dedicated helper files for web and IO operations with proper stub implementations
- Cross-Platform: Now supports all Flutter platforms (web, Android, iOS, desktop) without compilation errors
๐ ๏ธ Technical Changes #
- Added conditional imports using
dart.library.io
anddart.library.html
checks - Created platform-specific helper files with stub implementations for unsupported platforms
- Replaced NetworkImage with ExtendedNetworkImageProvider for improved reliability
- Removed direct platform-specific imports that caused compatibility issues
- Enhanced error handling and retry mechanisms across all platforms
0.3.4 - Raw Bytes Clipboard Support Release #
๐ New Major Features #
Raw Image Bytes Clipboard Support
- NEW:
copyImageBytesToClipboard(Uint8List fileData, {required int width, required int height})
method for copying raw image bytes to clipboard - NEW: Alternative to
copyImageToClipboard()
when working with rawUint8List
data instead ofImageDataInfo
wrapper - NEW: Required
width
andheight
parameters to ensure proper canvas rendering on web platforms - NEW: Full platform support with dedicated helper methods for mobile and desktop
Enhanced Clipboard Architecture
- NEW:
_copyImageBytesOnMobile()
- Platform-specific implementation for Android/iOS - NEW:
_copyImageBytesOnDesktop()
- Platform-specific implementation for desktop platforms - NEW:
saveImageBytesToTempFile()
- Utility method for saving raw bytes to temporary files - NEW: Automatic
ImageDataInfo
wrapper creation for web compatibility
๐ฏ Use Cases & Benefits #
When to Use Each Method
// โ
Use copyImageToClipboard when you have ImageDataInfo from CustomNetworkImage
CustomNetworkImage(
url: 'https://example.com/image.jpg',
onImageLoaded: (imageData) async {
await ImageClipboardHelper.copyImageToClipboard(imageData);
},
)
// โ
Use copyImageBytesToClipboard when working with raw bytes from other sources
final Uint8List cameraImageBytes = await camera.takePicture();
final success = await ImageClipboardHelper.copyImageBytesToClipboard(
cameraImageBytes,
width: 1920,
height: 1080,
);
Perfect for Integration With
- Camera plugins - Copy photos directly from camera capture
- File picker plugins - Copy selected images without loading into CustomNetworkImage
- Image processing libraries - Copy processed/filtered images
- Custom image generation - Copy programmatically created images
- Screenshot functionality - Copy captured screen regions
๐ ๏ธ Technical Implementation #
Function Signature
static Future<bool> copyImageBytesToClipboard(
Uint8List fileData, {
required int width,
required int height,
}) async
Platform-Specific Behavior
- Web: Creates canvas with specified dimensions for proper image rendering
- Mobile: Uses platform channels with raw bytes, falls back to temp file + path copying
- Desktop: Saves to temp file and copies file path to clipboard
Canvas Rendering Fix
- Problem Solved: Canvas creation with 0x0 dimensions was failing on web
- Solution: Required
width
andheight
parameters ensure valid canvas dimensions - Web Compatibility: Proper
ImageDataInfo
wrapper creation for existing web clipboard methods
๐ง Integration Examples #
Camera Integration
import 'package:camera/camera.dart';
import 'package:flutter_cors_image/flutter_cors_image.dart';
import 'package:image/image.dart' as img;
class CameraExample extends StatelessWidget {
final CameraController controller;
Future<void> captureAndCopy() async {
try {
final XFile photo = await controller.takePicture();
final Uint8List imageBytes = await photo.readAsBytes();
// Decode image to get actual dimensions
final img.Image? decodedImage = img.decodeImage(imageBytes);
if (decodedImage == null) {
print('Failed to decode image');
return;
}
final width = decodedImage.width;
final height = decodedImage.height;
print('Copying camera image: ${imageBytes.length} bytes, ${width}x${height}');
final success = await ImageClipboardHelper.copyImageBytesToClipboard(
imageBytes,
width: width,
height: height,
);
if (success) {
print('โ
Camera image copied to clipboard! Size: ${(imageBytes.length / 1024 / 1024).toStringAsFixed(1)} MB');
// User can now paste with Ctrl+V in other applications
} else {
print('โ Failed to copy camera image');
}
} catch (e) {
print('Error copying camera image: $e');
}
}
// Enhanced example with error handling and user feedback
Future<void> captureAndCopyWithFeedback(BuildContext context) async {
try {
// Show loading indicator
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AlertDialog(
content: Row(
children: [
CircularProgressIndicator(),
SizedBox(width: 16),
Text('Capturing and copying image...'),
],
),
),
);
final XFile photo = await controller.takePicture();
final Uint8List imageBytes = await photo.readAsBytes();
final img.Image? decodedImage = img.decodeImage(imageBytes);
if (decodedImage == null) throw Exception('Failed to decode image');
final success = await ImageClipboardHelper.copyImageBytesToClipboard(
imageBytes,
width: decodedImage.width,
height: decodedImage.height,
);
Navigator.of(context).pop(); // Dismiss loading dialog
// Show result
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success
? '๐ธ Photo copied! Size: ${(imageBytes.length / 1024 / 1024).toStringAsFixed(1)} MB - Press Ctrl+V to paste'
: 'โ Failed to copy photo'),
backgroundColor: success ? Colors.green : Colors.red,
duration: const Duration(seconds: 3),
),
);
} catch (e) {
Navigator.of(context).pop(); // Dismiss loading dialog
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red),
);
}
}
}
File Picker Integration
import 'package:file_picker/file_picker.dart';
import 'package:flutter_cors_image/flutter_cors_image.dart';
import 'package:image/image.dart' as img;
class FilePickerExample extends StatelessWidget {
Future<void> pickAndCopyImage() async {
try {
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.image,
withData: true,
);
if (result != null && result.files.single.bytes != null) {
final imageBytes = result.files.single.bytes!;
final fileName = result.files.single.name;
print('Processing selected image: $fileName (${imageBytes.length} bytes)');
// Decode image to get actual dimensions
final img.Image? decodedImage = img.decodeImage(imageBytes);
if (decodedImage == null) {
print('Failed to decode selected image');
return;
}
final width = decodedImage.width;
final height = decodedImage.height;
final sizeInMB = (imageBytes.length / 1024 / 1024);
print('Image info: ${width}x${height}, ${sizeInMB.toStringAsFixed(1)} MB');
final success = await ImageClipboardHelper.copyImageBytesToClipboard(
imageBytes,
width: width,
height: height,
);
if (success) {
print('โ
Selected image copied to clipboard! Size: ${sizeInMB.toStringAsFixed(1)} MB');
print('๐ You can now paste it anywhere with Ctrl+V');
} else {
print('โ Failed to copy selected image');
}
}
} catch (e) {
print('Error copying selected image: $e');
}
}
// Enhanced example for heavy images with progress feedback
Future<void> pickAndCopyHeavyImageWithFeedback(BuildContext context) async {
try {
// Pick image
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.image,
withData: true,
);
if (result == null || result.files.single.bytes == null) return;
final imageBytes = result.files.single.bytes!;
final fileName = result.files.single.name;
final sizeInMB = (imageBytes.length / 1024 / 1024);
// Show processing dialog for heavy images
if (sizeInMB > 2.0) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text('Processing heavy image...'),
Text('$fileName (${sizeInMB.toStringAsFixed(1)} MB)'),
],
),
),
);
}
// Decode and copy
final img.Image? decodedImage = img.decodeImage(imageBytes);
if (decodedImage == null) throw Exception('Failed to decode image');
final success = await ImageClipboardHelper.copyImageBytesToClipboard(
imageBytes,
width: decodedImage.width,
height: decodedImage.height,
);
if (sizeInMB > 2.0) {
Navigator.of(context).pop(); // Dismiss loading dialog
}
// Show result
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success
? '๐ ${fileName} copied! (${sizeInMB.toStringAsFixed(1)} MB) - Press Ctrl+V to paste'
: 'โ Failed to copy ${fileName}'),
backgroundColor: success ? Colors.green : Colors.red,
duration: Duration(seconds: sizeInMB > 5 ? 5 : 3), // Longer for heavy images
),
);
} catch (e) {
Navigator.of(context).pop(); // Dismiss any open dialog
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red),
);
}
}
}
Image Processing Integration
import 'package:image/image.dart' as img;
import 'package:flutter_cors_image/flutter_cors_image.dart';
class ImageProcessingExample extends StatelessWidget {
Future<void> processAndCopyImage(Uint8List originalBytes) async {
try {
// Decode original image
final img.Image? originalImage = img.decodeImage(originalBytes);
if (originalImage == null) return;
// Apply filters/processing
final processedImage = img.adjustColor(originalImage, brightness: 1.2);
final filteredImage = img.gaussianBlur(processedImage, radius: 2);
// Encode back to bytes
final processedBytes = Uint8List.fromList(img.encodePng(filteredImage));
// Copy processed image to clipboard
final success = await ImageClipboardHelper.copyImageBytesToClipboard(
processedBytes,
width: filteredImage.width,
height: filteredImage.height,
);
if (success) {
print('Processed image copied to clipboard!');
}
} catch (e) {
print('Failed to copy processed image: $e');
}
}
}
๐ฑ Platform Support #
Platform | Raw Bytes Support | Canvas Rendering | Temp File Fallback |
---|---|---|---|
Web | โ Full support | โ Required dimensions | โ Not applicable |
Android | โ Platform channels | โ Not applicable | โ Fallback method |
iOS | โ Platform channels | โ Not applicable | โ Fallback method |
Desktop | โ File path copy | โ Not applicable | โ Primary method |
๐ Backward Compatibility #
- 100% backward compatible - existing
copyImageToClipboard()
continues to work unchanged - Optional enhancement - new method provides alternative for different use cases
- No conflicts - both methods can be used in the same application
- Same dependencies - no additional packages required
Heavy Image Performance Testing
import 'package:flutter/material.dart';
import 'package:flutter_cors_image/flutter_cors_image.dart';
import 'dart:typed_data';
class HeavyImageCopyExample extends StatefulWidget {
@override
_HeavyImageCopyExampleState createState() => _HeavyImageCopyExampleState();
}
class _HeavyImageCopyExampleState extends State<HeavyImageCopyExample> {
ImageDataInfo? _heavyImageData;
bool _isProcessing = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Heavy Image Copy Testing')),
body: Column(
children: [
// Heavy image display
CustomNetworkImage(
url: 'https://picsum.photos/4000/3000?random=heavy', // 4K image (~3-5 MB)
width: 400,
height: 300,
fit: BoxFit.cover,
onImageLoaded: (imageData) {
setState(() => _heavyImageData = imageData);
final sizeInMB = (imageData.imageBytes.length / 1024 / 1024);
print('Heavy image loaded: ${sizeInMB.toStringAsFixed(1)} MB');
},
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),
Text('Loading heavy image...'),
Text('4K Resolution (~3-5 MB)'),
],
),
),
);
},
),
const SizedBox(height: 20),
// Copy buttons
if (_heavyImageData != null) ...[
Text('Image loaded: ${(_heavyImageData!.imageBytes.length / 1024 / 1024).toStringAsFixed(1)} MB'),
Text('Resolution: ${_heavyImageData!.width}x${_heavyImageData!.height}'),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
onPressed: _isProcessing ? null : () => _copyImageDataMethod(),
icon: Icon(Icons.copy),
label: Text('Copy via ImageDataInfo'),
style: ElevatedButton.styleFrom(backgroundColor: Colors.blue),
),
ElevatedButton.icon(
onPressed: _isProcessing ? null : () => _copyRawBytesMethod(),
icon: Icon(Icons.content_copy),
label: Text('Copy via Raw Bytes'),
style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
),
],
),
const SizedBox(height: 16),
// Performance comparison
ElevatedButton.icon(
onPressed: _isProcessing ? null : () => _performanceComparison(),
icon: Icon(Icons.speed),
label: Text('Performance Comparison'),
style: ElevatedButton.styleFrom(backgroundColor: Colors.orange),
),
if (_isProcessing) ...[
const SizedBox(height: 16),
CircularProgressIndicator(),
Text('Processing heavy image...'),
],
],
],
),
);
}
Future<void> _copyImageDataMethod() async {
if (_heavyImageData == null) return;
setState(() => _isProcessing = true);
final stopwatch = Stopwatch()..start();
try {
final success = await ImageClipboardHelper.copyImageToClipboard(_heavyImageData!);
stopwatch.stop();
final sizeInMB = (_heavyImageData!.imageBytes.length / 1024 / 1024);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success
? 'โ
ImageDataInfo method: ${sizeInMB.toStringAsFixed(1)} MB copied in ${stopwatch.elapsedMilliseconds}ms'
: 'โ ImageDataInfo method failed'),
backgroundColor: success ? Colors.green : Colors.red,
duration: Duration(seconds: 4),
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red),
);
} finally {
setState(() => _isProcessing = false);
}
}
Future<void> _copyRawBytesMethod() async {
if (_heavyImageData == null) return;
setState(() => _isProcessing = true);
final stopwatch = Stopwatch()..start();
try {
final success = await ImageClipboardHelper.copyImageBytesToClipboard(
_heavyImageData!.imageBytes,
width: _heavyImageData!.width,
height: _heavyImageData!.height,
);
stopwatch.stop();
final sizeInMB = (_heavyImageData!.imageBytes.length / 1024 / 1024);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success
? 'โ
Raw Bytes method: ${sizeInMB.toStringAsFixed(1)} MB copied in ${stopwatch.elapsedMilliseconds}ms'
: 'โ Raw Bytes method failed'),
backgroundColor: success ? Colors.green : Colors.red,
duration: Duration(seconds: 4),
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red),
);
} finally {
setState(() => _isProcessing = false);
}
}
Future<void> _performanceComparison() async {
if (_heavyImageData == null) return;
setState(() => _isProcessing = true);
try {
final results = <String, int>{};
// Test ImageDataInfo method
final stopwatch1 = Stopwatch()..start();
final success1 = await ImageClipboardHelper.copyImageToClipboard(_heavyImageData!);
stopwatch1.stop();
results['ImageDataInfo'] = stopwatch1.elapsedMilliseconds;
await Future.delayed(Duration(milliseconds: 500)); // Brief pause
// Test Raw Bytes method
final stopwatch2 = Stopwatch()..start();
final success2 = await ImageClipboardHelper.copyImageBytesToClipboard(
_heavyImageData!.imageBytes,
width: _heavyImageData!.width,
height: _heavyImageData!.height,
);
stopwatch2.stop();
results['Raw Bytes'] = stopwatch2.elapsedMilliseconds;
final sizeInMB = (_heavyImageData!.imageBytes.length / 1024 / 1024);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Performance Comparison'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Image Size: ${sizeInMB.toStringAsFixed(1)} MB'),
Text('Resolution: ${_heavyImageData!.width}x${_heavyImageData!.height}'),
Divider(),
Text('ImageDataInfo: ${results['ImageDataInfo']}ms'),
Text('Raw Bytes: ${results['Raw Bytes']}ms'),
Divider(),
Text(
'Both methods provide identical clipboard functionality. '
'Use ImageDataInfo with CustomNetworkImage, Raw Bytes with external sources.',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('OK'),
),
],
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Comparison failed: $e'), backgroundColor: Colors.red),
);
} finally {
setState(() => _isProcessing = false);
}
}
}
๐งช Testing & Validation #
- Web canvas rendering - verified proper canvas creation with valid dimensions
- Platform channel integration - tested on Android/iOS with platform-specific methods
- Temp file management - validated file creation and cleanup on desktop platforms
- Error handling - comprehensive error catching and graceful fallbacks
- Memory management - proper disposal of temporary resources
- Heavy image performance - tested with 4K-8K images up to 10MB in size
- Performance comparison - both methods provide identical performance characteristics
0.3.3 - Right-Click Context Menu Release #
๐ New Major Features #
Right-Click Context Menu System (Web Only)
- NEW: Native-like right-click context menu for images with browser-style actions
- NEW:
enableContextMenu
parameter to enable/disable context menu functionality - NEW:
customContextMenuItems
for adding custom menu items with icons and callbacks - NEW: Built-in context menu actions:
- Copy image - Copies image to system clipboard for Ctrl+V pasting
- Save image as... - Shows browser save dialog with File System Access API
- Open image in new tab - Opens image URL in new browser tab
- Copy image address - Copies image URL to clipboard
Advanced Context Menu Customization
- NEW:
ContextMenuItem
class for defining custom menu items with icons and actions - NEW:
ContextMenuAction
enum for built-in and custom actions - NEW: Context menu styling options:
contextMenuBackgroundColor
- Custom background colorcontextMenuTextColor
- Custom text colorcontextMenuElevation
- Shadow elevationcontextMenuBorderRadius
- Corner radiuscontextMenuPadding
- Internal padding
- NEW:
onContextMenuAction
callback for handling menu item selections
Smart Context Menu Behavior
- NEW: Automatic browser context menu prevention only when hovering over images
- NEW: Smart positioning to keep menu on screen (auto-adjusts near edges)
- NEW: Overlay-based rendering for proper z-index and click-outside dismissal
- NEW: Works across all image loading states (loading, loaded, error, HTML fallback)
Enhanced Download & Save Experience
- NEW: File System Access API integration for proper save dialogs with location picker
- NEW: Traditional blob download fallback for browser compatibility
- NEW: Success/failure toast notifications with color-coded feedback
- NEW: Proper file permissions and visibility in file system
- NEW: Enhanced error handling with detailed logging for debugging
๐จ Usage Examples #
Basic Context Menu
CustomNetworkImage(
url: 'https://example.com/image.jpg',
width: 300,
height: 200,
// โ
NEW: Enable right-click context menu
enableContextMenu: true,
// โ
NEW: Handle context menu actions
onContextMenuAction: (action) {
print('Context menu action: $action');
},
)
Custom Context Menu Items
CustomNetworkImage(
url: 'https://example.com/image.jpg',
enableContextMenu: true,
// โ
NEW: Custom menu items with icons
customContextMenuItems: [
ContextMenuItem(
title: 'Download Image',
icon: Icons.download,
action: ContextMenuAction.saveImage,
),
ContextMenuItem(
title: 'Copy to Clipboard',
icon: Icons.copy,
action: ContextMenuAction.copyImage,
),
ContextMenuItem(
title: 'Custom Action',
icon: Icons.star,
action: ContextMenuAction.custom,
onTap: () {
// Custom action handler
print('Custom action executed!');
},
),
],
// โ
NEW: Custom styling
contextMenuBackgroundColor: Colors.grey[800],
contextMenuTextColor: Colors.white,
contextMenuElevation: 8.0,
contextMenuBorderRadius: BorderRadius.circular(12),
)
Context Menu with Toast Notifications
CustomNetworkImage(
url: 'https://example.com/image.jpg',
enableContextMenu: true,
onContextMenuAction: (action) {
// Toast notifications automatically shown for:
// โ
Image saved successfully!
// โ Failed to save image
// Image copied to clipboard
// Image URL copied to clipboard
// Opening image in new tab
},
)
๐ ๏ธ Technical Implementation #
New Classes & Enums
// Context menu item definition
class ContextMenuItem {
final String title;
final IconData? icon;
final ContextMenuAction action;
final VoidCallback? onTap;
}
// Available context menu actions
enum ContextMenuAction {
copyImage, // Copy image to clipboard
saveImage, // Save image with file picker
openImageInNewTab, // Open in new browser tab
copyImageUrl, // Copy URL to clipboard
custom, // Custom action with onTap callback
}
Context Menu Parameters
CustomNetworkImage(
// Context menu control
enableContextMenu: true, // Enable/disable context menu
customContextMenuItems: [...], // Custom menu items
onContextMenuAction: (action) => {...}, // Action callback
// Styling options
contextMenuBackgroundColor: Colors.white, // Background color
contextMenuTextColor: Colors.black, // Text color
contextMenuElevation: 8.0, // Shadow elevation
contextMenuBorderRadius: BorderRadius.circular(8), // Corner radius
contextMenuPadding: EdgeInsets.all(8), // Internal padding
)
๐ Platform Support & Behavior #
Web Platform (Primary Target)
- โ Full context menu functionality with native-like behavior
- โ File System Access API for proper save dialogs with location picker
- โ Clipboard API integration for image and URL copying
- โ Browser context menu prevention only when hovering over images
- โ Toast notifications for user feedback
Mobile/Desktop Platforms
- โ ๏ธ Limited support - Context menus are primarily a web/desktop concept
- โ Hover icons remain available as alternative for touch interfaces
- โ Manual download/copy buttons continue to work normally
๐ฏ Smart Context Menu Features #
Intelligent Positioning
- Auto-adjustment: Menu automatically repositions to stay on screen
- Edge detection: Prevents menu from going off screen edges
- Responsive sizing: Adapts to different screen sizes and orientations
State-Aware Behavior
- Loading state: Context menu available during image loading
- Loaded state: Full functionality with image data access
- Error state: Context menu still works with URL-based actions
- HTML fallback: Context menu overlays HTML img elements
Browser Integration
- Selective prevention: Only prevents browser context menu when over images
- Normal browsing: Browser context menu works normally on text, links, etc.
- Download integration: Uses browser's download system for familiar UX
๐ง Enhanced Download System #
File System Access API (Modern Browsers)
- Save dialog: Shows native file picker for choosing save location
- File permissions: Proper file permissions for visibility in file managers
- Progress feedback: Toast notifications for save success/failure
- User control: Full control over save location and filename
Fallback Download Methods
- Blob download: Traditional browser download to Downloads folder
- Direct download: URL-based download as last resort
- Error handling: Graceful degradation with detailed error logging
๐ฑ Example Integration #
Complete Context Menu Demo
class ContextMenuDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomNetworkImage(
url: 'https://picsum.photos/300/200',
width: 300,
height: 200,
// Enable context menu with default items
enableContextMenu: true,
// Custom styling
contextMenuBackgroundColor: Colors.grey[800],
contextMenuTextColor: Colors.white,
contextMenuElevation: 12.0,
contextMenuBorderRadius: BorderRadius.circular(8),
// Handle actions
onContextMenuAction: (action) {
switch (action) {
case ContextMenuAction.copyImage:
// Image copied to clipboard automatically
break;
case ContextMenuAction.saveImage:
// Save dialog shown automatically
break;
case ContextMenuAction.openImageInNewTab:
// Image opened in new tab automatically
break;
case ContextMenuAction.copyImageUrl:
// URL copied to clipboard automatically
break;
}
},
);
}
}
๐ Backward Compatibility #
- 100% backward compatible - existing code continues to work unchanged
- Optional feature - context menu is opt-in via
enableContextMenu
parameter - No conflicts - works alongside existing hover icons and callbacks
- Progressive enhancement - add context menu to existing implementations gradually
๐งช Comprehensive Testing #
- Cross-browser testing on Chrome, Firefox, Safari, Edge
- File System Access API testing with save dialog functionality
- Clipboard integration testing with copy/paste operations
- Context menu positioning testing near screen edges
- Toast notification testing for user feedback
- Error handling testing for various failure scenarios
0.3.2 - Controller Feature Release #
๐ New Major Features #
External Controller System
- NEW:
CustomNetworkImageController
for external image management and control - NEW:
controller
parameter inCustomNetworkImage
for linking external controller - NEW: External methods for image operations:
controller.reload()
- Reload image from URLcontroller.downloadImage()
- Download image to devicecontroller.copyImageToClipboard()
- Copy image to system clipboardcontroller.getCurrentImageData()
- Get current image datacontroller.waitForLoad()
- Wait for image loading with timeout
Real-time State Management
- NEW: Live state monitoring with
ChangeNotifier
integration:controller.isLoading
- Check if image is currently loadingcontroller.isLoaded
- Check if image loaded successfullycontroller.isFailed
- Check if image failed to loadcontroller.hasImageData
- Check if image data is availablecontroller.loadingProgress
- Get current loading progresscontroller.errorMessage
- Get current error message
Multiple Controller Support
- NEW: Use separate controllers for different images in same widget tree
- NEW: Independent state management for each image instance
- NEW: External control of multiple images simultaneously
๐๏ธ Architecture Improvements #
Code Refactoring & Separation
- NEW:
lib/src/types.dart
- Shared types and enums for better organization - NEW:
lib/src/custom_network_image_controller.dart
- Dedicated controller implementation - IMPROVED: Removed duplicate type definitions across files
- IMPROVED: Better import structure and dependency management
- IMPROVED: Cleaner codebase with separated concerns
Enhanced Library Exports
- NEW: Export controller and types for external usage
- NEW: Public API access to
CustomNetworkImageController
- NEW: Access to
ImageLoadingState
,CustomImageProgress
,ImageDataInfo
- NEW: Public access to
ImageClipboardHelper
for manual operations
๐ฎ Usage Examples #
Basic Controller Usage
final controller = CustomNetworkImageController();
// Listen to state changes
controller.addListener(() {
print('Loading state: ${controller.loadingState}');
if (controller.isLoaded) {
print('Image ready: ${controller.imageData?.width}x${controller.imageData?.height}');
}
});
// Use with widget
CustomNetworkImage(
url: 'https://example.com/image.jpg',
controller: controller,
downloadIcon: Icon(Icons.download),
copyIcon: Icon(Icons.copy),
)
// External control
await controller.downloadImage();
await controller.copyImageToClipboard();
controller.reload();
Multiple Controllers
class MultiImageWidget extends StatefulWidget {
@override
_MultiImageWidgetState createState() => _MultiImageWidgetState();
}
class _MultiImageWidgetState extends State<MultiImageWidget> {
late CustomNetworkImageController controller1;
late CustomNetworkImageController controller2;
@override
void initState() {
super.initState();
controller1 = CustomNetworkImageController();
controller2 = CustomNetworkImageController();
}
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: Column(
children: [
CustomNetworkImage(
url: 'https://example.com/image1.jpg',
controller: controller1,
),
ElevatedButton(
onPressed: () => controller1.reload(),
child: Text('Reload Image 1'),
),
],
),
),
Expanded(
child: Column(
children: [
CustomNetworkImage(
url: 'https://example.com/image2.jpg',
controller: controller2,
),
ElevatedButton(
onPressed: () => controller2.downloadImage(),
child: Text('Download Image 2'),
),
],
),
),
],
);
}
@override
void dispose() {
controller1.dispose();
controller2.dispose();
super.dispose();
}
}
๐ง Technical Implementation #
Controller State Management
class CustomNetworkImageController extends ChangeNotifier {
// State properties
ImageLoadingState get loadingState;
CustomImageProgress? get loadingProgress;
ImageDataInfo? get imageData;
String? get errorMessage;
// Convenience getters
bool get isLoading;
bool get isLoaded;
bool get isFailed;
bool get hasImageData;
// Control methods
void reload();
Future<bool> downloadImage();
Future<bool> copyImageToClipboard();
Future<ImageDataInfo> waitForLoad({Duration timeout});
}
Integration with Existing Features
- Full Compatibility: Controller works alongside all existing hover icons and callbacks
- State Sync: Widget state automatically syncs with controller state
- Error Handling: Controller captures and exposes all error states
- Progress Tracking: Real-time loading progress available through controller
๐ฑ Enhanced Example App #
- NEW: Comprehensive controller demonstration in
example/simple_usage_example.dart
- NEW: Live status panel showing controller state in real-time
- NEW: External action buttons for controller methods
- NEW: Multiple controller examples with independent control
- NEW: Status tracking and error handling demonstrations
๐ Backward Compatibility #
- 100% Backward Compatible: All existing code continues to work unchanged
- Optional Enhancement: Controller is optional - existing callback/hover icon patterns still work
- Progressive Adoption: Add controller gradually to existing implementations
- No Breaking Changes: All existing parameters and functionality preserved
๐ ๏ธ Developer Experience #
- Type Safety: Full TypeScript-like type safety with controller state
- IntelliSense: Rich IDE support for controller methods and properties
- Documentation: Comprehensive inline documentation for all new features
- Examples: Multiple usage patterns and best practices demonstrated
0.3.1 - Clipboard Fix Release #
๐ Bug Fixes #
- Fixed Clipboard Copying Issue: Resolved critical bug where clipboard copying failed with "DataError: Failed to read or decode ClipboardItemData for type image/png"
- Root Cause: Complex JavaScript object manipulation wasn't reliable across browsers
- Solution: Implemented simplified JavaScript approach using direct script injection
- New Method: Created
_simpleClipboardCopy()
with cleaner function definition in global scope - Better Error Handling: Added proper cleanup and graceful fallback to alternative methods
- Multi-Method Fallback: Now tries 3 different approaches if one fails
๐ง Technical Changes #
- Simplified Clipboard API: More reliable ClipboardItem creation using direct JavaScript functions
- Enhanced Fallback System: Canvas-based approach as secondary method, legacy fallback as tertiary
- Improved Error Logging: Better debugging information to identify which copy method succeeds
- Resource Management: Proper cleanup of created script elements to prevent memory leaks
๐งช Validation #
- โ Copy icon click now successfully copies images to clipboard
- โ Ctrl+V pasting works correctly in external applications
- โ Multiple fallback methods ensure compatibility across different browsers
- โ Proper error messages and graceful degradation when clipboard access is restricted
0.3.0 - Major Feature Release: Hover Icons & Image Data Access #
๐ New Major Features #
Image Data Callback System
- NEW:
onImageLoaded
callback provides immediate access to image data when loading completes - NEW:
ImageDataInfo
class contains image bytes, dimensions, and URL for copy/download operations - Feature: No waiting required - image data available instantly after load for clipboard/download functionality
Hover Icons with Smart Positioning
- NEW:
downloadIcon
andcopyIcon
parameters for custom action icons that appear on hover - NEW: 6 positioning options via
HoverIconPosition
enum:topLeft
,topRight
,bottomLeft
,bottomRight
,topCenter
,bottomCenter
- NEW: 3 layout modes via
HoverIconLayout
enum:auto
- Smart layout based on positionrow
- Always horizontal arrangementcolumn
- Always vertical arrangement
- NEW: Customizable spacing (
hoverIconSpacing
) and padding (hoverIconPadding
) - NEW:
enableHoverIcons
toggle for full control
Advanced Clipboard & Download System
- NEW: Separate
downloadImage()
andcopyImageToClipboard()
methods with distinct behaviors:- Download: Saves PNG file to computer (web: Downloads folder, mobile: temp directory)
- Copy: Copies image to system clipboard for pasting with Ctrl+V
- NEW: Cross-platform clipboard support using modern Clipboard API on web
- NEW: Custom action callbacks:
onDownloadTap
andonCopyTap
for overriding default behaviors
Enhanced User Experience
- NEW: Smart hover detection with smooth icon transitions (web/desktop only)
- NEW: Material design click feedback with
InkWell
integration - NEW: Comprehensive example app showcasing all features with live controls
- NEW: Real-time customization via control panel (position, layout, spacing, padding)
๐จ Usage Examples #
Basic Hover Icons
CustomNetworkImage(
url: 'https://example.com/image.jpg',
// โ
NEW: 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,
// โ
NEW: 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
},
)
Advanced Styled Icons
CustomNetworkImage(
url: imageUrl,
downloadIcon: Container(
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.blue, Colors.blueAccent]),
borderRadius: BorderRadius.circular(8),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 4)],
),
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)),
],
),
),
hoverIconPosition: HoverIconPosition.bottomRight,
hoverIconLayout: HoverIconLayout.row,
// โ
Custom action callbacks
onDownloadTap: () => customDownloadHandler(),
onCopyTap: () => customCopyHandler(),
)
Using Image Data for Copy Operations
ImageDataInfo? _imageData;
CustomNetworkImage(
url: imageUrl,
onImageLoaded: (imageData) {
setState(() => _imageData = imageData);
},
)
// Later, copy to clipboard
ElevatedButton(
onPressed: () async {
if (_imageData != null) {
final success = await ImageClipboardHelper.copyImageToClipboard(_imageData!);
// Image is now in clipboard, ready for Ctrl+V pasting!
}
},
child: Text('Copy Image'),
)
๐ ๏ธ Technical Implementation #
New Classes & Enums
// Image data container
class ImageDataInfo {
final Uint8List imageBytes; // Raw PNG data
final int width, height; // Dimensions
final String url; // Original URL
}
// Icon positioning
enum HoverIconPosition {
topLeft, topRight, bottomLeft, bottomRight, topCenter, bottomCenter
}
// Layout direction
enum HoverIconLayout {
auto, row, column
}
New Helper Methods
// Clipboard & Download
ImageClipboardHelper.copyImageToClipboard(imageData) // Clipboard copy
ImageClipboardHelper.downloadImage(imageData) // File download
// Platform-specific implementations
copyImageToClipboardWeb(imageData) // Web Clipboard API
downloadImageWeb(imageData) // Web file download
๐ง Platform Support #
- Web: Full clipboard copying + file downloads using modern Clipboard API and blob downloads
- Mobile: Temp file saving + basic clipboard support (extensible with plugins)
- Desktop: File path copying + temp file saving
๐ฑ Responsive Design #
- Hover icons: Auto-enabled on web/desktop, disabled on mobile (no hover support)
- Touch support: Icons work with touch on platforms that support hover simulation
- Adaptive layouts: Smart icon positioning based on screen real estate
๐งช Comprehensive Testing #
- Interactive example app with live controls for all parameters
- Position examples grid showing all 6 positions
- Layout comparison demonstrating row vs column vs auto layouts
- Real-time customization via sliders and toggles
๐ Backward Compatibility #
- 100% backward compatible - existing code continues to work unchanged
- Optional features - all new parameters have sensible defaults
- Progressive enhancement - add hover icons gradually to existing implementations
0.2.1 - Bug Fix Release #
๐ Bug Fixes #
- Alternative to Buggy Flutter loadingBuilder: Implemented custom loading state management to replace Flutter's problematic
loadingBuilder
- Replaced
loadingBuilder
parameter withcustomLoadingBuilder
that usesCustomImageProgress
- Added reliable progress tracking via
ImageStream
andImageStreamListener
- Fixed memory leaks and inconsistent progress reporting issues
- Added proper resource cleanup with
_cleanupImageStream()
method
- Replaced
๐ New Features #
- Custom Loading Progress Tracking: New
CustomImageProgress
class provides reliable loading progress information - Enhanced Loading State Management: Added
ImageLoadingState
enum for better loading state control - Improved Resource Management: Automatic cleanup of image streams and listeners to prevent memory leaks
๐ Migration Guide #
// OLD (buggy Flutter loadingBuilder)
CustomNetworkImage(
url: imageUrl,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return CircularProgressIndicator(
value: loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!,
);
},
)
// NEW (reliable customLoadingBuilder)
CustomNetworkImage(
url: imageUrl,
customLoadingBuilder: (context, child, progress) {
if (progress == null) return child;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(value: progress.progress),
if (progress.progress != null)
Text('${(progress.progress! * 100).toInt()}%'),
],
);
},
)
๐ ๏ธ Technical Changes #
- Manual image loading progress tracking via
onChunk
callback - Proper state management for loading, loaded, and failed states
- Enhanced error handling integration with existing fallback mechanisms
- Backward compatibility maintained for existing error handling features
0.2.0 - BREAKING CHANGES #
๐ New Features #
- Widget-based Error Handling: Introduced new widget parameters for more flexible error UI customization:
errorWidget
- Custom widget to show when image fails to loadreloadWidget
- Custom widget for retry functionalityopenUrlWidget
- Custom widget for opening image URL in new tab
- Flutter-based Error UI: HTML errors now callback to Flutter for consistent error handling across platforms
- Enhanced Error Flow: When HTML image loading fails, errors are now handled in Flutter using the new widget parameters
โ ๏ธ Breaking Changes #
- HTML Error Handling: HTML errors no longer show HTML-based error UI. Instead, they trigger Flutter callbacks for consistent widget-based error handling.
- Deprecated Parameters: The following string-based parameters are now deprecated and will be removed in v1.0.0:
errorText
โ UseerrorWidget
insteadreloadText
โ UsereloadWidget
insteadopenUrlText
โ UseopenUrlWidget
instead
๐ Migration Guide #
// 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'),
],
),
)
๐ ๏ธ Technical Changes #
- Added HTML error callback mechanism for Flutter integration
- Removed HTML-based error UI generation from
web_image_loader.dart
- Enhanced
CustomNetworkImage
state management for error handling - Improved backward compatibility with automatic fallback from deprecated parameters
๐ Documentation #
- Updated examples to demonstrate new widget-based error handling
- Added migration guide in example app
- Updated README with v0.2.0 usage patterns
0.1.9 #
- Fixed: Resolved issue where error placeholder was not being displayed correctly
- Fixed: Resolved issue where error placeholder was not being displayed correctly
0.1.8 #
- Fixed: Resolved issue where error placeholder was not being displayed correctly
- Fixed: Resolved issue where error placeholder was not being displayed correctly
0.1.7 #
- Added: Internationalization support for error handling - supports custom text for error messages and buttons
- Enhanced: Error placeholder now includes reload and "open in new tab" functionality when HTML image loading fails
- Added:
errorText
,reloadText
, andopenUrlText
parameters to CustomNetworkImage for multilingual support - Improved: Error UI can show only icons when no text is provided (icon-only mode for universal understanding)
- Added: Reload button to retry failed images without page refresh
- Added: "Open in new tab" button to view the problematic image URL directly
0.1.6 #
- Fixed: Resolved tap event conflicts in ListViews with mixed HTML fallback and normal images
- Update README.md
0.1.5 #
- Fixed: Resolved tap event conflicts in ListViews with mixed HTML fallback and normal images
- Added: Per-image tap callback tracking to prevent event confusion
- Added: uniqueId parameter to CustomNetworkImage for better control in lists
- Improved: Event propagation handling with stopPropagation() to isolate tap events
- Added: Proper cleanup of resources when widgets are disposed
0.1.4 #
- Fixed: Dramatically improved smoothness of panning/dragging in InteractiveViewer with HTML fallback images
- Added: Animation controller for continuous transformation updates during gestures
- Improved: Full matrix transformation for more accurate CSS transforms
- Fixed: Pointer events handling for smoother gesture recognition
0.1.3 #
- Fixed: InteractiveViewer zoom functionality now works with HTML fallback images
- Added: TransformationController support for CustomNetworkImage
- Improved: Object-fit set to 'contain' for better zooming behavior
- Fixed: Proper transformation handling for HTML elements
0.1.2 #
- Fixed: GestureDetector tap events now work correctly with problematic images using HTML fallback
- Added: onTap callback property to CustomNetworkImage for easier tap handling
- Improved: Visual feedback with cursor pointer on HTML fallback images
0.1.1 #
- Updated intl dependency to support a wider range of versions (>=0.19.0 <0.21.0)
- Improved compatibility with projects that depend on intl 0.19.0
0.1.0 #
- Initial release with image loading solution:
- CustomNetworkImage: Uses HTML img tag as fallback
- Support for all standard Image.network parameters
- Full web platform support for problematic images
- Example app showing how to use the approach