16 Commits
4 changed files with 154 additions and 59 deletions

No files matched your search

+6 -8
View File
@@ -3,13 +3,14 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
> >
<!-- Allow notifications to work. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.USE_EXACT_ALARM"/> <uses-permission android:name="android.permission.USE_EXACT_ALARM"/>
<!-- <!-- Allow alarms to work. -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.VIBRATE"/> <uses-permission android:name="android.permission.VIBRATE"/>
@@ -25,10 +26,9 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
-->
<application <application
android:label="sunset_alarm" android:label="Sunset Alarm"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity
@@ -59,6 +59,7 @@
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
<!-- Allow notifications to work. -->
<receiver <receiver
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"
android:enabled="true" /> android:enabled="true" />
@@ -74,16 +75,14 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<!-- <!-- Allow alarms to work. -->
<service <service
android:name="com.gdelataillade.alarm.AlarmService" android:name="com.gdelataillade.alarm.AlarmService"
android:foregroundServiceType="mediaPlayback" android:foregroundServiceType="mediaPlayback"
android:exported="false" /> android:exported="false" />
<receiver <receiver
android:name="com.gdelataillade.alarm.AlarmBroadcastReceiver" android:name="com.gdelataillade.alarm.AlarmBroadcastReceiver"
android:exported="false" /> android:exported="false" />
<receiver <receiver
android:name="com.gdelataillade.alarm.BootBroadcastReceiver" android:name="com.gdelataillade.alarm.BootBroadcastReceiver"
android:enabled="false" android:enabled="false"
@@ -92,7 +91,6 @@
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<service <service
android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmService" android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmService"
android:permission="android.permission.BIND_JOB_SERVICE" android:permission="android.permission.BIND_JOB_SERVICE"
@@ -111,7 +109,7 @@
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
-->
</application> </application>
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and https://developer.android.com/training/package-visibility and
+16
View File
@@ -0,0 +1,16 @@
AuthorName: 'Chad Michael "Hyperling" Greenwood'
AuthorEmail: 'me@hyperling.com'
AuthorWebSite: 'https://hyperling.com/about/'
Donate: 'https://hyperling.com/donate/'
Liberapay: 'HyperVegan'
Bitcoin: 'bc1qsfe8dkvry3d34kztz449gkq67wq8fu2nkgfkh0'
Litecoin: 'ltc1qavmpu5d6ljntxsd6jj548m4yys83zwscl0dzkx'
IssueTracker: 'https://hyperling.com/contact/'
Categories:
- fdroid
- hyperling
- utilities
Name: Sunset Timer
Summary: 'Play a notification at your chosen time before sunset.'
SourceCode: 'https://git.hyperling.com/me/flutter-sunset-alarm/'
WebSite: 'https://hyperling.com/apps/#sunset'
+131 -50
View File
@@ -6,16 +6,22 @@ import 'package:geolocator/geolocator.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/data/latest_all.dart' as tz; import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
//import 'package:alarm/alarm.dart'; import 'package:alarm/alarm.dart';
import 'dart:convert'; import 'dart:convert';
/// TBD:
/// v0.3.0
/// - Change sunset grabber to checkbox and retrieve it each alarm or open.
/// v1.0.0
/// - Add sunrise section and change app name to Sunshine Alarm?
/// :TBD
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
tz.initializeTimeZones(); tz.initializeTimeZones();
//await Alarm.init(); await Alarm.init();
runApp(const SunsetAlarmApp()); runApp(const SunsetAlarmApp());
} }
@@ -24,12 +30,22 @@ class SunsetAlarmApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const seedColor = Colors.deepOrange;
return MaterialApp( return MaterialApp(
title: 'Sunset Alarm', title: 'Sunset Alarm',
theme: ThemeData( theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange), colorScheme: ColorScheme.fromSeed(
useMaterial3: true, seedColor: seedColor,
brightness: Brightness.light,
), ),
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: seedColor,
brightness: Brightness.dark,
),
),
themeMode: ThemeMode.system,
home: const HomeScreen(), home: const HomeScreen(),
); );
} }
@@ -55,6 +71,9 @@ class _HomeScreenState extends State<HomeScreen> {
int _minutesBefore = 30; int _minutesBefore = 30;
bool _loading = false; bool _loading = false;
String? _sunsetErrorText = null;
String? _minuteErrorText = null;
static const _alarmId = 1001; static const _alarmId = 1001;
@override @override
@@ -114,14 +133,14 @@ class _HomeScreenState extends State<HomeScreen> {
try { try {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) { if (!serviceEnabled) {
_showSnack('Location services are disabled.'); _showSnack('Location Services Disabled');
return; return;
} }
LocationPermission permission = await Geolocator.checkPermission(); LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) { if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission(); permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) { if (permission == LocationPermission.denied) {
_showSnack('Location permission denied.'); _showSnack('Location Permission Denied');
return; return;
} }
} }
@@ -133,9 +152,9 @@ class _HomeScreenState extends State<HomeScreen> {
_lngController.text = position.longitude.toString(); _lngController.text = position.longitude.toString();
}); });
await _savePrefs(); await _savePrefs();
_showSnack('Location updated.'); _showSnack('Location Updated.');
} catch (e) { } catch (e) {
_showSnack('GPS error: $e'); _showSnack('GPS Error: $e');
} }
} }
@@ -143,7 +162,7 @@ class _HomeScreenState extends State<HomeScreen> {
final lat = double.tryParse(_latController.text); final lat = double.tryParse(_latController.text);
final lng = double.tryParse(_lngController.text); final lng = double.tryParse(_lngController.text);
if (lat == null || lng == null) { if (lat == null || lng == null) {
_showSnack('Enter valid latitude and longitude first.'); _showSnack('Enter Valid Latitude & Longitude');
return; return;
} }
@@ -161,12 +180,12 @@ class _HomeScreenState extends State<HomeScreen> {
final timePart = sunsetStr.split('T')[1]; final timePart = sunsetStr.split('T')[1];
setState(() => _sunsetController.text = timePart); setState(() => _sunsetController.text = timePart);
await _savePrefs(); await _savePrefs();
_showSnack('Sunset: $timePart'); _showSnack('Sunset Time Retrieved: $timePart');
} else { } else {
_showSnack('API error: ${response.statusCode}'); _showSnack('API Error: ${response.statusCode}');
} }
} catch (e) { } catch (e) {
_showSnack('Network error: $e'); _showSnack('Network Error: $e');
} finally { } finally {
setState(() => _loading = false); setState(() => _loading = false);
} }
@@ -186,7 +205,7 @@ class _HomeScreenState extends State<HomeScreen> {
// Ensure that Android has the correct alarm permissions. // Ensure that Android has the correct alarm permissions.
final canSchedule = await android?.canScheduleExactNotifications(); final canSchedule = await android?.canScheduleExactNotifications();
if (canSchedule != true) { if (canSchedule != true) {
_showSnack('Permissions not yet granted, requesting...'); _showSnack('Permissions Need Granted, Requesting...');
await android?.requestExactAlarmsPermission(); await android?.requestExactAlarmsPermission();
} }
@@ -207,7 +226,7 @@ class _HomeScreenState extends State<HomeScreen> {
alarmTime = alarmTime.subtract(Duration(minutes: _minutesBefore)); alarmTime = alarmTime.subtract(Duration(minutes: _minutesBefore));
if (alarmTime.isBefore(now)) { if (alarmTime.isBefore(now)) {
_showSnack('Alarm time is in the past. Will fire tomorrow.'); _showSnack('Alarm Time In Past - Will Start Tomorrow');
alarmTime = alarmTime.add(const Duration(days: 1)); alarmTime = alarmTime.add(const Duration(days: 1));
} }
@@ -223,16 +242,16 @@ class _HomeScreenState extends State<HomeScreen> {
); );
_notifications.show( _notifications.show(
id: _alarmId, id: _alarmId,
title: 'Sunset in ${_minutesBefore} min', title: 'Sunset in ${_minutesBefore} min!',
body: 'The sun sets at $sunsetText. Time to get ready!', body: 'The sun sets at $sunsetText.',
notificationDetails: const NotificationDetails(linux: linuxDetails), notificationDetails: const NotificationDetails(linux: linuxDetails),
); );
}); });
_showSnack( _showSnack(
'Alarm set for ${alarmTime.hour.toString().padLeft(2, '0')}:${alarmTime.minute.toString().padLeft(2, '0')} (app must stay open)', 'Alarm Enabled @ ${alarmTime.hour.toString().padLeft(2, '0')}:${alarmTime.minute.toString().padLeft(2, '0')} (App Must Stay Open!)',
); );
} else if (Platform.isAndroid) { } else if (Platform.isAndroid) {
/*** Notifcation Method - Works ***/ /*** Notifcation Method - Works *** /
// Android: use zonedSchedule (works in background) // Android: use zonedSchedule (works in background)
final tzAlarm = tz.TZDateTime.from(alarmTime, tz.local); final tzAlarm = tz.TZDateTime.from(alarmTime, tz.local);
const androidDetails = AndroidNotificationDetails( const androidDetails = AndroidNotificationDetails(
@@ -258,29 +277,30 @@ class _HomeScreenState extends State<HomeScreen> {
debugPrint('DEBUG: ${_notifications.toString()}'); debugPrint('DEBUG: ${_notifications.toString()}');
// End Notification Method ***/ // End Notification Method ***/
/*** Alarm Method - WIP *** / /*** Alarm Method - WIP ***/
// Schedule: // Schedule:
final settings = AlarmSettings( final settings = AlarmSettings(
id: 42, id: _alarmId,
dateTime: alarmTime, // your computed sunset time dateTime: alarmTime,
assetAudioPath: 'assets/alarm.mp3', // put an alarm sound in assets/ //assetAudioPath: 'assets/alarm.mp3', // use default rather than custom
loopAudio: true, // keeps ringing until dismissed loopAudio: true, // keeps ringing until dismissed
vibrate: true, vibrate: true,
androidFullScreenIntent: true, // wakes screen like clock app androidFullScreenIntent: true, // wakes screen like clock app
volumeSettings: VolumeSettings.fade( volumeSettings: VolumeSettings.fade(
volume: 0.8, //volume: 0.8, // use default rather than custom
fadeDuration: Duration(seconds: 5), fadeDuration: Duration(seconds: 60),
), ),
notificationSettings: const NotificationSettings( notificationSettings: NotificationSettings(
title: 'Sunset Alarm', title: 'Sunset Alarm',
body: 'The sun sets now. Time to get ready!', body: 'Sun is setting in ${_minutesBefore} minute(s)!',
stopButton: 'Dismiss', stopButton: 'Dismiss',
), ),
); );
await Alarm.set(alarmSettings: settings); await Alarm.set(alarmSettings: settings);
// End Alarm Method ***/ // End Alarm Method ***/
_showSnack('Alarm Scheduled @ ${alarmTime}');
} else { } else {
_showSnack('Platform not detected, unsure how to show alarm.'); _showSnack('Platform Not Detected - Contact Developer');
} }
} }
@@ -292,9 +312,13 @@ class _HomeScreenState extends State<HomeScreen> {
} else { } else {
_alarmTimer?.cancel(); _alarmTimer?.cancel();
if (!Platform.isLinux) { if (!Platform.isLinux) {
await _notifications.cancel(id: _alarmId); // Notification Method
//await _notifications.cancel(id: _alarmId);
// Alarm Method
await Alarm.stop(_alarmId);
} }
_showSnack('Alarm cancelled.'); _showSnack('Alarm Cancelled');
} }
} }
@@ -311,6 +335,17 @@ class _HomeScreenState extends State<HomeScreen> {
super.dispose(); super.dispose();
} }
String? _validateTime(String? value) {
if (value == null || !RegExp(r'^\d{2}:\d{2}$').hasMatch(value)) {
return 'Use HH:MM Format';
}
final h = int.parse(value.substring(0, 2));
final m = int.parse(value.substring(3, 5));
if (h > 23) return 'Hour Must Be 0–23';
if (m > 59) return 'Minute Must Be 0–59';
return null; // valid
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -360,7 +395,7 @@ class _HomeScreenState extends State<HomeScreen> {
const SizedBox(width: 8), const SizedBox(width: 8),
IconButton.filledTonal( IconButton.filledTonal(
icon: const Icon(Icons.my_location), icon: const Icon(Icons.my_location),
tooltip: 'Get GPS location', tooltip: 'Get GPS Location',
onPressed: _getGpsLocation, onPressed: _getGpsLocation,
), ),
], ],
@@ -378,11 +413,43 @@ class _HomeScreenState extends State<HomeScreen> {
Expanded( Expanded(
child: TextField( child: TextField(
controller: _sunsetController, controller: _sunsetController,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Sunset time (HH:MM)', labelText: 'Sunset Time (HH:MM)',
border: OutlineInputBorder(), border: OutlineInputBorder(),
errorText: _sunsetErrorText,
suffixIcon: IconButton(
icon: Icon(Icons.access_time),
onPressed: () async {
final picked = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (picked != null) {
_sunsetController.text =
'${picked.hour.toString().padLeft(2, '0')}:'
'${picked.minute.toString().padLeft(2, '0')}';
setState(
() =>
_sunsetErrorText = _validateTime(
_sunsetController.text,
), ),
onChanged: (_) => _savePrefs(), );
}
},
),
),
onChanged: (v) {
final val = _validateTime(v);
if (val == null) {
_savePrefs();
if (_alarmEnabled) {
_scheduleAlarm();
}
}
setState(() {
_sunsetErrorText = val;
});
},
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@@ -395,55 +462,69 @@ class _HomeScreenState extends State<HomeScreen> {
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(strokeWidth: 2),
) )
: const Icon(Icons.cloud_done_outlined), : const Icon(Icons.cloud_done_outlined),
tooltip: 'Fetch sunset from Open-Meteo', tooltip: 'Fetch Open-Meteo Sunset Data',
onPressed: _loading ? null : _fetchSunset, onPressed: () {
if (!_loading) {
_fetchSunset();
if (_alarmEnabled) {
_scheduleAlarm();
}
}
},
), ),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// --- Minutes before ---
const Text( const Text(
'Alert Before Sunset', 'Alert',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
// --- Minutes before ---
Row( Row(
children: [ children: [
const Text('Minutes before: '), if (!_alarmEnabled)
SizedBox( Expanded(
width: 80,
child: TextField( child: TextField(
controller: _minutesController, controller: _minutesController,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
decoration: const InputDecoration( decoration: InputDecoration(
border: OutlineInputBorder(), border: OutlineInputBorder(),
labelText: 'Minutes Before Sunset',
errorText: _minuteErrorText,
), ),
onChanged: (v) { onChanged: (v) {
final val = int.tryParse(v); final val = int.tryParse(v);
if (val != null && val >= 0) { setState(() {
if (val != null) {
_minuteErrorText = null;
_minutesBefore = val; _minutesBefore = val;
} else { } else {
_showSnack('Invalid Minute Value'); _minuteErrorText = 'Invalid Minute Value';
} }
});
}, },
), ),
), ),
],
),
const SizedBox(height: 24),
// --- Toggle --- // --- Toggle ---
SwitchListTile( Expanded(
title: const Text('Enable Sunset Alarm'), child: SwitchListTile(
//title: const Text('Enable Sunset Alarm'),
title: Text(_alarmEnabled ? 'Sunset Alarm Enabled' : ''),
subtitle: Text( subtitle: Text(
_alarmEnabled _alarmEnabled
? 'Will alert ${_minutesBefore} min before sunset. ($_alarmDisplayTime)' ? 'Will alert ${_minutesBefore} min before sunset. ($_alarmDisplayTime)'
: 'Alarm is off', : '',
), ),
value: _alarmEnabled, value: _alarmEnabled,
onChanged: _toggleAlarm, onChanged: _toggleAlarm,
), ),
),
],
),
], ],
), ),
), ),
+1 -1
View File
@@ -1,7 +1,7 @@
name: sunset_alarm name: sunset_alarm
description: "Set an alarm to go off a specified time before sunset." description: "Set an alarm to go off a specified time before sunset."
publish_to: 'none' publish_to: 'none'
version: 0.1.2 version: 0.2.0
environment: environment:
sdk: ^3.7.2 sdk: ^3.7.2