Flutter CORS Image
A Flutter package that provides advanced image loading solutions for handling CORS issues, with ListView performance optimization, IndexedDB caching, modern hover icons, clipboard functionality, right-click context menus, and image data access.
๐ Live Demo
Try out all the features interactively: https://nguyenhuutukhtn.github.io/flutter_cors_image/
The demo includes 6 comprehensive examples:
- Basic Usage: Core image loading with CORS handling and error states
- Tap Events: Interactive tap functionality with counter examples
- Zoom Support: Pinch-to-zoom and click-to-zoom with transformation controls
- Context Menu: Right-click context menus with custom actions (web only)
- ListView Test: Performance testing with 50+ images demonstrating zero network requests after caching
- Advanced: Hover icons, clipboard operations, and external controller management
Features
This package provides comprehensive image loading solutions with advanced performance optimizations:
ListView Performance Optimization (v0.3.7)
๐ Major Performance Fix: Resolves the critical ListView scrolling issue where repeated server requests were stressing servers.
IndexedDB Caching System:
- Zero network requests after initial cache population in ListView scrolling
- Cross-session persistence - images cached across browser sessions
- Automatic cache management with configurable size limits (100MB default)
- FIFO cleanup when storage quota is reached
- Binary storage in IndexedDB for better performance than localStorage
CustomNetworkImage
This approach follows this strategy:
- Check IndexedDB cache - Instant display from browser storage (web only)
- Try Flutter's Image.network - Standard Flutter image loading
- HTML img fallback - Automatic fallback for CORS issues on web
- ExtendedImage fallback - Enhanced compatibility on native platforms
- Cache in IndexedDB - Store for future instant loading
New in v0.3.7: ListView performance optimization with IndexedDB caching system that eliminates server stress from repeated requests.
New in v0.3.3: Right-click context menu with native browser-like functionality for web platforms.
New in v0.3.0: Hover icons with customizable positioning, image data callbacks, and advanced clipboard/download functionality.
New in v0.2.0: Widget-based error handling with customizable error, reload, and open URL widgets. HTML errors now callback to Flutter for consistent UI across platforms.
Installation
Add the dependency to your pubspec.yaml
:
dependencies:
flutter_cors_image: ^0.3.7
Usage
Import the package:
import 'package:flutter_cors_image/flutter_cors_image.dart';
ListView Performance with IndexedDB Caching (v0.3.7+):
// โ
NEW v0.3.7: Optimized for ListView with IndexedDB caching
ListView.builder(
itemCount: 100, // Large list of images
itemBuilder: (context, index) {
return CustomNetworkImage(
url: 'https://picsum.photos/400/300?random=$index',
width: 400,
height: 300,
fit: BoxFit.cover,
// โ
IndexedDB caching configuration (enabled by default)
webStorageCacheConfig: WebStorageCacheConfig(
enabled: true, // Enable persistent caching
maxCacheSize: 100 * 1024 * 1024, // 100MB cache limit
cacheExpirationHours: 168, // 7 days expiration
),
);
},
)
// ๐ฏ Performance Results:
// - First scroll: Normal network requests (expected)
// - Refresh page (F5): ZERO network requests (cached)
// - Rapid scrolling: Instant image display from IndexedDB
// - Server stress: Eliminated for repeat views
Using Right-Click Context Menu (v0.3.3+):
CustomNetworkImage(
url: 'https://example.com/image.jpg',
width: 300,
height: 200,
fit: BoxFit.cover,
// โ
NEW v0.3.3: Right-click context menu (web only)
enableContextMenu: true,
// โ
Handle context menu actions
onContextMenuAction: (action) {
print('Context menu action: $action');
// Actions: copyImage, saveImage, openImageInNewTab, copyImageUrl, custom
},
// โ
Custom context menu styling
contextMenuBackgroundColor: Colors.grey[800],
contextMenuTextColor: Colors.white,
contextMenuElevation: 8.0,
contextMenuBorderRadius: BorderRadius.circular(8),
)
Custom Context Menu Items (v0.3.3+):
CustomNetworkImage(
url: 'https://example.com/image.jpg',
enableContextMenu: true,
// โ
NEW: Custom menu items with icons and actions
customContextMenuItems: [
ContextMenuItem(
title: 'Download Image',
icon: Icons.download,
action: ContextMenuAction.saveImage,
),
ContextMenuItem(
title: 'Copy to Clipboard',
icon: Icons.copy,
action: ContextMenuAction.copyImage,
),
ContextMenuItem(
title: 'Share Image',
icon: Icons.share,
action: ContextMenuAction.custom,
onTap: () {
// Custom share functionality
print('Share image action!');
},
),
],
// โ
Custom styling
contextMenuBackgroundColor: Colors.grey[800],
contextMenuTextColor: Colors.white,
contextMenuElevation: 12.0,
contextMenuBorderRadius: BorderRadius.circular(12),
contextMenuPadding: EdgeInsets.all(8),
)
Using CustomNetworkImage with Hover Icons (v0.3.0+):
CustomNetworkImage(
url: 'https://example.com/image.jpg',
width: 300,
height: 200,
fit: BoxFit.cover,
// โ
v0.3.0: Hover icons for quick actions
downloadIcon: Icon(Icons.download, color: Colors.white, size: 20),
copyIcon: Icon(Icons.copy, color: Colors.white, size: 20),
hoverIconPosition: HoverIconPosition.topRight,
hoverIconLayout: HoverIconLayout.auto,
hoverIconSpacing: 8.0,
hoverIconPadding: EdgeInsets.all(8),
// โ
v0.3.3: Combine with context menu
enableContextMenu: true,
// โ
v0.3.0: Get image data when loaded
onImageLoaded: (ImageDataInfo imageData) {
print('Image ready! Size: ${imageData.width}x${imageData.height}');
// imageData.imageBytes contains raw PNG data for copying/saving
},
// โ
Custom action callbacks
onDownloadTap: () => print('Custom download action!'),
onCopyTap: () => print('Custom copy action!'),
onContextMenuAction: (action) => print('Context menu: $action'),
)
Advanced Hover Icon Styling:
CustomNetworkImage(
url: 'https://example.com/image.jpg',
width: 400,
height: 300,
// Custom styled download icon
downloadIcon: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue[400]!, Colors.blue[600]!],
),
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.download, color: Colors.white, size: 16),
SizedBox(width: 4),
Text('Download', style: TextStyle(color: Colors.white, fontSize: 12)),
],
),
),
// Custom styled copy icon
copyIcon: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.green[400]!, Colors.green[600]!],
),
borderRadius: BorderRadius.circular(8),
),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.copy, color: Colors.white, size: 16),
SizedBox(width: 4),
Text('Copy', style: TextStyle(color: Colors.white, fontSize: 12)),
],
),
),
hoverIconPosition: HoverIconPosition.bottomRight,
hoverIconLayout: HoverIconLayout.row,
)
Using Image Data for Copy/Download Operations (v0.3.0+):
class ImageCopyExample extends StatefulWidget {
@override
_ImageCopyExampleState createState() => _ImageCopyExampleState();
}
class _ImageCopyExampleState extends State<ImageCopyExample> {
ImageDataInfo? _imageData;
@override
Widget build(BuildContext context) {
return Column(
children: [
CustomNetworkImage(
url: 'https://example.com/image.jpg',
onImageLoaded: (imageData) {
setState(() => _imageData = imageData);
},
),
if (_imageData != null) ...[
ElevatedButton(
onPressed: () async {
final success = await ImageClipboardHelper.downloadImage(_imageData!);
// Downloads PNG file to computer
},
child: Text('Download Image'),
),
ElevatedButton(
onPressed: () async {
final success = await ImageClipboardHelper.copyImageToClipboard(_imageData!);
// Copies image to clipboard for Ctrl+V pasting
},
child: Text('Copy to Clipboard'),
),
// โ
NEW: Copy raw bytes with dimensions
ElevatedButton(
onPressed: () async {
final success = await ImageClipboardHelper.copyImageBytesToClipboard(
_imageData!.imageBytes,
width: _imageData!.width,
height: _imageData!.height,
);
// Copies raw image bytes to clipboard for Ctrl+V pasting
},
child: Text('Copy Raw Bytes'),
),
],
],
);
}
}
When to use each method:
copyImageToClipboard
: When usingCustomNetworkImage
withonImageLoaded
callbackcopyImageBytesToClipboard
: When working with raw image data from other sources (camera, file picker, image processing, etc.)- Both methods: Provide identical clipboard functionality with different input formats
Platform Support
Platform | IndexedDB Cache | Clipboard Copy | File Download | ListView Performance |
---|---|---|---|---|
Web | โ Full support | โ Modern Clipboard API | โ Blob download | โ Optimized |
Mobile | โ Not applicable | โ ๏ธ Basic support* | โ Temp directory | โ Memory optimization |
Desktop | โ Not applicable | โ ๏ธ File path copy | โ Temp directory | โ Memory optimization |
*For enhanced mobile clipboard support, consider adding plugins like clipboard_manager
or pasteboard
.
Widget-based Error Handling (v0.2.0+)
The CustomNetworkImage
widget supports flexible widget-based error handling:
Parameter | Type | Description |
---|---|---|
errorWidget |
Widget? |
Custom widget to show when image fails to load |
reloadWidget |
Widget? |
Custom widget for retry functionality |
openUrlWidget |
Widget? |
Custom widget for opening image URL in new tab |
Examples for Different UI Styles:
// Material Design Style
CustomNetworkImage(
url: imageUrl,
errorWidget: Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline, color: Colors.red),
SizedBox(width: 8),
Text('Failed to load image', style: TextStyle(color: Colors.red)),
],
),
),
reloadWidget: ElevatedButton.icon(
onPressed: null, // Handled automatically
icon: Icon(Icons.refresh),
label: Text('Retry'),
),
openUrlWidget: TextButton.icon(
onPressed: null, // Handled automatically
icon: Icon(Icons.open_in_new),
label: Text('Open URL'),
),
)
// Icon-only (minimal)
CustomNetworkImage(
url: imageUrl,
errorWidget: Icon(Icons.broken_image, size: 48, color: Colors.grey),
reloadWidget: Icon(Icons.refresh, size: 24),
openUrlWidget: Icon(Icons.open_in_new, size: 24),
)
Right-Click Context Menu (v0.3.3+)
Available Context Menu Actions
enum ContextMenuAction {
copyImage, // Copy image to system clipboard
saveImage, // Save image with file picker dialog
openImageInNewTab, // Open image URL in new browser tab
copyImageUrl, // Copy image URL to clipboard
custom, // Custom action with onTap callback
}
Context Menu Customization
CustomNetworkImage(
url: imageUrl,
enableContextMenu: true,
// Custom menu items
customContextMenuItems: [
ContextMenuItem(
title: 'Download HD',
icon: Icons.hd,
action: ContextMenuAction.custom,
onTap: () => downloadHDVersion(),
),
ContextMenuItem(
title: 'Set as Wallpaper',
icon: Icons.wallpaper,
action: ContextMenuAction.custom,
onTap: () => setAsWallpaper(),
),
],
// Styling options
contextMenuBackgroundColor: Colors.black87,
contextMenuTextColor: Colors.white,
contextMenuElevation: 16.0,
contextMenuBorderRadius: BorderRadius.circular(12),
contextMenuPadding: EdgeInsets.symmetric(vertical: 8),
// Action handler
onContextMenuAction: (action) {
switch (action) {
case ContextMenuAction.copyImage:
// Image automatically copied to clipboard
showSnackBar('Image copied to clipboard!');
break;
case ContextMenuAction.saveImage:
// Save dialog automatically shown
showSnackBar('Save dialog opened');
break;
// ... handle other actions
}
},
)
Context Menu Features
Feature | Description |
---|---|
Smart Positioning | Automatically adjusts position to stay on screen |
Browser Integration | Uses File System Access API for native save dialogs |
Toast Notifications | Shows success/failure feedback for actions |
Clipboard Support | Copies images and URLs to system clipboard |
Custom Actions | Add your own menu items with custom callbacks |
Styling Control | Full control over colors, elevation, and spacing |
State Awareness | Works during loading, loaded, error, and HTML fallback states |
Platform Support
Platform | Context Menu | Save Dialog | Clipboard Copy |
---|---|---|---|
Web | โ Full support | โ File System Access API | โ Clipboard API |
Desktop | โ ๏ธ Limited | โ ๏ธ Downloads folder | โ ๏ธ Basic support |
Mobile | โ Not applicable | โ Not applicable | โ Not applicable |
Note: Context menus are primarily designed for web platforms where right-click functionality is standard.
IndexedDB Caching Configuration (v0.3.7+)
WebStorageCacheConfig Options
CustomNetworkImage(
url: 'https://example.com/image.jpg',
// โ
Configure IndexedDB caching
webStorageCacheConfig: WebStorageCacheConfig(
enabled: true, // Enable/disable caching
maxCacheSize: 100 * 1024 * 1024, // 100MB cache limit
cacheExpirationHours: 168, // 7 days expiration
cacheVersion: 1, // Cache version for invalidation
),
)
Cache Management
// Get cache statistics
final stats = await WebStorageCache.instance.getCacheStats();
print('Cached images: ${stats['count']}');
print('Cache size: ${stats['totalSizeMB']} MB');
print('Platform: ${stats['platform']}'); // 'web-IndexedDB'
// Clear cache manually
await WebStorageCache.instance.clearCache();
print('Cache cleared successfully');
// Clean up expired entries manually
final cleanedCount = await WebStorageCache.instance.cleanupExpiredEntries();
print('Cleaned up $cleanedCount expired entries');
// Clean up with custom expiration (e.g., 24 hours)
final customCleanedCount = await WebStorageCache.instance.cleanupExpiredEntries(
customExpirationHours: 24,
);
print('Cleaned up $customCleanedCount entries older than 24 hours');
// Check if caching is available
final isAvailable = await WebStorageCache.instance.isAvailable();
print('IndexedDB available: $isAvailable');
ListView Performance Testing
class ListViewPerformanceTest extends StatefulWidget {
@override
_ListViewPerformanceTestState createState() => _ListViewPerformanceTestState();
}
class _ListViewPerformanceTestState extends State<ListViewPerformanceTest> {
Map<String, dynamic> _cacheStats = {};
@override
void initState() {
super.initState();
_updateCacheStats();
}
Future<void> _updateCacheStats() async {
final stats = await WebStorageCache.instance.getCacheStats();
setState(() => _cacheStats = stats);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ListView Performance Test'),
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: _updateCacheStats,
),
],
),
body: Column(
children: [
// Cache statistics
Container(
padding: EdgeInsets.all(16),
color: Colors.blue.shade50,
child: Row(
children: [
Icon(Icons.storage, color: Colors.blue),
SizedBox(width: 8),
Text('Cache: ${_cacheStats['count'] ?? 0} images'),
Spacer(),
Text('${_cacheStats['totalSizeMB']?.toStringAsFixed(1) ?? '0'} MB'),
],
),
),
// Performance test instructions
Container(
padding: EdgeInsets.all(16),
color: Colors.green.shade50,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('๐งช Performance Test Instructions:',
style: TextStyle(fontWeight: FontWeight.bold)),
Text('1. First scroll: Network requests occur (normal)'),
Text('2. Refresh page (F5): Zero network requests'),
Text('3. Rapid scrolling: Instant image display'),
Text('4. Check DevTools Network tab for validation'),
],
),
),
// ListView with 50+ images
Expanded(
child: ListView.builder(
itemCount: 50,
itemBuilder: (context, index) {
return Container(
margin: EdgeInsets.all(8),
child: CustomNetworkImage(
url: 'https://picsum.photos/400/300?random=$index',
width: double.infinity,
height: 200,
fit: BoxFit.cover,
webStorageCacheConfig: WebStorageCacheConfig(
enabled: true,
maxCacheSize: 100 * 1024 * 1024,
cacheExpirationHours: 168,
),
customLoadingBuilder: (context, child, progress) {
return Container(
height: 200,
color: Colors.grey[200],
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(value: progress?.progress),
SizedBox(height: 8),
Text('Loading image ${index + 1}...'),
],
),
),
);
},
),
);
},
),
),
],
),
);
}
}
Migration Guide
v0.3.6 โ v0.3.7
No Breaking Changes
All v0.3.6 code continues to work unchanged. IndexedDB caching is enabled by default and provides automatic performance improvements.
Automatic Performance Enhancement
// Existing code (automatically gets IndexedDB caching)
CustomNetworkImage(
url: 'https://example.com/image.jpg',
width: 300,
height: 200,
)
// Enhanced with custom cache configuration
CustomNetworkImage(
url: 'https://example.com/image.jpg',
width: 300,
height: 200,
webStorageCacheConfig: WebStorageCacheConfig(
maxCacheSize: 200 * 1024 * 1024, // 200MB
cacheExpirationHours: 336, // 14 days
),
)
v0.2.x โ v0.3.0
No Breaking Changes
All v0.2.x code continues to work unchanged. New features are additive and optional.
Gradual Enhancement
Add new features progressively:
// Step 1: Start with basic image loading (existing code works)
CustomNetworkImage(
url: imageUrl,
width: 300,
height: 200,
)
// Step 2: Add image data callback
CustomNetworkImage(
url: imageUrl,
width: 300,
height: 200,
onImageLoaded: (imageData) {
// Now you have access to image bytes, dimensions, etc.
},
)
// Step 3: Add hover icons
CustomNetworkImage(
url: imageUrl,
width: 300,
height: 200,
onImageLoaded: (imageData) => _imageData = imageData,
downloadIcon: Icon(Icons.download, color: Colors.white),
copyIcon: Icon(Icons.copy, color: Colors.white),
)
// Step 4: Customize positioning and styling
CustomNetworkImage(
url: imageUrl,
width: 300,
height: 200,
onImageLoaded: (imageData) => _imageData = imageData,
downloadIcon: _buildStyledDownloadIcon(),
copyIcon: _buildStyledCopyIcon(),
hoverIconPosition: HoverIconPosition.bottomRight,
hoverIconLayout: HoverIconLayout.row,
hoverIconSpacing: 12.0,
hoverIconPadding: EdgeInsets.all(8),
)
v0.1.x โ v0.3.0
Update your dependency and replace deprecated parameters:
// OLD (deprecated but still works)
CustomNetworkImage(
url: imageUrl,
errorText: 'Image failed to load',
reloadText: 'Reload Image',
openUrlText: 'Open in New Tab',
)
// NEW (recommended)
CustomNetworkImage(
url: imageUrl,
errorWidget: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error, color: Colors.red),
SizedBox(width: 8),
Text('Image failed to load'),
],
),
reloadWidget: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.refresh),
SizedBox(width: 8),
Text('Reload Image'),
],
),
openUrlWidget: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.open_in_new),
SizedBox(width: 8),
Text('Open in New Tab'),
],
),
// โ
Add new features
downloadIcon: Icon(Icons.download, color: Colors.white),
copyIcon: Icon(Icons.copy, color: Colors.white),
onImageLoaded: (imageData) {
// Access to image data for copy/download operations
},
)
v0.3.2 โ v0.3.3
No Breaking Changes
All v0.3.2 code continues to work unchanged. Context menu is an optional feature.
Adding Context Menu
// Existing code (still works)
CustomNetworkImage(
url: imageUrl,
downloadIcon: Icon(Icons.download),
copyIcon: Icon(Icons.copy),
)
// Enhanced with context menu
CustomNetworkImage(
url: imageUrl,
downloadIcon: Icon(Icons.download),
copyIcon: Icon(Icons.copy),
// โ
Add context menu
enableContextMenu: true,
onContextMenuAction: (action) {
print('Context action: $action');
},
)
Complete Example
Here's a comprehensive example showing all v0.3.0 features:
import 'package:flutter/material.dart';
import 'package:flutter_cors_image/flutter_cors_image.dart';
class AdvancedImageExample extends StatefulWidget {
@override
_AdvancedImageExampleState createState() => _AdvancedImageExampleState();
}
class _AdvancedImageExampleState extends State<AdvancedImageExample> {
ImageDataInfo? _imageData;
HoverIconPosition _position = HoverIconPosition.topRight;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Flutter CORS Image v0.3.0')),
body: Column(
children: [
// Position selector
Wrap(
children: HoverIconPosition.values.map((pos) {
return ChoiceChip(
label: Text(pos.name),
selected: _position == pos,
onSelected: (selected) {
if (selected) setState(() => _position = pos);
},
);
}).toList(),
),
// Main image with hover icons
Expanded(
child: Center(
child: CustomNetworkImage(
url: 'https://example.com/image.jpg',
width: 400,
height: 300,
fit: BoxFit.cover,
// Styled hover icons
downloadIcon: _buildDownloadIcon(),
copyIcon: _buildCopyIcon(),
hoverIconPosition: _position,
hoverIconLayout: HoverIconLayout.auto,
hoverIconSpacing: 8.0,
hoverIconPadding: EdgeInsets.all(8),
// Image data callback
onImageLoaded: (imageData) {
setState(() => _imageData = imageData);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Image loaded! Hover to see icons')),
);
},
// Custom actions
onDownloadTap: () => _handleDownload(),
onCopyTap: () => _handleCopy(),
// Error handling
errorWidget: Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error, color: Colors.red, size: 48),
Text('Failed to load image'),
],
),
),
// Loading state
customLoadingBuilder: (context, child, progress) {
return Container(
width: 400,
height: 300,
color: Colors.grey[200],
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(value: progress?.progress),
if (progress?.progress != null)
Text('${(progress!.progress! * 100).toInt()}%'),
],
),
),
);
},
),
),
),
// Manual action buttons
if (_imageData != null) ...[
Padding(
padding: EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
onPressed: _handleDownload,
icon: Icon(Icons.download),
label: Text('Download'),
),
ElevatedButton.icon(
onPressed: _handleCopy,
icon: Icon(Icons.copy),
label: Text('Copy'),
),
],
),
),
],
],
),
);
}
Widget _buildDownloadIcon() {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.blue, Colors.blueAccent]),
borderRadius: BorderRadius.circular(6),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 3)],
),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.download, color: Colors.white, size: 16),
SizedBox(width: 4),
Text('Download', style: TextStyle(color: Colors.white, fontSize: 12)),
],
),
);
}
Widget _buildCopyIcon() {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.green, Colors.greenAccent]),
borderRadius: BorderRadius.circular(6),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 3)],
),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.copy, color: Colors.white, size: 16),
SizedBox(width: 4),
Text('Copy', style: TextStyle(color: Colors.white, fontSize: 12)),
],
),
);
}
Future<void> _handleDownload() async {
if (_imageData == null) return;
final success = await ImageClipboardHelper.downloadImage(_imageData!);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success ? 'Image downloaded!' : 'Download failed'),
backgroundColor: success ? Colors.green : Colors.red,
),
);
}
Future<void> _handleCopy() async {
if (_imageData == null) return;
final success = await ImageClipboardHelper.copyImageToClipboard(_imageData!);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success
? 'Image copied! Press Ctrl+V to paste.'
: 'Copy failed'),
backgroundColor: success ? Colors.green : Colors.red,
),
);
}
}
License
MIT
Libraries
- flutter_cors_image
- Flutter CORS Image Library
- stub_image_loader
- web_plugin_registrant