flutter_cors_image 0.3.5 copy "flutter_cors_image: ^0.3.5" to clipboard
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 and dart.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 raw Uint8List data instead of ImageDataInfo wrapper
  • NEW: Required width and height 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 and height 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 color
    • contextMenuTextColor - Custom text color
    • contextMenuElevation - Shadow elevation
    • contextMenuBorderRadius - Corner radius
    • contextMenuPadding - 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 in CustomNetworkImage for linking external controller
  • NEW: External methods for image operations:
    • controller.reload() - Reload image from URL
    • controller.downloadImage() - Download image to device
    • controller.copyImageToClipboard() - Copy image to system clipboard
    • controller.getCurrentImageData() - Get current image data
    • controller.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 loading
    • controller.isLoaded - Check if image loaded successfully
    • controller.isFailed - Check if image failed to load
    • controller.hasImageData - Check if image data is available
    • controller.loadingProgress - Get current loading progress
    • controller.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 and copyIcon 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 position
    • row - Always horizontal arrangement
    • column - Always vertical arrangement
  • NEW: Customizable spacing (hoverIconSpacing) and padding (hoverIconPadding)
  • NEW: enableHoverIcons toggle for full control

Advanced Clipboard & Download System

  • NEW: Separate downloadImage() and copyImageToClipboard() 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 and onCopyTap 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 with customLoadingBuilder that uses CustomImageProgress
    • Added reliable progress tracking via ImageStream and ImageStreamListener
    • Fixed memory leaks and inconsistent progress reporting issues
    • Added proper resource cleanup with _cleanupImageStream() method

๐Ÿš€ 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 load
    • reloadWidget - Custom widget for retry functionality
    • openUrlWidget - 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 โ†’ Use errorWidget instead
    • reloadText โ†’ Use reloadWidget instead
    • openUrlText โ†’ Use openUrlWidget 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, and openUrlText 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
1
likes
140
points
635
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter package that provides image loading solutions for handling CORS issues and problematic images on web platforms.

Repository (GitHub)
View/report issues

Topics

#image #cors #web #html

Documentation

API reference

License

MIT (license)

Dependencies

cupertino_icons, extended_image, flutter, flutter_web_plugins, http, image, intl, path_provider, web

More

Packages that depend on flutter_cors_image