11 Commits
31 changed files with 327 additions and 245 deletions

No files matched your search

+23 -1
View File
@@ -15,6 +15,8 @@ android {
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion ndkVersion = flutter.ndkVersion
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true isCoreLibraryDesugaringEnabled = true
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
@@ -40,10 +42,30 @@ android {
} }
buildTypes { buildTypes {
release { getByName("release") {
// TODO: Add your own signing config for the release build. // TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works. // Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("debug")
/// 20260928 - Without minifying.
/// Built build/app/outputs/flutter-apk/app-release.apk (54.3MB)
/// Built build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk (17.2MB)
/// Built build/app/outputs/flutter-apk/app-arm64-v8a-release.apk (19.8MB)
/// Built build/app/outputs/flutter-apk/app-x86_64-release.apk (21.2MB)
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
/// 20260928 - With minifying, helped some, but also used
/// in combination with `--obfuscate --split-debug-info=./debug-info/`
/// Built build/app/outputs/flutter-apk/app-release.apk (54.3MB)
/// Built build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk (16.1MB)
/// Built build/app/outputs/flutter-apk/app-arm64-v8a-release.apk (18.8MB)
/// Built build/app/outputs/flutter-apk/app-x86_64-release.apk (20.3MB)
} }
} }
} }
View File
Whitespace-only changes.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 816 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 910 B

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 818 B

After

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 602 B

After

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1022 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 946 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

+1 -1
View File
@@ -1,5 +1,5 @@
AuthorName: 'Chad Michael "Hyperling" Greenwood' AuthorName: 'Chad Michael "Hyperling" Greenwood'
AuthorEmail: 'me@hyperling.com' AuthorEmail: 'me+sunset@hyperling.com'
AuthorWebSite: 'https://hyperling.com/about/' AuthorWebSite: 'https://hyperling.com/about/'
Donate: 'https://hyperling.com/donate/' Donate: 'https://hyperling.com/donate/'
Liberapay: 'HyperVegan' Liberapay: 'HyperVegan'
+49 -11
View File
@@ -15,6 +15,7 @@ function log {
function clean_flutter { function clean_flutter {
log "Cleaning..." log "Cleaning..."
flutter clean flutter clean
echo "Done!"
} }
## Variables ## ## Variables ##
@@ -41,12 +42,23 @@ fi
RELEASE="release/$name-$version" RELEASE="release/$name-$version"
echo "Running for '$RELEASE'." echo "Running for '$RELEASE'."
BUILD_OPTIONS=(
--release
--obfuscate
--split-debug-info=./debug-info/
--extra-gen-snapshot-options="--strip"
)
## Main ## ## Main ##
clean_flutter clean_flutter
# Linux
log "Building Linux Bundle..." log "Building Linux Bundle..."
flutter build linux set -x
flutter build linux ${BUILD_OPTIONS[@]}
set +x
log "Saving Linux Bundle..." log "Saving Linux Bundle..."
filename=$RELEASE.linux-bundle.zip filename=$RELEASE.linux-bundle.zip
@@ -56,15 +68,41 @@ fi
zip -r $filename build/linux/x64/release/bundle/* zip -r $filename build/linux/x64/release/bundle/*
ls -sh $filename ls -sh $filename
clean_flutter # Android
log "Building APK..." log "Building Universal APK..."
flutter build apk # Creates a generic (and large) app-release.apk
set -x
flutter build apk ${BUILD_OPTIONS[@]}
set +x
log "Saving APK..." #log "Saving Universal APK..."
filename=$RELEASE.apk #apk="build/app/outputs/flutter-apk/app-release.apk"
if [[ -e $filename ]]; then #filename=`basename $apk`
rm -v $filename #filename=${filename//app/${RELEASE}}
fi #if [[ -e $filename ]]; then
cp -v build/app/outputs/flutter-apk/app-release.apk $filename # rm -v $filename
ls -sh $filename #fi
#cp -v "$apk" "$filename"
#ls -sh "$filename"
log "Building Individual APKs..."
# Creates files such as build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
set -x
flutter build apk ${BUILD_OPTIONS[@]} --split-per-abi
set +x
log "Saving APKs..."
for apk in `ls build/app/outputs/flutter-apk/app*release.apk`; do
#if [[ "$apk" == *"x86"* ]]; then echo "Skipping $apk" continue; fi
#log "APK: $apk"
filename=`basename $apk`
filename=${filename//app/${RELEASE}}
cp -v "$apk" "$filename"
ls -sh "$filename"
echo " "
done
# Finish
exit 0
+253 -231
View File
@@ -6,15 +6,17 @@ 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: /// TBD:
/// v0.3.0 /// - Add other languages (Spanish, Croatian)
/// v1.1.0
/// - Change sunset grabber to checkbox and retrieve it each alarm or open. /// - Change sunset grabber to checkbox and retrieve it each alarm or open.
/// v1.0.0 /// v2.0.0
/// - Add sunrise section and change app name to Sunshine Alarm? /// - Add sunrise section and change app name to Sunshine Alarm?
/// :TBD /// :TBD
@@ -25,6 +27,9 @@ void main() async {
runApp(const SunsetAlarmApp()); runApp(const SunsetAlarmApp());
} }
const ANDROID_USE_NOTIF = false;
const ANDROID_USE_ALARM = !ANDROID_USE_NOTIF;
class SunsetAlarmApp extends StatelessWidget { class SunsetAlarmApp extends StatelessWidget {
const SunsetAlarmApp({super.key}); const SunsetAlarmApp({super.key});
@@ -67,12 +72,13 @@ class _HomeScreenState extends State<HomeScreen> {
final FlutterLocalNotificationsPlugin _notifications = final FlutterLocalNotificationsPlugin _notifications =
FlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlugin();
bool _loading = false;
bool _alarmEnabled = false; bool _alarmEnabled = false;
int _minutesBefore = 30; int _minutesBefore = 30;
bool _loading = false; String _plural = 's';
String? _sunsetErrorText = null; String? _sunsetErrorText;
String? _minuteErrorText = null; String? _minuteErrorText;
static const _alarmId = 1001; static const _alarmId = 1001;
@@ -189,26 +195,15 @@ class _HomeScreenState extends State<HomeScreen> {
} finally { } finally {
setState(() => _loading = false); setState(() => _loading = false);
} }
if (_alarmEnabled) {
_scheduleAlarm();
}
} }
Timer? _alarmTimer; Timer? _alarmTimer;
Future<void> _scheduleAlarm() async { Future<void> _scheduleAlarm() async {
// Get the Notifications permission:
final android =
_notifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
await android?.requestNotificationsPermission();
// Ensure that Android has the correct alarm permissions.
final canSchedule = await android?.canScheduleExactNotifications();
if (canSchedule != true) {
_showSnack('Permissions Need Granted, Requesting...');
await android?.requestExactAlarmsPermission();
}
final sunsetText = _sunsetController.text.trim(); final sunsetText = _sunsetController.text.trim();
if (sunsetText.isEmpty) return; if (sunsetText.isEmpty) return;
@@ -251,52 +246,74 @@ class _HomeScreenState extends State<HomeScreen> {
'Alarm Enabled @ ${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 *** / // Get the Notifications permission:
// Android: use zonedSchedule (works in background) final android =
final tzAlarm = tz.TZDateTime.from(alarmTime, tz.local); _notifications
const androidDetails = AndroidNotificationDetails( .resolvePlatformSpecificImplementation<
'sunset_alarm', AndroidFlutterLocalNotificationsPlugin
'Sunset Alarm', >();
channelDescription: 'Alarm X minutes before sunset', await android?.requestNotificationsPermission();
importance: Importance.max,
priority: Priority.high, // Ensure that Android has the correct alarm permissions.
fullScreenIntent: true, final canSchedule = await android?.canScheduleExactNotifications();
category: AndroidNotificationCategory.alarm, if (canSchedule != true) {
); _showSnack('Permissions Need Granted, Requesting...');
await _notifications.zonedSchedule( await android?.requestExactAlarmsPermission();
id: _alarmId, }
title: 'Sunset in ${_minutesBefore} min',
body: 'The sun sets at $sunsetText. Time to get ready!', /*** Notification Method - Works ***/
scheduledDate: tzAlarm, if (ANDROID_USE_NOTIF) {
notificationDetails: const NotificationDetails(android: androidDetails), // Android: use zonedSchedule (works in background)
androidScheduleMode: AndroidScheduleMode.alarmClock, final tzAlarm = tz.TZDateTime.from(alarmTime, tz.local);
); const androidDetails = AndroidNotificationDetails(
_showSnack('Alarm scheduled!'); 'sunset_alarm',
debugPrint('DEBUG: Alarm time: ${tzAlarm.toString()}'); 'Sunset Alarm',
debugPrint('DEBUG: Current time: ${DateTime.now().toUtc().toString()}'); channelDescription: 'Alarm X minutes before sunset',
debugPrint('DEBUG: ${_notifications.toString()}'); importance: Importance.max,
priority: Priority.high,
fullScreenIntent: true,
category: AndroidNotificationCategory.alarm,
);
await _notifications.zonedSchedule(
id: _alarmId,
title: 'Sunset in ${_minutesBefore} min',
body: 'The sun sets at $sunsetText. Time to get ready!',
scheduledDate: tzAlarm,
notificationDetails: const NotificationDetails(
android: androidDetails,
),
androidScheduleMode: AndroidScheduleMode.alarmClock,
);
_showSnack('Alarm scheduled!');
debugPrint('DEBUG: Alarm time: ${tzAlarm.toString()}');
debugPrint('DEBUG: Current time: ${DateTime.now().toUtc().toString()}');
debugPrint('DEBUG: ${_notifications.toString()}');
}
// End Notification Method ***/ // End Notification Method ***/
/*** Alarm Method - WIP ***/ /*** Alarm Method - Works ***/
// Schedule: if (ANDROID_USE_ALARM) {
final settings = AlarmSettings( // Schedule:
id: _alarmId, final settings = AlarmSettings(
dateTime: alarmTime, id: _alarmId,
//assetAudioPath: 'assets/alarm.mp3', // use default rather than custom dateTime: alarmTime,
loopAudio: true, // keeps ringing until dismissed //assetAudioPath: 'assets/alarm.mp3', // use default rather than custom
vibrate: true, loopAudio: true, // keeps ringing until dismissed
androidFullScreenIntent: true, // wakes screen like clock app vibrate: true,
volumeSettings: VolumeSettings.fade( androidFullScreenIntent: true, // wakes screen like clock app
//volume: 0.8, // use default rather than custom volumeSettings: VolumeSettings.fade(
fadeDuration: Duration(seconds: 60), //volume: 0.8, // use default rather than custom
), fadeDuration: Duration(seconds: 60),
notificationSettings: NotificationSettings( ),
title: 'Sunset Alarm', notificationSettings: NotificationSettings(
body: 'Sun is setting in ${_minutesBefore} minute(s)!', title: 'Sunset Alarm',
stopButton: 'Dismiss', body: 'Sun is setting in ${_minutesBefore} minute${_plural}!',
), stopButton: 'Dismiss',
); icon: 'ic_notification',
await Alarm.set(alarmSettings: settings); ),
);
await Alarm.set(alarmSettings: settings);
}
// End Alarm Method ***/ // End Alarm Method ***/
_showSnack('Alarm Scheduled @ ${alarmTime}'); _showSnack('Alarm Scheduled @ ${alarmTime}');
} else { } else {
@@ -313,10 +330,10 @@ class _HomeScreenState extends State<HomeScreen> {
_alarmTimer?.cancel(); _alarmTimer?.cancel();
if (!Platform.isLinux) { if (!Platform.isLinux) {
// Notification Method // Notification Method
//await _notifications.cancel(id: _alarmId); if (ANDROID_USE_NOTIF) await _notifications.cancel(id: _alarmId);
// Alarm Method // Alarm Method
await Alarm.stop(_alarmId); if (ANDROID_USE_ALARM) await Alarm.stop(_alarmId);
} }
_showSnack('Alarm Cancelled'); _showSnack('Alarm Cancelled');
} }
@@ -328,6 +345,7 @@ class _HomeScreenState extends State<HomeScreen> {
@override @override
void dispose() { void dispose() {
_savePrefs();
_latController.dispose(); _latController.dispose();
_lngController.dispose(); _lngController.dispose();
_sunsetController.dispose(); _sunsetController.dispose();
@@ -350,182 +368,186 @@ class _HomeScreenState extends State<HomeScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Sunset Alarm')), appBar: AppBar(title: const Text('Sunset Alarm')),
body: Padding( resizeToAvoidBottomInset: true, // default, but be explicit
padding: const EdgeInsets.all(16), body: SingleChildScrollView(
child: Column( child: Padding(
crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.all(16),
children: [ child: Column(
// --- Location section --- crossAxisAlignment: CrossAxisAlignment.start,
const Text( children: [
'Location', // --- Location section ---
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), const Text(
), 'Location',
const SizedBox(height: 8), style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
Row( ),
children: [ const SizedBox(height: 8),
Expanded( Row(
child: TextField( children: [
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: InputDecoration(
labelText: 'Sunset Time (HH:MM)',
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: (v) {
final val = _validateTime(v);
if (val == null) {
_savePrefs();
if (_alarmEnabled) {
_scheduleAlarm();
}
}
setState(() {
_sunsetErrorText = val;
});
},
),
),
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 Open-Meteo Sunset Data',
onPressed: () {
if (!_loading) {
_fetchSunset();
if (_alarmEnabled) {
_scheduleAlarm();
}
}
},
),
],
),
const SizedBox(height: 16),
const Text(
'Alert',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
// --- Minutes before ---
Row(
children: [
if (!_alarmEnabled)
Expanded( Expanded(
child: TextField( child: TextField(
controller: _minutesController, controller: _latController,
keyboardType: TextInputType.number, keyboardType: const TextInputType.numberWithOptions(
decoration: InputDecoration( signed: true,
decimal: true,
),
decoration: const InputDecoration(
labelText: 'Latitude',
border: OutlineInputBorder(), border: OutlineInputBorder(),
labelText: 'Minutes Before Sunset', ),
errorText: _minuteErrorText, 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: InputDecoration(
labelText: 'Sunset Time (HH:MM)',
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,
),
);
if (_alarmEnabled) {
_scheduleAlarm();
}
}
},
),
), ),
onChanged: (v) { onChanged: (v) {
final val = int.tryParse(v); final val = _validateTime(v);
setState(() { if (val == null) {
if (val != null) { _savePrefs();
_minuteErrorText = null; if (_alarmEnabled) {
_minutesBefore = val; _scheduleAlarm();
} else {
_minuteErrorText = 'Invalid Minute Value';
} }
}
setState(() {
_sunsetErrorText = val;
}); });
}, },
), ),
), ),
const SizedBox(width: 8),
// --- Toggle --- IconButton.filledTonal(
Expanded( icon:
child: SwitchListTile( _loading
//title: const Text('Enable Sunset Alarm'), ? const SizedBox(
title: Text(_alarmEnabled ? 'Sunset Alarm Enabled' : ''), width: 20,
subtitle: Text( height: 20,
_alarmEnabled child: CircularProgressIndicator(strokeWidth: 2),
? 'Will alert ${_minutesBefore} min before sunset. ($_alarmDisplayTime)' )
: '', : const Icon(Icons.cloud_done_outlined),
), tooltip: 'Fetch Open-Meteo Sunset Data',
value: _alarmEnabled, onPressed: () {
onChanged: _toggleAlarm, if (!_loading) {
_fetchSunset();
}
},
), ),
), ],
], ),
), const SizedBox(height: 16),
],
const Text(
'Alert',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
// --- Minutes before ---
Row(
children: [
if (!_alarmEnabled)
Expanded(
child: TextField(
controller: _minutesController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Minutes Before Sunset',
errorText: _minuteErrorText,
),
onChanged: (v) {
final val = int.tryParse(v);
setState(() {
if (val != null) {
_minuteErrorText = null;
_minutesBefore = val;
} else {
_minuteErrorText = 'Invalid Minute Value';
}
_plural = (_minutesBefore == 1) ? '' : 's';
});
},
),
),
// --- Toggle ---
Expanded(
child: SwitchListTile(
//title: const Text('Enable Sunset Alarm'),
title: Text(_alarmEnabled ? 'Sunset Alarm Enabled' : ''),
subtitle: Text(
_alarmEnabled
? '${_minutesBefore} Minute${_plural} Before @ $_alarmDisplayTime'
: '',
),
value: _alarmEnabled,
onChanged: _toggleAlarm,
),
),
],
),
],
),
), ),
), ),
); );
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.
+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.2.0 version: 1.0.0
environment: environment:
sdk: ^3.7.2 sdk: ^3.7.2