687 lines
23 KiB
Dart
687 lines
23 KiB
Dart
import 'dart:io';
|
|
import 'dart:ui' as ui;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:lucide_icons/lucide_icons.dart';
|
|
import 'package:share_plus/share_plus.dart';
|
|
|
|
import '../../app/icons/app_icons.dart';
|
|
import '../../app/theme/app_colors.dart';
|
|
import '../../core/widgets/arabic_text.dart';
|
|
|
|
String buildAyatShareText(Map<String, dynamic> ayat) {
|
|
final arabic = (ayat['teksArab'] ?? '').toString().trim();
|
|
final translation = (ayat['teksIndonesia'] ?? '').toString().trim();
|
|
final surahName = (ayat['surahName'] ?? '').toString().trim();
|
|
final verseNumber = (ayat['nomorAyat'] ?? '').toString().trim();
|
|
final reference = surahName.isNotEmpty && verseNumber.isNotEmpty
|
|
? 'QS. $surahName: $verseNumber'
|
|
: 'Ayat Hari Ini';
|
|
|
|
final parts = <String>[
|
|
if (arabic.isNotEmpty) arabic,
|
|
if (translation.isNotEmpty) '"$translation"',
|
|
reference,
|
|
'Dibagikan dari Jam Shalat Diary',
|
|
];
|
|
|
|
return parts.join('\n\n');
|
|
}
|
|
|
|
Future<void> showAyatShareSheet(
|
|
BuildContext context,
|
|
Map<String, dynamic> ayat,
|
|
) async {
|
|
final shareText = buildAyatShareText(ayat);
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
await showModalBottomSheet<void>(
|
|
context: context,
|
|
useSafeArea: true,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
|
),
|
|
builder: (sheetContext) {
|
|
Future<void> handleShareImage() async {
|
|
Navigator.of(sheetContext).pop();
|
|
|
|
try {
|
|
final pngBytes = await _captureAyatShareCardPng(context, ayat);
|
|
final file = await _writeAyatShareImage(pngBytes);
|
|
await Share.shareXFiles(
|
|
[XFile(file.path)],
|
|
text: 'Ayat Hari Ini',
|
|
subject: 'Ayat Hari Ini',
|
|
);
|
|
} catch (_) {
|
|
if (!context.mounted) return;
|
|
ScaffoldMessenger.of(context)
|
|
..hideCurrentSnackBar()
|
|
..showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Gagal menyiapkan gambar ayat'),
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> handleShareText() async {
|
|
Navigator.of(sheetContext).pop();
|
|
await Share.share(
|
|
shareText,
|
|
subject: 'Ayat Hari Ini',
|
|
);
|
|
}
|
|
|
|
Future<void> handleCopyText() async {
|
|
await Clipboard.setData(ClipboardData(text: shareText));
|
|
if (!sheetContext.mounted) return;
|
|
Navigator.of(sheetContext).pop();
|
|
ScaffoldMessenger.of(context)
|
|
..hideCurrentSnackBar()
|
|
..showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Teks ayat disalin ke clipboard'),
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
}
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 20, 16, 24),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Bagikan Ayat',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
'Pilih cara tercepat untuk membagikan ayat hari ini.',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
height: 1.5,
|
|
color: isDark
|
|
? AppColors.textSecondaryDark
|
|
: AppColors.textSecondaryLight,
|
|
),
|
|
),
|
|
const SizedBox(height: 18),
|
|
_AyatShareActionTile(
|
|
icon: const Icon(
|
|
LucideIcons.image,
|
|
size: 20,
|
|
color: AppColors.primary,
|
|
),
|
|
title: 'Bagikan Gambar',
|
|
subtitle: 'Kirim kartu ayat yang siap dibagikan',
|
|
badge: 'Utama',
|
|
onTap: handleShareImage,
|
|
),
|
|
const SizedBox(height: 10),
|
|
_AyatShareActionTile(
|
|
icon: const AppIcon(
|
|
glyph: AppIcons.share,
|
|
size: 20,
|
|
color: AppColors.primary,
|
|
),
|
|
title: 'Bagikan Teks',
|
|
subtitle: 'Kirim ayat dan terjemahan ke aplikasi lain',
|
|
onTap: handleShareText,
|
|
),
|
|
const SizedBox(height: 10),
|
|
_AyatShareActionTile(
|
|
icon: const Icon(
|
|
LucideIcons.copy,
|
|
size: 20,
|
|
color: AppColors.primary,
|
|
),
|
|
title: 'Salin Teks',
|
|
subtitle: 'Simpan ke clipboard untuk ditempel manual',
|
|
onTap: handleCopyText,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<Uint8List> _captureAyatShareCardPng(
|
|
BuildContext context,
|
|
Map<String, dynamic> ayat,
|
|
) async {
|
|
final overlay = Overlay.of(context, rootOverlay: true);
|
|
final boundaryKey = GlobalKey();
|
|
final theme = Theme.of(context);
|
|
final mediaQuery = MediaQuery.of(context);
|
|
final textDirection = Directionality.of(context);
|
|
|
|
late final OverlayEntry entry;
|
|
entry = OverlayEntry(
|
|
builder: (_) => IgnorePointer(
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: Align(
|
|
alignment: Alignment.topCenter,
|
|
child: Opacity(
|
|
opacity: 0.01,
|
|
child: MediaQuery(
|
|
data: mediaQuery,
|
|
child: Theme(
|
|
data: theme,
|
|
child: Directionality(
|
|
textDirection: textDirection,
|
|
child: UnconstrainedBox(
|
|
constrainedAxis: Axis.horizontal,
|
|
child: RepaintBoundary(
|
|
key: boundaryKey,
|
|
child: _AyatShareCard(
|
|
ayat: ayat,
|
|
isDark: theme.brightness == Brightness.dark,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
overlay.insert(entry);
|
|
|
|
try {
|
|
await Future<void>.delayed(const Duration(milliseconds: 20));
|
|
await WidgetsBinding.instance.endOfFrame;
|
|
await Future<void>.delayed(const Duration(milliseconds: 20));
|
|
|
|
final boundary = boundaryKey.currentContext?.findRenderObject()
|
|
as RenderRepaintBoundary?;
|
|
if (boundary == null) {
|
|
throw StateError('Ayat share card is not ready');
|
|
}
|
|
|
|
final image = await boundary.toImage(pixelRatio: 3);
|
|
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
|
if (byteData == null) {
|
|
throw StateError('Failed to encode ayat share card');
|
|
}
|
|
return byteData.buffer.asUint8List();
|
|
} finally {
|
|
entry.remove();
|
|
}
|
|
}
|
|
|
|
Future<File> _writeAyatShareImage(Uint8List pngBytes) async {
|
|
final directory = await Directory.systemTemp.createTemp('jamshalat_ayat_');
|
|
final file = File('${directory.path}/ayat_hari_ini.png');
|
|
await file.writeAsBytes(pngBytes, flush: true);
|
|
return file;
|
|
}
|
|
|
|
class _AyatShareCard extends StatelessWidget {
|
|
const _AyatShareCard({
|
|
required this.ayat,
|
|
required this.isDark,
|
|
});
|
|
|
|
final Map<String, dynamic> ayat;
|
|
final bool isDark;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final arabic = (ayat['teksArab'] ?? '').toString().trim();
|
|
final translation = (ayat['teksIndonesia'] ?? '').toString().trim();
|
|
final surahName = (ayat['surahName'] ?? '').toString().trim();
|
|
final verseNumber = (ayat['nomorAyat'] ?? '').toString().trim();
|
|
final reference = surahName.isNotEmpty && verseNumber.isNotEmpty
|
|
? 'QS. $surahName: $verseNumber'
|
|
: 'Ayat Hari Ini';
|
|
final isLongArabic = arabic.length > 120;
|
|
final isVeryLongArabic = arabic.length > 180;
|
|
final isLongTranslation = translation.length > 140;
|
|
final isVeryLongTranslation = translation.length > 220;
|
|
final arabicFontSize = isVeryLongArabic
|
|
? 22.0
|
|
: isLongArabic
|
|
? 24.0
|
|
: 28.0;
|
|
final arabicHeight = isVeryLongArabic
|
|
? 1.55
|
|
: isLongArabic
|
|
? 1.62
|
|
: 1.75;
|
|
final translationFontSize = isVeryLongTranslation
|
|
? 13.0
|
|
: isLongTranslation
|
|
? 14.0
|
|
: 15.0;
|
|
final translationHeight = isVeryLongTranslation ? 1.5 : 1.6;
|
|
final verticalPadding = isVeryLongTranslation ? 22.0 : 24.0;
|
|
|
|
return SizedBox(
|
|
width: 360,
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(30),
|
|
child: DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(30),
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: isDark
|
|
? const [
|
|
Color(0xFF102028),
|
|
Color(0xFF0F1217),
|
|
Color(0xFF16343A),
|
|
]
|
|
: const [
|
|
Color(0xFFF6FBFB),
|
|
Color(0xFFFFFFFF),
|
|
Color(0xFFEAF7F7),
|
|
],
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color:
|
|
AppColors.primary.withValues(alpha: isDark ? 0.24 : 0.12),
|
|
blurRadius: 28,
|
|
offset: const Offset(0, 18),
|
|
),
|
|
],
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: IgnorePointer(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(14),
|
|
child: CustomPaint(
|
|
painter: _AyatFramePainter(isDark: isDark),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
top: -38,
|
|
right: -34,
|
|
child: Container(
|
|
width: 116,
|
|
height: 116,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: AppColors.primary.withValues(alpha: 0.05),
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
bottom: -46,
|
|
left: -28,
|
|
child: Container(
|
|
width: 132,
|
|
height: 132,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: AppColors.navActiveGold.withValues(alpha: 0.05),
|
|
),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: EdgeInsets.fromLTRB(
|
|
28,
|
|
28,
|
|
28,
|
|
verticalPadding,
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 42,
|
|
height: 42,
|
|
decoration: BoxDecoration(
|
|
color: AppColors.primary.withValues(alpha: 0.14),
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
child: const Icon(
|
|
LucideIcons.bookMarked,
|
|
size: 20,
|
|
color: AppColors.primary,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Ayat Hari Ini',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 1.3,
|
|
color: AppColors.primary,
|
|
),
|
|
),
|
|
const SizedBox(height: 3),
|
|
Text(
|
|
reference,
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w800,
|
|
color: isDark
|
|
? Colors.white
|
|
: AppColors.textPrimaryLight,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
if (arabic.isNotEmpty) ...[
|
|
const SizedBox(height: 28),
|
|
ArabicText(
|
|
arabic,
|
|
baseFontSize: arabicFontSize,
|
|
height: arabicHeight,
|
|
textAlign: TextAlign.right,
|
|
color: isDark
|
|
? AppColors.textPrimaryDark
|
|
: AppColors.textPrimaryLight,
|
|
),
|
|
],
|
|
if (translation.isNotEmpty) ...[
|
|
const SizedBox(height: 24),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(18),
|
|
decoration: BoxDecoration(
|
|
color: (isDark ? Colors.white : AppColors.primary)
|
|
.withValues(alpha: isDark ? 0.05 : 0.08),
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(
|
|
color: (isDark ? Colors.white : AppColors.primary)
|
|
.withValues(alpha: 0.08),
|
|
),
|
|
),
|
|
child: Text(
|
|
'"$translation"',
|
|
textAlign: TextAlign.left,
|
|
style: TextStyle(
|
|
fontSize: translationFontSize,
|
|
height: translationHeight,
|
|
fontStyle: FontStyle.italic,
|
|
color: isDark
|
|
? AppColors.textPrimaryDark
|
|
: AppColors.textPrimaryLight,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: 22),
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 8,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color:
|
|
AppColors.navActiveGold.withValues(alpha: 0.16),
|
|
borderRadius: BorderRadius.circular(999),
|
|
),
|
|
child: const Text(
|
|
'Jam Shalat Diary',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w700,
|
|
color: AppColors.navActiveGoldDeep,
|
|
),
|
|
),
|
|
),
|
|
const Spacer(),
|
|
Flexible(
|
|
child: Text(
|
|
'Bagikan kebaikan',
|
|
textAlign: TextAlign.right,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
color: isDark
|
|
? AppColors.textSecondaryDark
|
|
: AppColors.textSecondaryLight,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AyatShareActionTile extends StatelessWidget {
|
|
const _AyatShareActionTile({
|
|
required this.icon,
|
|
required this.title,
|
|
required this.subtitle,
|
|
required this.onTap,
|
|
this.badge,
|
|
});
|
|
|
|
final Widget icon;
|
|
final String title;
|
|
final String subtitle;
|
|
final VoidCallback onTap;
|
|
final String? badge;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
return Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(18),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight,
|
|
borderRadius: BorderRadius.circular(18),
|
|
border: Border.all(
|
|
color: isDark
|
|
? AppColors.primary.withValues(alpha: 0.12)
|
|
: AppColors.cream,
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 42,
|
|
height: 42,
|
|
decoration: BoxDecoration(
|
|
color: AppColors.primary.withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
child: Center(child: icon),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: const TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
if (badge != null) ...[
|
|
const SizedBox(height: 6),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color:
|
|
AppColors.navActiveGold.withValues(alpha: 0.18),
|
|
borderRadius: BorderRadius.circular(999),
|
|
),
|
|
child: Text(
|
|
badge!,
|
|
style: const TextStyle(
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w800,
|
|
color: AppColors.navActiveGoldDeep,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
subtitle,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
height: 1.4,
|
|
color: isDark
|
|
? AppColors.textSecondaryDark
|
|
: AppColors.textSecondaryLight,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Icon(
|
|
LucideIcons.chevronRight,
|
|
size: 18,
|
|
color: isDark
|
|
? AppColors.textSecondaryDark
|
|
: AppColors.textSecondaryLight,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AyatFramePainter extends CustomPainter {
|
|
const _AyatFramePainter({required this.isDark});
|
|
|
|
final bool isDark;
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final outerRect = RRect.fromRectAndRadius(
|
|
Offset.zero & size,
|
|
const Radius.circular(22),
|
|
);
|
|
|
|
final outerPaint = Paint()
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = 1.6
|
|
..shader = const LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [
|
|
AppColors.navActiveGoldPale,
|
|
AppColors.navActiveGold,
|
|
AppColors.navActiveGoldDeep,
|
|
],
|
|
).createShader(Offset.zero & size);
|
|
|
|
final outerGlow = Paint()
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = 5
|
|
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6)
|
|
..color = AppColors.navActiveGold.withValues(alpha: isDark ? 0.08 : 0.05);
|
|
|
|
final innerBounds = Rect.fromLTWH(8, 8, size.width - 16, size.height - 16);
|
|
final innerFrame = RRect.fromRectAndRadius(
|
|
innerBounds,
|
|
const Radius.circular(18),
|
|
);
|
|
final innerPaint = Paint()
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = 0.8
|
|
..color = (isDark ? Colors.white : AppColors.primary)
|
|
.withValues(alpha: isDark ? 0.08 : 0.10);
|
|
|
|
canvas.drawRRect(outerRect, outerGlow);
|
|
canvas.drawRRect(outerRect, outerPaint);
|
|
canvas.drawRRect(innerFrame, innerPaint);
|
|
|
|
_drawMidMotif(canvas, size, top: true);
|
|
_drawMidMotif(canvas, size, top: false);
|
|
}
|
|
|
|
void _drawMidMotif(Canvas canvas, Size size, {required bool top}) {
|
|
final y = top ? 14.0 : size.height - 14.0;
|
|
final centerX = size.width / 2;
|
|
|
|
final linePaint = Paint()
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = 0.9
|
|
..strokeCap = StrokeCap.round
|
|
..color = AppColors.navActiveGold.withValues(alpha: isDark ? 0.26 : 0.22);
|
|
final diamondPaint = Paint()
|
|
..style = PaintingStyle.fill
|
|
..color = AppColors.primary.withValues(alpha: isDark ? 0.34 : 0.22);
|
|
final diamondStroke = Paint()
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = 0.9
|
|
..color = AppColors.navActiveGold.withValues(alpha: isDark ? 0.58 : 0.48);
|
|
|
|
canvas.drawLine(
|
|
Offset(centerX - 26, y),
|
|
Offset(centerX - 10, y),
|
|
linePaint,
|
|
);
|
|
canvas.drawLine(
|
|
Offset(centerX + 10, y),
|
|
Offset(centerX + 26, y),
|
|
linePaint,
|
|
);
|
|
|
|
final diamondPath = Path()
|
|
..moveTo(centerX, y - 5)
|
|
..lineTo(centerX + 5, y)
|
|
..lineTo(centerX, y + 5)
|
|
..lineTo(centerX - 5, y)
|
|
..close();
|
|
canvas.drawPath(diamondPath, diamondPaint);
|
|
canvas.drawPath(diamondPath, diamondStroke);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant _AyatFramePainter oldDelegate) {
|
|
return oldDelegate.isDark != isDark;
|
|
}
|
|
}
|