mirror of
https://github.com/darkmoonight/Rain.git
synced 2026-03-13 10:31:53 +08:00
Call updateWidget() after reading cached weather on Android so the home screen widget immediately shows the current temperature and icon instead of only the date. Wrap the “Add widget” setting in isRequestPinWidgetSupported() and show the addWidgetLauncher snackbar on launchers that don’t support pinning widgets (e.g. some GrapheneOS setups), avoiding silent failures. No changes to layouts or existing comments; only widget update logic and settings behavior were adjusted.
640 lines
19 KiB
Dart
Executable File
640 lines
19 KiB
Dart
Executable File
import 'dart:io';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_timezone/flutter_timezone.dart';
|
|
import 'package:geocoding/geocoding.dart';
|
|
import 'package:geolocator/geolocator.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:home_widget/home_widget.dart';
|
|
import 'package:isar_community/isar.dart';
|
|
import 'package:lat_lng_to_timezone/lat_lng_to_timezone.dart' as tzmap;
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:rain/app/api/api.dart';
|
|
import 'package:rain/app/data/db.dart';
|
|
import 'package:rain/app/utils/notification.dart';
|
|
import 'package:rain/app/utils/show_snack_bar.dart';
|
|
import 'package:rain/app/ui/widgets/weather/status/status_data.dart';
|
|
import 'package:rain/app/ui/widgets/weather/status/status_weather.dart';
|
|
import 'package:rain/main.dart';
|
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
|
import 'package:timezone/data/latest_all.dart' as tz;
|
|
import 'package:timezone/standalone.dart' as tz;
|
|
import 'package:timezone/timezone.dart' as tz;
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
import 'package:workmanager/workmanager.dart';
|
|
import 'package:intl/intl.dart';
|
|
|
|
class WeatherController extends GetxController {
|
|
final isLoading = true.obs;
|
|
final _district = ''.obs;
|
|
final _city = ''.obs;
|
|
final _latitude = 0.0.obs;
|
|
final _longitude = 0.0.obs;
|
|
|
|
String get district => _district.value;
|
|
String get city => _city.value;
|
|
double get latitude => _latitude.value;
|
|
double get longitude => _longitude.value;
|
|
|
|
final _mainWeather = MainWeatherCache().obs;
|
|
final _location = LocationCache().obs;
|
|
final _weatherCard = WeatherCard().obs;
|
|
|
|
final weatherCards = <WeatherCard>[].obs;
|
|
|
|
MainWeatherCache get mainWeather => _mainWeather.value;
|
|
LocationCache get location => _location.value;
|
|
WeatherCard get weatherCard => _weatherCard.value;
|
|
|
|
final hourOfDay = 0.obs;
|
|
final dayOfNow = 0.obs;
|
|
final itemScrollController = ItemScrollController();
|
|
final cacheExpiry = DateTime.now().subtract(const Duration(hours: 12));
|
|
|
|
@override
|
|
void onInit() {
|
|
weatherCards.assignAll(
|
|
isar.weatherCards.where().sortByIndex().findAllSync(),
|
|
);
|
|
super.onInit();
|
|
}
|
|
|
|
Future<Position> _determinePosition() async {
|
|
LocationPermission permission = await Geolocator.checkPermission();
|
|
if (permission == LocationPermission.denied) {
|
|
permission = await Geolocator.requestPermission();
|
|
if (permission == LocationPermission.denied) {
|
|
return Future.error('Location permissions are denied');
|
|
}
|
|
}
|
|
|
|
if (permission == LocationPermission.deniedForever) {
|
|
return Future.error(
|
|
'Location permissions are permanently denied, we cannot request permissions.',
|
|
);
|
|
}
|
|
return await Geolocator.getCurrentPosition();
|
|
}
|
|
|
|
Future<void> setLocation() async {
|
|
if (settings.location) {
|
|
await getCurrentLocation();
|
|
} else {
|
|
final locationCity = isar.locationCaches.where().findFirstSync();
|
|
if (locationCity != null) {
|
|
await getLocation(
|
|
locationCity.lat!,
|
|
locationCity.lon!,
|
|
locationCity.district!,
|
|
locationCity.city!,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> getCurrentLocation() async {
|
|
if (!(await isOnline.value)) {
|
|
showSnackBar(content: 'no_inter'.tr);
|
|
await readCache();
|
|
return;
|
|
}
|
|
|
|
if (!await Geolocator.isLocationServiceEnabled()) {
|
|
showSnackBar(
|
|
content: 'no_location'.tr,
|
|
onPressed: () => Geolocator.openLocationSettings(),
|
|
);
|
|
await readCache();
|
|
return;
|
|
}
|
|
|
|
if (isar.mainWeatherCaches.where().findAllSync().isNotEmpty) {
|
|
await readCache();
|
|
return;
|
|
}
|
|
|
|
final position = await _determinePosition();
|
|
final placemarks = await placemarkFromCoordinates(
|
|
position.latitude,
|
|
position.longitude,
|
|
);
|
|
final place = placemarks[0];
|
|
|
|
_latitude.value = position.latitude;
|
|
_longitude.value = position.longitude;
|
|
_district.value = place.administrativeArea ?? '';
|
|
_city.value = place.locality ?? '';
|
|
|
|
_mainWeather.value = await WeatherAPI().getWeatherData(
|
|
_latitude.value,
|
|
_longitude.value,
|
|
);
|
|
|
|
notificationCheck();
|
|
await writeCache();
|
|
await readCache();
|
|
}
|
|
|
|
Future<Map<String, dynamic>> getCurrentLocationSearch() async {
|
|
if (!(await isOnline.value)) {
|
|
showSnackBar(content: 'no_inter'.tr);
|
|
}
|
|
|
|
if (!await Geolocator.isLocationServiceEnabled()) {
|
|
showSnackBar(
|
|
content: 'no_location'.tr,
|
|
onPressed: () => Geolocator.openLocationSettings(),
|
|
);
|
|
}
|
|
|
|
final position = await _determinePosition();
|
|
final placemarks = await placemarkFromCoordinates(
|
|
position.latitude,
|
|
position.longitude,
|
|
);
|
|
final place = placemarks[0];
|
|
|
|
return {
|
|
'lat': position.latitude,
|
|
'lon': position.longitude,
|
|
'city': place.administrativeArea ?? '',
|
|
'district': place.locality ?? '',
|
|
};
|
|
}
|
|
|
|
Future<void> getLocation(
|
|
double latitude,
|
|
double longitude,
|
|
String district,
|
|
String locality,
|
|
) async {
|
|
if (!(await isOnline.value)) {
|
|
showSnackBar(content: 'no_inter'.tr);
|
|
await readCache();
|
|
return;
|
|
}
|
|
|
|
if (isar.mainWeatherCaches.where().findAllSync().isNotEmpty) {
|
|
await readCache();
|
|
return;
|
|
}
|
|
|
|
_latitude.value = latitude;
|
|
_longitude.value = longitude;
|
|
_district.value = district;
|
|
_city.value = locality;
|
|
|
|
_mainWeather.value = await WeatherAPI().getWeatherData(
|
|
_latitude.value,
|
|
_longitude.value,
|
|
);
|
|
|
|
notificationCheck();
|
|
await writeCache();
|
|
await readCache();
|
|
}
|
|
|
|
Future<void> readCache() async {
|
|
MainWeatherCache? mainWeatherCache =
|
|
isar.mainWeatherCaches.where().findFirstSync();
|
|
LocationCache? locationCache =
|
|
isar.locationCaches.where().findFirstSync();
|
|
|
|
if (mainWeatherCache == null || locationCache == null) {
|
|
isLoading.value = false;
|
|
return;
|
|
}
|
|
|
|
_mainWeather.value = mainWeatherCache;
|
|
_location.value = locationCache;
|
|
|
|
hourOfDay.value = getTime(
|
|
_mainWeather.value.time!,
|
|
_mainWeather.value.timezone!,
|
|
);
|
|
dayOfNow.value = getDay(
|
|
_mainWeather.value.timeDaily!,
|
|
_mainWeather.value.timezone!,
|
|
);
|
|
|
|
if (Platform.isAndroid) {
|
|
Workmanager().registerPeriodicTask(
|
|
'widgetUpdate',
|
|
'widgetBackgroundUpdate',
|
|
frequency: const Duration(minutes: 15),
|
|
existingWorkPolicy: ExistingPeriodicWorkPolicy.update,
|
|
);
|
|
await updateWidget();
|
|
}
|
|
|
|
isLoading.value = false;
|
|
|
|
Future.delayed(const Duration(milliseconds: 30), scrollToCurrentHour);
|
|
}
|
|
|
|
void scrollToCurrentHour() {
|
|
if (itemScrollController.isAttached) {
|
|
itemScrollController.scrollTo(
|
|
index: hourOfDay.value,
|
|
duration: const Duration(seconds: 2),
|
|
curve: Curves.easeInOutCubic,
|
|
);
|
|
} else {
|
|
Future.delayed(const Duration(milliseconds: 100), scrollToCurrentHour);
|
|
}
|
|
}
|
|
|
|
Future<void> writeCache() async {
|
|
final locationCaches = LocationCache(
|
|
lat: _latitude.value,
|
|
lon: _longitude.value,
|
|
city: _city.value,
|
|
district: _district.value,
|
|
);
|
|
|
|
isar.writeTxnSync(() {
|
|
if (isar.mainWeatherCaches.where().findAllSync().isEmpty) {
|
|
isar.mainWeatherCaches.putSync(_mainWeather.value);
|
|
}
|
|
if (isar.locationCaches.where().findAllSync().isEmpty) {
|
|
isar.locationCaches.putSync(locationCaches);
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> deleteCache() async {
|
|
if (!(await isOnline.value)) {
|
|
return;
|
|
}
|
|
|
|
isar.writeTxnSync(() {
|
|
isar.mainWeatherCaches
|
|
.filter()
|
|
.timestampLessThan(cacheExpiry)
|
|
.deleteAllSync();
|
|
});
|
|
if (isar.mainWeatherCaches.where().findAllSync().isEmpty) {
|
|
await flutterLocalNotificationsPlugin.cancelAll();
|
|
}
|
|
}
|
|
|
|
Future<void> deleteAll(bool changeCity) async {
|
|
if (!(await isOnline.value)) {
|
|
return;
|
|
}
|
|
|
|
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
|
await flutterLocalNotificationsPlugin.cancelAll();
|
|
|
|
isar.writeTxnSync(() {
|
|
if (!settings.location) {
|
|
isar.mainWeatherCaches.where().deleteAllSync();
|
|
}
|
|
if (settings.location && serviceEnabled || changeCity) {
|
|
isar.mainWeatherCaches.where().deleteAllSync();
|
|
isar.locationCaches.where().deleteAllSync();
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> addCardWeather(
|
|
double latitude,
|
|
double longitude,
|
|
String city,
|
|
String district,
|
|
) async {
|
|
if (!(await isOnline.value)) {
|
|
showSnackBar(content: 'no_inter'.tr);
|
|
return;
|
|
}
|
|
|
|
final tz = tzmap.latLngToTimezoneString(latitude, longitude);
|
|
_weatherCard.value = await WeatherAPI().getWeatherCard(
|
|
latitude,
|
|
longitude,
|
|
city,
|
|
district,
|
|
tz,
|
|
);
|
|
isar.writeTxnSync(() {
|
|
weatherCards.add(_weatherCard.value);
|
|
isar.weatherCards.putSync(_weatherCard.value);
|
|
});
|
|
}
|
|
|
|
Future<void> updateCardLocation(
|
|
WeatherCard card,
|
|
double latitude,
|
|
double longitude,
|
|
String city,
|
|
String district,
|
|
) async {
|
|
if (!(await isOnline.value)) {
|
|
showSnackBar(content: 'no_inter'.tr);
|
|
return;
|
|
}
|
|
|
|
final tz = tzmap.latLngToTimezoneString(latitude, longitude);
|
|
final updatedCard = await WeatherAPI().getWeatherCard(
|
|
latitude,
|
|
longitude,
|
|
city,
|
|
district,
|
|
tz,
|
|
);
|
|
|
|
isar.writeTxnSync(() {
|
|
card.lat = latitude;
|
|
card.lon = longitude;
|
|
card.city = city;
|
|
card.district = district;
|
|
card.timezone = tz;
|
|
|
|
_updateWeatherCard(card, updatedCard);
|
|
isar.weatherCards.putSync(card);
|
|
});
|
|
|
|
weatherCards.refresh();
|
|
}
|
|
|
|
Future<void> updateCacheCard(bool refresh) async {
|
|
final weatherCard = refresh
|
|
? isar.weatherCards.where().sortByIndex().findAllSync()
|
|
: isar.weatherCards
|
|
.filter()
|
|
.timestampLessThan(cacheExpiry)
|
|
.sortByIndex()
|
|
.findAllSync();
|
|
|
|
if (!(await isOnline.value) || weatherCard.isEmpty) {
|
|
return;
|
|
}
|
|
|
|
for (var oldCard in weatherCard) {
|
|
final updatedCard = await WeatherAPI().getWeatherCard(
|
|
oldCard.lat!,
|
|
oldCard.lon!,
|
|
oldCard.city!,
|
|
oldCard.district!,
|
|
oldCard.timezone!,
|
|
);
|
|
isar.writeTxnSync(() {
|
|
_updateWeatherCard(oldCard, updatedCard);
|
|
weatherCards.refresh();
|
|
});
|
|
}
|
|
}
|
|
|
|
void _updateWeatherCard(WeatherCard oldCard, WeatherCard updatedCard) {
|
|
oldCard
|
|
..time = updatedCard.time
|
|
..weathercode = updatedCard.weathercode
|
|
..temperature2M = updatedCard.temperature2M
|
|
..apparentTemperature = updatedCard.apparentTemperature
|
|
..relativehumidity2M = updatedCard.relativehumidity2M
|
|
..precipitation = updatedCard.precipitation
|
|
..rain = updatedCard.rain
|
|
..surfacePressure = updatedCard.surfacePressure
|
|
..visibility = updatedCard.visibility
|
|
..evapotranspiration = updatedCard.evapotranspiration
|
|
..windspeed10M = updatedCard.windspeed10M
|
|
..winddirection10M = updatedCard.winddirection10M
|
|
..windgusts10M = updatedCard.windgusts10M
|
|
..cloudcover = updatedCard.cloudcover
|
|
..uvIndex = updatedCard.uvIndex
|
|
..dewpoint2M = updatedCard.dewpoint2M
|
|
..precipitationProbability = updatedCard.precipitationProbability
|
|
..shortwaveRadiation = updatedCard.shortwaveRadiation
|
|
..timeDaily = updatedCard.timeDaily
|
|
..weathercodeDaily = updatedCard.weathercodeDaily
|
|
..temperature2MMax = updatedCard.temperature2MMax
|
|
..temperature2MMin = updatedCard.temperature2MMin
|
|
..apparentTemperatureMax = updatedCard.apparentTemperatureMax
|
|
..apparentTemperatureMin = updatedCard.apparentTemperatureMin
|
|
..sunrise = updatedCard.sunrise
|
|
..sunset = updatedCard.sunset
|
|
..precipitationSum = updatedCard.precipitationSum
|
|
..precipitationProbabilityMax = updatedCard.precipitationProbabilityMax
|
|
..windspeed10MMax = updatedCard.windspeed10MMax
|
|
..windgusts10MMax = updatedCard.windgusts10MMax
|
|
..uvIndexMax = updatedCard.uvIndexMax
|
|
..rainSum = updatedCard.rainSum
|
|
..winddirection10MDominant = updatedCard.winddirection10MDominant
|
|
..timestamp = DateTime.now();
|
|
|
|
isar.weatherCards.putSync(oldCard);
|
|
}
|
|
|
|
Future<void> updateCard(WeatherCard weatherCard) async {
|
|
if (!(await isOnline.value)) {
|
|
return;
|
|
}
|
|
|
|
final updatedCard = await WeatherAPI().getWeatherCard(
|
|
weatherCard.lat!,
|
|
weatherCard.lon!,
|
|
weatherCard.city!,
|
|
weatherCard.district!,
|
|
weatherCard.timezone!,
|
|
);
|
|
|
|
isar.writeTxnSync(() {
|
|
_updateWeatherCard(weatherCard, updatedCard);
|
|
});
|
|
}
|
|
|
|
Future<void> deleteCardWeather(WeatherCard weatherCard) async {
|
|
isar.writeTxnSync(() {
|
|
weatherCards.remove(weatherCard);
|
|
isar.weatherCards.deleteSync(weatherCard.id);
|
|
});
|
|
}
|
|
|
|
int getTime(List<String> time, String timezone) {
|
|
return time.indexWhere((t) {
|
|
final dateTime = DateTime.parse(t);
|
|
return tz.TZDateTime.now(tz.getLocation(timezone)).hour ==
|
|
dateTime.hour &&
|
|
tz.TZDateTime.now(tz.getLocation(timezone)).day == dateTime.day;
|
|
});
|
|
}
|
|
|
|
int getDay(List<DateTime> time, String timezone) {
|
|
return time.indexWhere(
|
|
(t) => tz.TZDateTime.now(tz.getLocation(timezone)).day == t.day,
|
|
);
|
|
}
|
|
|
|
TimeOfDay parseTime(String? timeStr) {
|
|
if (timeStr == null) {
|
|
return const TimeOfDay(hour: 0, minute: 0);
|
|
}
|
|
if (timeStr.contains(' ')) {
|
|
final isPM = timeStr.endsWith('PM');
|
|
final timeParts = timeStr.split(' ')[0].split(':');
|
|
int hour = int.parse(timeParts[0]);
|
|
if (isPM) hour += 12;
|
|
final minute = int.parse(timeParts[1]);
|
|
return TimeOfDay(hour: hour % 24, minute: minute);
|
|
} else {
|
|
final parts = timeStr.split(':');
|
|
return TimeOfDay(hour: int.parse(parts[0]), minute: int.parse(parts[1]));
|
|
}
|
|
}
|
|
|
|
String timeTo24h(TimeOfDay time) {
|
|
return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
|
|
}
|
|
|
|
String formatTime(String? timeStr) {
|
|
final time = parseTime(timeStr);
|
|
final dateTime = DateTime(0, 0, 0, time.hour, time.minute);
|
|
if (settings.timeformat == '12') {
|
|
return DateFormat.jm(locale.languageCode).format(dateTime);
|
|
} else {
|
|
return DateFormat.Hm(locale.languageCode).format(dateTime);
|
|
}
|
|
}
|
|
|
|
Future<String> getLocalImagePath(String icon) async {
|
|
final directory = await getTemporaryDirectory();
|
|
final imagePath = '${directory.path}/$icon';
|
|
|
|
final data = await rootBundle.load('assets/images/$icon');
|
|
final bytes = data.buffer.asUint8List();
|
|
|
|
await File(imagePath).writeAsBytes(bytes);
|
|
|
|
return imagePath;
|
|
}
|
|
|
|
void notification(MainWeatherCache mainWeatherCache) async {
|
|
final now = DateTime.now();
|
|
final startHour = parseTime(timeStart).hour;
|
|
final endHour = parseTime(timeEnd).hour;
|
|
|
|
for (var i = 0; i < mainWeatherCache.time!.length; i += timeRange) {
|
|
final notificationTime = DateTime.parse(mainWeatherCache.time![i]);
|
|
|
|
if (notificationTime.isAfter(now) &&
|
|
notificationTime.hour >= startHour &&
|
|
notificationTime.hour <= endHour) {
|
|
for (var j = 0; j < mainWeatherCache.timeDaily!.length; j++) {
|
|
if (mainWeatherCache.timeDaily![j].day == notificationTime.day) {
|
|
NotificationShow().showNotification(
|
|
UniqueKey().hashCode,
|
|
'$city: ${mainWeatherCache.temperature2M![i]}°',
|
|
'${StatusWeather().getText(mainWeatherCache.weathercode![i])} · ${StatusData().getTimeFormat(mainWeatherCache.time![i])}',
|
|
notificationTime,
|
|
StatusWeather().getImageNotification(
|
|
mainWeatherCache.weathercode![i],
|
|
mainWeatherCache.time![i],
|
|
mainWeatherCache.sunrise![j],
|
|
mainWeatherCache.sunset![j],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void notificationCheck() async {
|
|
if (settings.notifications) {
|
|
final pendingNotificationRequests = await flutterLocalNotificationsPlugin
|
|
.pendingNotificationRequests();
|
|
if (pendingNotificationRequests.isEmpty) {
|
|
notification(_mainWeather.value);
|
|
}
|
|
}
|
|
}
|
|
|
|
void reorder(int oldIndex, int newIndex) {
|
|
if (newIndex > oldIndex) {
|
|
newIndex -= 1;
|
|
}
|
|
final element = weatherCards.removeAt(oldIndex);
|
|
weatherCards.insert(newIndex, element);
|
|
|
|
for (int i = 0; i < weatherCards.length; i++) {
|
|
final item = weatherCards[i];
|
|
item.index = i;
|
|
isar.writeTxnSync(() => isar.weatherCards.putSync(item));
|
|
}
|
|
}
|
|
|
|
Future<bool> updateWidgetBackgroundColor(String color) async {
|
|
settings.widgetBackgroundColor = color;
|
|
isar.writeTxnSync(() {
|
|
isar.settings.putSync(settings);
|
|
});
|
|
|
|
final results = await Future.wait<bool?>([
|
|
HomeWidget.saveWidgetData('background_color', color),
|
|
HomeWidget.updateWidget(androidName: androidWidgetName),
|
|
]);
|
|
return !results.contains(false);
|
|
}
|
|
|
|
Future<bool> updateWidgetTextColor(String color) async {
|
|
settings.widgetTextColor = color;
|
|
isar.writeTxnSync(() {
|
|
isar.settings.putSync(settings);
|
|
});
|
|
|
|
final results = await Future.wait<bool?>([
|
|
HomeWidget.saveWidgetData('text_color', color),
|
|
HomeWidget.updateWidget(androidName: androidWidgetName),
|
|
]);
|
|
return !results.contains(false);
|
|
}
|
|
|
|
Future<bool> updateWidget() async {
|
|
final TimezoneInfo timeZoneName = await FlutterTimezone.getLocalTimezone();
|
|
tz.initializeTimeZones();
|
|
tz.setLocalLocation(tz.getLocation(timeZoneName.identifier));
|
|
|
|
final isarWidget = await Isar.open([
|
|
SettingsSchema,
|
|
MainWeatherCacheSchema,
|
|
LocationCacheSchema,
|
|
WeatherCardSchema,
|
|
], directory: (await getApplicationSupportDirectory()).path);
|
|
|
|
final mainWeatherCache = isarWidget.mainWeatherCaches
|
|
.where()
|
|
.findFirstSync();
|
|
if (mainWeatherCache == null) return false;
|
|
|
|
final hour = getTime(mainWeatherCache.time!, mainWeatherCache.timezone!);
|
|
final day = getDay(mainWeatherCache.timeDaily!, mainWeatherCache.timezone!);
|
|
|
|
final results = await Future.wait<bool?>([
|
|
HomeWidget.saveWidgetData(
|
|
'weather_icon',
|
|
await getLocalImagePath(
|
|
StatusWeather().getImageNotification(
|
|
mainWeatherCache.weathercode![hour],
|
|
mainWeatherCache.time![hour],
|
|
mainWeatherCache.sunrise![day],
|
|
mainWeatherCache.sunset![day],
|
|
),
|
|
),
|
|
),
|
|
HomeWidget.saveWidgetData(
|
|
'weather_degree',
|
|
'${mainWeatherCache.temperature2M?[hour].round()}°',
|
|
),
|
|
HomeWidget.updateWidget(androidName: androidWidgetName),
|
|
]);
|
|
return !results.contains(false);
|
|
}
|
|
|
|
void urlLauncher(String uri) async {
|
|
final url = Uri.parse(uri);
|
|
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
|
|
throw Exception('Could not launch $url');
|
|
}
|
|
}
|
|
}
|