7 Commits
6 changed files with 269 additions and 202 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.
+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
+195 -188
View File
@@ -13,9 +13,10 @@ 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
@@ -71,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;
@@ -202,21 +204,6 @@ class _HomeScreenState extends State<HomeScreen> {
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;
@@ -259,7 +246,22 @@ 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:
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();
}
/*** Notification Method - Works ***/
if (ANDROID_USE_NOTIF) { if (ANDROID_USE_NOTIF) {
// 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);
@@ -289,7 +291,7 @@ class _HomeScreenState extends State<HomeScreen> {
} }
// End Notification Method ***/ // End Notification Method ***/
/*** Alarm Method - WIP ***/ /*** Alarm Method - Works ***/
if (ANDROID_USE_ALARM) { if (ANDROID_USE_ALARM) {
// Schedule: // Schedule:
final settings = AlarmSettings( final settings = AlarmSettings(
@@ -305,7 +307,7 @@ class _HomeScreenState extends State<HomeScreen> {
), ),
notificationSettings: NotificationSettings( notificationSettings: NotificationSettings(
title: 'Sunset Alarm', title: 'Sunset Alarm',
body: 'Sun is setting in ${_minutesBefore} minute(s)!', body: 'Sun is setting in ${_minutesBefore} minute${_plural}!',
stopButton: 'Dismiss', stopButton: 'Dismiss',
icon: 'ic_notification', icon: 'ic_notification',
), ),
@@ -343,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();
@@ -365,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,
),
);
if (_alarmEnabled) {
_scheduleAlarm();
}
}
},
),
),
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();
}
},
),
],
),
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,
),
),
],
),
],
),
), ),
), ),
); );
+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