import 'dart:io' show Platform; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_qiblah/flutter_qiblah.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../../../app/theme/app_colors.dart'; class QiblaScreen extends ConsumerStatefulWidget { const QiblaScreen({super.key}); @override ConsumerState createState() => _QiblaScreenState(); } class _QiblaScreenState extends ConsumerState { // Fallback simulated data for environments without compass hardware (like macOS emulator) double _qiblaAngle = 295.0; // Default Jakarta to Mecca String _direction = 'NW'; bool _hasHardwareSupport = false; late final Future _deviceSupport = _checkDeviceSupport(); Future _checkDeviceSupport() async { if (Platform.isAndroid || Platform.isIOS) { try { return await FlutterQiblah.androidDeviceSensorSupport(); } catch (e) { return false; } } return false; } @override void initState() { super.initState(); // Pre-calculate static fallback _calculateStaticQibla(); } void _calculateStaticQibla() { // Default to Jakarta coordinates const lat = -6.2088; const lng = 106.8456; // Mecca coordinates const meccaLat = 21.4225; const meccaLng = 39.8262; // Calculate qibla direction final dLng = (meccaLng - lng) * math.pi / 180; final lat1 = lat * math.pi / 180; final lat2 = meccaLat * math.pi / 180; final y = math.sin(dLng) * math.cos(lat2); final x = math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(dLng); var bearing = math.atan2(y, x) * 180 / math.pi; bearing = (bearing + 360) % 360; setState(() { _qiblaAngle = bearing; _updateDirectionText(bearing); }); } void _updateDirectionText(double angle) { if (angle >= 337.5 || angle < 22.5) { _direction = 'N'; } else if (angle < 67.5) { _direction = 'NE'; } else if (angle < 112.5) { _direction = 'E'; } else if (angle < 157.5) { _direction = 'SE'; } else if (angle < 202.5) { _direction = 'S'; } else if (angle < 247.5) { _direction = 'SW'; } else if (angle < 292.5) { _direction = 'W'; } else { _direction = 'NW'; } } @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return FutureBuilder( future: _deviceSupport, builder: (_, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Scaffold(body: Center(child: CircularProgressIndicator())); } // If device has a compass sensor (true on physical phones) if (snapshot.data == true) { return _buildLiveQibla(context, isDark); } // If device lacks compass (macOS/emulators) return _buildSimulatedQibla(context, isDark); }, ); } Widget _buildLiveQibla(BuildContext context, bool isDark) { return StreamBuilder( stream: FlutterQiblah.qiblahStream, builder: (_, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Scaffold(body: Center(child: CircularProgressIndicator())); } final qiblahDirection = snapshot.data; if (qiblahDirection == null) { return Scaffold(body: Center(child: Text('Menunggu sensor arah...', style: TextStyle(color: isDark ? Colors.white : Colors.black)))); } _updateDirectionText(qiblahDirection.qiblah); return _buildQiblaLayout( context: context, isDark: isDark, angleRad: qiblahDirection.qiblah * (math.pi / 180), displayAngle: qiblahDirection.qiblah, isLive: true, ); }, ); } Widget _buildSimulatedQibla(BuildContext context, bool isDark) { return _buildQiblaLayout( context: context, isDark: isDark, angleRad: _qiblaAngle * (math.pi / 180), displayAngle: _qiblaAngle, isLive: false, ); } Widget _buildQiblaLayout({ required BuildContext context, required bool isDark, required double angleRad, required double displayAngle, required bool isLive, }) { return Scaffold( appBar: AppBar( title: const Text('Qibla Finder'), leading: IconButton( icon: Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( shape: BoxShape.circle, color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight, border: Border.all(color: AppColors.cream), ), child: const Icon(LucideIcons.arrowLeft, size: 18), ), onPressed: () => Navigator.pop(context), ), actions: [ IconButton( icon: Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( shape: BoxShape.circle, color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight, border: Border.all(color: AppColors.cream), ), child: Icon(isLive ? LucideIcons.locate : LucideIcons.locateOff, size: 18), ), onPressed: () { if (isLive) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Menggunakan sensor perangkat aktual')), ); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Mode Simulasi: Hardware kompas tidak terdeteksi')), ); } }, ), ], ), body: Container( width: double.infinity, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: isDark ? [AppColors.backgroundDark, AppColors.surfaceDark] : [ AppColors.backgroundLight, AppColors.primary.withValues(alpha: 0.05), ], ), ), child: Column( children: [ const SizedBox(height: 32), // Location label Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(LucideIcons.mapPin, size: 16, color: AppColors.primary), const SizedBox(width: 4), Text( 'Mecca, Saudi Arabia', style: TextStyle( fontSize: 14, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), ), ], ), const SizedBox(height: 8), // Degree + direction Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '${displayAngle.round()}°', style: const TextStyle( fontSize: 48, fontWeight: FontWeight.w800, ), ), const SizedBox(width: 12), Text( _direction, style: TextStyle( fontSize: 48, fontWeight: FontWeight.w800, color: AppColors.primary, ), ), ], ), const SizedBox(height: 32), // Compass Expanded( child: Center( child: SizedBox( width: 300, height: 300, child: CustomPaint( painter: _CompassPainter( qiblaAngle: angleRad, isDark: isDark, ), ), ), ), ), const SizedBox(height: 16), // Calibration status Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10), decoration: BoxDecoration( color: isLive ? AppColors.primary.withValues(alpha: 0.1) : Colors.orange.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(50), ), child: Text( isLive ? 'SENSOR AKTIF' : 'MODE SIMULASI (TIDAK ADA SENSOR)', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w700, letterSpacing: 1.5, color: isLive ? AppColors.primary : Colors.orange, ), ), ), const SizedBox(height: 48), ], ), ), ); } } class _CompassPainter extends CustomPainter { final double qiblaAngle; final bool isDark; _CompassPainter({required this.qiblaAngle, required this.isDark}); @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); final radius = size.width / 2 - 8; // Outer circle final outerPaint = Paint() ..color = AppColors.primary.withValues(alpha: 0.15) ..style = PaintingStyle.stroke ..strokeWidth = 2; canvas.drawCircle(center, radius, outerPaint); // Inner dashed circle final innerPaint = Paint() ..color = AppColors.primary.withValues(alpha: 0.08) ..style = PaintingStyle.stroke ..strokeWidth = 1; canvas.drawCircle(center, radius * 0.7, innerPaint); // Cross lines final crossPaint = Paint() ..color = AppColors.primary.withValues(alpha: 0.1) ..strokeWidth = 1; canvas.drawLine( Offset(center.dx, center.dy - radius), Offset(center.dx, center.dy + radius), crossPaint); canvas.drawLine( Offset(center.dx - radius, center.dy), Offset(center.dx + radius, center.dy), crossPaint); // Diagonals final diagOffset = radius * 0.707; canvas.drawLine( Offset(center.dx - diagOffset, center.dy - diagOffset), Offset(center.dx + diagOffset, center.dy + diagOffset), crossPaint); canvas.drawLine( Offset(center.dx + diagOffset, center.dy - diagOffset), Offset(center.dx - diagOffset, center.dy + diagOffset), crossPaint); // Center dot final centerDotPaint = Paint() ..color = AppColors.primary.withValues(alpha: 0.3) ..style = PaintingStyle.stroke ..strokeWidth = 2; canvas.drawCircle(center, 6, centerDotPaint); // Qibla direction line final qiblaEndX = center.dx + radius * 0.85 * math.cos(qiblaAngle - math.pi / 2); final qiblaEndY = center.dy + radius * 0.85 * math.sin(qiblaAngle - math.pi / 2); // Glow effect final glowPaint = Paint() ..color = AppColors.primary.withValues(alpha: 0.3) ..strokeWidth = 6 ..strokeCap = StrokeCap.round; canvas.drawLine(center, Offset(qiblaEndX, qiblaEndY), glowPaint); // Main line final linePaint = Paint() ..color = AppColors.primary ..strokeWidth = 3 ..strokeCap = StrokeCap.round; canvas.drawLine(center, Offset(qiblaEndX, qiblaEndY), linePaint); // Qibla icon circle at end final iconPaint = Paint()..color = AppColors.primary; canvas.drawCircle(Offset(qiblaEndX, qiblaEndY), 16, iconPaint); // Kaaba icon (simplified) final kaabaPaint = Paint() ..color = Colors.white ..style = PaintingStyle.fill; canvas.drawRect( Rect.fromCenter( center: Offset(qiblaEndX, qiblaEndY), width: 12, height: 12, ), kaabaPaint, ); } @override bool shouldRepaint(covariant _CompassPainter oldDelegate) => qiblaAngle != oldDelegate.qiblaAngle; }