universal_share 1.0.1 copy "universal_share: ^1.0.1" to clipboard
universal_share: ^1.0.1 copied to clipboard

Premium, zero-configuration Flutter sharing plugin for WhatsApp, Instagram, Facebook, Telegram, SMS, and HTML Emails with multiple attachments.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:path_provider/path_provider.dart';
import 'package:universal_share/universal_share.dart';
import 'package:file_selector/file_selector.dart';
import 'dart:io';
import 'dart:async';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  ThemeMode _themeMode = ThemeMode.dark;

  void toggleTheme() {
    setState(() {
      _themeMode = _themeMode == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Universal Share Example',
      themeMode: _themeMode,
      theme: ThemeData(
        useMaterial3: true,
        brightness: Brightness.light,
        colorSchemeSeed: Colors.deepPurple,
        scaffoldBackgroundColor: const Color(0xFFF9F9FB),
        cardColor: Colors.white,
        appBarTheme: const AppBarTheme(
          backgroundColor: Colors.white,
          foregroundColor: Colors.black87,
          elevation: 0,
        ),
      ),
      darkTheme: ThemeData(
        useMaterial3: true,
        brightness: Brightness.dark,
        colorSchemeSeed: Colors.deepPurple,
        scaffoldBackgroundColor: const Color(0xFF0F0E17),
        cardColor: const Color(0xFF1E1B2E),
        appBarTheme: const AppBarTheme(
          backgroundColor: Color(0xFF151324),
          foregroundColor: Colors.white,
          elevation: 0,
        ),
      ),
      home: HomeScreen(onToggleTheme: toggleTheme, themeMode: _themeMode),
    );
  }
}

class HomeScreen extends StatefulWidget {
  final VoidCallback onToggleTheme;
  final ThemeMode themeMode;
  const HomeScreen({required this.onToggleTheme, required this.themeMode, super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  int _currentTab = 0;

  // Sharing Inputs
  final _generalTextController = TextEditingController(text: 'Hello from Universal Share!');
  final _whatsappTextController = TextEditingController(text: 'Hey! Check out this direct message.');
  final _whatsappPhoneController = TextEditingController(text: '+919876543210');
  
  // Instagram Inputs
  final _instagramStoryController = TextEditingController(text: 'My Instagram Story overlay text!');
  final _instagramDirectController = TextEditingController(text: 'Direct message via Instagram Direct!');
  
  // Facebook Inputs
  final _facebookStoryController = TextEditingController(text: 'My Facebook Story overlay text!');
  final _facebookFeedController = TextEditingController(text: 'Posting to Facebook Feed using Universal Share.');
  final _facebookMessengerController = TextEditingController(text: 'Chatting via Facebook Messenger!');
  
  // Email Inputs
  final _emailRecipientController = TextEditingController(text: 'hello@example.com');
  final _emailSubjectController = TextEditingController(text: 'Project Invoice');
  final _emailBodyController = TextEditingController(text: '<h3>Dear Partner,</h3><p>Please find the attached invoice PDF and test image.</p>');
  
  // SMS/Telegram Inputs
  final _telegramTextController = TextEditingController(text: 'Shared directly to Telegram via universal_share!');
  final _smsRecipientsController = TextEditingController(text: '+919876543210');
  final _smsMessageController = TextEditingController(text: 'Universal Share SMS works!');
  final _customPathController = TextEditingController();

  // File Paths
  String? _mockImagePath;
  String? _mockPdfPath;
  List<String> _customFilePaths = [];
  bool _isGeneratingFiles = false;

  // Status Log
  final List<String> _logs = [];

  // Feature Toggles
  bool _generalIncludeText = true;
  bool _generalIncludeAttachments = true;

  bool _whatsappIncludeText = true;
  bool _whatsappIncludeAttachment = false;
  bool _whatsappDirectChat = true;

  bool _telegramIncludeText = true;
  bool _telegramIncludeAttachment = false;

  bool _emailIncludeSubject = true;
  bool _emailIncludeBody = true;
  bool _emailIncludeAttachments = true;

  bool _instagramIncludeBackground = true;
  bool _instagramIncludeSticker = false;
  bool _instagramIncludeCaption = true;
  bool _instagramDirectIncludeAttachment = false;

  bool _facebookStoryIncludeBackground = true;
  bool _facebookStoryIncludeSticker = false;
  bool _facebookStoryIncludeCaption = true;

  bool _facebookFeedIncludeText = true;
  bool _facebookFeedIncludeImage = true;

  Widget _buildToggleRow({
    required String label,
    required bool value,
    required ValueChanged<bool> onChanged,
    bool enabled = true,
  }) {
    final isDark = widget.themeMode == ThemeMode.dark;
    return Opacity(
      opacity: enabled ? 1.0 : 0.4,
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 4.0),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(
              label,
              style: TextStyle(
                fontSize: 13,
                fontWeight: FontWeight.w500,
                color: isDark ? Colors.white70 : Colors.black87,
              ),
            ),
            Transform.scale(
              scale: 0.8,
              child: CupertinoSwitch(
                value: value,
                activeColor: Colors.deepPurpleAccent,
                onChanged: enabled ? onChanged : null,
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildInfoBox(String text, Color color) {
    final isDark = widget.themeMode == ThemeMode.dark;
    return Container(
      margin: const EdgeInsets.only(top: 8, bottom: 12),
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: color.withOpacity(0.08),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: color.withOpacity(0.2), width: 0.8),
      ),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Icon(CupertinoIcons.info_circle_fill, color: color, size: 16),
          const SizedBox(width: 8),
          Expanded(
            child: Text(
              text,
              style: TextStyle(
                fontSize: 11,
                fontWeight: FontWeight.w500,
                color: isDark ? color.withOpacity(0.9) : color.withOpacity(0.8),
                height: 1.3,
              ),
            ),
          ),
        ],
      ),
    );
  }


  @override
  void initState() {
    super.initState();
    _addLog('Universal Share Initialized.');
  }

  void _addLog(String message) {
    final time = DateTime.now().toString().split(' ')[1].substring(0, 8);
    setState(() {
      _logs.insert(0, '[$time] $message');
    });
  }

  Future<void> _generateMockFiles() async {
    setState(() {
      _isGeneratingFiles = true;
    });

    try {
      final tempDir = await getTemporaryDirectory();

      // 1. Generate Valid Mock PNG File
      final imageFile = File('${tempDir.path}/universal_share_test.png');
      final pngBytes = [
        137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1,
        0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 11, 73, 68, 65, 84,
        120, 156, 99, 96, 0, 0, 0, 2, 0, 1, 226, 33, 188, 51, 0, 0, 0, 0, 73, 69,
        78, 68, 174, 66, 96, 130
      ];
      await imageFile.writeAsBytes(pngBytes);
      _mockImagePath = imageFile.path;

      // 2. Generate Mock PDF File
      final pdfFile = File('${tempDir.path}/invoice_copy.pdf');
      await pdfFile.writeAsString('%PDF-1.5\n%Mock Document\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n...');
      _mockPdfPath = pdfFile.path;

      _addLog('Mock files generated.');
    } catch (e) {
      _addLog('Failed to generate mock files: $e');
    } finally {
      setState(() {
        _isGeneratingFiles = false;
      });
    }
  }

  Future<void> _pickRealFiles() async {
    try {
      final List<XFile> files = await openFiles();
      if (files.isNotEmpty) {
        final paths = files.map((file) => file.path).toList();
        setState(() {
          _customFilePaths = paths;
          // Clear manual path controller and generated mock files to prevent confusion
          _customPathController.clear();
          _mockImagePath = null;
          _mockPdfPath = null;
        });
        _addLog('Picked ${paths.length} custom files:');
        for (var p in paths) {
          _addLog(' - ${p.split("/").last.split("\\").last}');
        }
      } else {
        _addLog('File picking cancelled.');
      }
    } catch (e) {
      _addLog('Failed to pick files: $e');
    }
  }

  List<String>? _getActiveFilePaths({bool singleOnly = false}) {
    final customPath = _customPathController.text.trim();
    if (customPath.isNotEmpty) {
      return [customPath];
    }
    if (_customFilePaths.isNotEmpty) {
      return singleOnly ? [_customFilePaths.first] : _customFilePaths;
    }
    final mockFiles = <String>[];
    if (_mockImagePath != null) mockFiles.add(_mockImagePath!);
    if (!singleOnly && _mockPdfPath != null) mockFiles.add(_mockPdfPath!);
    return mockFiles.isNotEmpty ? mockFiles : null;
  }

  void _removePickedFile(String path) {
    setState(() {
      if (_customFilePaths.contains(path)) {
        _customFilePaths.remove(path);
      } else {
        if (_mockImagePath == path) _mockImagePath = null;
        if (_mockPdfPath == path) _mockPdfPath = null;
      }
      _addLog('Removed file: ${path.split("/").last.split("\\").last}');
    });
  }

  String _getFileSizeString(String path) {
    try {
      final file = File(path);
      if (file.existsSync()) {
        final bytes = file.lengthSync();
        if (bytes < 1024) return '$bytes B';
        if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
        return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
      }
    } catch (_) {}
    return '';
  }

  Widget _buildAttachmentBadge() {
    final paths = _getActiveFilePaths() ?? [];
    final isDark = widget.themeMode == ThemeMode.dark;
    
    if (paths.isEmpty) {
      return Container(
        padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
        decoration: BoxDecoration(
          color: isDark ? Colors.white.withOpacity(0.04) : const Color(0xFFEBE6F8),
          borderRadius: BorderRadius.circular(10),
        ),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(CupertinoIcons.paperclip, size: 12, color: isDark ? Colors.white38 : Colors.black45),
            const SizedBox(width: 4),
            Text(
              'No Attachments',
              style: TextStyle(
                fontSize: 11,
                fontWeight: FontWeight.w600,
                color: isDark ? Colors.white38 : Colors.black45,
              ),
            ),
          ],
        ),
      );
    }
    
    final isMock = _customFilePaths.isEmpty && (_mockImagePath != null || _mockPdfPath != null);
    final color = isMock ? Colors.amber : Colors.green;
    
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
      decoration: BoxDecoration(
        color: color.withOpacity(0.12),
        borderRadius: BorderRadius.circular(10),
        border: Border.all(color: color.withOpacity(0.25), width: 0.8),
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(CupertinoIcons.paperclip, size: 12, color: color),
          const SizedBox(width: 4),
          Text(
            isMock ? 'Mock Assets (${paths.length})' : '${paths.length} File(s) Attached',
            style: TextStyle(
              fontSize: 11,
              fontWeight: FontWeight.bold,
              color: color,
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildVisualFilePreview() {
    final customPath = _customPathController.text.trim();
    final hasCustomPath = customPath.isNotEmpty;
    final hasCustomFiles = _customFilePaths.isNotEmpty;
    final hasMockFiles = _mockImagePath != null || _mockPdfPath != null;
    final isDark = widget.themeMode == ThemeMode.dark;
    
    if (!hasCustomPath && !hasCustomFiles && !hasMockFiles) {
      return Container(
        margin: const EdgeInsets.only(top: 12),
        padding: const EdgeInsets.all(16),
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(16),
          border: Border.all(
            color: isDark ? Colors.white.withOpacity(0.08) : Colors.black.withOpacity(0.08),
          ),
          color: isDark ? Colors.white.withOpacity(0.01) : Colors.black.withOpacity(0.01),
        ),
        alignment: Alignment.center,
        child: Column(
          children: [
            Icon(CupertinoIcons.paperclip, color: isDark ? Colors.white24 : Colors.black26, size: 24),
            const SizedBox(height: 8),
            Text(
              'No Attachments Active',
              style: TextStyle(
                fontSize: 13,
                fontWeight: FontWeight.bold,
                color: isDark ? Colors.white38 : Colors.black45,
              ),
            ),
            const SizedBox(height: 2),
            Text(
              'Pick real files or generate mock assets to test sharing.',
              style: TextStyle(
                fontSize: 11,
                color: isDark ? Colors.white24 : Colors.black38,
              ),
              textAlign: TextAlign.center,
            ),
          ],
        ),
      );
    }

    final filesToRender = <Map<String, String>>[];
    if (hasCustomPath) {
      filesToRender.add({'path': customPath, 'type': 'custom_path'});
    }
    for (var p in _customFilePaths) {
      filesToRender.add({'path': p, 'type': 'custom'});
    }
    if (hasMockFiles) {
      if (_mockImagePath != null) {
        filesToRender.add({'path': _mockImagePath!, 'type': 'mock_image'});
      }
      if (_mockPdfPath != null) {
        filesToRender.add({'path': _mockPdfPath!, 'type': 'mock_pdf'});
      }
    }

    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        const SizedBox(height: 16),
        Text(
          'ACTIVE ATTACHMENTS GALLERY',
          style: TextStyle(
            fontSize: 10,
            fontWeight: FontWeight.bold,
            color: isDark ? Colors.white54 : Colors.black54,
            letterSpacing: 1.0,
          ),
        ),
        const SizedBox(height: 8),
        ListView.separated(
          shrinkWrap: true,
          physics: const NeverScrollableScrollPhysics(),
          itemCount: filesToRender.length,
          separatorBuilder: (_, __) => const SizedBox(height: 8),
          itemBuilder: (context, index) {
            final item = filesToRender[index];
            final path = item['path']!;
            final type = item['type']!;
            final name = path.split('/').last.split('\\').last;
            final size = _getFileSizeString(path);
            
            Widget leadingIcon;
            Color themeColor;
            
            final isImage = path.toLowerCase().endsWith('.png') ||
                path.toLowerCase().endsWith('.jpg') ||
                path.toLowerCase().endsWith('.jpeg') ||
                path.toLowerCase().endsWith('.gif') ||
                path.toLowerCase().endsWith('.webp');
                
            final isVideo = path.toLowerCase().endsWith('.mp4') || path.toLowerCase().endsWith('.mov') || path.toLowerCase().endsWith('.avi');
            final isPdf = path.toLowerCase().endsWith('.pdf');
            
            if (isImage) {
              themeColor = Colors.green;
              leadingIcon = ClipRRect(
                borderRadius: BorderRadius.circular(8),
                child: Image.file(
                  File(path),
                  width: 44,
                  height: 44,
                  fit: BoxFit.cover,
                  errorBuilder: (_, __, ___) => Container(
                    width: 44,
                    height: 44,
                    color: Colors.green.withOpacity(0.1),
                    child: const Icon(CupertinoIcons.photo, color: Colors.green, size: 20),
                  ),
                ),
              );
            } else if (isPdf) {
              themeColor = Colors.redAccent;
              leadingIcon = Container(
                width: 44,
                height: 44,
                decoration: BoxDecoration(
                  color: Colors.redAccent.withOpacity(0.12),
                  borderRadius: BorderRadius.circular(8),
                ),
                child: const Icon(CupertinoIcons.doc_text_fill, color: Colors.redAccent, size: 20),
              );
            } else if (isVideo) {
              themeColor = Colors.blueAccent;
              leadingIcon = Container(
                width: 44,
                height: 44,
                decoration: BoxDecoration(
                  color: Colors.blueAccent.withOpacity(0.12),
                  borderRadius: BorderRadius.circular(8),
                ),
                child: const Icon(CupertinoIcons.video_camera_solid, color: Colors.blueAccent, size: 20),
              );
            } else {
              themeColor = Colors.deepPurpleAccent;
              leadingIcon = Container(
                width: 44,
                height: 44,
                decoration: BoxDecoration(
                  color: Colors.deepPurpleAccent.withOpacity(0.12),
                  borderRadius: BorderRadius.circular(8),
                ),
                child: const Icon(CupertinoIcons.doc_fill, color: Colors.deepPurpleAccent, size: 20),
              );
            }
            
            return Container(
              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
              decoration: BoxDecoration(
                color: isDark ? const Color(0xFF1E1B2E).withOpacity(0.4) : Colors.white,
                borderRadius: BorderRadius.circular(14),
                border: Border.all(
                  color: isDark ? Colors.white.withOpacity(0.04) : Colors.black.withOpacity(0.04),
                ),
              ),
              child: Row(
                children: [
                  leadingIcon,
                  const SizedBox(width: 12),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          name,
                          style: const TextStyle(
                            fontSize: 13,
                            fontWeight: FontWeight.bold,
                          ),
                          maxLines: 1,
                          overflow: TextOverflow.ellipsis,
                        ),
                        const SizedBox(height: 2),
                        Row(
                          children: [
                            Container(
                              padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
                              decoration: BoxDecoration(
                                color: themeColor.withOpacity(0.15),
                                borderRadius: BorderRadius.circular(6),
                              ),
                              child: Text(
                                type.toUpperCase().replaceAll('_', ' '),
                                style: TextStyle(
                                  fontSize: 8,
                                  fontWeight: FontWeight.bold,
                                  color: themeColor,
                                ),
                              ),
                            ),
                            if (size.isNotEmpty) ...[
                              const SizedBox(width: 8),
                              Text(
                                size,
                                style: TextStyle(
                                  fontSize: 11,
                                  color: isDark ? Colors.white38 : Colors.black45,
                                ),
                              ),
                            ]
                          ],
                        )
                      ],
                    ),
                  ),
                  const SizedBox(width: 8),
                  AnimatedBouncyButton(
                    onPressed: () => _removePickedFile(path),
                    child: Container(
                      padding: const EdgeInsets.all(8),
                      decoration: BoxDecoration(
                        color: Colors.red.withOpacity(0.08),
                        borderRadius: BorderRadius.circular(10),
                      ),
                      child: const Icon(
                        CupertinoIcons.trash,
                        color: Colors.redAccent,
                        size: 16,
                      ),
                    ),
                  ),
                ],
              ),
            );
          },
        ),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    final isDark = widget.themeMode == ThemeMode.dark;

    return Scaffold(
      appBar: AppBar(
        title: Row(
          children: [
            Icon(CupertinoIcons.share, color: Theme.of(context).colorScheme.primary),
            const SizedBox(width: 10),
            const Text(
              'Universal Share',
              style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 0.5),
            ),
          ],
        ),
        actions: [
          // Theme Toggle
          AnimatedBouncyButton(
            onPressed: widget.onToggleTheme,
            child: Container(
              padding: const EdgeInsets.all(8),
              margin: const EdgeInsets.symmetric(horizontal: 4),
              child: AnimatedSwitcher(
                duration: const Duration(milliseconds: 300),
                transitionBuilder: (child, anim) => ScaleTransition(scale: anim, child: child),
                child: Icon(
                  isDark ? CupertinoIcons.sun_max_fill : CupertinoIcons.moon_fill,
                  key: ValueKey(isDark),
                  color: isDark ? Colors.amber : Colors.deepPurple,
                ),
              ),
            ),
          ),
          // Clear logs
          AnimatedBouncyButton(
            onPressed: () {
              setState(() {
                _logs.clear();
                _addLog('Logs cleared.');
              });
            },
            child: Container(
              padding: const EdgeInsets.all(8),
              margin: const EdgeInsets.only(right: 8),
              child: const Icon(CupertinoIcons.refresh_thin),
            ),
          )
        ],
      ),
      body: SafeArea(
        child: AnimatedSwitcher(
          duration: const Duration(milliseconds: 250),
          child: _buildCurrentTabContent(),
        ),
      ),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _currentTab,
        onDestinationSelected: (index) {
          setState(() {
            _currentTab = index;
          });
        },
        destinations: const [
          NavigationDestination(
            icon: Icon(CupertinoIcons.share),
            selectedIcon: Icon(CupertinoIcons.share_solid, color: Colors.deepPurpleAccent),
            label: 'General',
          ),
          NavigationDestination(
            icon: Icon(CupertinoIcons.chat_bubble_2),
            selectedIcon: Icon(CupertinoIcons.chat_bubble_2_fill, color: Colors.deepPurpleAccent),
            label: 'Socials',
          ),
          NavigationDestination(
            icon: Icon(CupertinoIcons.camera),
            selectedIcon: Icon(CupertinoIcons.camera_fill, color: Colors.deepPurpleAccent),
            label: 'Meta Apps',
          ),
          NavigationDestination(
            icon: Icon(CupertinoIcons.doc_text),
            selectedIcon: Icon(CupertinoIcons.doc_text_fill, color: Colors.deepPurpleAccent),
            label: 'Logs',
          ),
        ],
      ),
    );
  }

  Widget _buildCurrentTabContent() {
    switch (_currentTab) {
      case 0:
        return ListView(
          key: const PageStorageKey('TabGeneral'),
          padding: const EdgeInsets.all(16.0),
          children: [
            _buildGeneratorCard(),
            const SizedBox(height: 20),
            _buildSectionHeader('System Shares'),
            _buildGeneralShareCard(),
            const SizedBox(height: 16),
            _buildEmailShareCard(),
            const SizedBox(height: 16),
            _buildSMSCard(),
            const SizedBox(height: 40),
          ],
        );
      case 1:
        return ListView(
          key: const PageStorageKey('TabSocials'),
          padding: const EdgeInsets.all(16.0),
          children: [
            _buildSectionHeader('Direct Chats'),
            _buildWhatsAppCard(),
            const SizedBox(height: 16),
            _buildTelegramCard(),
            const SizedBox(height: 40),
          ],
        );
      case 2:
        return ListView(
          key: const PageStorageKey('TabMeta'),
          padding: const EdgeInsets.all(16.0),
          children: [
            _buildSectionHeader('Direct Platforms'),
            _buildInstagramCard(),
            const SizedBox(height: 16),
            _buildFacebookCard(),
            const SizedBox(height: 40),
          ],
        );
      case 3:
        return Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              _buildSectionHeader('Developer Live Logs'),
              Expanded(child: _buildLogsCard()),
            ],
          ),
        );
      default:
        return const SizedBox();
    }
  }

  Widget _buildSectionHeader(String title) {
    return Padding(
      padding: const EdgeInsets.only(left: 4.0, bottom: 12.0),
      child: Text(
        title.toUpperCase(),
        style: TextStyle(
          fontSize: 11,
          fontWeight: FontWeight.bold,
          color: Theme.of(context).colorScheme.primary,
          letterSpacing: 1.5,
        ),
      ),
    );
  }

  Widget _buildCard({
    required String title,
    required IconData icon,
    required List<Widget> children,
    required Color accentColor,
  }) {
    final isDark = widget.themeMode == ThemeMode.dark;

    return Container(
      decoration: BoxDecoration(
        color: Theme.of(context).cardColor,
        borderRadius: BorderRadius.circular(20),
        border: Border.all(color: isDark ? Colors.white.withOpacity(0.04) : Colors.black.withOpacity(0.05)),
        boxShadow: [
          BoxShadow(
            color: accentColor.withOpacity(isDark ? 0.03 : 0.05),
            blurRadius: 15,
            offset: const Offset(0, 5),
          )
        ],
      ),
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Row(
            children: [
              Icon(icon, color: accentColor, size: 22),
              const SizedBox(width: 10),
              Text(
                title,
                style: const TextStyle(
                  fontSize: 17,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ],
          ),
          const Divider(height: 24, color: Colors.white10),
          ...children,
        ],
      ),
    );
  }

  Widget _buildTextField({
    required TextEditingController controller,
    required String label,
    required IconData icon,
    int maxLines = 1,
  }) {
    final isDark = widget.themeMode == ThemeMode.dark;
    final primaryColor = Theme.of(context).colorScheme.primary;

    return Padding(
      padding: const EdgeInsets.only(bottom: 16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          // Spacious, clean label above input field (Airbnb style) to prevent overlaps
          Text(
            label.toUpperCase(),
            style: TextStyle(
              fontSize: 10,
              fontWeight: FontWeight.bold,
              color: isDark ? Colors.white60 : Colors.black54,
              letterSpacing: 1.0,
            ),
          ),
          const SizedBox(height: 6),
          TextField(
            controller: controller,
            maxLines: maxLines,
            style: TextStyle(
              fontSize: 14,
              color: isDark ? Colors.white : Colors.black87,
            ),
            decoration: InputDecoration(
              prefixIcon: Icon(
                icon,
                size: 18,
                color: isDark ? Colors.white38 : Colors.black38,
              ),
              hintText: 'Enter $label...',
              hintStyle: TextStyle(
                fontSize: 13,
                color: isDark ? Colors.white24 : Colors.black26,
              ),
              filled: true,
              fillColor: isDark ? Colors.black26 : const Color(0xFFF1F1F4),
              contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(14),
                borderSide: BorderSide.none,
              ),
              focusedBorder: OutlineInputBorder(
                borderRadius: BorderRadius.circular(14),
                borderSide: BorderSide(color: primaryColor, width: 1.2),
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildActionButton({
    required String label,
    required VoidCallback onPressed,
    required Color color,
    required IconData icon,
  }) {
    return AnimatedBouncyButton(
      onPressed: onPressed,
      child: Container(
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(14),
          gradient: LinearGradient(
            colors: [color, color.withOpacity(0.85)],
          ),
          boxShadow: [
            BoxShadow(
              color: color.withOpacity(0.2),
              blurRadius: 10,
              offset: const Offset(0, 4),
            )
          ],
        ),
        padding: const EdgeInsets.symmetric(vertical: 14),
        alignment: Alignment.center,
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(icon, color: Colors.white, size: 18),
            const SizedBox(width: 8),
            Text(
              label,
              style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white, fontSize: 14),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildGeneratorCard() {
    final isDark = widget.themeMode == ThemeMode.dark;

    return Container(
      decoration: BoxDecoration(
        gradient: LinearGradient(
          colors: isDark
              ? [const Color(0xFF2C1B4D), const Color(0xFF1E1B2E)]
              : [const Color(0xFFEBE6F8), Colors.white],
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
        ),
        borderRadius: BorderRadius.circular(20),
        border: Border.all(color: Colors.deepPurple.withOpacity(0.15)),
        boxShadow: [
          BoxShadow(
            color: Colors.deepPurple.withOpacity(0.05),
            blurRadius: 10,
            offset: const Offset(0, 4),
          )
        ],
      ),
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const Row(
            children: [
              Icon(CupertinoIcons.folder_open, color: Colors.amber),
              SizedBox(width: 10),
              Expanded(
                child: Text(
                  'Test Attachments & Assets Manager',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
              ),
            ],
          ),
          const SizedBox(height: 8),
          Text(
            'Select actual files using a clean file selector, enter custom file paths, or automatically generate temporary test mock files.',
            style: TextStyle(fontSize: 12, color: isDark ? Colors.white60 : Colors.black54),
          ),
          const SizedBox(height: 16),
          // Clean text field for custom manual path input
          _buildTextField(
            controller: _customPathController,
            label: 'Custom File Path (e.g. /sdcard/Download/photo.jpg)',
            icon: CupertinoIcons.folder,
          ),
          const SizedBox(height: 4),
          Row(
            children: [
              Expanded(
                child: AnimatedBouncyButton(
                  onPressed: _pickRealFiles,
                  child: Container(
                    decoration: BoxDecoration(
                      border: Border.all(color: Theme.of(context).colorScheme.primary.withOpacity(0.5)),
                      borderRadius: BorderRadius.circular(12),
                      color: Theme.of(context).colorScheme.primary.withOpacity(0.08),
                    ),
                    padding: const EdgeInsets.symmetric(vertical: 12),
                    alignment: Alignment.center,
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Icon(CupertinoIcons.doc_checkmark, size: 16, color: Theme.of(context).colorScheme.primary),
                        const SizedBox(width: 8),
                        Text(
                          'Pick Files',
                          style: TextStyle(
                            fontWeight: FontWeight.bold,
                            fontSize: 13,
                            color: Theme.of(context).colorScheme.primary,
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
              const SizedBox(width: 10),
              Expanded(
                child: AnimatedBouncyButton(
                  onPressed: _isGeneratingFiles
                      ? null
                      : () async {
                          await _generateMockFiles();
                          if (_mockImagePath != null) {
                            _addLog('Mock image: ${_mockImagePath}');
                          }
                        },
                  child: Container(
                    decoration: BoxDecoration(
                      border: Border.all(color: Colors.amber.withOpacity(0.5)),
                      borderRadius: BorderRadius.circular(12),
                      color: Colors.amber.withOpacity(0.08),
                    ),
                    padding: const EdgeInsets.symmetric(vertical: 12),
                    alignment: Alignment.center,
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        _isGeneratingFiles
                            ? const SizedBox(
                                width: 14,
                                height: 14,
                                child: CircularProgressIndicator(strokeWidth: 2),
                              )
                            : const Icon(CupertinoIcons.hammer_fill, size: 16, color: Colors.amber),
                        const SizedBox(width: 8),
                        const Text(
                          'Generate Mock',
                          style: TextStyle(
                            fontWeight: FontWeight.bold,
                            fontSize: 13,
                            color: Colors.amber,
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ],
          ),
          _buildVisualFilePreview(),
        ],
      ),
    );
  }

  Widget _buildFileIndicator(String label, Color color) {
    return Row(
      children: [
        Icon(CupertinoIcons.checkmark_seal_fill, color: color, size: 14),
        const SizedBox(width: 6),
        Expanded(
          child: Text(
            label,
            style: TextStyle(fontSize: 11, color: color.withOpacity(0.85), fontWeight: FontWeight.bold),
          ),
        ),
      ],
    );
  }

  Widget _buildGeneralShareCard() {
    final hasFiles = _getActiveFilePaths() != null;
    return _buildCard(
      title: 'General System Share',
      icon: CupertinoIcons.share,
      accentColor: Colors.deepPurpleAccent,
      children: [
        _buildTextField(
          controller: _generalTextController,
          label: 'Share Content Text',
          icon: CupertinoIcons.textformat,
        ),
        _buildToggleRow(
          label: 'Include Plain Text Caption',
          value: _generalIncludeText,
          onChanged: (val) => setState(() => _generalIncludeText = val),
        ),
        _buildToggleRow(
          label: 'Include Active Attachments',
          value: _generalIncludeAttachments && hasFiles,
          onChanged: (val) => setState(() => _generalIncludeAttachments = val),
          enabled: hasFiles,
        ),
        if (!hasFiles)
          _buildInfoBox(
            'No files active. Use the Attachments Manager above to add files if you want to test sharing text + files!',
            Colors.orange,
          )
        else
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              const Text('Active Files Status:', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500)),
              _buildAttachmentBadge(),
            ],
          ),
        const SizedBox(height: 14),
        _buildActionButton(
          label: 'Open Native Share Sheet',
          icon: CupertinoIcons.share_up,
          color: Colors.deepPurpleAccent,
          onPressed: () async {
            try {
              final text = _generalIncludeText ? _generalTextController.text : '';
              final files = (_generalIncludeAttachments && hasFiles) ? _getActiveFilePaths() : null;
              
              _addLog('Triggering shareGeneral (Text length: ${text.length}, Files: ${files?.length ?? 0})...');
              final success = await UniversalShare.shareGeneral(
                text: text,
                filePaths: files,
              );
              _addLog('shareGeneral result: $success');
            } catch (e) {
              _addLog('shareGeneral failed: $e');
            }
          },
        )
      ],
    );
  }

  Widget _buildWhatsAppCard() {
    final hasFiles = _getActiveFilePaths() != null;
    return _buildCard(
      title: 'Direct WhatsApp',
      icon: CupertinoIcons.chat_bubble_2_fill,
      accentColor: const Color(0xFF25D366),
      children: [
        _buildTextField(
          controller: _whatsappPhoneController,
          label: 'Phone Number (with Country Code)',
          icon: CupertinoIcons.phone,
        ),
        _buildTextField(
          controller: _whatsappTextController,
          label: 'Message Text',
          icon: CupertinoIcons.conversation_bubble,
          maxLines: 2,
        ),
        _buildToggleRow(
          label: 'Include Text Message',
          value: _whatsappIncludeText,
          onChanged: (val) => setState(() => _whatsappIncludeText = val),
        ),
        _buildToggleRow(
          label: 'Attach Media File',
          value: _whatsappIncludeAttachment && hasFiles,
          onChanged: (val) => setState(() => _whatsappIncludeAttachment = val),
          enabled: hasFiles,
        ),
        _buildToggleRow(
          label: 'Send Directly to Phone Number',
          value: _whatsappDirectChat,
          onChanged: (val) => setState(() => _whatsappDirectChat = val),
        ),
        if (!hasFiles)
          _buildInfoBox(
            'Add files in the Attachments Manager above to test WhatsApp direct file sharing!',
            Colors.orange,
          )
        else
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              const Text('Active Files Status:', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500)),
              _buildAttachmentBadge(),
            ],
          ),
        const SizedBox(height: 14),
        _buildActionButton(
          label: 'Share via WhatsApp',
          icon: CupertinoIcons.paperplane_fill,
          color: const Color(0xFF25D366),
          onPressed: () async {
            try {
              final text = _whatsappIncludeText ? _whatsappTextController.text : '';
              final phone = _whatsappDirectChat ? _whatsappPhoneController.text.trim() : null;
              final file = (_whatsappIncludeAttachment && hasFiles) ? _getActiveFilePaths(singleOnly: true)?.first : null;

              _addLog('Triggering shareToWhatsApp (Direct Phone: $phone, File: $file)...');
              final success = await UniversalShare.shareToWhatsApp(
                text: text,
                phoneNumber: phone,
                filePath: file,
              );
              _addLog('shareToWhatsApp result: $success');
              if (!success) {
                _showAppMissingDialog('WhatsApp / WhatsApp Business');
              }
            } catch (e) {
              _addLog('WhatsApp share failed: $e');
            }
          },
        )
      ],
    );
  }

  Widget _buildInstagramCard() {
    final hasFiles = _getActiveFilePaths() != null;
    return _buildCard(
      title: 'Direct Instagram',
      icon: CupertinoIcons.camera,
      accentColor: const Color(0xFFE1306C),
      children: [
        // 1. Stories Share
        const Text(
          'INSTAGRAM STORIES',
          style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Color(0xFFE1306C), letterSpacing: 1.0),
        ),
        const SizedBox(height: 10),
        _buildTextField(
          controller: _instagramStoryController,
          label: 'Story Text Caption (Auto Clipboard Copy)',
          icon: CupertinoIcons.doc_on_clipboard,
        ),
        _buildToggleRow(
          label: 'Include Background Media',
          value: _instagramIncludeBackground && hasFiles,
          onChanged: (val) => setState(() => _instagramIncludeBackground = val),
          enabled: hasFiles,
        ),
        _buildToggleRow(
          label: 'Include Sticker Media',
          value: _instagramIncludeSticker && hasFiles,
          onChanged: (val) => setState(() => _instagramIncludeSticker = val),
          enabled: hasFiles,
        ),
        _buildToggleRow(
          label: 'Include Text Caption (Clipboard)',
          value: _instagramIncludeCaption,
          onChanged: (val) => setState(() => _instagramIncludeCaption = val),
        ),
        _buildInfoBox(
          'Meta Stories doesn\'t natively support text overlay parameter. The caption text will be copied to your clipboard automatically—tap the "Aa" tool in Instagram and paste it!',
          Colors.pinkAccent,
        ),
        _buildActionButton(
          label: 'Share to Instagram Stories',
          icon: CupertinoIcons.add,
          color: const Color(0xFFE1306C),
          onPressed: () async {
            try {
              if ((_instagramIncludeBackground || _instagramIncludeSticker) && !hasFiles) {
                await _generateMockFiles();
              }
              final finalPath = _getActiveFilePaths(singleOnly: true)?.first;
              
              _addLog('Triggering shareToInstagramStory...');
              final success = await UniversalShare.shareToInstagramStory(
                stickerAssetPath: (_instagramIncludeSticker && finalPath != null) ? finalPath : null,
                backgroundAssetPath: (_instagramIncludeBackground && finalPath != null) ? finalPath : null,
                topBackgroundColor: '#2C1B4D',
                bottomBackgroundColor: '#0F0E17',
                facebookAppId: '1234567890',
                text: _instagramIncludeCaption ? _instagramStoryController.text : null,
              );
              _addLog('shareToInstagramStory result: $success');
              if (success) {
                if (_instagramIncludeCaption && _instagramStoryController.text.isNotEmpty) {
                  _addLog('Caption text copied to clipboard! Paste it inside Instagram Stories.');
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(
                      content: Text('Caption text copied to clipboard! Paste it inside Stories.'),
                      backgroundColor: Color(0xFFE1306C),
                      behavior: SnackBarBehavior.floating,
                    ),
                  );
                }
              } else {
                _showAppMissingDialog('Instagram');
              }
            } catch (e) {
              _addLog('Instagram Stories share failed: $e');
            }
          },
        ),
        const SizedBox(height: 24),
        const Divider(height: 1, color: Colors.white10),
        const SizedBox(height: 20),

        // 2. Feed Share
        const Text(
          'INSTAGRAM FEED',
          style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Color(0xFFC13584), letterSpacing: 1.0),
        ),
        const SizedBox(height: 10),
        if (!hasFiles)
          _buildInfoBox(
            'Feed sharing requires an active video or image. Select a file in the Attachments Manager above first!',
            Colors.orange,
          )
        else
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              const Text('Selected Feed Media:', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500)),
              _buildAttachmentBadge(),
            ],
          ),
        const SizedBox(height: 8),
        _buildActionButton(
          label: 'Share to Instagram Feed (Post)',
          icon: CupertinoIcons.photo_on_rectangle,
          color: const Color(0xFFC13584),
          onPressed: () async {
            try {
              if (!hasFiles) {
                await _generateMockFiles();
              }
              final finalPath = _getActiveFilePaths(singleOnly: true)?.first;
              if (finalPath == null) {
                _addLog('No media file active to share to Feed.');
                return;
              }
              _addLog('Triggering shareToInstagramFeed (File: $finalPath)...');
              final success = await UniversalShare.shareToInstagramFeed(
                filePath: finalPath,
              );
              _addLog('shareToInstagramFeed result: $success');
              if (!success) {
                _showAppMissingDialog('Instagram');
              }
            } catch (e) {
              _addLog('Instagram Feed share failed: $e');
            }
          },
        ),
        const SizedBox(height: 24),
        const Divider(height: 1, color: Colors.white10),
        const SizedBox(height: 20),

        // 3. Direct DM Share
        const Text(
          'INSTAGRAM DIRECT DM',
          style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Color(0xFF833AB4), letterSpacing: 1.0),
        ),
        const SizedBox(height: 10),
        _buildTextField(
          controller: _instagramDirectController,
          label: 'Direct Chat Message Text',
          icon: CupertinoIcons.chat_bubble,
        ),
        _buildToggleRow(
          label: 'Include Attachment Media',
          value: _instagramDirectIncludeAttachment && hasFiles,
          onChanged: (val) => setState(() => _instagramDirectIncludeAttachment = val),
          enabled: hasFiles,
        ),
        _buildActionButton(
          label: 'Share to Instagram Direct DM',
          icon: CupertinoIcons.paperplane_fill,
          color: const Color(0xFF833AB4),
          onPressed: () async {
            try {
              if (_instagramDirectIncludeAttachment && !hasFiles) {
                await _generateMockFiles();
              }
              final finalPath = _getActiveFilePaths(singleOnly: true)?.first;
              
              _addLog('Triggering shareToInstagramDirect...');
              final success = await UniversalShare.shareToInstagramDirect(
                text: _instagramDirectController.text,
                filePath: (_instagramDirectIncludeAttachment && finalPath != null) ? finalPath : null,
              );
              _addLog('shareToInstagramDirect result: $success');
              if (!success) {
                _showAppMissingDialog('Instagram');
              }
            } catch (e) {
              _addLog('Instagram Direct share failed: $e');
            }
          },
        )
      ],
    );
  }

  Widget _buildFacebookCard() {
    final hasFiles = _getActiveFilePaths() != null;
    return _buildCard(
      title: 'Direct Facebook',
      icon: CupertinoIcons.group_solid,
      accentColor: const Color(0xFF1877F2),
      children: [
        // 1. Stories Share
        const Text(
          'FACEBOOK STORIES',
          style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Color(0xFF1877F2), letterSpacing: 1.0),
        ),
        const SizedBox(height: 10),
        _buildTextField(
          controller: _facebookStoryController,
          label: 'Story Text Caption (Auto Clipboard Copy)',
          icon: CupertinoIcons.doc_on_clipboard,
        ),
        _buildToggleRow(
          label: 'Include Background Media',
          value: _facebookStoryIncludeBackground && hasFiles,
          onChanged: (val) => setState(() => _facebookStoryIncludeBackground = val),
          enabled: hasFiles,
        ),
        _buildToggleRow(
          label: 'Include Sticker Media',
          value: _facebookStoryIncludeSticker && hasFiles,
          onChanged: (val) => setState(() => _facebookStoryIncludeSticker = val),
          enabled: hasFiles,
        ),
        _buildToggleRow(
          label: 'Include Text Caption (Clipboard)',
          value: _facebookStoryIncludeCaption,
          onChanged: (val) => setState(() => _facebookStoryIncludeCaption = val),
        ),
        _buildInfoBox(
          'Meta Stories doesn\'t natively support text overlay parameter. The caption text will be copied to your clipboard automatically—paste it as a text sticker in Facebook Stories.',
          Colors.blueAccent,
        ),
        _buildActionButton(
          label: 'Share to Facebook Stories',
          icon: CupertinoIcons.add,
          color: const Color(0xFF1877F2),
          onPressed: () async {
            try {
              if ((_facebookStoryIncludeBackground || _facebookStoryIncludeSticker) && !hasFiles) {
                await _generateMockFiles();
              }
              final finalPath = _getActiveFilePaths(singleOnly: true)?.first;
              
              _addLog('Triggering shareToFacebookStory...');
              final success = await UniversalShare.shareToFacebookStory(
                stickerAssetPath: (_facebookStoryIncludeSticker && finalPath != null) ? finalPath : null,
                backgroundAssetPath: (_facebookStoryIncludeBackground && finalPath != null) ? finalPath : null,
                facebookAppId: '1234567890',
                text: _facebookStoryIncludeCaption ? _facebookStoryController.text : null,
              );
              _addLog('shareToFacebookStory result: $success');
              if (success) {
                if (_facebookStoryIncludeCaption && _facebookStoryController.text.isNotEmpty) {
                  _addLog('Caption text copied to clipboard! Paste it inside Facebook Stories.');
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(
                      content: Text('Caption text copied to clipboard! Paste it inside Facebook Stories.'),
                      backgroundColor: Color(0xFF1877F2),
                      behavior: SnackBarBehavior.floating,
                    ),
                  );
                }
              } else {
                _showAppMissingDialog('Facebook');
              }
            } catch (e) {
              _addLog('Facebook Stories share failed: $e');
            }
          },
        ),
        const SizedBox(height: 24),
        const Divider(height: 1, color: Colors.white10),
        const SizedBox(height: 20),

        // 2. Feed Share
        const Text(
          'FACEBOOK FEED',
          style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Color(0xFF3B5998), letterSpacing: 1.0),
        ),
        const SizedBox(height: 10),
        _buildTextField(
          controller: _facebookFeedController,
          label: 'Feed Post Message Text',
          icon: CupertinoIcons.textformat_abc,
        ),
        _buildToggleRow(
          label: 'Include Text Message',
          value: _facebookFeedIncludeText,
          onChanged: (val) => setState(() => _facebookFeedIncludeText = val),
        ),
        _buildToggleRow(
          label: 'Attach Image File',
          value: _facebookFeedIncludeImage && hasFiles,
          onChanged: (val) => setState(() => _facebookFeedIncludeImage = val),
          enabled: hasFiles,
        ),
        if (!hasFiles)
          _buildInfoBox(
            'Add files in the Attachments Manager above to test Facebook Feed image attachments!',
            Colors.orange,
          )
        else
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              const Text('Active Files Status:', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500)),
              _buildAttachmentBadge(),
            ],
          ),
        const SizedBox(height: 8),
        _buildActionButton(
          label: 'Share to Facebook Feed',
          icon: CupertinoIcons.checkmark_seal,
          color: const Color(0xFF3B5998),
          onPressed: () async {
            try {
              final text = _facebookFeedIncludeText ? _facebookFeedController.text : '';
              final path = (_facebookFeedIncludeImage && hasFiles) ? _getActiveFilePaths(singleOnly: true)?.first : null;
              
              _addLog('Triggering shareToFacebookFeed (Text: $text, Image: $path)...');
              final success = await UniversalShare.shareToFacebookFeed(
                text: text,
                imagePath: path,
              );
              _addLog('shareToFacebookFeed result: $success');
              if (!success) {
                _showAppMissingDialog('Facebook');
              }
            } catch (e) {
              _addLog('Facebook Feed share failed: $e');
            }
          },
        ),
        const SizedBox(height: 24),
        const Divider(height: 1, color: Colors.white10),
        const SizedBox(height: 20),

        // 3. Messenger Share
        const Text(
          'FACEBOOK MESSENGER',
          style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Color(0xFF00B2FF), letterSpacing: 1.0),
        ),
        const SizedBox(height: 10),
        _buildTextField(
          controller: _facebookMessengerController,
          label: 'Messenger Message Text',
          icon: CupertinoIcons.chat_bubble_fill,
        ),
        _buildActionButton(
          label: 'Send via Facebook Messenger',
          icon: CupertinoIcons.paperplane,
          color: const Color(0xFF00B2FF),
          onPressed: () async {
            try {
              _addLog('Triggering shareToFacebookMessenger...');
              final success = await UniversalShare.shareToFacebookMessenger(
                text: _facebookMessengerController.text,
              );
              _addLog('shareToFacebookMessenger result: $success');
              if (!success) {
                _showAppMissingDialog('Facebook Messenger');
              }
            } catch (e) {
              _addLog('Facebook Messenger share failed: $e');
            }
          },
        )
      ],
    );
  }

  Widget _buildEmailShareCard() {
    final hasFiles = _getActiveFilePaths() != null;
    return _buildCard(
      title: 'Native Email Composer',
      icon: CupertinoIcons.mail,
      accentColor: Colors.blueAccent,
      children: [
        _buildTextField(
          controller: _emailRecipientController,
          label: 'Recipient Email Address',
          icon: CupertinoIcons.mail_solid,
        ),
        _buildTextField(
          controller: _emailSubjectController,
          label: 'Subject',
          icon: CupertinoIcons.textformat_abc,
        ),
        _buildTextField(
          controller: _emailBodyController,
          label: 'Email Body (HTML supported)',
          icon: CupertinoIcons.doc_text,
          maxLines: 3,
        ),
        _buildToggleRow(
          label: 'Include Subject',
          value: _emailIncludeSubject,
          onChanged: (val) => setState(() => _emailIncludeSubject = val),
        ),
        _buildToggleRow(
          label: 'Include Body Text',
          value: _emailIncludeBody,
          onChanged: (val) => setState(() => _emailIncludeBody = val),
        ),
        _buildToggleRow(
          label: 'Include Active Attachments',
          value: _emailIncludeAttachments && hasFiles,
          onChanged: (val) => setState(() => _emailIncludeAttachments = val),
          enabled: hasFiles,
        ),
        if (!hasFiles)
          _buildInfoBox(
            'Attach files in the Attachments Manager above to test email file attachments!',
            Colors.orange,
          )
        else
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              const Text('Active Files Status:', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500)),
              _buildAttachmentBadge(),
            ],
          ),
        const SizedBox(height: 14),
        _buildActionButton(
          label: 'Send Rich Email',
          icon: CupertinoIcons.mail,
          color: Colors.blueAccent,
          onPressed: () async {
            try {
              final recipient = _emailRecipientController.text.trim();
              final listRecipients = recipient.isNotEmpty ? [recipient] : <String>[];
              final subject = _emailIncludeSubject ? _emailSubjectController.text : '';
              final body = _emailIncludeBody ? _emailBodyController.text : '';
              final attachments = (_emailIncludeAttachments && hasFiles) ? _getActiveFilePaths() : null;

              _addLog('Triggering shareToEmail (Attachments count: ${attachments?.length ?? 0})...');
              final success = await UniversalShare.shareToEmail(
                recipients: listRecipients,
                subject: subject,
                body: body,
                attachmentPaths: attachments,
                isHtml: body.contains('<'),
              );
              _addLog('shareToEmail result: $success');
            } catch (e) {
              _addLog('Email composition failed: $e');
            }
          },
        )
      ],
    );
  }

  Widget _buildTelegramCard() {
    final hasFiles = _getActiveFilePaths() != null;
    return _buildCard(
      title: 'Direct Telegram',
      icon: CupertinoIcons.paperplane,
      accentColor: const Color(0xFF0088cc),
      children: [
        _buildTextField(
          controller: _telegramTextController,
          label: 'Telegram Message',
          icon: CupertinoIcons.chat_bubble_text,
        ),
        _buildToggleRow(
          label: 'Include Text Message',
          value: _telegramIncludeText,
          onChanged: (val) => setState(() => _telegramIncludeText = val),
        ),
        _buildToggleRow(
          label: 'Attach Media File',
          value: _telegramIncludeAttachment && hasFiles,
          onChanged: (val) => setState(() => _telegramIncludeAttachment = val),
          enabled: hasFiles,
        ),
        if (!hasFiles)
          _buildInfoBox(
            'Add files in the Attachments Manager above to test direct file sharing to Telegram!',
            Colors.orange,
          )
        else
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              const Text('Active Files Status:', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500)),
              _buildAttachmentBadge(),
            ],
          ),
        const SizedBox(height: 14),
        _buildActionButton(
          label: 'Share via Telegram',
          icon: CupertinoIcons.paperplane_fill,
          color: const Color(0xFF0088cc),
          onPressed: () async {
            try {
              final text = _telegramIncludeText ? _telegramTextController.text : '';
              final file = (_telegramIncludeAttachment && hasFiles) ? _getActiveFilePaths(singleOnly: true)?.first : null;

              _addLog('Triggering shareToTelegram (File: $file)...');
              final success = await UniversalShare.shareToTelegram(
                text: text,
                filePath: file,
              );
              _addLog('shareToTelegram result: $success');
              if (!success) {
                _showAppMissingDialog('Telegram / Telegram X');
              }
            } catch (e) {
              _addLog('Telegram share failed: $e');
            }
          },
        )
      ],
    );
  }

  Widget _buildSMSCard() {
    return _buildCard(
      title: 'Native SMS Composer',
      icon: CupertinoIcons.chat_bubble_text,
      accentColor: Colors.teal,
      children: [
        _buildTextField(
          controller: _smsRecipientsController,
          label: 'Recipient Phone Numbers',
          icon: CupertinoIcons.phone_fill,
        ),
        _buildTextField(
          controller: _smsMessageController,
          label: 'SMS Message',
          icon: CupertinoIcons.conversation_bubble,
        ),
        const SizedBox(height: 14),
        _buildActionButton(
          label: 'Compose SMS',
          icon: CupertinoIcons.text_bubble,
          color: Colors.teal,
          onPressed: () async {
            try {
              final listRecipients = _smsRecipientsController.text
                  .split(',')
                  .map((e) => e.trim())
                  .where((e) => e.isNotEmpty)
                  .toList();

              _addLog('Triggering shareToSMS...');
              final success = await UniversalShare.shareToSMS(
                recipients: listRecipients,
                message: _smsMessageController.text,
              );
              _addLog('shareToSMS result: $success');
            } catch (e) {
              _addLog('SMS composition failed: $e');
            }
          },
        )
      ],
    );
  }

  Widget _buildLogsCard() {
    return Container(
      decoration: BoxDecoration(
        color: widget.themeMode == ThemeMode.dark ? Colors.black38 : const Color(0xFFEEEEF2),
        borderRadius: BorderRadius.circular(20),
        border: Border.all(color: Colors.white10),
      ),
      padding: const EdgeInsets.all(12),
      child: ListView.builder(
        itemCount: _logs.length,
        itemBuilder: (context, index) {
          return Padding(
            padding: const EdgeInsets.only(bottom: 6.0),
            child: Text(
              _logs[index],
              style: const TextStyle(
                fontFamily: 'monospace',
                fontSize: 11,
                color: Colors.green,
                fontWeight: FontWeight.bold,
              ),
            ),
          );
        },
      ),
    );
  }

  void _showAppMissingDialog(String appName) {
    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: Row(
          children: [
            const Icon(CupertinoIcons.exclamationmark_triangle_fill, color: Colors.orangeAccent),
            const SizedBox(width: 8),
            Expanded(
              child: Text('$appName Not Installed', style: const TextStyle(fontSize: 16)),
            ),
          ],
        ),
        content: Text('The package verified that $appName is not installed on this device, so direct sharing could not be started.'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(ctx),
            child: const Text('OK'),
          )
        ],
      ),
    );
  }
}

// Bouncy scale-down transition wrapper for iOS feeling buttons!
class AnimatedBouncyButton extends StatefulWidget {
  final Widget child;
  final VoidCallback? onPressed;
  const AnimatedBouncyButton({required this.child, required this.onPressed, super.key});

  @override
  State<AnimatedBouncyButton> createState() => _AnimatedBouncyButtonState();
}

class _AnimatedBouncyButtonState extends State<AnimatedBouncyButton> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scaleAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 100),
    );
    _scaleAnimation = Tween<double>(begin: 1.0, end: 0.94).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeOut),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTapDown: widget.onPressed == null ? null : (_) => _controller.forward(),
      onTapUp: widget.onPressed == null ? null : (_) {
        _controller.reverse();
        widget.onPressed!();
      },
      onTapCancel: widget.onPressed == null ? null : () => _controller.reverse(),
      child: ScaleTransition(
        scale: _scaleAnimation,
        child: widget.child,
      ),
    );
  }
}
1
likes
150
points
97
downloads
screenshot

Documentation

API reference

Publisher

verified publisheryashdodani.me

Weekly Downloads

Premium, zero-configuration Flutter sharing plugin for WhatsApp, Instagram, Facebook, Telegram, SMS, and HTML Emails with multiple attachments.

Homepage

Topics

#share #social-share #sharing #email-share #files-share

License

BSD-3-Clause (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on universal_share

Packages that implement universal_share