import 'dart:async'; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:timezone/data/latest_all.dart' as tz; import 'package:timezone/timezone.dart' as tz; import 'package:http/http.dart' as http; import 'dart:convert'; void main() async { WidgetsFlutterBinding.ensureInitialized(); tz.initializeTimeZones(); runApp(const SunsetAlarmApp()); } class SunsetAlarmApp extends StatelessWidget { const SunsetAlarmApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Sunset Alarm', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange), useMaterial3: true, ), home: const HomeScreen(), ); } } class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State { final _latController = TextEditingController(); final _lngController = TextEditingController(); final _sunsetController = TextEditingController(); final _minutesController = TextEditingController(text: '30'); final FlutterLocalNotificationsPlugin _notifications = FlutterLocalNotificationsPlugin(); bool _alarmEnabled = false; int _minutesBefore = 30; bool _loading = false; static const _alarmId = 1001; @override void initState() { super.initState(); _initNotifications(); _loadPrefs(); } Future _initNotifications() async { const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher'); const iosInit = DarwinInitializationSettings(); await _notifications.initialize( const InitializationSettings(android: androidInit, iOS: iosInit), ); } Future _loadPrefs() async { final prefs = await SharedPreferences.getInstance(); setState(() { final lat = prefs.getDouble('lat'); final lng = prefs.getDouble('lng'); final sunset = prefs.getString('sunset_time'); _alarmEnabled = prefs.getBool('alarm_enabled') ?? false; _minutesBefore = prefs.getInt('minutes_before') ?? 30; if (lat != null) _latController.text = lat.toString(); if (lng != null) _lngController.text = lng.toString(); if (sunset != null) _sunsetController.text = sunset; _minutesController.text = '$_minutesBefore'; }); if (_alarmEnabled && _sunsetController.text.isNotEmpty) { _scheduleAlarm(); } } Future _savePrefs() async { final prefs = await SharedPreferences.getInstance(); final lat = double.tryParse(_latController.text); final lng = double.tryParse(_lngController.text); if (lat != null) await prefs.setDouble('lat', lat); if (lng != null) await prefs.setDouble('lng', lng); if (_sunsetController.text.isNotEmpty) { await prefs.setString('sunset_time', _sunsetController.text); } await prefs.setBool('alarm_enabled', _alarmEnabled); await prefs.setInt('minutes_before', _minutesBefore); } Future _getGpsLocation() async { try { bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { _showSnack('Location services are disabled.'); return; } LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { _showSnack('Location permission denied.'); return; } } final position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high, ); setState(() { _latController.text = position.latitude.toString(); _lngController.text = position.longitude.toString(); }); await _savePrefs(); _showSnack('Location updated.'); } catch (e) { _showSnack('GPS error: $e'); } } Future _fetchSunset() async { final lat = double.tryParse(_latController.text); final lng = double.tryParse(_lngController.text); if (lat == null || lng == null) { _showSnack('Enter valid latitude and longitude first.'); return; } setState(() => _loading = true); try { final url = Uri.parse( 'https://api.open-meteo.com/v1/forecast' '?latitude=$lat&longitude=$lng' '&daily=sunset&timezone=auto&forecast_days=1', ); final response = await http.get(url); if (response.statusCode == 200) { final data = jsonDecode(response.body); final sunsetStr = data['daily']['sunset'][0] as String; final timePart = sunsetStr.split('T')[1]; setState(() => _sunsetController.text = timePart); await _savePrefs(); _showSnack('Sunset: $timePart'); } else { _showSnack('API error: ${response.statusCode}'); } } catch (e) { _showSnack('Network error: $e'); } finally { setState(() => _loading = false); } } Future _scheduleAlarm() async { final sunsetText = _sunsetController.text.trim(); if (sunsetText.isEmpty) return; final now = DateTime.now(); final parts = sunsetText.split(':'); if (parts.length < 2) return; final h = int.parse(parts[0]); final m = int.parse(parts[1]); var alarmTime = DateTime(now.year, now.month, now.day, h, m); if (alarmTime.isBefore(now)) { alarmTime = alarmTime.add(const Duration(days: 1)); } alarmTime = alarmTime.subtract(Duration(minutes: _minutesBefore)); if (alarmTime.isBefore(now)) { _showSnack('Alarm time is in the past. Will fire tomorrow.'); alarmTime = alarmTime.add(const Duration(days: 1)); } final tzAlarm = tz.TZDateTime.from(alarmTime, tz.local); const androidDetails = AndroidNotificationDetails( 'sunset_alarm', 'Sunset Alarm', channelDescription: 'Alarm X minutes before sunset', importance: Importance.max, priority: Priority.high, fullScreenIntent: true, category: AndroidNotificationCategory.alarm, ); const iosDetails = DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, ); await _notifications.zonedSchedule( _alarmId, 'Sunset in ${_minutesBefore} min', 'The sun sets at $sunsetText. Time to get ready!', tzAlarm, NotificationDetails(android: androidDetails, iOS: iosDetails), androidScheduleMode: AndroidScheduleMode.alarmClock, uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, ); } Future _toggleAlarm(bool value) async { setState(() => _alarmEnabled = value); await _savePrefs(); if (value) { await _scheduleAlarm(); _showSnack('Alarm scheduled.'); } else { await _notifications.cancel(_alarmId); _showSnack('Alarm cancelled.'); } } void _showSnack(String msg) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); } @override void dispose() { _latController.dispose(); _lngController.dispose(); _sunsetController.dispose(); _minutesController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Sunset Alarm')), body: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // --- Location section --- const Text( 'Location', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Row( children: [ Expanded( child: TextField( controller: _latController, keyboardType: const TextInputType.numberWithOptions( signed: true, decimal: true, ), decoration: const InputDecoration( labelText: 'Latitude', border: OutlineInputBorder(), ), onChanged: (_) => _savePrefs(), ), ), const SizedBox(width: 8), Expanded( child: TextField( controller: _lngController, keyboardType: const TextInputType.numberWithOptions( signed: true, decimal: true, ), decoration: const InputDecoration( labelText: 'Longitude', border: OutlineInputBorder(), ), onChanged: (_) => _savePrefs(), ), ), const SizedBox(width: 8), IconButton.filledTonal( icon: const Icon(Icons.my_location), tooltip: 'Get GPS location', onPressed: _getGpsLocation, ), ], ), const SizedBox(height: 16), // --- Sunset time section --- const Text( 'Sunset', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Row( children: [ Expanded( child: TextField( controller: _sunsetController, decoration: const InputDecoration( labelText: 'Sunset time (HH:MM)', border: OutlineInputBorder(), ), onChanged: (_) => _savePrefs(), ), ), const SizedBox(width: 8), IconButton.filledTonal( icon: _loading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.cloud_done_outlined), tooltip: 'Fetch sunset from Open-Meteo', onPressed: _loading ? null : _fetchSunset, ), ], ), const SizedBox(height: 16), // --- Minutes before --- const Text( 'Alert Before Sunset', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Row( children: [ const Text('Minutes before: '), SizedBox( width: 80, child: TextField( controller: _minutesController, keyboardType: TextInputType.number, decoration: const InputDecoration( border: OutlineInputBorder(), ), onChanged: (v) { final val = int.tryParse(v); if (val != null && val > 0) { _minutesBefore = val; } }, ), ), ], ), const SizedBox(height: 24), // --- Toggle --- SwitchListTile( title: const Text('Enable Sunset Alarm'), subtitle: Text( _alarmEnabled ? 'Will alert ${_minutesBefore} min before sunset' : 'Alarm is off', ), value: _alarmEnabled, onChanged: _toggleAlarm, ), ], ), ), ); } }