universal_share 1.0.1
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,
),
);
}
}