Compare commits

...

18 commits
v1.3.5 ... main

Author SHA1 Message Date
Yoshi
9ba80c3609 Refactor code 2025-05-28 17:42:15 +03:00
Yoshi
33be8dcdc6 Update dependencies 2025-04-25 18:57:26 +03:00
Yoshi
089f3ae0b7 issue #201 2025-03-16 21:24:22 +03:00
Yoshi
fb150421a6 Fix bugs 2025-03-15 23:40:48 +03:00
Yoshi
d169f6237f Update README.md 2025-03-13 22:24:56 +03:00
Yoshi
e14dc03524 Update README.md 2025-03-12 22:21:22 +03:00
Yoshi
97aa5024b4 Update README.md 2025-03-11 21:01:16 +03:00
Yoshi
f880c5d274 Update dependencies and delete support 2025-03-02 15:58:04 +03:00
Yoshi
0973fb5230 Update dependencies 2025-02-08 13:09:25 +03:00
Yoshi
416f77765c Update dependencies 2025-01-08 14:26:35 +03:00
Yoshi
a59c6b7338 Android 15 2025-01-08 12:22:43 +03:00
Yoshi
0552a84e6f Fix dependencies 2024-12-15 13:41:23 +03:00
Yoshi
3a23dd6288 Update java 2024-11-10 19:55:18 +03:00
Yoshi
b7d0e8012a Update dependencies 2024-11-03 20:51:49 +03:00
Yoshi
fa0921d1b0 Fix settings gradle 2024-10-12 22:27:27 +03:00
Yoshi
7a00917ca2 Update build.gradle 2024-10-05 22:23:21 +03:00
Yoshi
ddf7115579 Update gradle 2024-10-05 17:49:08 +03:00
Yoshi
014a52c215 Rename folders 2024-09-06 22:07:50 +03:00
117 changed files with 10231 additions and 9577 deletions

1
.gitignore vendored
View file

@ -15,6 +15,7 @@ migrate_working_dir/
*.ipr
*.iws
.idea/
.cxx/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line

View file

@ -1,11 +1,11 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled.
# This file should be version controlled and should not be manually edited.
version:
revision: 135454af32477f815a7525073027a3ff9eff1bfd
channel: stable
revision: "2663184aa79047d0a33a14a3b607954f8fdd8730"
channel: "stable"
project_type: app
@ -13,26 +13,26 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: 135454af32477f815a7525073027a3ff9eff1bfd
base_revision: 135454af32477f815a7525073027a3ff9eff1bfd
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
- platform: android
create_revision: 135454af32477f815a7525073027a3ff9eff1bfd
base_revision: 135454af32477f815a7525073027a3ff9eff1bfd
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
- platform: ios
create_revision: 135454af32477f815a7525073027a3ff9eff1bfd
base_revision: 135454af32477f815a7525073027a3ff9eff1bfd
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
- platform: linux
create_revision: 135454af32477f815a7525073027a3ff9eff1bfd
base_revision: 135454af32477f815a7525073027a3ff9eff1bfd
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
- platform: macos
create_revision: 135454af32477f815a7525073027a3ff9eff1bfd
base_revision: 135454af32477f815a7525073027a3ff9eff1bfd
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
- platform: web
create_revision: 135454af32477f815a7525073027a3ff9eff1bfd
base_revision: 135454af32477f815a7525073027a3ff9eff1bfd
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
- platform: windows
create_revision: 135454af32477f815a7525073027a3ff9eff1bfd
base_revision: 135454af32477f815a7525073027a3ff9eff1bfd
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
# User provided section

View file

@ -1,6 +1,6 @@
<div align='center'>
<img src='/assets/icons/icon.png' width='150'/>
<h2>🌦️ Rain</h2>
<img src='/readme/icon.png' width='150'/>
<h2>🌦️ Rain</h2>
</div>
<p align='center'>
@ -40,7 +40,7 @@ We fetch weather data from [Open-Meteo](https://open-meteo.com/en/docs) and use
### 📸 Screenshots
<img src='/readme/1.png' width='200'/> <img src='/readme/2.png' width='200'/> <img src='/readme/3.png' width='200'/> <img src='/readme/4.png' width='200'/>
<img src='/readme/1.png' width='200'/> <img src='/readme/2.png' width='200'/> <img src='/readme/3.png' width='200'/> <img src='/readme/4.png' width='200'/> <img src='/readme/5.png' width='200'/> <img src='/readme/6.png' width='200'/> <img src='/readme/7.png' width='200'/> <img src='/readme/8.png' width='200'/>
### 💰 Support Us
@ -52,7 +52,6 @@ If you find Rain valuable and worthy for future innovation, consider supporting
### 📥 Get Rain Now
[![Play Store](https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white)](https://play.google.com/store/apps/details?id=com.yoshi.rain)
[![RuStore](https://img.shields.io/badge/RuStore-blue?style=for-the-badge&logo=vk&logoColor=white)](https://apps.rustore.ru/app/com.yoshi.rain)
Or get the latest APK from the [Releases Section](https://github.com/DarkMooNight/Rain/releases/latest). You can also find the app on IzzyOnDroid via a F-Droid client [here](https://apt.izzysoft.de/fdroid/index/apk/com.yoshi.rain).
@ -63,5 +62,5 @@ This project is licensed under the [MIT License](./LICENSE).
### 👨‍💻 Our Contributors
<a href='https://github.com/darkmoonight/Rain/graphs/contributors'>
<img src='https://contrib.rocks/image?repo=darkmoonight/Rain' />
<img src='https://contrib.rocks/image?repo=darkmoonight/Rain'/>
</a>

View file

@ -4,24 +4,6 @@ plugins {
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
@ -29,64 +11,73 @@ if (keystorePropertiesFile.exists()) {
}
android {
namespace 'com.yoshi.rain'
compileSdkVersion 34
ndkVersion flutter.ndkVersion
namespace = 'com.yoshi.rain'
compileSdk = 35
ndkVersion = '29.0.13113456'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
coreLibraryDesugaringEnabled = true
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = JavaVersion.VERSION_17
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
defaultConfig {
applicationId "com.yoshi.rain"
minSdkVersion 23
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
applicationId = 'com.yoshi.rain'
minSdk = 23
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
keyAlias = keystoreProperties['keyAlias']
keyPassword = keystoreProperties['keyPassword']
storeFile = keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword = keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
signingConfig = signingConfigs.release
}
debug {
signingConfig signingConfigs.debug
minifyEnabled true
signingConfig = signingConfigs.debug
minifyEnabled = true
}
}
buildFeatures {
viewBinding true
viewBinding = true
}
}
flutter {
source '../..'
source = "../.."
}
dependencies {
implementation("androidx.core:core-remoteviews:1.1.0")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.work:work-runtime-ktx:2.9.0")
implementation('androidx.work:work-runtime-ktx:2.10.0')
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
}
// Remove this for FLOSS version

View file

@ -18,6 +18,7 @@
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:enableOnBackInvokedCallback="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"

View file

@ -1,3 +1,4 @@
org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true
android.enableR8.fullMode = false

View file

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip

View file

@ -5,10 +5,9 @@ pluginManagement {
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}
settings.ext.flutterSdkPath = flutterSdkPath()
}()
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
@ -19,8 +18,8 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.4.2" apply false
id "org.jetbrains.kotlin.android" version "2.0.0" apply false
id "com.android.application" version "8.9.0" apply false
id "org.jetbrains.kotlin.android" version "2.1.10" apply false
}
include ":app"

View file

@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

33
lib/app/api/api.dart Normal file → Executable file
View file

@ -3,12 +3,12 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:rain/app/api/city_api.dart';
import 'package:rain/app/api/weather_api.dart';
import 'package:rain/app/data/weather.dart';
import 'package:rain/app/data/db.dart';
import 'package:rain/main.dart';
class WeatherAPI {
final Dio dio = Dio()
..options.baseUrl = 'https://api.open-meteo.com/v1/forecast?';
final Dio dio =
Dio()..options.baseUrl = 'https://api.open-meteo.com/v1/forecast?';
final Dio dioLocation = Dio();
static const String _weatherParams =
@ -41,14 +41,25 @@ class WeatherAPI {
}
}
Future<WeatherCard> getWeatherCard(double lat, double lon, String city,
String district, String timezone) async {
Future<WeatherCard> getWeatherCard(
double lat,
double lon,
String city,
String district,
String timezone,
) async {
final String urlWeather = _buildWeatherUrl(lat, lon);
try {
Response response = await dio.get(urlWeather);
WeatherDataApi weatherData = WeatherDataApi.fromJson(response.data);
return _mapWeatherDataToCard(
weatherData, lat, lon, city, district, timezone);
weatherData,
lat,
lon,
city,
district,
timezone,
);
} on DioException catch (e) {
if (kDebugMode) {
print(e);
@ -124,8 +135,14 @@ class WeatherAPI {
);
}
WeatherCard _mapWeatherDataToCard(WeatherDataApi weatherData, double lat,
double lon, String city, String district, String timezone) {
WeatherCard _mapWeatherDataToCard(
WeatherDataApi weatherData,
double lat,
double lon,
String city,
String district,
String timezone,
) {
return WeatherCard(
time: weatherData.hourly.time,
temperature2M: weatherData.hourly.temperature2M,

7
lib/app/api/city_api.dart Normal file → Executable file
View file

@ -1,12 +1,11 @@
class CityApi {
CityApi({
required this.results,
});
CityApi({required this.results});
List<Result> results;
factory CityApi.fromJson(Map<String, dynamic> json) => CityApi(
results: json['results'] == null
results:
json['results'] == null
? List<Result>.empty()
: List<Result>.from(json['results'].map((x) => Result.fromJson(x))),
);

0
lib/app/api/weather_api.dart Normal file → Executable file
View file

0
lib/app/api/weather_api.freezed.dart Normal file → Executable file
View file

0
lib/app/api/weather_api.g.dart Normal file → Executable file
View file

354
lib/app/controller/controller.dart Normal file → Executable file
View file

@ -1,7 +1,6 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:geocoding/geocoding.dart';
import 'package:geolocator/geolocator.dart';
@ -11,11 +10,11 @@ import 'package:isar/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/weather.dart';
import 'package:rain/app/services/notification.dart';
import 'package:rain/app/services/utils.dart';
import 'package:rain/app/widgets/status/status_data.dart';
import 'package:rain/app/widgets/status/status_weather.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;
@ -53,15 +52,14 @@ class WeatherController extends GetxController {
@override
void onInit() {
weatherCards
.assignAll(isar.weatherCards.where().sortByIndex().findAllSync());
weatherCards.assignAll(
isar.weatherCards.where().sortByIndex().findAllSync(),
);
super.onInit();
}
Future<Position> determinePosition() async {
LocationPermission permission;
permission = await Geolocator.checkPermission();
Future<Position> _determinePosition() async {
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
@ -71,7 +69,8 @@ class WeatherController extends GetxController {
if (permission == LocationPermission.deniedForever) {
return Future.error(
'Location permissions are permanently denied, we cannot request permissions.');
'Location permissions are permanently denied, we cannot request permissions.',
);
}
return await Geolocator.getCurrentPosition();
}
@ -80,25 +79,26 @@ class WeatherController extends GetxController {
if (settings.location) {
await getCurrentLocation();
} else {
if ((isar.locationCaches.where().findAllSync()).isNotEmpty) {
LocationCache locationCity =
(isar.locationCaches.where().findFirstSync())!;
await getLocation(locationCity.lat!, locationCity.lon!,
locationCity.district!, locationCity.city!);
final locationCity = isar.locationCaches.where().findFirstSync();
if (locationCity != null) {
await getLocation(
locationCity.lat!,
locationCity.lon!,
locationCity.district!,
locationCity.city!,
);
}
}
}
Future<void> getCurrentLocation() async {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!(await isOnline.value)) {
showSnackBar(content: 'no_inter'.tr);
await readCache();
return;
}
if (!serviceEnabled) {
if (!await Geolocator.isLocationServiceEnabled()) {
showSnackBar(
content: 'no_location'.tr,
onPressed: () => Geolocator.openLocationSettings(),
@ -107,70 +107,73 @@ class WeatherController extends GetxController {
return;
}
if ((isar.mainWeatherCaches.where().findAllSync()).isNotEmpty) {
if (isar.mainWeatherCaches.where().findAllSync().isNotEmpty) {
await readCache();
return;
}
Position position = await determinePosition();
List<Placemark> placemarks =
await placemarkFromCoordinates(position.latitude, position.longitude);
Placemark place = placemarks[0];
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}';
_district.value = place.administrativeArea ?? '';
_city.value = place.locality ?? '';
_mainWeather.value =
await WeatherAPI().getWeatherData(_latitude.value, _longitude.value);
_mainWeather.value = await WeatherAPI().getWeatherData(
_latitude.value,
_longitude.value,
);
notificationCheck();
await writeCache();
await readCache();
}
Future<Map> getCurrentLocationSearch() async {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
double lat, lon;
String city, district;
Future<Map<String, dynamic>> getCurrentLocationSearch() async {
if (!(await isOnline.value)) {
showSnackBar(content: 'no_inter'.tr);
}
if (!serviceEnabled) {
if (!await Geolocator.isLocationServiceEnabled()) {
showSnackBar(
content: 'no_location'.tr,
onPressed: () => Geolocator.openLocationSettings(),
);
}
Position position = await determinePosition();
List<Placemark> placemarks =
await placemarkFromCoordinates(position.latitude, position.longitude);
Placemark place = placemarks[0];
final position = await _determinePosition();
final placemarks = await placemarkFromCoordinates(
position.latitude,
position.longitude,
);
final place = placemarks[0];
lat = position.latitude;
lon = position.longitude;
city = '${place.administrativeArea}';
district = '${place.locality}';
Map location = {'lat': lat, 'lon': lon, 'city': city, 'district': district};
return location;
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 {
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) {
if (isar.mainWeatherCaches.where().findAllSync().isNotEmpty) {
await readCache();
return;
}
@ -180,11 +183,12 @@ class WeatherController extends GetxController {
_district.value = district;
_city.value = locality;
_mainWeather.value =
await WeatherAPI().getWeatherData(_latitude.value, _longitude.value);
_mainWeather.value = await WeatherAPI().getWeatherData(
_latitude.value,
_longitude.value,
);
notificationCheck();
await writeCache();
await readCache();
}
@ -201,10 +205,14 @@ class WeatherController extends GetxController {
_mainWeather.value = mainWeatherCache;
_location.value = locationCache;
hourOfDay.value =
getTime(_mainWeather.value.time!, _mainWeather.value.timezone!);
dayOfNow.value =
getDay(_mainWeather.value.timeDaily!, _mainWeather.value.timezone!);
hourOfDay.value = getTime(
_mainWeather.value.time!,
_mainWeather.value.timezone!,
);
dayOfNow.value = getDay(
_mainWeather.value.timeDaily!,
_mainWeather.value.timezone!,
);
if (Platform.isAndroid) {
Workmanager().registerPeriodicTask(
@ -235,16 +243,10 @@ class WeatherController extends GetxController {
);
isar.writeTxnSync(() {
final mainWeatherCachesIsEmpty =
(isar.mainWeatherCaches.where().findAllSync()).isEmpty;
final locationCachesIsEmpty =
(isar.locationCaches.where().findAllSync()).isEmpty;
if (mainWeatherCachesIsEmpty) {
if (isar.mainWeatherCaches.where().findAllSync().isEmpty) {
isar.mainWeatherCaches.putSync(_mainWeather.value);
}
if (locationCachesIsEmpty) {
if (isar.locationCaches.where().findAllSync().isEmpty) {
isar.locationCaches.putSync(locationCaches);
}
});
@ -261,7 +263,7 @@ class WeatherController extends GetxController {
.timestampLessThan(cacheExpiry)
.deleteAllSync();
});
if ((isar.mainWeatherCaches.where().findAllSync()).isEmpty) {
if (isar.mainWeatherCaches.where().findAllSync().isEmpty) {
await flutterLocalNotificationsPlugin.cancelAll();
}
}
@ -271,31 +273,39 @@ class WeatherController extends GetxController {
return;
}
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
await flutterLocalNotificationsPlugin.cancelAll();
isar.writeTxnSync(() {
if (!settings.location) {
isar.mainWeatherCaches.where().deleteAllSync();
}
if ((settings.location && serviceEnabled) || changeCity) {
if (settings.location && serviceEnabled || changeCity) {
isar.mainWeatherCaches.where().deleteAllSync();
isar.locationCaches.where().deleteAllSync();
}
});
}
// Card Weather
Future<void> addCardWeather(
double latitude, double longitude, String city, String district) async {
double latitude,
double longitude,
String city,
String district,
) async {
if (!(await isOnline.value)) {
showSnackBar(content: 'no_inter'.tr);
return;
}
String tz = tzmap.latLngToTimezoneString(latitude, longitude);
_weatherCard.value = await WeatherAPI()
.getWeatherCard(latitude, longitude, city, district, tz);
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);
@ -303,7 +313,7 @@ class WeatherController extends GetxController {
}
Future<void> updateCacheCard(bool refresh) async {
List<WeatherCard> weatherCard = refresh
final weatherCard = refresh
? isar.weatherCards.where().sortByIndex().findAllSync()
: isar.weatherCards
.filter()
@ -311,76 +321,27 @@ class WeatherController extends GetxController {
.sortByIndex()
.findAllSync();
if ((!(await isOnline.value)) || weatherCard.isEmpty) {
if (!(await isOnline.value) || weatherCard.isEmpty) {
return;
}
for (var oldCard in weatherCard) {
var updatedCard = await WeatherAPI().getWeatherCard(oldCard.lat!,
oldCard.lon!, oldCard.city!, oldCard.district!, oldCard.timezone!);
final updatedCard = await WeatherAPI().getWeatherCard(
oldCard.lat!,
oldCard.lon!,
oldCard.city!,
oldCard.district!,
oldCard.timezone!,
);
isar.writeTxnSync(() {
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);
var newCard = oldCard;
int oldIdx = weatherCard.indexOf(oldCard);
weatherCards[oldIdx] = newCard;
_updateWeatherCard(oldCard, updatedCard);
weatherCards.refresh();
});
}
}
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(() {
weatherCard
void _updateWeatherCard(WeatherCard oldCard, WeatherCard updatedCard) {
oldCard
..time = updatedCard.time
..weathercode = updatedCard.weathercode
..temperature2M = updatedCard.temperature2M
@ -416,7 +377,24 @@ class WeatherController extends GetxController {
..winddirection10MDominant = updatedCard.winddirection10MDominant
..timestamp = DateTime.now();
isar.weatherCards.putSync(weatherCard);
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);
});
}
@ -428,35 +406,26 @@ class WeatherController extends GetxController {
}
int getTime(List<String> time, String timezone) {
int getTime = 0;
for (var i = 0; i < time.length; i++) {
if (tz.TZDateTime.now(tz.getLocation(timezone)).hour ==
DateTime.parse(time[i]).hour &&
tz.TZDateTime.now(tz.getLocation(timezone)).day ==
DateTime.parse(time[i]).day) {
getTime = i;
}
}
return getTime;
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) {
int getDay = 0;
for (var i = 0; i < time.length; i++) {
if (tz.TZDateTime.now(tz.getLocation(timezone)).day == time[i].day) {
getDay = i;
}
}
return getDay;
return time.indexWhere(
(t) => tz.TZDateTime.now(tz.getLocation(timezone)).day == t.day,
);
}
TimeOfDay timeConvert(String normTime) {
int hh = 0;
if (normTime.endsWith('PM')) hh = 12;
normTime = normTime.split(' ')[0];
final hh = normTime.endsWith('PM') ? 12 : 0;
final timeParts = normTime.split(' ')[0].split(':');
return TimeOfDay(
hour: hh + int.parse(normTime.split(':')[0]) % 24,
minute: int.parse(normTime.split(':')[1]) % 60,
hour: hh + int.parse(timeParts[0]) % 24,
minute: int.parse(timeParts[1]) % 60,
);
}
@ -464,8 +433,8 @@ class WeatherController extends GetxController {
final directory = await getTemporaryDirectory();
final imagePath = '${directory.path}/$icon';
final ByteData data = await rootBundle.load('assets/images/$icon');
final List<int> bytes = data.buffer.asUint8List();
final data = await rootBundle.load('assets/images/$icon');
final bytes = data.buffer.asUint8List();
await File(imagePath).writeAsBytes(bytes);
@ -473,12 +442,12 @@ class WeatherController extends GetxController {
}
void notification(MainWeatherCache mainWeatherCache) async {
DateTime now = DateTime.now();
int startHour = timeConvert(timeStart).hour;
int endHour = timeConvert(timeEnd).hour;
final now = DateTime.now();
final startHour = timeConvert(timeStart).hour;
final endHour = timeConvert(timeEnd).hour;
for (var i = 0; i < mainWeatherCache.time!.length; i += timeRange) {
DateTime notificationTime = DateTime.parse(mainWeatherCache.time![i]);
final notificationTime = DateTime.parse(mainWeatherCache.time![i]);
if (notificationTime.isAfter(now) &&
notificationTime.hour >= startHour &&
@ -505,15 +474,15 @@ class WeatherController extends GetxController {
void notificationCheck() async {
if (settings.notifications) {
final List<PendingNotificationRequest> pendingNotificationRequests =
await flutterLocalNotificationsPlugin.pendingNotificationRequests();
final pendingNotificationRequests = await flutterLocalNotificationsPlugin
.pendingNotificationRequests();
if (pendingNotificationRequests.isEmpty) {
notification(_mainWeather.value);
}
}
}
void reorder(oldIndex, newIndex) {
void reorder(int oldIndex, int newIndex) {
if (newIndex > oldIndex) {
newIndex -= 1;
}
@ -533,15 +502,11 @@ class WeatherController extends GetxController {
isar.settings.putSync(settings);
});
return Future.wait<bool?>([
HomeWidget.saveWidgetData(
'background_color',
color,
),
final results = await Future.wait<bool?>([
HomeWidget.saveWidgetData('background_color', color),
HomeWidget.updateWidget(androidName: androidWidgetName),
]).then((value) {
return !value.contains(false);
});
]);
return !results.contains(false);
}
Future<bool> updateWidgetTextColor(String color) async {
@ -550,15 +515,11 @@ class WeatherController extends GetxController {
isar.settings.putSync(settings);
});
return Future.wait<bool?>([
HomeWidget.saveWidgetData(
'text_color',
color,
),
final results = await Future.wait<bool?>([
HomeWidget.saveWidgetData('text_color', color),
HomeWidget.updateWidget(androidName: androidWidgetName),
]).then((value) {
return !value.contains(false);
});
]);
return !results.contains(false);
}
Future<bool> updateWidget() async {
@ -573,34 +534,37 @@ class WeatherController extends GetxController {
WeatherCardSchema,
], directory: (await getApplicationSupportDirectory()).path);
MainWeatherCache? mainWeatherCache;
mainWeatherCache = isarWidget.mainWeatherCaches.where().findFirstSync();
final mainWeatherCache = isarWidget.mainWeatherCaches
.where()
.findFirstSync();
if (mainWeatherCache == null) return false;
int hour = getTime(mainWeatherCache.time!, mainWeatherCache.timezone!);
int day = getDay(mainWeatherCache.timeDaily!, mainWeatherCache.timezone!);
final hour = getTime(mainWeatherCache.time!, mainWeatherCache.timezone!);
final day = getDay(mainWeatherCache.timeDaily!, mainWeatherCache.timezone!);
return Future.wait<bool?>([
final results = await Future.wait<bool?>([
HomeWidget.saveWidgetData(
'weather_icon',
await getLocalImagePath(StatusWeather().getImageNotification(
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),
]).then((value) {
return !value.contains(false);
});
]);
return !results.contains(false);
}
void urlLauncher(String uri) async {
final Uri url = Uri.parse(uri);
final url = Uri.parse(uri);
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
throw Exception('Could not launch $url');
}

39
lib/app/data/weather.dart → lib/app/data/db.dart Normal file → Executable file
View file

@ -1,6 +1,6 @@
import 'package:isar/isar.dart';
part 'weather.g.dart';
part 'db.g.dart';
@collection
class Settings {
@ -152,12 +152,7 @@ class LocationCache {
String? city;
String? district;
LocationCache({
this.lat,
this.lon,
this.city,
this.district,
});
LocationCache({this.lat, this.lon, this.city, this.district});
Map<String, dynamic> toJson() => {
'id': id,
@ -304,8 +299,9 @@ class WeatherCard {
time: List<String>.from(json['time'] ?? []),
weathercode: List<int>.from(json['weathercode'] ?? []),
temperature2M: List<double>.from(json['temperature2M'] ?? []),
apparentTemperature:
List<double?>.from(json['apparentTemperature'] ?? []),
apparentTemperature: List<double?>.from(
json['apparentTemperature'] ?? [],
),
relativehumidity2M: List<int?>.from(json['relativehumidity2M'] ?? []),
precipitation: List<double>.from(json['precipitation'] ?? []),
rain: List<double?>.from(json['rain'] ?? []),
@ -318,26 +314,31 @@ class WeatherCard {
cloudcover: List<int?>.from(json['cloudcover'] ?? []),
uvIndex: List<double?>.from(json['uvIndex'] ?? []),
dewpoint2M: List<double?>.from(json['dewpoint2M'] ?? []),
precipitationProbability:
List<int?>.from(json['precipitationProbability'] ?? []),
precipitationProbability: List<int?>.from(
json['precipitationProbability'] ?? [],
),
shortwaveRadiation: List<double?>.from(json['shortwaveRadiation'] ?? []),
timeDaily: List<DateTime>.from(json['timeDaily'] ?? []),
weathercodeDaily: List<int?>.from(json['weathercodeDaily'] ?? []),
temperature2MMax: List<double?>.from(json['temperature2MMax'] ?? []),
temperature2MMin: List<double?>.from(json['temperature2MMin'] ?? []),
apparentTemperatureMax:
List<double?>.from(json['apparentTemperatureMax'] ?? []),
apparentTemperatureMin:
List<double?>.from(json['apparentTemperatureMin'] ?? []),
apparentTemperatureMax: List<double?>.from(
json['apparentTemperatureMax'] ?? [],
),
apparentTemperatureMin: List<double?>.from(
json['apparentTemperatureMin'] ?? [],
),
windspeed10MMax: List<double?>.from(json['windspeed10MMax'] ?? []),
windgusts10MMax: List<double?>.from(json['windgusts10MMax'] ?? []),
uvIndexMax: List<double?>.from(json['uvIndexMax'] ?? []),
rainSum: List<double?>.from(json['rainSum'] ?? []),
winddirection10MDominant:
List<int?>.from(json['winddirection10MDominant'] ?? []),
winddirection10MDominant: List<int?>.from(
json['winddirection10MDominant'] ?? [],
),
precipitationSum: List<double?>.from(json['precipitationSum'] ?? []),
precipitationProbabilityMax:
List<int?>.from(json['precipitationProbabilityMax'] ?? []),
precipitationProbabilityMax: List<int?>.from(
json['precipitationProbabilityMax'] ?? [],
),
sunrise: List<String>.from(json['sunrise'] ?? []),
sunset: List<String>.from(json['sunset'] ?? []),
lat: json['lat'],

2
lib/app/data/weather.g.dart → lib/app/data/db.g.dart Normal file → Executable file
View file

@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'weather.dart';
part of 'db.dart';
// **************************************************************************
// IsarCollectionGenerator

View file

@ -1,192 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:rain/app/controller/controller.dart';
import 'package:rain/app/data/weather.dart';
import 'package:rain/app/widgets/daily/weather_daily.dart';
import 'package:rain/app/widgets/daily/weather_more.dart';
import 'package:rain/app/widgets/desc/desc_container.dart';
import 'package:rain/app/widgets/hourly/weather_hourly.dart';
import 'package:rain/app/widgets/now/weather_now.dart';
import 'package:rain/app/widgets/sun_moon/sunset_sunrise.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
class InfoWeatherCard extends StatefulWidget {
const InfoWeatherCard({
super.key,
required this.weatherCard,
});
final WeatherCard weatherCard;
@override
State<InfoWeatherCard> createState() => _InfoWeatherCardState();
}
class _InfoWeatherCardState extends State<InfoWeatherCard> {
int timeNow = 0;
int dayNow = 0;
final weatherController = Get.put(WeatherController());
final itemScrollController = ItemScrollController();
@override
void initState() {
getTime();
super.initState();
}
void getTime() {
final weatherCard = widget.weatherCard;
timeNow =
weatherController.getTime(weatherCard.time!, weatherCard.timezone!);
dayNow =
weatherController.getDay(weatherCard.timeDaily!, weatherCard.timezone!);
Future.delayed(const Duration(milliseconds: 30), () {
itemScrollController.scrollTo(
index: timeNow,
duration: const Duration(seconds: 2),
curve: Curves.easeInOutCubic,
);
});
}
@override
Widget build(BuildContext context) {
final weatherCard = widget.weatherCard;
return RefreshIndicator(
onRefresh: () async {
await weatherController.updateCard(weatherCard);
getTime();
setState(() {});
},
child: Scaffold(
appBar: AppBar(
centerTitle: true,
automaticallyImplyLeading: false,
leading: IconButton(
onPressed: () => Get.back(),
icon: const Icon(
IconsaxPlusLinear.arrow_left_3,
size: 20,
),
),
title: Text(
weatherCard.district!.isNotEmpty
? '${weatherCard.city}'
', ${weatherCard.district}'
: '${weatherCard.city}',
style: context.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: ListView(
children: [
WeatherNow(
time: weatherCard.time![timeNow],
weather: weatherCard.weathercode![timeNow],
degree: weatherCard.temperature2M![timeNow],
feels: weatherCard.apparentTemperature![timeNow]!,
timeDay: weatherCard.sunrise![dayNow],
timeNight: weatherCard.sunset![dayNow],
tempMax: weatherCard.temperature2MMax![dayNow]!,
tempMin: weatherCard.temperature2MMin![dayNow]!,
),
Card(
margin: const EdgeInsets.only(bottom: 15),
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: SizedBox(
height: 135,
child: ScrollablePositionedList.separated(
key: const PageStorageKey(1),
separatorBuilder: (BuildContext context, int index) {
return const VerticalDivider(
width: 10,
indent: 40,
endIndent: 40,
);
},
scrollDirection: Axis.horizontal,
itemScrollController: itemScrollController,
itemCount: weatherCard.time!.length,
itemBuilder: (ctx, i) => GestureDetector(
onTap: () {
timeNow = i;
dayNow = (i / 24).floor();
setState(() {});
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 5),
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 5,
),
decoration: BoxDecoration(
color: i == timeNow
? context.theme.colorScheme.secondaryContainer
: Colors.transparent,
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
),
child: WeatherHourly(
time: weatherCard.time![i],
weather: weatherCard.weathercode![i],
degree: weatherCard.temperature2M![i],
timeDay: weatherCard.sunrise![(i / 24).floor()],
timeNight: weatherCard.sunset![(i / 24).floor()],
),
),
),
),
),
),
),
SunsetSunrise(
timeSunrise: weatherCard.sunrise![dayNow],
timeSunset: weatherCard.sunset![dayNow],
),
DescContainer(
humidity: weatherCard.relativehumidity2M?[timeNow],
wind: weatherCard.windspeed10M?[timeNow],
visibility: weatherCard.visibility?[timeNow],
feels: weatherCard.apparentTemperature?[timeNow],
evaporation: weatherCard.evapotranspiration?[timeNow],
precipitation: weatherCard.precipitation?[timeNow],
direction: weatherCard.winddirection10M?[timeNow],
pressure: weatherCard.surfacePressure?[timeNow],
rain: weatherCard.rain?[timeNow],
cloudcover: weatherCard.cloudcover?[timeNow],
windgusts: weatherCard.windgusts10M?[timeNow],
uvIndex: weatherCard.uvIndex?[timeNow],
dewpoint2M: weatherCard.dewpoint2M?[timeNow],
precipitationProbability:
weatherCard.precipitationProbability?[timeNow],
shortwaveRadiation: weatherCard.shortwaveRadiation?[timeNow],
initiallyExpanded: false,
title: 'hourlyVariables'.tr,
),
WeatherDaily(
weatherData: weatherCard,
onTap: () => Get.to(
() => WeatherMore(
weatherData: weatherCard,
),
transition: Transition.downToUp,
),
),
],
),
),
),
),
);
}
}

View file

@ -1,103 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:rain/app/controller/controller.dart';
import 'package:rain/app/modules/cards/widgets/weather_card_list.dart';
import 'package:rain/app/widgets/text_form.dart';
class ListWeatherCard extends StatefulWidget {
const ListWeatherCard({super.key});
@override
State<ListWeatherCard> createState() => _ListWeatherCardState();
}
class _ListWeatherCardState extends State<ListWeatherCard> {
final weatherController = Get.put(WeatherController());
TextEditingController searchTasks = TextEditingController();
String filter = '';
applyFilter(String value) async {
filter = value.toLowerCase();
setState(() {});
}
@override
void initState() {
super.initState();
applyFilter('');
}
@override
Widget build(BuildContext context) {
final textTheme = context.textTheme;
final titleMedium = textTheme.titleMedium;
return Obx(
() => weatherController.weatherCards.isEmpty
? Center(
child: SingleChildScrollView(
child: Column(
children: [
Image.asset(
'assets/icons/City.png',
scale: 6,
),
SizedBox(
width: Get.size.width * 0.8,
child: Text(
'noWeatherCard'.tr,
textAlign: TextAlign.center,
style: titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
),
],
),
),
)
: NestedScrollView(
physics: const NeverScrollableScrollPhysics(),
headerSliverBuilder: (context, innerBoxIsScrolled) {
return [
SliverToBoxAdapter(
child: MyTextForm(
labelText: 'search'.tr,
type: TextInputType.text,
icon: const Icon(
IconsaxPlusLinear.search_normal_1,
size: 20,
),
controller: searchTasks,
margin: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
onChanged: applyFilter,
iconButton: searchTasks.text.isNotEmpty
? IconButton(
onPressed: () {
searchTasks.clear();
applyFilter('');
},
icon: const Icon(
IconsaxPlusLinear.close_circle,
color: Colors.grey,
size: 20,
),
)
: null,
),
),
];
},
body: RefreshIndicator(
onRefresh: () async {
await weatherController.updateCacheCard(true);
setState(() {});
},
child: WeatherCardList(searchCity: filter),
),
),
);
}
}

View file

@ -1,304 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:rain/app/api/api.dart';
import 'package:rain/app/api/city_api.dart';
import 'package:rain/app/controller/controller.dart';
import 'package:rain/app/widgets/button.dart';
import 'package:rain/app/widgets/text_form.dart';
import 'package:rain/main.dart';
class CreateWeatherCard extends StatefulWidget {
const CreateWeatherCard({
super.key,
this.latitude,
this.longitude,
});
final String? latitude;
final String? longitude;
@override
State<CreateWeatherCard> createState() => _CreateWeatherCardState();
}
class _CreateWeatherCardState extends State<CreateWeatherCard>
with SingleTickerProviderStateMixin {
bool isLoading = false;
final formKey = GlobalKey<FormState>();
final _focusNode = FocusNode();
final weatherController = Get.put(WeatherController());
late final TextEditingController _controller = TextEditingController();
late TextEditingController _controllerLat = TextEditingController();
late TextEditingController _controllerLon = TextEditingController();
late final TextEditingController _controllerCity = TextEditingController();
late final TextEditingController _controllerDistrict =
TextEditingController();
late AnimationController _animationController;
late Animation<double> _animation;
@override
void initState() {
super.initState();
if (widget.latitude != null && widget.longitude != null) {
_controllerLat = TextEditingController(text: widget.latitude);
_controllerLon = TextEditingController(text: widget.longitude);
}
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_animation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
}
@override
void dispose() {
_animationController.dispose();
_controller.dispose();
_controllerLat.dispose();
_controllerLon.dispose();
_controllerCity.dispose();
_controllerDistrict.dispose();
super.dispose();
}
void textTrim(TextEditingController value) {
value.text = value.text.trim();
while (value.text.contains(' ')) {
value.text = value.text.replaceAll(' ', ' ');
}
}
void fillController(Result selection) {
_controllerLat.text = '${selection.latitude}';
_controllerLon.text = '${selection.longitude}';
_controllerCity.text = selection.name;
_controllerDistrict.text = selection.admin1;
_controller.clear();
_focusNode.unfocus();
setState(() {});
}
@override
Widget build(BuildContext context) {
const kTextFieldElevation = 4.0;
bool showButton = _controllerLon.text.isNotEmpty &&
_controllerLat.text.isNotEmpty &&
_controllerCity.text.isNotEmpty &&
_controllerDistrict.text.isNotEmpty;
if (showButton) {
_animationController.forward();
} else {
_animationController.reverse();
}
return Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Form(
key: formKey,
child: SingleChildScrollView(
child: Stack(
children: [
Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(top: 14, bottom: 7),
child: Text(
'create'.tr,
style: context.textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
),
RawAutocomplete<Result>(
focusNode: _focusNode,
textEditingController: _controller,
fieldViewBuilder: (BuildContext context,
TextEditingController fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted) {
return MyTextForm(
elevation: kTextFieldElevation,
labelText: 'search'.tr,
type: TextInputType.text,
icon: const Icon(IconsaxPlusLinear.global_search),
controller: _controller,
margin: const EdgeInsets.only(
left: 10, right: 10, top: 10),
focusNode: _focusNode,
);
},
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty) {
return const Iterable<Result>.empty();
}
return WeatherAPI()
.getCity(textEditingValue.text, locale);
},
onSelected: (Result selection) =>
fillController(selection),
displayStringForOption: (Result option) =>
'${option.name}, ${option.admin1}',
optionsViewBuilder: (BuildContext context,
AutocompleteOnSelected<Result> onSelected,
Iterable<Result> options) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Align(
alignment: Alignment.topCenter,
child: Material(
borderRadius: BorderRadius.circular(20),
elevation: 4.0,
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final Result option =
options.elementAt(index);
return InkWell(
onTap: () => onSelected(option),
child: ListTile(
title: Text(
'${option.name}, ${option.admin1}',
style: context.textTheme.labelLarge,
),
),
);
},
),
),
),
);
},
),
MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerLat,
labelText: 'lat'.tr,
type: TextInputType.number,
icon: const Icon(IconsaxPlusLinear.location),
onChanged: (value) => setState(() {}),
margin:
const EdgeInsets.only(left: 10, right: 10, top: 10),
validator: (value) {
if (value == null || value.isEmpty) {
return 'validateValue'.tr;
}
double? numericValue = double.tryParse(value);
if (numericValue == null) {
return 'validateNumber'.tr;
}
if (numericValue < -90 || numericValue > 90) {
return 'validate90'.tr;
}
return null;
},
),
MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerLon,
labelText: 'lon'.tr,
type: TextInputType.number,
icon: const Icon(IconsaxPlusLinear.location),
onChanged: (value) => setState(() {}),
margin:
const EdgeInsets.only(left: 10, right: 10, top: 10),
validator: (value) {
if (value == null || value.isEmpty) {
return 'validateValue'.tr;
}
double? numericValue = double.tryParse(value);
if (numericValue == null) {
return 'validateNumber'.tr;
}
if (numericValue < -180 || numericValue > 180) {
return 'validate180'.tr;
}
return null;
},
),
MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerCity,
labelText: 'city'.tr,
type: TextInputType.name,
icon: const Icon(IconsaxPlusLinear.building_3),
onChanged: (value) => setState(() {}),
margin:
const EdgeInsets.only(left: 10, right: 10, top: 10),
validator: (value) {
if (value == null || value.isEmpty) {
return 'validateName'.tr;
}
return null;
},
),
MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerDistrict,
labelText: 'district'.tr,
type: TextInputType.streetAddress,
icon: const Icon(IconsaxPlusLinear.global),
onChanged: (value) => setState(() {}),
margin:
const EdgeInsets.only(left: 10, right: 10, top: 10),
validator: (value) {
if (value == null || value.isEmpty) {
return 'validateName'.tr;
}
return null;
},
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 10),
child: SizeTransition(
sizeFactor: _animation,
axisAlignment: -1.0,
child: MyTextButton(
buttonName: 'done'.tr,
onPressed: () async {
if (formKey.currentState!.validate()) {
textTrim(_controllerLat);
textTrim(_controllerLon);
textTrim(_controllerCity);
textTrim(_controllerDistrict);
setState(() => isLoading = true);
await weatherController.addCardWeather(
double.parse(_controllerLat.text),
double.parse(_controllerLon.text),
_controllerCity.text,
_controllerDistrict.text,
);
setState(() => isLoading = false);
Get.back();
}
},
),
),
),
],
),
),
if (isLoading)
const Center(
child: CircularProgressIndicator(),
),
],
),
),
),
);
}
}

View file

@ -1,125 +0,0 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:rain/app/controller/controller.dart';
import 'package:rain/app/widgets/status/status_weather.dart';
import 'package:rain/app/widgets/status/status_data.dart';
import 'package:timezone/standalone.dart' as tz;
class WeatherCardContainer extends StatefulWidget {
const WeatherCardContainer({
super.key,
required this.time,
required this.weather,
required this.degree,
required this.district,
required this.city,
required this.timezone,
required this.timeDay,
required this.timeNight,
required this.timeDaily,
});
final List<String> time;
final List<String> timeDay;
final List<String> timeNight;
final List<DateTime> timeDaily;
final String district;
final String city;
final List<int> weather;
final List<double> degree;
final String timezone;
@override
State<WeatherCardContainer> createState() => _WeatherCardContainerState();
}
class _WeatherCardContainerState extends State<WeatherCardContainer> {
final statusWeather = StatusWeather();
final statusData = StatusData();
final weatherController = Get.put(WeatherController());
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
statusData.getDegree(widget.degree[weatherController
.getTime(widget.time, widget.timezone)]
.round()
.toInt()),
style: context.textTheme.titleLarge?.copyWith(
fontSize: 22,
fontWeight: FontWeight.w600,
),
),
const Gap(7),
Text(
statusWeather.getText(widget.weather[weatherController
.getTime(widget.time, widget.timezone)]),
style: context.textTheme.titleMedium?.copyWith(
color: Colors.grey,
fontWeight: FontWeight.w400,
),
),
],
),
const Gap(10),
Text(
widget.district.isEmpty
? widget.city
: widget.city.isEmpty
? widget.district
: widget.city == widget.district
? widget.city
: '${widget.city}'
', ${widget.district}',
style: context.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
const Gap(5),
StreamBuilder(
stream: Stream.periodic(const Duration(seconds: 1)),
builder: (context, snapshot) {
return Text(
'${'time'.tr}: ${statusData.getTimeFormatTz(tz.TZDateTime.now(tz.getLocation(widget.timezone)))}',
style: context.textTheme.titleMedium?.copyWith(
color: Colors.grey,
fontWeight: FontWeight.w400,
),
);
},
),
],
),
),
const Gap(5),
Image.asset(
statusWeather.getImageNow(
widget.weather[
weatherController.getTime(widget.time, widget.timezone)],
widget.time[
weatherController.getTime(widget.time, widget.timezone)],
widget.timeDay[weatherController.getDay(
widget.timeDaily, widget.timezone)],
widget.timeNight[weatherController.getDay(
widget.timeDaily, widget.timezone)]),
scale: 6.5,
),
],
),
),
);
}
}

View file

@ -1,115 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:rain/app/controller/controller.dart';
import 'package:rain/app/modules/cards/view/info_weather_card.dart';
import 'package:rain/app/modules/cards/widgets/weather_card_container.dart';
class WeatherCardList extends StatefulWidget {
const WeatherCardList({
super.key,
required this.searchCity,
});
final String searchCity;
@override
State<WeatherCardList> createState() => _WeatherCardListState();
}
class _WeatherCardListState extends State<WeatherCardList> {
final weatherController = Get.put(WeatherController());
@override
Widget build(BuildContext context) {
final textTheme = context.textTheme;
final titleMedium = textTheme.titleMedium;
var weatherCards = weatherController.weatherCards
.where((weatherCard) => (widget.searchCity.isEmpty ||
weatherCard.city!.toLowerCase().contains(widget.searchCity)))
.toList()
.obs;
return ReorderableListView(
onReorder: (oldIndex, newIndex) =>
weatherController.reorder(oldIndex, newIndex),
children: [
...weatherCards.map(
(weatherCardList) => Dismissible(
key: ValueKey(weatherCardList),
direction: DismissDirection.endToStart,
background: Container(
alignment: Alignment.centerRight,
child: const Padding(
padding: EdgeInsets.only(right: 15),
child: Icon(
IconsaxPlusLinear.trash_square,
color: Colors.red,
),
),
),
confirmDismiss: (DismissDirection direction) async {
return await showAdaptiveDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog.adaptive(
title: Text(
'deletedCardWeather'.tr,
style: textTheme.titleLarge,
),
content: Text(
'deletedCardWeatherQuery'.tr,
style: titleMedium,
),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: Text(
'cancel'.tr,
style: titleMedium?.copyWith(
color: Colors.blueAccent,
),
),
),
TextButton(
onPressed: () => Get.back(result: true),
child: Text(
'delete'.tr,
style: titleMedium?.copyWith(
color: Colors.red,
),
),
),
],
);
},
);
},
onDismissed: (DismissDirection direction) async {
await weatherController.deleteCardWeather(weatherCardList);
},
child: GestureDetector(
onTap: () => Get.to(
() => InfoWeatherCard(
weatherCard: weatherCardList,
),
transition: Transition.downToUp,
),
child: WeatherCardContainer(
time: weatherCardList.time!,
timeDaily: weatherCardList.timeDaily!,
timeDay: weatherCardList.sunrise!,
timeNight: weatherCardList.sunset!,
weather: weatherCardList.weathercode!,
degree: weatherCardList.temperature2M!,
district: weatherCardList.district!,
city: weatherCardList.city!,
timezone: weatherCardList.timezone!,
),
),
),
),
],
);
}
}

View file

@ -1,500 +0,0 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:gap/gap.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:latlong2/latlong.dart';
import 'package:rain/app/api/api.dart';
import 'package:rain/app/api/city_api.dart';
import 'package:rain/app/controller/controller.dart';
import 'package:rain/app/modules/home.dart';
import 'package:rain/app/widgets/button.dart';
import 'package:rain/app/widgets/text_form.dart';
import 'package:rain/main.dart';
class SelectGeolocation extends StatefulWidget {
const SelectGeolocation({
super.key,
required this.isStart,
});
final bool isStart;
@override
State<SelectGeolocation> createState() => _SelectGeolocationState();
}
class _SelectGeolocationState extends State<SelectGeolocation> {
bool isLoading = false;
final formKeySearch = GlobalKey<FormState>();
final _focusNode = FocusNode();
final weatherController = Get.put(WeatherController());
final _controller = TextEditingController();
final _controllerLat = TextEditingController();
final _controllerLon = TextEditingController();
final _controllerCity = TextEditingController();
final _controllerDistrict = TextEditingController();
static const colorFilter = ColorFilter.matrix(<double>[
-0.2, -0.7, -0.08, 0, 255, // Red channel
-0.2, -0.7, -0.08, 0, 255, // Green channel
-0.2, -0.7, -0.08, 0, 255, // Blue channel
0, 0, 0, 1, 0, // Alpha channel
]);
final bool _isDarkMode = Get.theme.brightness == Brightness.dark;
final mapController = MapController();
textTrim(value) {
value.text = value.text.trim();
while (value.text.contains(' ')) {
value.text = value.text.replaceAll(' ', ' ');
}
}
void fillController(selection) {
_controllerLat.text = '${selection.latitude}';
_controllerLon.text = '${selection.longitude}';
_controllerCity.text = selection.name;
_controllerDistrict.text = selection.admin1;
_controller.clear();
_focusNode.unfocus();
setState(() {});
}
void fillControllerGeo(location) {
_controllerLat.text = '${location['lat']}';
_controllerLon.text = '${location['lon']}';
_controllerCity.text = location['district'];
_controllerDistrict.text = location['city'];
setState(() {});
}
void fillMap(double latitude, double longitude) {
_controllerLat.text = '$latitude';
_controllerLon.text = '$longitude';
setState(() {});
}
Widget _buildMapTileLayer() {
return TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.darkmoonight.rain',
);
}
@override
Widget build(BuildContext context) {
const kTextFieldElevation = 4.0;
return Form(
key: formKeySearch,
child: Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(
centerTitle: true,
leading: widget.isStart
? null
: IconButton(
onPressed: () {
Get.back();
},
icon: const Icon(
IconsaxPlusLinear.arrow_left_3,
size: 20,
),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
automaticallyImplyLeading: false,
title: Text(
'searchCity'.tr,
style: context.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
),
body: SafeArea(
child: Stack(
children: [
Column(
children: [
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: SingleChildScrollView(
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10),
child: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
child: SizedBox(
height: Get.size.height * 0.3,
child: FlutterMap(
mapController: mapController,
options: MapOptions(
backgroundColor:
context.theme.colorScheme.surface,
initialCenter: const LatLng(
55.7522,
37.6156,
),
initialZoom: 3,
interactionOptions:
const InteractionOptions(
flags: InteractiveFlag.all &
~InteractiveFlag.rotate,
),
cameraConstraint:
CameraConstraint.contain(
bounds: LatLngBounds(
const LatLng(-90, -180),
const LatLng(90, 180),
),
),
onLongPress: (tapPosition, point) =>
fillMap(point.latitude,
point.longitude),
),
children: [
if (_isDarkMode)
ColorFiltered(
colorFilter: colorFilter,
child: _buildMapTileLayer(),
)
else
_buildMapTileLayer(),
RichAttributionWidget(
animationConfig: const ScaleRAWA(),
attributions: [
TextSourceAttribution(
'OpenStreetMap contributors',
onTap: () => weatherController
.urlLauncher(
'https://openstreetmap.org/copyright'),
),
],
),
],
),
),
),
),
Padding(
padding:
const EdgeInsets.fromLTRB(10, 15, 10, 5),
child: Text(
'searchMethod'.tr,
style: context.theme.textTheme.bodyLarge
?.copyWith(fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
),
Row(
children: [
Flexible(
child: RawAutocomplete<Result>(
focusNode: _focusNode,
textEditingController: _controller,
fieldViewBuilder: (BuildContext context,
TextEditingController
fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted) {
return MyTextForm(
elevation: kTextFieldElevation,
labelText: 'search'.tr,
type: TextInputType.text,
icon: const Icon(IconsaxPlusLinear
.global_search),
controller: _controller,
margin: const EdgeInsets.only(
left: 10, right: 10, top: 10),
focusNode: _focusNode,
);
},
optionsBuilder: (TextEditingValue
textEditingValue) {
if (textEditingValue.text.isEmpty) {
return const Iterable<
Result>.empty();
}
return WeatherAPI().getCity(
textEditingValue.text, locale);
},
onSelected: (Result selection) =>
fillController(selection),
displayStringForOption: (Result
option) =>
'${option.name}, ${option.admin1}',
optionsViewBuilder:
(BuildContext context,
AutocompleteOnSelected<Result>
onSelected,
Iterable<Result> options) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
child: Align(
alignment: Alignment.topCenter,
child: Material(
borderRadius:
BorderRadius.circular(20),
elevation: 4.0,
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder:
(BuildContext context,
int index) {
final Result option =
options
.elementAt(index);
return InkWell(
onTap: () =>
onSelected(option),
child: ListTile(
title: Text(
'${option.name}, ${option.admin1}',
style: context
.textTheme
.labelLarge,
),
),
);
},
),
),
),
);
},
),
),
Card(
elevation: kTextFieldElevation,
margin: const EdgeInsets.only(
top: 10,
right: 10,
),
child: Container(
margin: const EdgeInsets.all(2.5),
child: IconButton(
onPressed: () async {
bool serviceEnabled =
await Geolocator
.isLocationServiceEnabled();
if (!serviceEnabled) {
if (!context.mounted) return;
await showAdaptiveDialog(
context: context,
builder:
(BuildContext context) {
return AlertDialog.adaptive(
title: Text(
'location'.tr,
style: context
.textTheme.titleLarge,
),
content: Text(
'no_location'.tr,
style: context.textTheme
.titleMedium,
),
actions: [
TextButton(
onPressed: () =>
Get.back(
result: false),
child: Text(
'cancel'.tr,
style: context
.textTheme
.titleMedium
?.copyWith(
color: Colors
.blueAccent,
),
),
),
TextButton(
onPressed: () {
Geolocator
.openLocationSettings();
Get.back(
result: true);
},
child: Text(
'settings'.tr,
style: context
.textTheme
.titleMedium
?.copyWith(
color: Colors
.green),
),
),
],
);
},
);
return;
}
setState(() => isLoading = true);
final location =
await weatherController
.getCurrentLocationSearch();
fillControllerGeo(location);
setState(() => isLoading = false);
},
icon: const Icon(
IconsaxPlusLinear.location,
),
),
),
),
],
),
MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerLat,
labelText: 'lat'.tr,
type: TextInputType.number,
icon: const Icon(IconsaxPlusLinear.location),
margin: const EdgeInsets.only(
left: 10,
right: 10,
top: 10,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'validateValue'.tr;
}
double? numericValue =
double.tryParse(value);
if (numericValue == null) {
return 'validateNumber'.tr;
}
if (numericValue < -90 ||
numericValue > 90) {
return 'validate90'.tr;
}
return null;
},
),
MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerLon,
labelText: 'lon'.tr,
type: TextInputType.number,
icon: const Icon(IconsaxPlusLinear.location),
margin: const EdgeInsets.only(
left: 10,
right: 10,
top: 10,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'validateValue'.tr;
}
double? numericValue =
double.tryParse(value);
if (numericValue == null) {
return 'validateNumber'.tr;
}
if (numericValue < -180 ||
numericValue > 180) {
return 'validate180'.tr;
}
return null;
},
),
MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerCity,
labelText: 'city'.tr,
type: TextInputType.name,
icon:
const Icon(IconsaxPlusLinear.building_3),
margin: const EdgeInsets.only(
left: 10, right: 10, top: 10),
validator: (value) {
if (value == null || value.isEmpty) {
return 'validateName'.tr;
}
return null;
},
),
MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerDistrict,
labelText: 'district'.tr,
type: TextInputType.streetAddress,
icon: const Icon(IconsaxPlusLinear.global),
margin: const EdgeInsets.only(
left: 10, right: 10, top: 10),
),
const Gap(20),
],
),
),
),
],
),
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: MyTextButton(
buttonName: 'done'.tr,
onPressed: () async {
if (formKeySearch.currentState!.validate()) {
textTrim(_controllerLat);
textTrim(_controllerLon);
textTrim(_controllerCity);
textTrim(_controllerDistrict);
try {
await weatherController.deleteAll(true);
await weatherController.getLocation(
double.parse(_controllerLat.text),
double.parse(_controllerLon.text),
_controllerDistrict.text,
_controllerCity.text,
);
widget.isStart
? Get.off(() => const HomePage(),
transition: Transition.downToUp)
: Get.back();
} catch (error) {
Future.error(error);
}
}
},
),
),
],
),
if (isLoading)
BackdropFilter(
filter: ImageFilter.blur(sigmaY: 3, sigmaX: 3),
child: const Center(
child: CircularProgressIndicator(),
),
),
],
),
),
),
);
}
}

View file

@ -1,304 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:isar/isar.dart';
import 'package:rain/app/api/api.dart';
import 'package:rain/app/api/city_api.dart';
import 'package:rain/app/controller/controller.dart';
import 'package:rain/app/data/weather.dart';
import 'package:rain/app/modules/cards/view/list_weather_card.dart';
import 'package:rain/app/modules/cards/widgets/create_card_weather.dart';
import 'package:rain/app/modules/geolocation.dart';
import 'package:rain/app/modules/main/view/weather_main.dart';
import 'package:rain/app/modules/map/view/map.dart';
import 'package:rain/app/modules/settings/view/settings.dart';
import 'package:rain/app/services/utils.dart';
import 'package:rain/main.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
int tabIndex = 0;
bool visible = false;
final _focusNode = FocusNode();
late TabController tabController;
final weatherController = Get.put(WeatherController());
final _controller = TextEditingController();
final List<Widget> pages = [
const WeatherPage(),
const ListWeatherCard(),
if (!settings.hideMap) const MapWeather(),
const SettingsPage(),
];
@override
void initState() {
super.initState();
getData();
setupTabController();
}
@override
void dispose() {
tabController.dispose();
super.dispose();
}
void setupTabController() {
tabController = TabController(
initialIndex: tabIndex,
length: pages.length,
vsync: this,
);
tabController.animation?.addListener(() {
int value = (tabController.animation!.value).round();
if (value != tabIndex) setState(() => tabIndex = value);
});
tabController.addListener(() {
setState(() {
tabIndex = tabController.index;
});
});
}
void getData() async {
await weatherController.deleteCache();
await weatherController.updateCacheCard(false);
await weatherController.setLocation();
}
void changeTabIndex(int index) {
setState(() {
tabIndex = index;
});
tabController.animateTo(tabIndex);
}
@override
Widget build(BuildContext context) {
final textTheme = context.textTheme;
final labelLarge = textTheme.labelLarge;
final textStyle = textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
);
return DefaultTabController(
length: pages.length,
child: ScaffoldMessenger(
key: globalKey,
child: Scaffold(
appBar: AppBar(
centerTitle: true,
automaticallyImplyLeading: false,
leading: switch (tabIndex) {
0 => IconButton(
onPressed: () {
Get.to(() => const SelectGeolocation(isStart: false),
transition: Transition.downToUp);
},
icon: const Icon(
IconsaxPlusLinear.global_search,
size: 18,
),
),
int() => null,
},
title: switch (tabIndex) {
0 => visible
? RawAutocomplete<Result>(
focusNode: _focusNode,
textEditingController: _controller,
fieldViewBuilder: (_, __, ___, ____) {
return TextField(
controller: _controller,
focusNode: _focusNode,
style: labelLarge?.copyWith(fontSize: 16),
decoration: InputDecoration(
hintText: 'search'.tr,
),
);
},
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty) {
return const Iterable<Result>.empty();
}
return WeatherAPI()
.getCity(textEditingValue.text, locale);
},
onSelected: (Result selection) async {
await weatherController.deleteAll(true);
await weatherController.getLocation(
double.parse('${selection.latitude}'),
double.parse('${selection.longitude}'),
selection.admin1,
selection.name,
);
visible = false;
_controller.clear();
_focusNode.unfocus();
setState(() {});
},
displayStringForOption: (Result option) =>
'${option.name}, ${option.admin1}',
optionsViewBuilder: (BuildContext context,
AutocompleteOnSelected<Result> onSelected,
Iterable<Result> options) {
return Align(
alignment: Alignment.topLeft,
child: Material(
borderRadius: BorderRadius.circular(20),
elevation: 4.0,
child: SizedBox(
width: 250,
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final Result option =
options.elementAt(index);
return InkWell(
onTap: () => onSelected(option),
child: ListTile(
title: Text(
'${option.name}, ${option.admin1}',
style: labelLarge,
),
),
);
},
),
),
),
);
},
)
: Obx(
() {
final location = weatherController.location;
final city = location.city;
final district = location.district;
return Text(
weatherController.isLoading.isFalse
? district!.isEmpty
? '$city'
: city!.isEmpty
? district
: city == district
? city
: '$city' ', $district'
: settings.location
? 'search'.tr
: (isar.locationCaches.where().findAllSync())
.isNotEmpty
? 'loading'.tr
: 'searchCity'.tr,
style: textStyle,
);
},
),
1 => Text(
'cities'.tr,
style: textStyle,
),
2 => settings.hideMap
? Text(
'settings_full'.tr,
style: textStyle,
)
: Text(
'map'.tr,
style: textStyle,
),
3 => Text(
'settings_full'.tr,
style: textStyle,
),
int() => null,
},
actions: switch (tabIndex) {
0 => [
IconButton(
onPressed: () {
if (visible) {
_controller.clear();
_focusNode.unfocus();
visible = false;
} else {
visible = true;
}
setState(() {});
},
icon: Icon(
visible
? IconsaxPlusLinear.close_circle
: IconsaxPlusLinear.search_normal_1,
size: 18,
),
)
],
int() => null,
},
),
body: SafeArea(
child: TabBarView(
controller: tabController,
children: pages,
),
),
bottomNavigationBar: NavigationBar(
onDestinationSelected: (int index) => changeTabIndex(index),
selectedIndex: tabIndex,
destinations: [
NavigationDestination(
icon: const Icon(IconsaxPlusLinear.cloud_sunny),
selectedIcon: const Icon(IconsaxPlusBold.cloud_sunny),
label: 'name'.tr,
),
NavigationDestination(
icon: const Icon(IconsaxPlusLinear.buildings),
selectedIcon: const Icon(IconsaxPlusBold.buildings),
label: 'cities'.tr,
),
if (!settings.hideMap)
NavigationDestination(
icon: const Icon(IconsaxPlusLinear.map),
selectedIcon: const Icon(IconsaxPlusBold.map),
label: 'map'.tr,
),
NavigationDestination(
icon: const Icon(IconsaxPlusLinear.category),
selectedIcon: const Icon(IconsaxPlusBold.category),
label: 'settings_full'.tr,
),
],
),
floatingActionButton: tabIndex == 1
? FloatingActionButton(
onPressed: () => showModalBottomSheet(
context: context,
isScrollControlled: true,
enableDrag: false,
builder: (BuildContext context) =>
const CreateWeatherCard(),
),
child: const Icon(
IconsaxPlusLinear.add,
),
)
: null,
),
),
);
}
}

View file

@ -1,178 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:rain/app/controller/controller.dart';
import 'package:rain/app/data/weather.dart';
import 'package:rain/app/widgets/daily/weather_daily.dart';
import 'package:rain/app/widgets/daily/weather_more.dart';
import 'package:rain/app/widgets/desc/desc_container.dart';
import 'package:rain/app/widgets/hourly/weather_hourly.dart';
import 'package:rain/app/widgets/now/weather_now.dart';
import 'package:rain/app/widgets/shimmer.dart';
import 'package:rain/app/widgets/sun_moon/sunset_sunrise.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
class WeatherPage extends StatefulWidget {
const WeatherPage({super.key});
@override
State<WeatherPage> createState() => _WeatherPageState();
}
class _WeatherPageState extends State<WeatherPage> {
final weatherController = Get.put(WeatherController());
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: () async {
await weatherController.deleteAll(false);
await weatherController.setLocation();
setState(() {});
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Obx(() {
if (weatherController.isLoading.isTrue) {
return ListView(
children: const [
MyShimmer(
hight: 200,
),
MyShimmer(
hight: 130,
edgeInsetsMargin: EdgeInsets.symmetric(vertical: 15),
),
MyShimmer(
hight: 90,
edgeInsetsMargin: EdgeInsets.only(bottom: 15),
),
MyShimmer(
hight: 400,
edgeInsetsMargin: EdgeInsets.only(bottom: 15),
),
MyShimmer(
hight: 450,
edgeInsetsMargin: EdgeInsets.only(bottom: 15),
)
],
);
}
final mainWeather = weatherController.mainWeather;
final weatherCard = WeatherCard.fromJson(mainWeather.toJson());
final hourOfDay = weatherController.hourOfDay.value;
final dayOfNow = weatherController.dayOfNow.value;
final sunrise = mainWeather.sunrise![dayOfNow];
final sunset = mainWeather.sunset![dayOfNow];
final tempMax = mainWeather.temperature2MMax![dayOfNow];
final tempMin = mainWeather.temperature2MMin![dayOfNow];
return ListView(
children: [
WeatherNow(
time: mainWeather.time![hourOfDay],
weather: mainWeather.weathercode![hourOfDay],
degree: mainWeather.temperature2M![hourOfDay],
feels: mainWeather.apparentTemperature![hourOfDay]!,
timeDay: sunrise,
timeNight: sunset,
tempMax: tempMax!,
tempMin: tempMin!,
),
Card(
margin: const EdgeInsets.only(bottom: 15),
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: SizedBox(
height: 135,
child: ScrollablePositionedList.separated(
key: const PageStorageKey(0),
separatorBuilder: (BuildContext context, int index) {
return const VerticalDivider(
width: 10,
indent: 40,
endIndent: 40,
);
},
scrollDirection: Axis.horizontal,
itemScrollController:
weatherController.itemScrollController,
itemCount: mainWeather.time!.length,
itemBuilder: (ctx, i) {
final i24 = (i / 24).floor();
return GestureDetector(
onTap: () {
weatherController.hourOfDay.value = i;
weatherController.dayOfNow.value = i24;
setState(() {});
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 5),
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 5,
),
decoration: BoxDecoration(
color: i == hourOfDay
? context.theme.colorScheme.secondaryContainer
: Colors.transparent,
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
),
child: WeatherHourly(
time: mainWeather.time![i],
weather: mainWeather.weathercode![i],
degree: mainWeather.temperature2M![i],
timeDay: mainWeather.sunrise![i24],
timeNight: mainWeather.sunset![i24],
),
),
);
},
),
),
),
),
SunsetSunrise(
timeSunrise: sunrise,
timeSunset: sunset,
),
DescContainer(
humidity: mainWeather.relativehumidity2M?[hourOfDay],
wind: mainWeather.windspeed10M?[hourOfDay],
visibility: mainWeather.visibility?[hourOfDay],
feels: mainWeather.apparentTemperature?[hourOfDay],
evaporation: mainWeather.evapotranspiration?[hourOfDay],
precipitation: mainWeather.precipitation?[hourOfDay],
direction: mainWeather.winddirection10M?[hourOfDay],
pressure: mainWeather.surfacePressure?[hourOfDay],
rain: mainWeather.rain?[hourOfDay],
cloudcover: mainWeather.cloudcover?[hourOfDay],
windgusts: mainWeather.windgusts10M?[hourOfDay],
uvIndex: mainWeather.uvIndex?[hourOfDay],
dewpoint2M: mainWeather.dewpoint2M?[hourOfDay],
precipitationProbability:
mainWeather.precipitationProbability?[hourOfDay],
shortwaveRadiation: mainWeather.shortwaveRadiation?[hourOfDay],
initiallyExpanded: false,
title: 'hourlyVariables'.tr,
),
WeatherDaily(
weatherData: weatherCard,
onTap: () => Get.to(
() => WeatherMore(
weatherData: weatherCard,
),
transition: Transition.downToUp,
),
)
],
);
}),
),
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,101 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
class SettingCard extends StatelessWidget {
const SettingCard({
super.key,
required this.icon,
required this.text,
this.switcher = false,
this.dropdown = false,
this.info = false,
this.infoSettings = false,
this.elevation,
this.dropdownName,
this.dropdownList,
this.dropdownCange,
this.value,
this.onPressed,
this.onChange,
this.infoWidget,
});
final Widget icon;
final String text;
final bool switcher;
final bool dropdown;
final bool info;
final bool infoSettings;
final Widget? infoWidget;
final String? dropdownName;
final List<String>? dropdownList;
final Function(String?)? dropdownCange;
final bool? value;
final Function()? onPressed;
final Function(bool)? onChange;
final double? elevation;
@override
Widget build(BuildContext context) {
return Card(
elevation: elevation ?? 1,
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: ListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
onTap: onPressed,
leading: icon,
title: Text(
text,
style: context.textTheme.titleMedium,
overflow: TextOverflow.visible,
),
trailing: switcher
? Transform.scale(
scale: 0.8,
child: Switch(
value: value!,
onChanged: onChange,
),
)
: dropdown
? DropdownButton<String>(
icon: const Padding(
padding: EdgeInsets.only(left: 7),
child: Icon(IconsaxPlusLinear.arrow_down),
),
iconSize: 15,
alignment: AlignmentDirectional.centerEnd,
borderRadius: const BorderRadius.all(Radius.circular(15)),
underline: Container(),
value: dropdownName,
items: dropdownList!
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: dropdownCange,
)
: info
? infoSettings
? Wrap(
children: [
infoWidget!,
const Icon(
IconsaxPlusLinear.arrow_right_3,
size: 18,
),
],
)
: infoWidget!
: const Icon(
IconsaxPlusLinear.arrow_right_3,
size: 18,
),
),
);
}
}

View file

@ -1,42 +0,0 @@
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:rain/app/controller/controller.dart';
import 'package:rain/main.dart';
import 'package:timezone/timezone.dart' as tz;
class NotificationShow {
Future showNotification(
int id,
String title,
String body,
DateTime date,
String icon,
) async {
final imagePath = await WeatherController().getLocalImagePath(icon);
AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails(
'Rain',
'DARK NIGHT',
priority: Priority.high,
importance: Importance.max,
playSound: false,
enableVibration: false,
largeIcon: FilePathAndroidBitmap(imagePath),
);
NotificationDetails notificationDetails =
NotificationDetails(android: androidNotificationDetails);
var scheduledTime = tz.TZDateTime.from(date, tz.local);
flutterLocalNotificationsPlugin.zonedSchedule(
id,
title,
body,
scheduledTime,
notificationDetails,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
payload: imagePath,
);
}
}

View file

@ -1,20 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
final globalKey = GlobalKey<ScaffoldMessengerState>();
void showSnackBar({required String content, Function? onPressed}) {
globalKey.currentState?.showSnackBar(
SnackBar(
content: Text(content),
action: onPressed != null
? SnackBarAction(
label: 'settings'.tr,
onPressed: () {
onPressed();
},
)
: null,
),
);
}

479
lib/app/ui/geolocation.dart Executable file
View file

@ -0,0 +1,479 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:gap/gap.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:latlong2/latlong.dart';
import 'package:rain/app/api/api.dart';
import 'package:rain/app/api/city_api.dart';
import 'package:rain/app/controller/controller.dart';
import 'package:rain/app/ui/home.dart';
import 'package:rain/app/ui/widgets/button.dart';
import 'package:rain/app/ui/widgets/text_form.dart';
import 'package:rain/main.dart';
class SelectGeolocation extends StatefulWidget {
const SelectGeolocation({super.key, required this.isStart});
final bool isStart;
@override
State<SelectGeolocation> createState() => _SelectGeolocationState();
}
class _SelectGeolocationState extends State<SelectGeolocation> {
bool isLoading = false;
final formKeySearch = GlobalKey<FormState>();
final _focusNode = FocusNode();
final weatherController = Get.put(WeatherController());
final _controller = TextEditingController();
final _controllerLat = TextEditingController();
final _controllerLon = TextEditingController();
final _controllerCity = TextEditingController();
final _controllerDistrict = TextEditingController();
static const kTextFieldElevation = 4.0;
static const colorFilter = ColorFilter.matrix(<double>[
-0.2, -0.7, -0.08, 0, 255, // Red channel
-0.2, -0.7, -0.08, 0, 255, // Green channel
-0.2, -0.7, -0.08, 0, 255, // Blue channel
0, 0, 0, 1, 0, // Alpha channel
]);
final bool _isDarkMode = Get.theme.brightness == Brightness.dark;
final mapController = MapController();
void textTrim(TextEditingController value) {
value.text = value.text.trim();
while (value.text.contains(' ')) {
value.text = value.text.replaceAll(' ', ' ');
}
}
void fillController(Result selection) {
_controllerLat.text = '${selection.latitude}';
_controllerLon.text = '${selection.longitude}';
_controllerCity.text = selection.name;
_controllerDistrict.text = selection.admin1;
_controller.clear();
_focusNode.unfocus();
setState(() {});
}
void fillControllerGeo(Map<String, dynamic> location) {
_controllerLat.text = '${location['lat']}';
_controllerLon.text = '${location['lon']}';
_controllerCity.text = location['district'];
_controllerDistrict.text = location['city'];
setState(() {});
}
void fillMap(double latitude, double longitude) {
_controllerLat.text = '$latitude';
_controllerLon.text = '$longitude';
setState(() {});
}
Widget _buildMapTileLayer() {
return TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.darkmoonight.rain',
);
}
Widget _buildMap() {
return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(20)),
child: SizedBox(
height: Get.size.height * 0.3,
child: FlutterMap(
mapController: mapController,
options: MapOptions(
backgroundColor: context.theme.colorScheme.surface,
initialCenter: const LatLng(55.7522, 37.6156),
initialZoom: 3,
interactionOptions: const InteractionOptions(
flags: InteractiveFlag.all & ~InteractiveFlag.rotate,
),
cameraConstraint: CameraConstraint.contain(
bounds: LatLngBounds(
const LatLng(-90, -180),
const LatLng(90, 180),
),
),
onLongPress: (tapPosition, point) =>
fillMap(point.latitude, point.longitude),
),
children: [
if (_isDarkMode)
ColorFiltered(
colorFilter: colorFilter,
child: _buildMapTileLayer(),
)
else
_buildMapTileLayer(),
RichAttributionWidget(
animationConfig: const ScaleRAWA(),
attributions: [
TextSourceAttribution(
'OpenStreetMap contributors',
onTap: () => weatherController.urlLauncher(
'https://openstreetmap.org/copyright',
),
),
],
),
],
),
),
);
}
Widget _buildSearchField() {
return RawAutocomplete<Result>(
focusNode: _focusNode,
textEditingController: _controller,
fieldViewBuilder:
(
BuildContext context,
TextEditingController fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted,
) {
return MyTextForm(
elevation: kTextFieldElevation,
labelText: 'search'.tr,
type: TextInputType.text,
icon: const Icon(IconsaxPlusLinear.global_search),
controller: _controller,
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
focusNode: _focusNode,
);
},
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty) {
return const Iterable<Result>.empty();
}
return WeatherAPI().getCity(textEditingValue.text, locale);
},
onSelected: (Result selection) => fillController(selection),
displayStringForOption: (Result option) =>
'${option.name}, ${option.admin1}',
optionsViewBuilder:
(
BuildContext context,
AutocompleteOnSelected<Result> onSelected,
Iterable<Result> options,
) {
return _buildOptionsView(context, onSelected, options);
},
);
}
Widget _buildOptionsView(
BuildContext context,
AutocompleteOnSelected<Result> onSelected,
Iterable<Result> options,
) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: Align(
alignment: Alignment.topCenter,
child: Material(
borderRadius: BorderRadius.circular(20),
elevation: 4.0,
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final Result option = options.elementAt(index);
return InkWell(
onTap: () => onSelected(option),
child: ListTile(
title: Text(
'${option.name}, ${option.admin1}',
style: context.textTheme.labelLarge,
),
),
);
},
),
),
),
);
}
Widget _buildLocationButton() {
return Card(
elevation: kTextFieldElevation,
margin: const EdgeInsets.only(top: 10, right: 10),
child: Container(
margin: const EdgeInsets.all(2.5),
child: IconButton(
onPressed: _handleLocationButtonPress,
icon: const Icon(IconsaxPlusLinear.location),
),
),
);
}
Future<void> _handleLocationButtonPress() async {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
if (!context.mounted) return;
await _showLocationDialog();
return;
}
setState(() => isLoading = true);
final location = await weatherController.getCurrentLocationSearch();
fillControllerGeo(location);
setState(() => isLoading = false);
}
Future<void> _showLocationDialog() async {
await showAdaptiveDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog.adaptive(
title: Text('location'.tr, style: context.textTheme.titleLarge),
content: Text('no_location'.tr, style: context.textTheme.titleMedium),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: Text(
'cancel'.tr,
style: context.textTheme.titleMedium?.copyWith(
color: Colors.blueAccent,
),
),
),
TextButton(
onPressed: () {
Geolocator.openLocationSettings();
Get.back(result: true);
},
child: Text(
'settings'.tr,
style: context.textTheme.titleMedium?.copyWith(
color: Colors.green,
),
),
),
],
);
},
);
}
Widget _buildLatitudeField() {
return MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerLat,
labelText: 'lat'.tr,
type: TextInputType.number,
icon: const Icon(IconsaxPlusLinear.location),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
validator: (value) => _validateLatitude(value),
);
}
Widget _buildLongitudeField() {
return MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerLon,
labelText: 'lon'.tr,
type: TextInputType.number,
icon: const Icon(IconsaxPlusLinear.location),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
validator: (value) => _validateLongitude(value),
);
}
Widget _buildCityField() {
return MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerCity,
labelText: 'city'.tr,
type: TextInputType.name,
icon: const Icon(IconsaxPlusLinear.building_3),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
validator: (value) => _validateCity(value),
);
}
Widget _buildDistrictField() {
return MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerDistrict,
labelText: 'district'.tr,
type: TextInputType.streetAddress,
icon: const Icon(IconsaxPlusLinear.global),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
);
}
Widget _buildSubmitButton() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: MyTextButton(buttonName: 'done'.tr, onPressed: _handleSubmit),
);
}
String? _validateLatitude(String? value) {
if (value == null || value.isEmpty) {
return 'validateValue'.tr;
}
double? numericValue = double.tryParse(value);
if (numericValue == null) {
return 'validateNumber'.tr;
}
if (numericValue < -90 || numericValue > 90) {
return 'validate90'.tr;
}
return null;
}
String? _validateLongitude(String? value) {
if (value == null || value.isEmpty) {
return 'validateValue'.tr;
}
double? numericValue = double.tryParse(value);
if (numericValue == null) {
return 'validateNumber'.tr;
}
if (numericValue < -180 || numericValue > 180) {
return 'validate180'.tr;
}
return null;
}
String? _validateCity(String? value) {
if (value == null || value.isEmpty) {
return 'validateName'.tr;
}
return null;
}
Future<void> _handleSubmit() async {
if (formKeySearch.currentState!.validate()) {
textTrim(_controllerLat);
textTrim(_controllerLon);
textTrim(_controllerCity);
textTrim(_controllerDistrict);
try {
await weatherController.deleteAll(true);
await weatherController.getLocation(
double.parse(_controllerLat.text),
double.parse(_controllerLon.text),
_controllerDistrict.text,
_controllerCity.text,
);
widget.isStart
? Get.off(() => const HomePage(), transition: Transition.downToUp)
: Get.back();
} catch (error) {
Future.error(error);
}
}
}
@override
Widget build(BuildContext context) {
return Form(
key: formKeySearch,
child: Scaffold(
resizeToAvoidBottomInset: true,
appBar: _buildAppBar(),
body: SafeArea(
child: Stack(
children: [
Column(
children: [
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: SingleChildScrollView(
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
),
child: _buildMap(),
),
Padding(
padding: const EdgeInsets.fromLTRB(
10,
15,
10,
5,
),
child: Text(
'searchMethod'.tr,
style: context.theme.textTheme.bodyLarge
?.copyWith(fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
),
Row(
children: [
Flexible(child: _buildSearchField()),
_buildLocationButton(),
],
),
_buildLatitudeField(),
_buildLongitudeField(),
_buildCityField(),
_buildDistrictField(),
const Gap(20),
],
),
),
),
],
),
),
_buildSubmitButton(),
],
),
if (isLoading)
BackdropFilter(
filter: ImageFilter.blur(sigmaY: 3, sigmaX: 3),
child: const Center(child: CircularProgressIndicator()),
),
],
),
),
),
);
}
AppBar _buildAppBar() {
return AppBar(
centerTitle: true,
leading: widget.isStart
? null
: IconButton(
onPressed: () {
Get.back();
},
icon: const Icon(IconsaxPlusLinear.arrow_left_3, size: 20),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
automaticallyImplyLeading: false,
title: Text(
'searchCity'.tr,
style: context.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
);
}
}

311
lib/app/ui/home.dart Executable file
View file

@ -0,0 +1,311 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:isar/isar.dart';
import 'package:rain/app/api/api.dart';
import 'package:rain/app/api/city_api.dart';
import 'package:rain/app/controller/controller.dart';
import 'package:rain/app/data/db.dart';
import 'package:rain/app/ui/places/view/place_list.dart';
import 'package:rain/app/ui/places/widgets/create_place.dart';
import 'package:rain/app/ui/geolocation.dart';
import 'package:rain/app/ui/main/view/main.dart';
import 'package:rain/app/ui/map/view/map.dart';
import 'package:rain/app/ui/settings/view/settings.dart';
import 'package:rain/app/utils/show_snack_bar.dart';
import 'package:rain/main.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
int tabIndex = 0;
bool visible = false;
final _focusNode = FocusNode();
late TabController tabController;
final weatherController = Get.put(WeatherController());
final _controller = TextEditingController();
final List<Widget> pages = [
const MainPage(),
const PlaceList(),
if (!settings.hideMap) const MapPage(),
const SettingsPage(),
];
@override
void initState() {
super.initState();
getData();
setupTabController();
}
@override
void dispose() {
tabController.dispose();
super.dispose();
}
void setupTabController() {
tabController = TabController(
initialIndex: tabIndex,
length: pages.length,
vsync: this,
);
tabController.animation?.addListener(() {
int value = (tabController.animation!.value).round();
if (value != tabIndex) setState(() => tabIndex = value);
});
tabController.addListener(() {
setState(() {
tabIndex = tabController.index;
});
});
}
void getData() async {
await weatherController.deleteCache();
await weatherController.updateCacheCard(false);
await weatherController.setLocation();
}
void changeTabIndex(int index) {
setState(() {
tabIndex = index;
});
tabController.animateTo(tabIndex);
}
Widget _buildAppBarTitle(
int tabIndex,
TextStyle? textStyle,
TextStyle? labelLarge,
) {
switch (tabIndex) {
case 0:
return visible
? _buildSearchField(labelLarge)
: Obx(() {
final location = weatherController.location;
final city = location.city;
final district = location.district;
return Text(
weatherController.isLoading.isFalse
? district!.isEmpty
? '$city'
: city!.isEmpty
? district
: city == district
? city
: '$city, $district'
: settings.location
? 'search'.tr
: (isar.locationCaches.where().findAllSync()).isNotEmpty
? 'loading'.tr
: 'searchCity'.tr,
style: textStyle,
);
});
case 1:
return Text('cities'.tr, style: textStyle);
case 2:
return settings.hideMap
? Text('settings_full'.tr, style: textStyle)
: Text('map'.tr, style: textStyle);
case 3:
return Text('settings_full'.tr, style: textStyle);
default:
return const SizedBox.shrink();
}
}
Widget _buildSearchField(TextStyle? labelLarge) {
return RawAutocomplete<Result>(
focusNode: _focusNode,
textEditingController: _controller,
fieldViewBuilder: (_, __, ___, ____) {
return TextField(
controller: _controller,
focusNode: _focusNode,
style: labelLarge?.copyWith(fontSize: 16),
decoration: InputDecoration(hintText: 'search'.tr),
);
},
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty) {
return const Iterable<Result>.empty();
}
return WeatherAPI().getCity(textEditingValue.text, locale);
},
onSelected: (Result selection) async {
await weatherController.deleteAll(true);
await weatherController.getLocation(
double.parse('${selection.latitude}'),
double.parse('${selection.longitude}'),
selection.admin1,
selection.name,
);
visible = false;
_controller.clear();
_focusNode.unfocus();
setState(() {});
},
displayStringForOption:
(Result option) => '${option.name}, ${option.admin1}',
optionsViewBuilder: (
BuildContext context,
AutocompleteOnSelected<Result> onSelected,
Iterable<Result> options,
) {
return _buildOptionsView(context, onSelected, options, labelLarge);
},
);
}
Widget _buildOptionsView(
BuildContext context,
AutocompleteOnSelected<Result> onSelected,
Iterable<Result> options,
TextStyle? labelLarge,
) {
return Align(
alignment: Alignment.topLeft,
child: Material(
borderRadius: BorderRadius.circular(20),
elevation: 4.0,
child: SizedBox(
width: 250,
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final Result option = options.elementAt(index);
return InkWell(
onTap: () => onSelected(option),
child: ListTile(
title: Text(
'${option.name}, ${option.admin1}',
style: labelLarge,
),
),
);
},
),
),
),
);
}
Widget _buildSearchIconButton() {
return IconButton(
onPressed: () {
if (visible) {
_controller.clear();
_focusNode.unfocus();
visible = false;
} else {
visible = true;
}
setState(() {});
},
icon: Icon(
visible
? IconsaxPlusLinear.close_circle
: IconsaxPlusLinear.search_normal_1,
size: 18,
),
);
}
@override
Widget build(BuildContext context) {
final textTheme = context.textTheme;
final labelLarge = textTheme.labelLarge;
final textStyle = textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
);
return DefaultTabController(
length: pages.length,
child: ScaffoldMessenger(
key: globalKey,
child: Scaffold(
appBar: AppBar(
centerTitle: true,
automaticallyImplyLeading: false,
leading:
tabIndex == 0
? IconButton(
onPressed: () {
Get.to(
() => const SelectGeolocation(isStart: false),
transition: Transition.downToUp,
);
},
icon: const Icon(
IconsaxPlusLinear.global_search,
size: 18,
),
)
: null,
title: _buildAppBarTitle(tabIndex, textStyle, labelLarge),
actions: tabIndex == 0 ? [_buildSearchIconButton()] : null,
),
body: SafeArea(
child: TabBarView(controller: tabController, children: pages),
),
bottomNavigationBar: NavigationBar(
onDestinationSelected: (int index) => changeTabIndex(index),
selectedIndex: tabIndex,
destinations: [
NavigationDestination(
icon: const Icon(IconsaxPlusLinear.cloud_sunny),
selectedIcon: const Icon(IconsaxPlusBold.cloud_sunny),
label: 'name'.tr,
),
NavigationDestination(
icon: const Icon(IconsaxPlusLinear.buildings),
selectedIcon: const Icon(IconsaxPlusBold.buildings),
label: 'cities'.tr,
),
if (!settings.hideMap)
NavigationDestination(
icon: const Icon(IconsaxPlusLinear.map),
selectedIcon: const Icon(IconsaxPlusBold.map),
label: 'map'.tr,
),
NavigationDestination(
icon: const Icon(IconsaxPlusLinear.category),
selectedIcon: const Icon(IconsaxPlusBold.category),
label: 'settings_full'.tr,
),
],
),
floatingActionButton:
tabIndex == 1
? FloatingActionButton(
onPressed:
() => showModalBottomSheet(
context: context,
isScrollControlled: true,
enableDrag: false,
builder:
(BuildContext context) => const CreatePlace(),
),
child: const Icon(IconsaxPlusLinear.add),
)
: null,
),
),
);
}
}

242
lib/app/ui/main/view/main.dart Executable file
View file

@ -0,0 +1,242 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:rain/app/controller/controller.dart';
import 'package:rain/app/data/db.dart';
import 'package:rain/app/ui/widgets/weather/daily/daily_card_list.dart';
import 'package:rain/app/ui/widgets/weather/daily/daily_container.dart';
import 'package:rain/app/ui/widgets/weather/desc/desc_container.dart';
import 'package:rain/app/ui/widgets/weather/hourly.dart';
import 'package:rain/app/ui/widgets/weather/now.dart';
import 'package:rain/app/ui/widgets/shimmer.dart';
import 'package:rain/app/ui/widgets/weather/sunset_sunrise.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
class MainPage extends StatefulWidget {
const MainPage({super.key});
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
final weatherController = Get.put(WeatherController());
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: _handleRefresh,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Obx(() {
if (weatherController.isLoading.isTrue) {
return _buildLoadingView();
}
final mainWeather = weatherController.mainWeather;
final weatherCard = WeatherCard.fromJson(mainWeather.toJson());
final hourOfDay = weatherController.hourOfDay.value;
final dayOfNow = weatherController.dayOfNow.value;
final sunrise = mainWeather.sunrise![dayOfNow];
final sunset = mainWeather.sunset![dayOfNow];
final tempMax = mainWeather.temperature2MMax![dayOfNow];
final tempMin = mainWeather.temperature2MMin![dayOfNow];
return _buildMainView(
context,
mainWeather,
weatherCard,
hourOfDay,
dayOfNow,
sunrise,
sunset,
tempMax!,
tempMin!,
);
}),
),
);
}
Future<void> _handleRefresh() async {
await weatherController.deleteAll(false);
await weatherController.setLocation();
setState(() {});
}
Widget _buildLoadingView() {
return ListView(
children: const [
MyShimmer(height: 200),
MyShimmer(height: 130, margin: EdgeInsets.symmetric(vertical: 15)),
MyShimmer(height: 90, margin: EdgeInsets.only(bottom: 15)),
MyShimmer(height: 400, margin: EdgeInsets.only(bottom: 15)),
MyShimmer(height: 450, margin: EdgeInsets.only(bottom: 15)),
],
);
}
Widget _buildMainView(
BuildContext context,
MainWeatherCache mainWeather,
WeatherCard weatherCard,
int hourOfDay,
int dayOfNow,
String sunrise,
String sunset,
double tempMax,
double tempMin,
) {
return ListView(
children: [
_buildNowWidget(
mainWeather,
hourOfDay,
dayOfNow,
sunrise,
sunset,
tempMax,
tempMin,
),
_buildHourlyList(context, mainWeather, hourOfDay, dayOfNow),
_buildSunsetSunriseWidget(sunrise, sunset),
_buildHourlyDescContainer(mainWeather, hourOfDay),
_buildDailyContainer(weatherCard),
],
);
}
Widget _buildNowWidget(
MainWeatherCache mainWeather,
int hourOfDay,
int dayOfNow,
String sunrise,
String sunset,
double tempMax,
double tempMin,
) {
return Now(
time: mainWeather.time![hourOfDay],
weather: mainWeather.weathercode![hourOfDay],
degree: mainWeather.temperature2M![hourOfDay],
feels: mainWeather.apparentTemperature![hourOfDay]!,
timeDay: sunrise,
timeNight: sunset,
tempMax: tempMax,
tempMin: tempMin,
);
}
Widget _buildHourlyList(
BuildContext context,
MainWeatherCache mainWeather,
int hourOfDay,
int dayOfNow,
) {
return Card(
margin: const EdgeInsets.only(bottom: 15),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: SizedBox(
height: 135,
child: ScrollablePositionedList.separated(
key: const PageStorageKey(0),
separatorBuilder: (BuildContext context, int index) {
return const VerticalDivider(
width: 10,
indent: 40,
endIndent: 40,
);
},
scrollDirection: Axis.horizontal,
itemScrollController: weatherController.itemScrollController,
itemCount: mainWeather.time!.length,
itemBuilder: (ctx, i) {
return _buildHourlyItem(
context,
mainWeather,
i,
hourOfDay,
dayOfNow,
);
},
),
),
),
);
}
Widget _buildHourlyItem(
BuildContext context,
MainWeatherCache mainWeather,
int i,
int hourOfDay,
int dayOfNow,
) {
final i24 = (i / 24).floor();
return GestureDetector(
onTap: () {
weatherController.hourOfDay.value = i;
weatherController.dayOfNow.value = i24;
setState(() {});
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
decoration: BoxDecoration(
color: i == hourOfDay
? context.theme.colorScheme.secondaryContainer
: Colors.transparent,
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
child: Hourly(
time: mainWeather.time![i],
weather: mainWeather.weathercode![i],
degree: mainWeather.temperature2M![i],
timeDay: mainWeather.sunrise![i24],
timeNight: mainWeather.sunset![i24],
),
),
);
}
Widget _buildSunsetSunriseWidget(String sunrise, String sunset) {
return SunsetSunrise(timeSunrise: sunrise, timeSunset: sunset);
}
Widget _buildHourlyDescContainer(
MainWeatherCache mainWeather,
int hourOfDay,
) {
return DescContainer(
humidity: mainWeather.relativehumidity2M?[hourOfDay],
wind: mainWeather.windspeed10M?[hourOfDay],
visibility: mainWeather.visibility?[hourOfDay],
feels: mainWeather.apparentTemperature?[hourOfDay],
evaporation: mainWeather.evapotranspiration?[hourOfDay],
precipitation: mainWeather.precipitation?[hourOfDay],
direction: mainWeather.winddirection10M?[hourOfDay],
pressure: mainWeather.surfacePressure?[hourOfDay],
rain: mainWeather.rain?[hourOfDay],
cloudcover: mainWeather.cloudcover?[hourOfDay],
windgusts: mainWeather.windgusts10M?[hourOfDay],
uvIndex: mainWeather.uvIndex?[hourOfDay],
dewpoint2M: mainWeather.dewpoint2M?[hourOfDay],
precipitationProbability:
mainWeather.precipitationProbability?[hourOfDay],
shortwaveRadiation: mainWeather.shortwaveRadiation?[hourOfDay],
initiallyExpanded: false,
title: 'hourlyVariables'.tr,
);
}
Widget _buildDailyContainer(WeatherCard weatherCard) {
return DailyContainer(
weatherData: weatherCard,
onTap: () => Get.to(
() => DailyCardList(weatherData: weatherCard),
transition: Transition.downToUp,
),
);
}
}

View file

@ -1,6 +1,5 @@
import 'dart:io';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:dio_cache_interceptor_file_store/dio_cache_interceptor_file_store.dart';
import 'package:flutter/material.dart';
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
import 'package:flutter_map/flutter_map.dart';
@ -8,29 +7,30 @@ import 'package:flutter_map_animations/flutter_map_animations.dart';
import 'package:flutter_map_cache/flutter_map_cache.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:http_cache_file_store/http_cache_file_store.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:latlong2/latlong.dart';
import 'package:path_provider/path_provider.dart';
import 'package:rain/app/api/api.dart';
import 'package:rain/app/api/city_api.dart';
import 'package:rain/app/controller/controller.dart';
import 'package:rain/app/data/weather.dart';
import 'package:rain/app/modules/cards/view/info_weather_card.dart';
import 'package:rain/app/modules/cards/widgets/create_card_weather.dart';
import 'package:rain/app/modules/cards/widgets/weather_card_container.dart';
import 'package:rain/app/widgets/status/status_data.dart';
import 'package:rain/app/widgets/status/status_weather.dart';
import 'package:rain/app/widgets/text_form.dart';
import 'package:rain/app/data/db.dart';
import 'package:rain/app/ui/places/view/place_info.dart';
import 'package:rain/app/ui/places/widgets/create_place.dart';
import 'package:rain/app/ui/places/widgets/place_card.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/app/ui/widgets/text_form.dart';
import 'package:rain/main.dart';
class MapWeather extends StatefulWidget {
const MapWeather({super.key});
class MapPage extends StatefulWidget {
const MapPage({super.key});
@override
State<MapWeather> createState() => _MapWeatherState();
State<MapPage> createState() => _MapPageState();
}
class _MapWeatherState extends State<MapWeather> with TickerProviderStateMixin {
class _MapPageState extends State<MapPage> with TickerProviderStateMixin {
late final AnimatedMapController _animatedMapController =
AnimatedMapController(vsync: this);
final weatherController = Get.put(WeatherController());
@ -66,10 +66,9 @@ class _MapWeatherState extends State<MapWeather> with TickerProviderStateMixin {
_offsetAnimation = Tween<Offset>(
begin: const Offset(0.0, 1.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
super.initState();
}
@ -114,25 +113,26 @@ class _MapWeatherState extends State<MapWeather> with TickerProviderStateMixin {
_focusNode.unfocus();
}
Widget _buidStyleMarkers(int weathercode, String time, String sunrise,
String sunset, double temperature2M) {
Widget _buildStyleMarkers(
int weathercode,
String time,
String sunrise,
String sunset,
double temperature2M,
) {
return Card(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
statusWeather.getImageNow(
weathercode,
time,
sunrise,
sunset,
),
statusWeather.getImageNow(weathercode, time, sunrise, sunset),
scale: 18,
),
const MaxGap(5),
Text(
statusData
.getDegree(roundDegree ? temperature2M.round() : temperature2M),
statusData.getDegree(
roundDegree ? temperature2M.round() : temperature2M,
),
style: context.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.bold,
fontSize: 16,
@ -144,14 +144,17 @@ class _MapWeatherState extends State<MapWeather> with TickerProviderStateMixin {
}
Marker _buildMainLocationMarker(
WeatherCard weatherCard, int hourOfDay, int dayOfNow) {
WeatherCard weatherCard,
int hourOfDay,
int dayOfNow,
) {
return Marker(
height: 50,
width: 100,
point: LatLng(weatherCard.lat!, weatherCard.lon!),
child: GestureDetector(
onTap: () => _onMarkerTap(weatherCard),
child: _buidStyleMarkers(
child: _buildStyleMarkers(
weatherCard.weathercode![hourOfDay],
weatherCard.time![hourOfDay],
weatherCard.sunrise![dayOfNow],
@ -163,23 +166,27 @@ class _MapWeatherState extends State<MapWeather> with TickerProviderStateMixin {
}
Marker _buildCardMarker(WeatherCard weatherCardList) {
final hourOfDay = weatherController.getTime(
weatherCardList.time!,
weatherCardList.timezone!,
);
final dayOfNow = weatherController.getDay(
weatherCardList.timeDaily!,
weatherCardList.timezone!,
);
return Marker(
height: 50,
width: 100,
point: LatLng(weatherCardList.lat!, weatherCardList.lon!),
child: GestureDetector(
onTap: () => _onMarkerTap(weatherCardList),
child: _buidStyleMarkers(
weatherCardList.weathercode![weatherController.getTime(
weatherCardList.time!, weatherCardList.timezone!)],
weatherCardList.time![weatherController.getTime(
weatherCardList.time!, weatherCardList.timezone!)],
weatherCardList.sunrise![weatherController.getDay(
weatherCardList.timeDaily!, weatherCardList.timezone!)],
weatherCardList.sunset![weatherController.getDay(
weatherCardList.timeDaily!, weatherCardList.timezone!)],
weatherCardList.temperature2M![weatherController.getTime(
weatherCardList.time!, weatherCardList.timezone!)],
child: _buildStyleMarkers(
weatherCardList.weathercode![hourOfDay],
weatherCardList.time![hourOfDay],
weatherCardList.sunrise![dayOfNow],
weatherCardList.sunset![dayOfNow],
weatherCardList.temperature2M![hourOfDay],
),
),
);
@ -201,11 +208,12 @@ class _MapWeatherState extends State<MapWeather> with TickerProviderStateMixin {
? SlideTransition(
position: _offsetAnimation,
child: GestureDetector(
onTap: () => Get.to(
() => InfoWeatherCard(weatherCard: _selectedWeatherCard!),
onTap:
() => Get.to(
() => PlaceInfo(weatherCard: _selectedWeatherCard!),
transition: Transition.downToUp,
),
child: WeatherCardContainer(
child: PlaceCard(
time: _selectedWeatherCard!.time!,
timeDaily: _selectedWeatherCard!.timeDaily!,
timeDay: _selectedWeatherCard!.sunrise!,
@ -221,6 +229,91 @@ class _MapWeatherState extends State<MapWeather> with TickerProviderStateMixin {
: const SizedBox.shrink();
}
Widget _buildSearchField() {
return RawAutocomplete<Result>(
focusNode: _focusNode,
textEditingController: _controllerSearch,
fieldViewBuilder: (
BuildContext context,
TextEditingController fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted,
) {
return MyTextForm(
labelText: 'search'.tr,
type: TextInputType.text,
icon: const Icon(IconsaxPlusLinear.global_search),
controller: _controllerSearch,
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
focusNode: _focusNode,
onChanged: (value) => setState(() {}),
iconButton:
_controllerSearch.text.isNotEmpty
? IconButton(
onPressed: () {
_controllerSearch.clear();
},
icon: const Icon(
IconsaxPlusLinear.close_circle,
color: Colors.grey,
size: 20,
),
)
: null,
);
},
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty) {
return const Iterable<Result>.empty();
}
return WeatherAPI().getCity(textEditingValue.text, locale);
},
onSelected: (Result selection) {
_animatedMapController.mapController.move(
LatLng(selection.latitude, selection.longitude),
14,
);
_controllerSearch.clear();
_focusNode.unfocus();
},
displayStringForOption:
(Result option) => '${option.name}, ${option.admin1}',
optionsViewBuilder: (
BuildContext context,
AutocompleteOnSelected<Result> onSelected,
Iterable<Result> options,
) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: Align(
alignment: Alignment.topCenter,
child: Material(
borderRadius: BorderRadius.circular(20),
elevation: 4.0,
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final Result option = options.elementAt(index);
return InkWell(
onTap: () => onSelected(option),
child: ListTile(
title: Text(
'${option.name}, ${option.admin1}',
style: context.textTheme.labelLarge,
),
),
);
},
),
),
),
);
},
);
}
@override
Widget build(BuildContext context) {
final mainLocation = weatherController.location;
@ -261,11 +354,13 @@ class _MapWeatherState extends State<MapWeather> with TickerProviderStateMixin {
),
),
onTap: (_, __) => _hideCard(),
onLongPress: (tapPosition, point) => showModalBottomSheet(
onLongPress:
(tapPosition, point) => showModalBottomSheet(
context: context,
isScrollControlled: true,
enableDrag: false,
builder: (BuildContext context) => CreateWeatherCard(
builder:
(BuildContext context) => CreatePlace(
latitude: '${point.latitude}',
longitude: '${point.longitude}',
),
@ -290,8 +385,10 @@ class _MapWeatherState extends State<MapWeather> with TickerProviderStateMixin {
attributions: [
TextSourceAttribution(
'OpenStreetMap contributors',
onTap: () => weatherController
.urlLauncher('https://openstreetmap.org/copyright'),
onTap:
() => weatherController.urlLauncher(
'https://openstreetmap.org/copyright',
),
),
],
),
@ -305,14 +402,15 @@ class _MapWeatherState extends State<MapWeather> with TickerProviderStateMixin {
dayOfNow,
);
final cardMarkers = weatherController.weatherCards
.map((weatherCardList) =>
_buildCardMarker(weatherCardList))
final cardMarkers =
weatherController.weatherCards
.map(
(weatherCardList) =>
_buildCardMarker(weatherCardList),
)
.toList();
return MarkerLayer(
markers: [mainMarker, ...cardMarkers],
);
return MarkerLayer(markers: [mainMarker, ...cardMarkers]);
}),
ExpandableFab(
key: _fabKey,
@ -331,23 +429,31 @@ class _MapWeatherState extends State<MapWeather> with TickerProviderStateMixin {
FloatingActionButton(
heroTag: null,
child: const Icon(IconsaxPlusLinear.home_2),
onPressed: () => _resetMapOrientation(
center:
LatLng(mainLocation.lat!, mainLocation.lon!),
zoom: 8),
onPressed:
() => _resetMapOrientation(
center: LatLng(
mainLocation.lat!,
mainLocation.lon!,
),
zoom: 8,
),
),
FloatingActionButton(
heroTag: null,
child: const Icon(IconsaxPlusLinear.search_zoom_out_1),
onPressed: () => _animatedMapController.animatedZoomOut(
customId: _useTransformer ? _useTransformerId : null,
onPressed:
() => _animatedMapController.animatedZoomOut(
customId:
_useTransformer ? _useTransformerId : null,
),
),
FloatingActionButton(
heroTag: null,
child: const Icon(IconsaxPlusLinear.search_zoom_in),
onPressed: () => _animatedMapController.animatedZoomIn(
customId: _useTransformer ? _useTransformerId : null,
onPressed:
() => _animatedMapController.animatedZoomIn(
customId:
_useTransformer ? _useTransformerId : null,
),
),
],
@ -360,82 +466,7 @@ class _MapWeatherState extends State<MapWeather> with TickerProviderStateMixin {
),
],
),
RawAutocomplete<Result>(
focusNode: _focusNode,
textEditingController: _controllerSearch,
fieldViewBuilder: (BuildContext context,
TextEditingController fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted) {
return MyTextForm(
labelText: 'search'.tr,
type: TextInputType.text,
icon: const Icon(IconsaxPlusLinear.global_search),
controller: _controllerSearch,
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
focusNode: _focusNode,
onChanged: (value) => setState(() {}),
iconButton: _controllerSearch.text.isNotEmpty
? IconButton(
onPressed: () {
_controllerSearch.clear();
},
icon: const Icon(
IconsaxPlusLinear.close_circle,
color: Colors.grey,
size: 20,
),
)
: null,
);
},
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty) {
return const Iterable<Result>.empty();
}
return WeatherAPI().getCity(textEditingValue.text, locale);
},
onSelected: (Result selection) {
_animatedMapController.mapController.move(
LatLng(selection.latitude, selection.longitude), 14);
_controllerSearch.clear();
_focusNode.unfocus();
},
displayStringForOption: (Result option) =>
'${option.name}, ${option.admin1}',
optionsViewBuilder: (BuildContext context,
AutocompleteOnSelected<Result> onSelected,
Iterable<Result> options) {
return Padding(
padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: Align(
alignment: Alignment.topCenter,
child: Material(
borderRadius: BorderRadius.circular(20),
elevation: 4.0,
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final Result option = options.elementAt(index);
return InkWell(
onTap: () => onSelected(option),
child: ListTile(
title: Text(
'${option.name}, ${option.admin1}',
style: context.textTheme.labelLarge,
),
),
);
},
),
),
),
);
},
),
_buildSearchField(),
],
);
},

View file

@ -1,9 +1,9 @@
import 'package:gap/gap.dart';
import 'package:rain/app/data/weather.dart';
import 'package:rain/app/modules/geolocation.dart';
import 'package:rain/app/widgets/button.dart';
import 'package:rain/main.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:rain/app/data/db.dart';
import 'package:rain/app/ui/geolocation.dart';
import 'package:rain/app/ui/widgets/button.dart';
import 'package:rain/main.dart';
import 'package:get/get.dart';
class OnBording extends StatefulWidget {
@ -19,8 +19,8 @@ class _OnBordingState extends State<OnBording> {
@override
void initState() {
pageController = PageController(initialPage: 0);
super.initState();
pageController = PageController(initialPage: 0);
}
@override
@ -32,8 +32,10 @@ class _OnBordingState extends State<OnBording> {
void onBoardHome() {
settings.onboard = true;
isar.writeTxnSync(() => isar.settings.putSync(settings));
Get.off(() => const SelectGeolocation(isStart: true),
transition: Transition.downToUp);
Get.off(
() => const SelectGeolocation(isStart: true),
transition: Transition.downToUp,
);
}
@override
@ -43,7 +45,17 @@ class _OnBordingState extends State<OnBording> {
body: SafeArea(
child: Column(
children: [
Expanded(
_buildPageView(),
_buildDotIndicators(),
_buildActionButton(),
],
),
),
);
}
Widget _buildPageView() {
return Expanded(
child: PageView.builder(
controller: pageController,
itemCount: data.length,
@ -58,45 +70,44 @@ class _OnBordingState extends State<OnBording> {
description: data[index].description,
),
),
),
Row(
);
}
Widget _buildDotIndicators() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
...List.generate(
children: List.generate(
data.length,
(index) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: DotIndicator(isActive: index == pageIndex),
)),
],
),
Padding(
),
);
}
Widget _buildActionButton() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: MyTextButton(
buttonName:
pageIndex == data.length - 1 ? 'start'.tr : 'next'.tr,
buttonName: pageIndex == data.length - 1 ? 'start'.tr : 'next'.tr,
onPressed: () {
pageIndex == data.length - 1
? onBoardHome()
: pageController.nextPage(
if (pageIndex == data.length - 1) {
onBoardHome();
} else {
pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.ease,
);
}
},
),
)
],
),
),
);
}
}
class DotIndicator extends StatelessWidget {
const DotIndicator({
super.key,
this.isActive = false,
});
const DotIndicator({super.key, this.isActive = false});
final bool isActive;
@ -130,15 +141,18 @@ final List<Onboard> data = [
Onboard(
image: 'assets/icons/Rain.png',
title: 'name'.tr,
description: 'description'.tr),
description: 'description'.tr,
),
Onboard(
image: 'assets/icons/Design.png',
title: 'name2'.tr,
description: 'description2'.tr),
description: 'description2'.tr,
),
Onboard(
image: 'assets/icons/Team.png',
title: 'name3'.tr,
description: 'description3'.tr),
description: 'description3'.tr,
),
];
class OnboardContent extends StatelessWidget {
@ -148,6 +162,7 @@ class OnboardContent extends StatelessWidget {
required this.title,
required this.description,
});
final String image, title, description;
@override
@ -158,14 +173,12 @@ class OnboardContent extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
image,
scale: 5,
),
Image.asset(image, scale: 5),
Text(
title,
style: context.textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.w600),
style: context.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const Gap(10),
SizedBox(

View file

@ -0,0 +1,212 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:rain/app/controller/controller.dart';
import 'package:rain/app/data/db.dart';
import 'package:rain/app/ui/widgets/weather/daily/daily_card_list.dart';
import 'package:rain/app/ui/widgets/weather/daily/daily_container.dart';
import 'package:rain/app/ui/widgets/weather/desc/desc_container.dart';
import 'package:rain/app/ui/widgets/weather/hourly.dart';
import 'package:rain/app/ui/widgets/weather/now.dart';
import 'package:rain/app/ui/widgets/weather/sunset_sunrise.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
class PlaceInfo extends StatefulWidget {
const PlaceInfo({super.key, required this.weatherCard});
final WeatherCard weatherCard;
@override
State<PlaceInfo> createState() => _PlaceInfoState();
}
class _PlaceInfoState extends State<PlaceInfo> {
int timeNow = 0;
int dayNow = 0;
final weatherController = Get.put(WeatherController());
final itemScrollController = ItemScrollController();
@override
void initState() {
getTime();
super.initState();
}
void getTime() {
final weatherCard = widget.weatherCard;
timeNow = weatherController.getTime(
weatherCard.time!,
weatherCard.timezone!,
);
dayNow = weatherController.getDay(
weatherCard.timeDaily!,
weatherCard.timezone!,
);
Future.delayed(const Duration(milliseconds: 30), () {
itemScrollController.scrollTo(
index: timeNow,
duration: const Duration(seconds: 2),
curve: Curves.easeInOutCubic,
);
});
}
@override
Widget build(BuildContext context) {
final weatherCard = widget.weatherCard;
return RefreshIndicator(
onRefresh: _handleRefresh,
child: Scaffold(
appBar: _buildAppBar(context, weatherCard),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: ListView(
children: [
_buildNowWidget(weatherCard),
_buildHourlyList(weatherCard),
_buildSunsetSunriseWidget(weatherCard),
_buildHourlyDescContainer(weatherCard),
_buildDailyContainer(weatherCard),
],
),
),
),
),
);
}
Future<void> _handleRefresh() async {
await weatherController.updateCard(widget.weatherCard);
getTime();
setState(() {});
}
AppBar _buildAppBar(BuildContext context, WeatherCard weatherCard) {
return AppBar(
centerTitle: true,
automaticallyImplyLeading: false,
leading: IconButton(
onPressed: () => Get.back(),
icon: const Icon(IconsaxPlusLinear.arrow_left_3, size: 20),
),
title: Text(
weatherCard.district!.isNotEmpty
? '${weatherCard.city}, ${weatherCard.district}'
: '${weatherCard.city}',
style: context.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
);
}
Widget _buildNowWidget(WeatherCard weatherCard) {
return Now(
time: weatherCard.time![timeNow],
weather: weatherCard.weathercode![timeNow],
degree: weatherCard.temperature2M![timeNow],
feels: weatherCard.apparentTemperature![timeNow]!,
timeDay: weatherCard.sunrise![dayNow],
timeNight: weatherCard.sunset![dayNow],
tempMax: weatherCard.temperature2MMax![dayNow]!,
tempMin: weatherCard.temperature2MMin![dayNow]!,
);
}
Widget _buildHourlyList(WeatherCard weatherCard) {
return Card(
margin: const EdgeInsets.only(bottom: 15),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: SizedBox(
height: 135,
child: ScrollablePositionedList.separated(
key: const PageStorageKey(1),
separatorBuilder: (BuildContext context, int index) {
return const VerticalDivider(
width: 10,
indent: 40,
endIndent: 40,
);
},
scrollDirection: Axis.horizontal,
itemScrollController: itemScrollController,
itemCount: weatherCard.time!.length,
itemBuilder: (ctx, i) => _buildHourlyItem(weatherCard, i),
),
),
),
);
}
Widget _buildHourlyItem(WeatherCard weatherCard, int i) {
return GestureDetector(
onTap: () {
timeNow = i;
dayNow = (i / 24).floor();
setState(() {});
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
decoration: BoxDecoration(
color:
i == timeNow
? context.theme.colorScheme.secondaryContainer
: Colors.transparent,
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
child: Hourly(
time: weatherCard.time![i],
weather: weatherCard.weathercode![i],
degree: weatherCard.temperature2M![i],
timeDay: weatherCard.sunrise![(i / 24).floor()],
timeNight: weatherCard.sunset![(i / 24).floor()],
),
),
);
}
Widget _buildSunsetSunriseWidget(WeatherCard weatherCard) {
return SunsetSunrise(
timeSunrise: weatherCard.sunrise![dayNow],
timeSunset: weatherCard.sunset![dayNow],
);
}
Widget _buildHourlyDescContainer(WeatherCard weatherCard) {
return DescContainer(
humidity: weatherCard.relativehumidity2M?[timeNow],
wind: weatherCard.windspeed10M?[timeNow],
visibility: weatherCard.visibility?[timeNow],
feels: weatherCard.apparentTemperature?[timeNow],
evaporation: weatherCard.evapotranspiration?[timeNow],
precipitation: weatherCard.precipitation?[timeNow],
direction: weatherCard.winddirection10M?[timeNow],
pressure: weatherCard.surfacePressure?[timeNow],
rain: weatherCard.rain?[timeNow],
cloudcover: weatherCard.cloudcover?[timeNow],
windgusts: weatherCard.windgusts10M?[timeNow],
uvIndex: weatherCard.uvIndex?[timeNow],
dewpoint2M: weatherCard.dewpoint2M?[timeNow],
precipitationProbability: weatherCard.precipitationProbability?[timeNow],
shortwaveRadiation: weatherCard.shortwaveRadiation?[timeNow],
initiallyExpanded: false,
title: 'hourlyVariables'.tr,
);
}
Widget _buildDailyContainer(WeatherCard weatherCard) {
return DailyContainer(
weatherData: weatherCard,
onTap:
() => Get.to(
() => DailyCardList(weatherData: weatherCard),
transition: Transition.downToUp,
),
);
}
}

View file

@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:rain/app/controller/controller.dart';
import 'package:rain/app/ui/places/widgets/place_card_list.dart';
import 'package:rain/app/ui/widgets/text_form.dart';
class PlaceList extends StatefulWidget {
const PlaceList({super.key});
@override
State<PlaceList> createState() => _PlaceListState();
}
class _PlaceListState extends State<PlaceList> {
final weatherController = Get.put(WeatherController());
final TextEditingController searchTasks = TextEditingController();
String filter = '';
@override
void initState() {
super.initState();
applyFilter('');
}
void applyFilter(String value) {
filter = value.toLowerCase();
setState(() {});
}
void clearSearch() {
searchTasks.clear();
applyFilter('');
}
@override
Widget build(BuildContext context) {
final textTheme = context.textTheme;
final titleMedium = textTheme.titleMedium;
return Obx(() => _buildContent(context, titleMedium));
}
Widget _buildContent(BuildContext context, TextStyle? titleMedium) {
if (weatherController.weatherCards.isEmpty) {
return _buildEmptyState(context, titleMedium);
} else {
return _buildListView(context);
}
}
Widget _buildEmptyState(BuildContext context, TextStyle? titleMedium) {
return Center(
child: SingleChildScrollView(
child: Column(
children: [
Image.asset('assets/icons/City.png', scale: 6),
SizedBox(
width: Get.size.width * 0.8,
child: Text(
'noWeatherCard'.tr,
textAlign: TextAlign.center,
style: titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
),
],
),
),
);
}
Widget _buildListView(BuildContext context) {
return NestedScrollView(
physics: const NeverScrollableScrollPhysics(),
headerSliverBuilder: (context, innerBoxIsScrolled) {
return [_buildSearchField(context)];
},
body: RefreshIndicator(
onRefresh: _handleRefresh,
child: PlaceCardList(searchCity: filter),
),
);
}
Widget _buildSearchField(BuildContext context) {
return SliverToBoxAdapter(
child: MyTextForm(
labelText: 'search'.tr,
type: TextInputType.text,
icon: const Icon(IconsaxPlusLinear.search_normal_1, size: 20),
controller: searchTasks,
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
onChanged: applyFilter,
iconButton:
searchTasks.text.isNotEmpty
? IconButton(
onPressed: clearSearch,
icon: const Icon(
IconsaxPlusLinear.close_circle,
color: Colors.grey,
size: 20,
),
)
: null,
),
);
}
Future<void> _handleRefresh() async {
await weatherController.updateCacheCard(true);
setState(() {});
}
}

View file

@ -0,0 +1,346 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:rain/app/api/api.dart';
import 'package:rain/app/api/city_api.dart';
import 'package:rain/app/controller/controller.dart';
import 'package:rain/app/ui/widgets/button.dart';
import 'package:rain/app/ui/widgets/text_form.dart';
import 'package:rain/main.dart';
class CreatePlace extends StatefulWidget {
const CreatePlace({super.key, this.latitude, this.longitude});
final String? latitude;
final String? longitude;
@override
State<CreatePlace> createState() => _CreatePlaceState();
}
class _CreatePlaceState extends State<CreatePlace>
with SingleTickerProviderStateMixin {
bool isLoading = false;
final formKey = GlobalKey<FormState>();
final _focusNode = FocusNode();
final weatherController = Get.put(WeatherController());
static const kTextFieldElevation = 4.0;
late TextEditingController _controller;
late TextEditingController _controllerLat;
late TextEditingController _controllerLon;
late TextEditingController _controllerCity;
late TextEditingController _controllerDistrict;
late AnimationController _animationController;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = TextEditingController();
_controllerLat = TextEditingController(text: widget.latitude);
_controllerLon = TextEditingController(text: widget.longitude);
_controllerCity = TextEditingController();
_controllerDistrict = TextEditingController();
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_animation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
}
@override
void dispose() {
_animationController.dispose();
_controller.dispose();
_controllerLat.dispose();
_controllerLon.dispose();
_controllerCity.dispose();
_controllerDistrict.dispose();
super.dispose();
}
void textTrim(TextEditingController value) {
value.text = value.text.trim();
while (value.text.contains(' ')) {
value.text = value.text.replaceAll(' ', ' ');
}
}
void fillController(Result selection) {
_controllerLat.text = '${selection.latitude}';
_controllerLon.text = '${selection.longitude}';
_controllerCity.text = selection.name;
_controllerDistrict.text = selection.admin1;
_controller.clear();
_focusNode.unfocus();
setState(() {});
}
bool get showButton {
return _controllerLon.text.isNotEmpty &&
_controllerLat.text.isNotEmpty &&
_controllerCity.text.isNotEmpty &&
_controllerDistrict.text.isNotEmpty;
}
void updateButtonVisibility() {
if (showButton) {
_animationController.forward();
} else {
_animationController.reverse();
}
}
@override
Widget build(BuildContext context) {
updateButtonVisibility();
return Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Form(
key: formKey,
child: SingleChildScrollView(
child: Stack(
children: [
Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
_buildTitleText(),
_buildSearchField(),
_buildLatitudeField(),
_buildLongitudeField(),
_buildCityField(),
_buildDistrictField(),
_buildSubmitButton(),
],
),
),
if (isLoading) const Center(child: CircularProgressIndicator()),
],
),
),
),
);
}
Widget _buildTitleText() {
return Padding(
padding: const EdgeInsets.only(top: 14, bottom: 7),
child: Text(
'create'.tr,
style: context.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
);
}
Widget _buildSearchField() {
return RawAutocomplete<Result>(
focusNode: _focusNode,
textEditingController: _controller,
fieldViewBuilder: (
BuildContext context,
TextEditingController fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted,
) {
return MyTextForm(
elevation: kTextFieldElevation,
labelText: 'search'.tr,
type: TextInputType.text,
icon: const Icon(IconsaxPlusLinear.global_search),
controller: _controller,
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
focusNode: _focusNode,
);
},
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty) {
return const Iterable<Result>.empty();
}
return WeatherAPI().getCity(textEditingValue.text, locale);
},
onSelected: (Result selection) => fillController(selection),
displayStringForOption:
(Result option) => '${option.name}, ${option.admin1}',
optionsViewBuilder: (
BuildContext context,
AutocompleteOnSelected<Result> onSelected,
Iterable<Result> options,
) {
return _buildOptionsView(context, onSelected, options);
},
);
}
Widget _buildOptionsView(
BuildContext context,
AutocompleteOnSelected<Result> onSelected,
Iterable<Result> options,
) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Align(
alignment: Alignment.topCenter,
child: Material(
borderRadius: BorderRadius.circular(20),
elevation: 4.0,
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final Result option = options.elementAt(index);
return InkWell(
onTap: () => onSelected(option),
child: ListTile(
title: Text(
'${option.name}, ${option.admin1}',
style: context.textTheme.labelLarge,
),
),
);
},
),
),
),
);
}
Widget _buildLatitudeField() {
return MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerLat,
labelText: 'lat'.tr,
type: TextInputType.number,
icon: const Icon(IconsaxPlusLinear.location),
onChanged: (value) => setState(() {}),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
validator: (value) => _validateLatitude(value),
);
}
Widget _buildLongitudeField() {
return MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerLon,
labelText: 'lon'.tr,
type: TextInputType.number,
icon: const Icon(IconsaxPlusLinear.location),
onChanged: (value) => setState(() {}),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
validator: (value) => _validateLongitude(value),
);
}
Widget _buildCityField() {
return MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerCity,
labelText: 'city'.tr,
type: TextInputType.name,
icon: const Icon(IconsaxPlusLinear.building_3),
onChanged: (value) => setState(() {}),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
validator: (value) => _validateCity(value),
);
}
Widget _buildDistrictField() {
return MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerDistrict,
labelText: 'district'.tr,
type: TextInputType.streetAddress,
icon: const Icon(IconsaxPlusLinear.global),
onChanged: (value) => setState(() {}),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
validator: (value) => _validateDistrict(value),
);
}
Widget _buildSubmitButton() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
child: SizeTransition(
sizeFactor: _animation,
axisAlignment: -1.0,
child: MyTextButton(buttonName: 'done'.tr, onPressed: _handleSubmit),
),
);
}
String? _validateLatitude(String? value) {
if (value == null || value.isEmpty) {
return 'validateValue'.tr;
}
double? numericValue = double.tryParse(value);
if (numericValue == null) {
return 'validateNumber'.tr;
}
if (numericValue < -90 || numericValue > 90) {
return 'validate90'.tr;
}
return null;
}
String? _validateLongitude(String? value) {
if (value == null || value.isEmpty) {
return 'validateValue'.tr;
}
double? numericValue = double.tryParse(value);
if (numericValue == null) {
return 'validateNumber'.tr;
}
if (numericValue < -180 || numericValue > 180) {
return 'validate180'.tr;
}
return null;
}
String? _validateCity(String? value) {
if (value == null || value.isEmpty) {
return 'validateName'.tr;
}
return null;
}
String? _validateDistrict(String? value) {
if (value == null || value.isEmpty) {
return 'validateName'.tr;
}
return null;
}
Future<void> _handleSubmit() async {
if (formKey.currentState!.validate()) {
textTrim(_controllerLat);
textTrim(_controllerLon);
textTrim(_controllerCity);
textTrim(_controllerDistrict);
setState(() => isLoading = true);
await weatherController.addCardWeather(
double.parse(_controllerLat.text),
double.parse(_controllerLon.text),
_controllerCity.text,
_controllerDistrict.text,
);
setState(() => isLoading = false);
Get.back();
}
}
}

View file

@ -0,0 +1,155 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:rain/app/controller/controller.dart';
import 'package:rain/app/ui/widgets/weather/status/status_weather.dart';
import 'package:rain/app/ui/widgets/weather/status/status_data.dart';
import 'package:timezone/standalone.dart' as tz;
class PlaceCard extends StatefulWidget {
const PlaceCard({
super.key,
required this.time,
required this.weather,
required this.degree,
required this.district,
required this.city,
required this.timezone,
required this.timeDay,
required this.timeNight,
required this.timeDaily,
});
final List<String> time;
final List<String> timeDay;
final List<String> timeNight;
final List<DateTime> timeDaily;
final String district;
final String city;
final List<int> weather;
final List<double> degree;
final String timezone;
@override
State<PlaceCard> createState() => _PlaceCardState();
}
class _PlaceCardState extends State<PlaceCard> {
final statusWeather = StatusWeather();
final statusData = StatusData();
final weatherController = Get.put(WeatherController());
@override
Widget build(BuildContext context) {
final currentTimeIndex = weatherController.getTime(
widget.time,
widget.timezone,
);
final currentDayIndex = weatherController.getDay(
widget.timeDaily,
widget.timezone,
);
return Card(
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
child: Row(
children: [
_buildWeatherInfo(context, currentTimeIndex, currentDayIndex),
const Gap(5),
_buildWeatherImage(currentTimeIndex, currentDayIndex),
],
),
),
);
}
Widget _buildWeatherInfo(
BuildContext context,
int currentTimeIndex,
int currentDayIndex,
) {
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
statusData.getDegree(
widget.degree[currentTimeIndex].round().toInt(),
),
style: context.textTheme.titleLarge?.copyWith(
fontSize: 22,
fontWeight: FontWeight.w600,
),
),
const Gap(7),
Text(
statusWeather.getText(widget.weather[currentTimeIndex]),
style: context.textTheme.titleMedium?.copyWith(
color: Colors.grey,
fontWeight: FontWeight.w400,
),
),
],
),
const Gap(10),
_buildLocationText(),
const Gap(5),
_buildCurrentTimeText(context),
],
),
);
}
Widget _buildLocationText() {
String locationText;
if (widget.district.isEmpty) {
locationText = widget.city;
} else if (widget.city.isEmpty) {
locationText = widget.district;
} else if (widget.city == widget.district) {
locationText = widget.city;
} else {
locationText = '${widget.city}, ${widget.district}';
}
return Text(
locationText,
style: context.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
),
);
}
Widget _buildCurrentTimeText(BuildContext context) {
return StreamBuilder(
stream: Stream.periodic(const Duration(seconds: 1)),
builder: (context, snapshot) {
return Text(
'${'time'.tr}: ${statusData.getTimeFormatTz(tz.TZDateTime.now(tz.getLocation(widget.timezone)))}',
style: context.textTheme.titleMedium?.copyWith(
color: Colors.grey,
fontWeight: FontWeight.w400,
),
);
},
);
}
Widget _buildWeatherImage(int currentTimeIndex, int currentDayIndex) {
return Image.asset(
statusWeather.getImageNow(
widget.weather[currentTimeIndex],
widget.time[currentTimeIndex],
widget.timeDay[currentDayIndex],
widget.timeNight[currentDayIndex],
),
scale: 6.5,
);
}
}

View file

@ -0,0 +1,155 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:rain/app/controller/controller.dart';
import 'package:rain/app/data/db.dart';
import 'package:rain/app/ui/places/view/place_info.dart';
import 'package:rain/app/ui/places/widgets/place_card.dart';
class PlaceCardList extends StatefulWidget {
const PlaceCardList({super.key, required this.searchCity});
final String searchCity;
@override
State<PlaceCardList> createState() => _PlaceCardListState();
}
class _PlaceCardListState extends State<PlaceCardList> {
final weatherController = Get.put(WeatherController());
@override
Widget build(BuildContext context) {
final textTheme = context.textTheme;
final titleMedium = textTheme.titleMedium;
final weatherCards = _filterWeatherCards(
weatherController.weatherCards,
widget.searchCity,
);
return ReorderableListView(
onReorder:
(oldIndex, newIndex) => weatherController.reorder(oldIndex, newIndex),
children: _buildWeatherCardList(
weatherCards,
context,
textTheme,
titleMedium,
),
);
}
List<WeatherCard> _filterWeatherCards(
List<WeatherCard> weatherCards,
String searchCity,
) {
return weatherCards
.where(
(weatherCard) =>
(searchCity.isEmpty ||
weatherCard.city!.toLowerCase().contains(searchCity)),
)
.toList();
}
List<Widget> _buildWeatherCardList(
List<WeatherCard> weatherCards,
BuildContext context,
TextTheme textTheme,
TextStyle? titleMedium,
) {
return weatherCards
.map(
(weatherCardList) => _buildDismissibleCard(
context,
weatherCardList,
textTheme,
titleMedium,
),
)
.toList();
}
Widget _buildDismissibleCard(
BuildContext context,
WeatherCard weatherCardList,
TextTheme textTheme,
TextStyle? titleMedium,
) {
return Dismissible(
key: ValueKey(weatherCardList),
direction: DismissDirection.endToStart,
background: _buildDismissibleBackground(),
confirmDismiss:
(DismissDirection direction) =>
_showDeleteConfirmationDialog(context, textTheme, titleMedium),
onDismissed: (DismissDirection direction) async {
await weatherController.deleteCardWeather(weatherCardList);
},
child: _buildCardGestureDetector(weatherCardList),
);
}
Widget _buildDismissibleBackground() {
return Container(
alignment: Alignment.centerRight,
child: const Padding(
padding: EdgeInsets.only(right: 15),
child: Icon(IconsaxPlusLinear.trash_square, color: Colors.red),
),
);
}
Future<bool> _showDeleteConfirmationDialog(
BuildContext context,
TextTheme textTheme,
TextStyle? titleMedium,
) async {
return await showAdaptiveDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog.adaptive(
title: Text('deletedCardWeather'.tr, style: textTheme.titleLarge),
content: Text('deletedCardWeatherQuery'.tr, style: titleMedium),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: Text(
'cancel'.tr,
style: titleMedium?.copyWith(color: Colors.blueAccent),
),
),
TextButton(
onPressed: () => Get.back(result: true),
child: Text(
'delete'.tr,
style: titleMedium?.copyWith(color: Colors.red),
),
),
],
);
},
);
}
Widget _buildCardGestureDetector(WeatherCard weatherCardList) {
return GestureDetector(
onTap:
() => Get.to(
() => PlaceInfo(weatherCard: weatherCardList),
transition: Transition.downToUp,
),
child: PlaceCard(
time: weatherCardList.time!,
timeDaily: weatherCardList.timeDaily!,
timeDay: weatherCardList.sunrise!,
timeNight: weatherCardList.sunset!,
weather: weatherCardList.weathercode!,
degree: weatherCardList.temperature2M!,
district: weatherCardList.district!,
city: weatherCardList.city!,
timezone: weatherCardList.timezone!,
),
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
class SettingCard extends StatelessWidget {
const SettingCard({
super.key,
required this.icon,
required this.text,
this.switcher = false,
this.dropdown = false,
this.info = false,
this.infoSettings = false,
this.elevation,
this.dropdownName,
this.dropdownList,
this.dropdownChange,
this.value,
this.onPressed,
this.onChange,
this.infoWidget,
});
final Widget icon;
final String text;
final bool switcher;
final bool dropdown;
final bool info;
final bool infoSettings;
final Widget? infoWidget;
final String? dropdownName;
final List<String>? dropdownList;
final ValueChanged<String?>? dropdownChange;
final bool? value;
final VoidCallback? onPressed;
final ValueChanged<bool>? onChange;
final double? elevation;
@override
Widget build(BuildContext context) {
return Card(
elevation: elevation ?? 1,
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: ListTile(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
onTap: onPressed,
leading: icon,
title: Text(
text,
style: context.textTheme.titleMedium,
overflow: TextOverflow.visible,
),
trailing: _buildTrailingWidget(context),
),
);
}
Widget _buildTrailingWidget(BuildContext context) {
if (switcher) {
return _buildSwitchWidget();
} else if (dropdown) {
return _buildDropdownWidget();
} else if (info) {
return _buildInfoWidget();
} else {
return const Icon(IconsaxPlusLinear.arrow_right_3, size: 18);
}
}
Widget _buildSwitchWidget() {
return Transform.scale(
scale: 0.8,
child: Switch(value: value!, onChanged: onChange),
);
}
Widget _buildDropdownWidget() {
return DropdownButton<String>(
icon: const Padding(
padding: EdgeInsets.only(left: 7),
child: Icon(IconsaxPlusLinear.arrow_down),
),
iconSize: 15,
alignment: AlignmentDirectional.centerEnd,
borderRadius: const BorderRadius.all(Radius.circular(15)),
underline: Container(),
value: dropdownName,
items: dropdownList!.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(value: value, child: Text(value));
}).toList(),
onChanged: dropdownChange,
);
}
Widget _buildInfoWidget() {
if (infoSettings) {
return Wrap(
children: [
infoWidget!,
const Icon(IconsaxPlusLinear.arrow_right_3, size: 18),
],
);
} else {
return infoWidget!;
}
}
}

37
lib/app/ui/widgets/button.dart Executable file
View file

@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class MyTextButton extends StatelessWidget {
const MyTextButton({
super.key,
required this.buttonName,
required this.onPressed,
this.height = 50.0,
});
final String buttonName;
final VoidCallback? onPressed;
final double height;
@override
Widget build(BuildContext context) {
return SizedBox(
height: height,
width: double.infinity,
child: ElevatedButton(
style: _buildButtonStyle(context),
onPressed: onPressed,
child: Text(buttonName, style: context.textTheme.titleMedium),
),
);
}
ButtonStyle _buildButtonStyle(BuildContext context) {
return ButtonStyle(
shadowColor: const WidgetStatePropertyAll(Colors.transparent),
backgroundColor: WidgetStatePropertyAll(
context.theme.colorScheme.secondaryContainer.withAlpha(80),
),
);
}
}

View file

@ -3,25 +3,21 @@ import 'package:get/get.dart';
import 'package:shimmer/shimmer.dart';
class MyShimmer extends StatelessWidget {
const MyShimmer({
super.key,
required this.hight,
this.edgeInsetsMargin,
});
final double hight;
final EdgeInsets? edgeInsetsMargin;
const MyShimmer({super.key, required this.height, this.margin});
final double height;
final EdgeInsets? margin;
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: context.theme.cardColor,
highlightColor: context.theme.primaryColor,
child: Card(
margin: edgeInsetsMargin,
child: SizedBox(
height: hight,
),
),
child: _buildShimmerCard(),
);
}
Widget _buildShimmerCard() {
return Card(margin: margin, child: SizedBox(height: height));
}
}

View file

@ -15,6 +15,7 @@ class MyTextForm extends StatelessWidget {
this.focusNode,
this.onChanged,
});
final String labelText;
final TextInputType type;
final Icon icon;
@ -31,23 +32,28 @@ class MyTextForm extends StatelessWidget {
return Card(
elevation: elevation,
margin: margin,
child: TextFormField(
child: _buildTextFormField(context),
);
}
Widget _buildTextFormField(BuildContext context) {
return TextFormField(
focusNode: focusNode,
controller: controller,
keyboardType: type,
style: context.textTheme.labelLarge,
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(
horizontal: 12.5,
vertical: 0,
),
decoration: _buildInputDecoration(),
validator: validator,
onChanged: onChanged,
);
}
InputDecoration _buildInputDecoration() {
return InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 12.5, vertical: 0),
prefixIcon: icon,
suffixIcon: iconButton,
labelText: labelText,
),
validator: validator,
onChanged: onChanged,
),
);
}
}

View file

@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:rain/app/ui/widgets/weather/status/status_weather.dart';
import 'package:rain/app/ui/widgets/weather/status/status_data.dart';
import 'package:rain/main.dart';
class DailyCard extends StatefulWidget {
const DailyCard({
super.key,
required this.timeDaily,
required this.weathercodeDaily,
required this.temperature2MMax,
required this.temperature2MMin,
});
final DateTime timeDaily;
final int? weathercodeDaily;
final double? temperature2MMax;
final double? temperature2MMin;
@override
State<DailyCard> createState() => _DailyCardState();
}
class _DailyCardState extends State<DailyCard> {
final statusWeather = StatusWeather();
final statusData = StatusData();
@override
Widget build(BuildContext context) {
if (widget.weathercodeDaily == null) {
return Container();
}
return Card(
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
child: Row(
children: [
_buildTemperatureInfo(context),
const Gap(5),
_buildWeatherImage(),
],
),
),
);
}
Widget _buildTemperatureInfo(BuildContext context) {
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${statusData.getDegree(widget.temperature2MMin?.round())} / ${statusData.getDegree(widget.temperature2MMax?.round())}',
style: context.textTheme.titleLarge?.copyWith(
fontSize: 22,
fontWeight: FontWeight.w600,
),
),
const Gap(5),
_buildDateText(context),
const Gap(5),
_buildWeatherDescription(context),
],
),
);
}
Widget _buildDateText(BuildContext context) {
return Text(
DateFormat.MMMMEEEEd(locale.languageCode).format(widget.timeDaily),
style: context.textTheme.titleMedium?.copyWith(
color: Colors.grey,
fontWeight: FontWeight.w400,
),
);
}
Widget _buildWeatherDescription(BuildContext context) {
return Text(
statusWeather.getText(widget.weathercodeDaily),
style: context.textTheme.titleMedium?.copyWith(
color: Colors.grey,
fontWeight: FontWeight.w400,
),
);
}
Widget _buildWeatherImage() {
return Image.asset(
statusWeather.getImageNowDaily(widget.weathercodeDaily),
scale: 6.5,
);
}
}

View file

@ -0,0 +1,348 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:intl/intl.dart';
import 'package:rain/app/data/db.dart';
import 'package:rain/app/ui/widgets/weather/desc/desc_container.dart';
import 'package:rain/app/ui/widgets/weather/desc/message.dart';
import 'package:rain/app/ui/widgets/weather/hourly.dart';
import 'package:rain/app/ui/widgets/weather/now.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/app/ui/widgets/weather/sunset_sunrise.dart';
import 'package:rain/main.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
class DailyCardInfo extends StatefulWidget {
const DailyCardInfo({
super.key,
required this.weatherData,
required this.index,
});
final WeatherCard weatherData;
final int index;
@override
State<DailyCardInfo> createState() => _DailyCardInfoState();
}
class _DailyCardInfoState extends State<DailyCardInfo> {
final statusWeather = StatusWeather();
final statusData = StatusData();
final message = Message();
late PageController pageController;
int pageIndex = 0;
int hourOfDay = 0;
@override
void initState() {
pageController = PageController(initialPage: widget.index);
pageIndex = widget.index;
super.initState();
}
@override
void dispose() {
pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final weatherData = widget.weatherData;
final timeDaily = weatherData.timeDaily ?? [];
final textTheme = context.textTheme;
return Scaffold(
appBar: _buildAppBar(context, textTheme, timeDaily),
body: SafeArea(
child: PageView.builder(
controller: pageController,
onPageChanged: (index) {
setState(() {
pageIndex = index;
hourOfDay = 0;
});
},
itemCount: timeDaily.length,
itemBuilder: (context, index) {
return _buildPageContent(context, weatherData, index);
},
),
),
);
}
AppBar _buildAppBar(
BuildContext context,
TextTheme textTheme,
List<DateTime> timeDaily,
) {
return AppBar(
automaticallyImplyLeading: false,
centerTitle: true,
leading: IconButton(
onPressed: () => Get.back(),
icon: const Icon(IconsaxPlusLinear.arrow_left_3, size: 20),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
title: Text(
DateFormat.MMMMEEEEd(locale.languageCode).format(timeDaily[pageIndex]),
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
);
}
Widget _buildPageContent(
BuildContext context,
WeatherCard weatherData,
int index,
) {
final weatherCodeDaily = weatherData.weathercodeDaily?[index];
if (weatherCodeDaily == null) {
return Container();
}
final startIndex = index * 24;
final temperature2MMin = weatherData.temperature2MMin?[index];
final temperature2MMax = weatherData.temperature2MMax?[index];
final apparentTemperatureMin = weatherData.apparentTemperatureMin?[index];
final apparentTemperatureMax = weatherData.apparentTemperatureMax?[index];
final uvIndexMax = weatherData.uvIndexMax?[index];
final windDirection10MDominant =
weatherData.winddirection10MDominant?[index];
final windSpeed10MMax = weatherData.windspeed10MMax?[index];
final windGusts10MMax = weatherData.windgusts10MMax?[index];
final precipitationProbabilityMax =
weatherData.precipitationProbabilityMax?[index];
final rainSum = weatherData.rainSum?[index];
final precipitationSum = weatherData.precipitationSum?[index];
final sunrise = weatherData.sunrise?[index];
final sunset = weatherData.sunset?[index];
if (sunrise == null || sunset == null) {
return Container();
}
return Container(
margin: const EdgeInsets.symmetric(horizontal: 10),
child: ListView(
children: [
_buildNowWidget(
weatherData,
index,
startIndex,
hourOfDay,
sunrise,
sunset,
),
_buildHourlyList(context, weatherData, startIndex, sunrise, sunset),
_buildSunsetSunriseWidget(sunrise, sunset),
_buildHourlyDescContainer(weatherData, startIndex, hourOfDay),
_buildDailyDescContainer(
weatherData,
temperature2MMin,
temperature2MMax,
apparentTemperatureMin,
apparentTemperatureMax,
uvIndexMax,
windDirection10MDominant,
windSpeed10MMax,
windGusts10MMax,
precipitationProbabilityMax,
rainSum,
precipitationSum,
),
],
),
);
}
Widget _buildNowWidget(
WeatherCard weatherData,
int index,
int startIndex,
int hourOfDay,
String sunrise,
String sunset,
) {
final weatherCode = weatherData.weathercode?[startIndex + hourOfDay];
final temperature = weatherData.temperature2M?[startIndex + hourOfDay];
final feels = weatherData.apparentTemperature?[startIndex + hourOfDay];
final time = weatherData.time?[startIndex + hourOfDay];
final tempMax = weatherData.temperature2MMax?[index];
final tempMin = weatherData.temperature2MMin?[index];
if (weatherCode == null ||
temperature == null ||
feels == null ||
time == null ||
tempMax == null ||
tempMin == null) {
return Container();
}
return Now(
weather: weatherCode,
degree: temperature,
feels: feels,
time: time,
timeDay: sunrise,
timeNight: sunset,
tempMax: tempMax,
tempMin: tempMin,
);
}
Widget _buildHourlyList(
BuildContext context,
WeatherCard weatherData,
int startIndex,
String sunrise,
String sunset,
) {
return Card(
margin: const EdgeInsets.only(bottom: 15),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: SizedBox(
height: 135,
child: ScrollablePositionedList.separated(
separatorBuilder: (BuildContext context, int index) {
return const VerticalDivider(
width: 10,
indent: 40,
endIndent: 40,
);
},
scrollDirection: Axis.horizontal,
itemCount: 24,
itemBuilder: (ctx, i) {
return _buildHourlyItem(
context,
weatherData,
startIndex,
i,
sunrise,
sunset,
);
},
),
),
),
);
}
Widget _buildHourlyItem(
BuildContext context,
WeatherCard weatherData,
int startIndex,
int i,
String sunrise,
String sunset,
) {
int hourlyIndex = startIndex + i;
bool isSelected = i == hourOfDay;
final time = weatherData.time?[hourlyIndex];
final weatherCode = weatherData.weathercode?[hourlyIndex];
final temperature = weatherData.temperature2M?[hourlyIndex];
if (time == null || weatherCode == null || temperature == null) {
return Container();
}
return GestureDetector(
onTap: () {
setState(() {
hourOfDay = i;
});
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
decoration: BoxDecoration(
color: isSelected
? context.theme.colorScheme.secondaryContainer
: Colors.transparent,
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
child: Hourly(
time: time,
weather: weatherCode,
degree: temperature,
timeDay: sunrise,
timeNight: sunset,
),
),
);
}
Widget _buildSunsetSunriseWidget(String sunrise, String sunset) {
return SunsetSunrise(timeSunrise: sunrise, timeSunset: sunset);
}
Widget _buildHourlyDescContainer(
WeatherCard weatherData,
int startIndex,
int hourOfDay,
) {
final hourlyIndex = startIndex + hourOfDay;
return DescContainer(
humidity: weatherData.relativehumidity2M?[hourlyIndex],
wind: weatherData.windspeed10M?[hourlyIndex],
visibility: weatherData.visibility?[hourlyIndex],
feels: weatherData.apparentTemperature?[hourlyIndex],
evaporation: weatherData.evapotranspiration?[hourlyIndex],
precipitation: weatherData.precipitation?[hourlyIndex],
direction: weatherData.winddirection10M?[hourlyIndex],
pressure: weatherData.surfacePressure?[hourlyIndex],
rain: weatherData.rain?[hourlyIndex],
cloudcover: weatherData.cloudcover?[hourlyIndex],
windgusts: weatherData.windgusts10M?[hourlyIndex],
uvIndex: weatherData.uvIndex?[hourlyIndex],
dewpoint2M: weatherData.dewpoint2M?[hourlyIndex],
precipitationProbability:
weatherData.precipitationProbability?[hourlyIndex],
shortwaveRadiation: weatherData.shortwaveRadiation?[hourlyIndex],
initiallyExpanded: true,
title: 'hourlyVariables'.tr,
);
}
Widget _buildDailyDescContainer(
WeatherCard weatherData,
double? temperature2MMin,
double? temperature2MMax,
double? apparentTemperatureMin,
double? apparentTemperatureMax,
double? uvIndexMax,
int? windDirection10MDominant,
double? windSpeed10MMax,
double? windGusts10MMax,
int? precipitationProbabilityMax,
double? rainSum,
double? precipitationSum,
) {
return DescContainer(
apparentTemperatureMin: apparentTemperatureMin,
apparentTemperatureMax: apparentTemperatureMax,
uvIndexMax: uvIndexMax,
windDirection10MDominant: windDirection10MDominant,
windSpeed10MMax: windSpeed10MMax,
windGusts10MMax: windGusts10MMax,
precipitationProbabilityMax: precipitationProbabilityMax,
rainSum: rainSum,
precipitationSum: precipitationSum,
initiallyExpanded: true,
title: 'dailyVariables'.tr,
);
}
}

View file

@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:rain/app/data/db.dart';
import 'package:rain/app/ui/widgets/weather/daily/daily_card_info.dart';
import 'package:rain/app/ui/widgets/weather/daily/daily_card.dart';
class DailyCardList extends StatefulWidget {
const DailyCardList({super.key, required this.weatherData});
final WeatherCard weatherData;
@override
State<DailyCardList> createState() => _DailyCardListState();
}
class _DailyCardListState extends State<DailyCardList> {
@override
Widget build(BuildContext context) {
final weatherData = widget.weatherData;
final timeDaily = weatherData.timeDaily ?? [];
return Scaffold(
appBar: _buildAppBar(context),
body: SafeArea(
child: ListView.builder(
itemCount: timeDaily.length,
itemBuilder: (context, index) =>
_buildDailyCardItem(context, weatherData, index),
),
),
);
}
AppBar _buildAppBar(BuildContext context) {
return AppBar(
automaticallyImplyLeading: false,
centerTitle: true,
leading: IconButton(
onPressed: () => Get.back(),
icon: const Icon(IconsaxPlusLinear.arrow_left_3, size: 20),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
title: Text(
'weatherMore'.tr,
style: context.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
);
}
Widget _buildDailyCardItem(
BuildContext context,
WeatherCard weatherData,
int index,
) {
final timeDaily = weatherData.timeDaily?[index];
final weathercodeDaily = weatherData.weathercodeDaily?[index];
final temperature2MMax = weatherData.temperature2MMax?[index];
final temperature2MMin = weatherData.temperature2MMin?[index];
if (timeDaily == null ||
weathercodeDaily == null ||
temperature2MMax == null ||
temperature2MMin == null) {
return Container();
}
return GestureDetector(
onTap: () => Get.to(
() => DailyCardInfo(weatherData: weatherData, index: index),
transition: Transition.downToUp,
),
child: DailyCard(
timeDaily: timeDaily,
weathercodeDaily: weathercodeDaily,
temperature2MMax: temperature2MMax,
temperature2MMin: temperature2MMin,
),
);
}
}

View file

@ -0,0 +1,206 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:rain/app/data/db.dart';
import 'package:rain/app/ui/widgets/weather/daily/daily_card_info.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';
class DailyContainer extends StatefulWidget {
const DailyContainer({
super.key,
required this.weatherData,
required this.onTap,
});
final WeatherCard weatherData;
final VoidCallback onTap;
@override
State<DailyContainer> createState() => _DailyContainerState();
}
class _DailyContainerState extends State<DailyContainer> {
final statusWeather = StatusWeather();
final statusData = StatusData();
@override
Widget build(BuildContext context) {
final splashColor = context.theme.colorScheme.primary.withValues(
alpha: 0.4,
);
const inkWellBorderRadius = BorderRadius.all(Radius.circular(16));
final weatherData = widget.weatherData;
final weatherCodeDaily = weatherData.weathercodeDaily ?? [];
final textTheme = context.textTheme;
final labelLarge = textTheme.labelLarge;
return Card(
margin: const EdgeInsets.only(bottom: 15),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 5),
child: Column(
children: [
_buildDailyListView(
context,
weatherData,
weatherCodeDaily,
labelLarge,
),
const Divider(),
_buildMoreInfoButton(context, splashColor, inkWellBorderRadius),
],
),
),
);
}
Widget _buildDailyListView(
BuildContext context,
WeatherCard weatherData,
List<int?> weatherCodeDaily,
TextStyle? labelLarge,
) {
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: 7,
itemBuilder: (ctx, index) {
return _buildDailyItem(
context,
weatherData,
weatherCodeDaily,
index,
labelLarge,
);
},
);
}
Widget _buildDailyItem(
BuildContext context,
WeatherCard weatherData,
List<int?> weatherCodeDaily,
int index,
TextStyle? labelLarge,
) {
final splashColor = context.theme.colorScheme.primary.withValues(
alpha: 0.4,
);
const inkWellBorderRadius = BorderRadius.all(Radius.circular(16));
return InkWell(
splashColor: splashColor,
borderRadius: inkWellBorderRadius,
onTap: () => Get.to(
() => DailyCardInfo(weatherData: weatherData, index: index),
transition: Transition.downToUp,
),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildDayText(weatherData, index, labelLarge),
_buildWeatherInfo(weatherCodeDaily, index, labelLarge),
_buildTemperatureRange(weatherData, index, labelLarge),
],
),
),
);
}
Widget _buildDayText(
WeatherCard weatherData,
int index,
TextStyle? labelLarge,
) {
return Expanded(
child: Text(
DateFormat.EEEE(
locale.languageCode,
).format((weatherData.timeDaily ?? [])[index]),
style: labelLarge,
overflow: TextOverflow.ellipsis,
),
);
}
Widget _buildWeatherInfo(
List<int?> weatherCodeDaily,
int index,
TextStyle? labelLarge,
) {
final weatherCode = weatherCodeDaily[index];
if (weatherCode == null) {
return const Expanded(child: SizedBox.shrink());
}
return Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(statusWeather.getImage7Day(weatherCode), scale: 3),
const Gap(5),
Expanded(
child: Text(
statusWeather.getText(weatherCode),
style: labelLarge,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
Widget _buildTemperatureRange(
WeatherCard weatherData,
int index,
TextStyle? labelLarge,
) {
return Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
statusData.getDegree(
(weatherData.temperature2MMax ?? [])[index]?.round(),
),
style: labelLarge,
),
Text(' / ', style: labelLarge),
Text(
statusData.getDegree(
(weatherData.temperature2MMin ?? [])[index]?.round(),
),
style: labelLarge,
),
],
),
);
}
Widget _buildMoreInfoButton(
BuildContext context,
Color splashColor,
BorderRadius inkWellBorderRadius,
) {
return InkWell(
splashColor: splashColor,
borderRadius: inkWellBorderRadius,
onTap: widget.onTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Text(
'weatherMore'.tr,
style: context.textTheme.titleMedium,
overflow: TextOverflow.ellipsis,
),
),
);
}
}

View file

@ -10,6 +10,7 @@ class DescWeather extends StatefulWidget {
required this.desc,
this.message = '',
});
final String imageName;
final String value;
final String desc;
@ -26,23 +27,32 @@ class _DescWeatherState extends State<DescWeather> {
Widget build(BuildContext context) {
final textTheme = context.textTheme;
return GestureDetector(
onTap: () => setState(() => hide = !hide),
onTap: _toggleDescriptionVisibility,
child: Tooltip(
message: widget.message,
child: SizedBox(
height: 90,
width: 100,
child: Column(
child: _buildContent(textTheme),
),
),
);
}
void _toggleDescriptionVisibility() {
setState(() => hide = !hide);
}
Widget _buildContent(TextTheme textTheme) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
widget.imageName,
scale: 20,
),
Image.asset(widget.imageName, scale: 20),
const Gap(5),
Text(
widget.value,
style: textTheme.labelLarge,
overflow: TextOverflow.ellipsis,
),
Expanded(
child: Text(
@ -53,9 +63,6 @@ class _DescWeatherState extends State<DescWeather> {
),
),
],
),
),
),
);
}
}

View file

@ -0,0 +1,263 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:rain/app/ui/widgets/weather/desc/desc.dart';
import 'package:rain/app/ui/widgets/weather/desc/message.dart';
import 'package:rain/app/ui/widgets/weather/status/status_data.dart';
class DescContainer extends StatefulWidget {
const DescContainer({
super.key,
this.humidity,
this.wind,
this.visibility,
this.feels,
this.evaporation,
this.precipitation,
this.direction,
this.pressure,
this.rain,
this.cloudcover,
this.windgusts,
this.uvIndex,
this.dewpoint2M,
this.precipitationProbability,
this.shortwaveRadiation,
this.apparentTemperatureMin,
this.apparentTemperatureMax,
this.uvIndexMax,
this.windDirection10MDominant,
this.windSpeed10MMax,
this.windGusts10MMax,
this.precipitationProbabilityMax,
this.rainSum,
this.precipitationSum,
required this.initiallyExpanded,
required this.title,
});
final int? humidity;
final double? wind;
final double? visibility;
final double? feels;
final double? evaporation;
final double? precipitation;
final int? direction;
final double? pressure;
final double? rain;
final int? cloudcover;
final double? windgusts;
final double? uvIndex;
final double? dewpoint2M;
final int? precipitationProbability;
final double? shortwaveRadiation;
final double? apparentTemperatureMin;
final double? apparentTemperatureMax;
final double? uvIndexMax;
final int? windDirection10MDominant;
final double? windSpeed10MMax;
final double? windGusts10MMax;
final int? precipitationProbabilityMax;
final double? rainSum;
final double? precipitationSum;
final bool initiallyExpanded;
final String title;
@override
State<DescContainer> createState() => _DescContainerState();
}
class _DescContainerState extends State<DescContainer> {
final statusData = StatusData();
final message = Message();
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 15),
child: ExpansionTile(
shape: const Border(),
title: Text(widget.title, style: context.textTheme.labelLarge),
initiallyExpanded: widget.initiallyExpanded,
children: [
Padding(
padding: const EdgeInsets.only(top: 20, bottom: 5),
child: Wrap(
alignment: WrapAlignment.spaceEvenly,
spacing: 5,
children: _buildWeatherDescriptions(context),
),
),
],
),
);
}
List<Widget> _buildWeatherDescriptions(BuildContext context) {
final List<Widget> descriptions = [];
void addDescriptionIfNotNull({
required dynamic value,
required String imageName,
required String desc,
String? message,
}) {
if (value != null &&
value != '' &&
value != 'null°C' &&
value != 'null°F' &&
value != 'null°' &&
value != 'null%' &&
value != 'null ${'W/m2'.tr}') {
descriptions.add(
DescWeather(
imageName: imageName,
value: value.toString(),
desc: desc,
message: message ?? '',
),
);
} else {
descriptions.add(Container());
}
}
final weatherData = [
{
'value': statusData.getDegree(widget.apparentTemperatureMin?.round()),
'imageName': 'assets/images/cold.png',
'desc': 'apparentTemperatureMin'.tr,
},
{
'value': statusData.getDegree(widget.apparentTemperatureMax?.round()),
'imageName': 'assets/images/hot.png',
'desc': 'apparentTemperatureMax'.tr,
},
{
'value': widget.uvIndexMax?.round(),
'imageName': 'assets/images/uv.png',
'desc': 'uvIndex'.tr,
'message': message.getUvIndex(widget.uvIndexMax?.round()),
},
{
'value': '${widget.windDirection10MDominant}°',
'imageName': 'assets/images/windsock.png',
'desc': 'direction'.tr,
'message': message.getDirection(widget.windDirection10MDominant),
},
{
'value': statusData.getSpeed(widget.windSpeed10MMax?.round()),
'imageName': 'assets/images/wind.png',
'desc': 'wind'.tr,
},
{
'value': statusData.getSpeed(widget.windGusts10MMax?.round()),
'imageName': 'assets/images/windgusts.png',
'desc': 'windgusts'.tr,
},
{
'value': '${widget.precipitationProbabilityMax}%',
'imageName': 'assets/images/precipitation_probability.png',
'desc': 'precipitationProbability'.tr,
},
{
'value': statusData.getPrecipitation(widget.rainSum),
'imageName': 'assets/images/water.png',
'desc': 'rain'.tr,
},
{
'value': statusData.getPrecipitation(widget.precipitationSum),
'imageName': 'assets/images/rainfall.png',
'desc': 'precipitation'.tr,
},
{
'value': statusData.getDegree(widget.dewpoint2M?.round()),
'imageName': 'assets/images/dew.png',
'desc': 'dewpoint'.tr,
},
{
'value': statusData.getDegree(widget.feels?.round()),
'imageName': 'assets/images/temperature.png',
'desc': 'feels'.tr,
},
{
'value': statusData.getVisibility(widget.visibility),
'imageName': 'assets/images/fog.png',
'desc': 'visibility'.tr,
},
{
'value': '${widget.direction}°',
'imageName': 'assets/images/windsock.png',
'desc': 'direction'.tr,
'message': message.getDirection(widget.direction),
},
{
'value': statusData.getSpeed(widget.wind?.round()),
'imageName': 'assets/images/wind.png',
'desc': 'wind'.tr,
},
{
'value': statusData.getSpeed(widget.windgusts?.round()),
'imageName': 'assets/images/windgusts.png',
'desc': 'windgusts'.tr,
},
{
'value': statusData.getPrecipitation(widget.evaporation?.abs()),
'imageName': 'assets/images/evaporation.png',
'desc': 'evaporation'.tr,
},
{
'value': statusData.getPrecipitation(widget.precipitation),
'imageName': 'assets/images/rainfall.png',
'desc': 'precipitation'.tr,
},
{
'value': statusData.getPrecipitation(widget.rain),
'imageName': 'assets/images/water.png',
'desc': 'rain'.tr,
},
{
'value': '${widget.precipitationProbability}%',
'imageName': 'assets/images/precipitation_probability.png',
'desc': 'precipitationProbability'.tr,
},
{
'value': '${widget.humidity}%',
'imageName': 'assets/images/humidity.png',
'desc': 'humidity'.tr,
},
{
'value': '${widget.cloudcover}%',
'imageName': 'assets/images/cloudy.png',
'desc': 'cloudcover'.tr,
},
{
'value': statusData.getPressure(widget.pressure?.round()),
'imageName': 'assets/images/atmospheric.png',
'desc': 'pressure'.tr,
'message': message.getPressure(widget.pressure?.round()),
},
{
'value': widget.uvIndex?.round(),
'imageName': 'assets/images/uv.png',
'desc': 'uvIndex'.tr,
'message': message.getUvIndex(widget.uvIndex?.round()),
},
{
'value': '${widget.shortwaveRadiation?.round()} ${'W/m2'.tr}',
'imageName': 'assets/images/shortwave_radiation.png',
'desc': 'shortwaveRadiation'.tr,
},
];
for (var data in weatherData) {
addDescriptionIfNotNull(
value: data['value'],
imageName: '${data['imageName']}',
desc: '${data['desc']}',
message: '${data['message']}',
);
}
return descriptions;
}
}

View file

@ -0,0 +1,65 @@
import 'package:get/get.dart';
class Message {
String getPressure(int? pressure) {
return _getPressureDescription(pressure);
}
String getUvIndex(int? uvIndex) {
return _getUvIndexDescription(uvIndex);
}
String getDirection(int? direction) {
return _getDirectionDescription(direction);
}
String _getPressureDescription(int? pressure) {
if (pressure == null) return '';
if (pressure < 1000) {
return 'low'.tr;
} else if (pressure > 1020) {
return 'high'.tr;
} else {
return 'normal'.tr;
}
}
String _getUvIndexDescription(int? uvIndex) {
if (uvIndex == null) return '';
if (uvIndex < 3) {
return 'uvLow'.tr;
} else if (uvIndex < 6) {
return 'uvAverage'.tr;
} else if (uvIndex < 8) {
return 'uvHigh'.tr;
} else if (uvIndex < 11) {
return 'uvVeryHigh'.tr;
} else {
return 'uvExtreme'.tr;
}
}
String _getDirectionDescription(int? direction) {
if (direction == null) return '';
if (direction >= 337.5 || direction < 22.5) {
return 'north'.tr;
} else if (direction >= 22.5 && direction < 67.5) {
return 'northeast'.tr;
} else if (direction >= 67.5 && direction < 112.5) {
return 'east'.tr;
} else if (direction >= 112.5 && direction < 157.5) {
return 'southeast'.tr;
} else if (direction >= 157.5 && direction < 202.5) {
return 'south'.tr;
} else if (direction >= 202.5 && direction < 247.5) {
return 'southwest'.tr;
} else if (direction >= 247.5 && direction < 292.5) {
return 'west'.tr;
} else {
return 'northwest'.tr;
}
}
}

View file

@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.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';
class Hourly extends StatefulWidget {
const Hourly({
super.key,
required this.time,
required this.weather,
required this.degree,
required this.timeDay,
required this.timeNight,
});
final String time;
final String timeDay;
final String timeNight;
final int weather;
final double degree;
@override
State<Hourly> createState() => _HourlyState();
}
class _HourlyState extends State<Hourly> {
final statusWeather = StatusWeather();
final statusData = StatusData();
@override
Widget build(BuildContext context) {
final textTheme = context.textTheme;
final time = widget.time;
return Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildTimeText(textTheme, time),
_buildWeatherImage(),
_buildTemperatureText(textTheme),
],
);
}
Widget _buildTimeText(TextTheme textTheme, String time) {
return Column(
children: [
Text(statusData.getTimeFormat(time), style: textTheme.labelLarge),
Text(
DateFormat('E', locale.languageCode).format(DateTime.tryParse(time)!),
style: textTheme.labelLarge?.copyWith(color: Colors.grey),
),
],
);
}
Widget _buildWeatherImage() {
return Image.asset(
statusWeather.getImageToday(
widget.weather,
widget.time,
widget.timeDay,
widget.timeNight,
),
scale: 3,
);
}
Widget _buildTemperatureText(TextTheme textTheme) {
return Text(
statusData.getDegree(widget.degree.round()),
style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
);
}
}

View file

@ -0,0 +1,182 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:intl/intl.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';
class Now extends StatefulWidget {
const Now({
super.key,
required this.weather,
required this.degree,
required this.time,
required this.timeDay,
required this.timeNight,
required this.tempMax,
required this.tempMin,
required this.feels,
});
final String time;
final String timeDay;
final String timeNight;
final int weather;
final double degree;
final double tempMax;
final double tempMin;
final double feels;
@override
State<Now> createState() => _NowState();
}
class _NowState extends State<Now> {
final statusWeather = StatusWeather();
final statusData = StatusData();
@override
Widget build(BuildContext context) {
return largeElement
? _buildLargeElementLayout(context)
: _buildCompactElementLayout(context);
}
Widget _buildLargeElementLayout(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 15),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Gap(15),
_buildWeatherImage(200),
_buildTemperatureText(context, widget.degree, 90),
Text(
statusWeather.getText(widget.weather),
style: context.textTheme.titleLarge,
),
const Gap(5),
_buildDateText(context),
],
),
);
}
Widget _buildCompactElementLayout(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 15),
child: Padding(
padding: const EdgeInsets.only(
top: 18,
bottom: 18,
left: 25,
right: 15,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDateText(context),
const Gap(5),
Text(
statusWeather.getText(widget.weather),
style: context.textTheme.titleLarge?.copyWith(fontSize: 20),
),
_buildFeelsLikeText(context),
const Gap(30),
_buildTemperatureCompactText(context, widget.degree),
const Gap(5),
_buildMinMaxTemperatureText(context),
],
),
),
_buildWeatherImage(140),
],
),
),
);
}
Widget _buildWeatherImage(double height) {
return Image(
image: AssetImage(
statusWeather.getImageNow(
widget.weather,
widget.time,
widget.timeDay,
widget.timeNight,
),
),
fit: BoxFit.fill,
height: height,
);
}
Widget _buildTemperatureText(
BuildContext context,
double degree,
double? fontSize,
) {
return Text(
'${roundDegree ? degree.round() : degree}',
style: context.textTheme.displayLarge?.copyWith(
fontSize: fontSize,
fontWeight: FontWeight.w800,
shadows: const [Shadow(blurRadius: 15, offset: Offset(5, 5))],
),
);
}
Widget _buildTemperatureCompactText(BuildContext context, double degree) {
return Text(
statusData.getDegree(roundDegree ? widget.degree.round() : widget.degree),
style: context.textTheme.displayMedium?.copyWith(
fontWeight: FontWeight.w800,
),
);
}
Widget _buildDateText(BuildContext context) {
return Text(
DateFormat.MMMMEEEEd(
locale.languageCode,
).format(DateTime.parse(widget.time)),
style: context.textTheme.labelLarge?.copyWith(color: Colors.grey),
);
}
Widget _buildFeelsLikeText(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('feels'.tr, style: context.textTheme.bodyMedium),
Text('', style: context.textTheme.bodyMedium),
Text(
statusData.getDegree(widget.feels.round()),
style: context.textTheme.bodyMedium,
),
],
);
}
Widget _buildMinMaxTemperatureText(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
statusData.getDegree((widget.tempMin.round())),
style: context.textTheme.labelLarge,
),
Text(' / ', style: context.textTheme.labelLarge),
Text(
statusData.getDegree((widget.tempMax.round())),
style: context.textTheme.labelLarge,
),
],
);
}
}

View file

@ -0,0 +1,127 @@
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:rain/main.dart';
import 'package:timezone/timezone.dart';
class StatusData {
String getDegree(dynamic degree) {
return _formatDegree(degree);
}
String getSpeed(int? speed) {
return _formatSpeed(speed);
}
String getPressure(int? pressure) {
return _formatPressure(pressure);
}
String getVisibility(double? length) {
return _formatVisibility(length);
}
String getPrecipitation(double? precipitation) {
return _formatPrecipitation(precipitation);
}
String getTimeFormat(String time) {
return _formatTime(time);
}
String getTimeFormatTz(TZDateTime time) {
return _formatTimeTz(time);
}
String _formatDegree(dynamic degree) {
switch (settings.degrees) {
case 'celsius':
return '$degree°C';
case 'fahrenheit':
return '$degree°F';
default:
return '$degree°C';
}
}
String _formatSpeed(int? speed) {
if (speed == null) return '';
switch (settings.measurements) {
case 'metric':
return settings.wind == 'm/s'
? '${(speed * (5 / 18)).toPrecision(1)} ${'m/s'.tr}'
: '$speed ${'kph'.tr}';
case 'imperial':
return '$speed ${'mph'.tr}';
default:
return '$speed ${'kph'.tr}';
}
}
String _formatPressure(int? pressure) {
if (pressure == null) return '';
return settings.pressure == 'mmHg'
? '${(pressure * (3 / 4)).toPrecision(1)} ${'mmHg'.tr}'
: '$pressure ${'hPa'.tr}';
}
String _formatVisibility(double? length) {
if (length == null) return '';
switch (settings.measurements) {
case 'metric':
return _formatMetricVisibility(length);
case 'imperial':
return _formatImperialVisibility(length);
default:
return _formatMetricVisibility(length);
}
}
String _formatMetricVisibility(double length) {
return '${length > 1000 ? (length / 1000).round() : (length / 1000).toStringAsFixed(2)} ${'km'.tr}';
}
String _formatImperialVisibility(double length) {
return '${length > 5280 ? (length / 5280).round() : (length / 5280).toStringAsFixed(2)} ${'mi'.tr}';
}
String _formatPrecipitation(double? precipitation) {
if (precipitation == null) return '';
switch (settings.measurements) {
case 'metric':
return '$precipitation ${'mm'.tr}';
case 'imperial':
return '$precipitation ${'inch'.tr}';
default:
return '$precipitation ${'mm'.tr}';
}
}
String _formatTime(String time) {
final parsedTime = DateTime.tryParse(time);
if (parsedTime == null) return '';
switch (settings.timeformat) {
case '12':
return DateFormat.jm(locale.languageCode).format(parsedTime);
case '24':
return DateFormat.Hm(locale.languageCode).format(parsedTime);
default:
return DateFormat.Hm(locale.languageCode).format(parsedTime);
}
}
String _formatTimeTz(TZDateTime time) {
switch (settings.timeformat) {
case '12':
return DateFormat.jm(locale.languageCode).format(time);
case '24':
return DateFormat.Hm(locale.languageCode).format(time);
default:
return DateFormat.Hm(locale.languageCode).format(time);
}
}
}

View file

@ -0,0 +1,385 @@
import 'package:get/get.dart';
const assetImageRoot = 'assets/images/';
class StatusWeather {
String getImageNow(
int weather,
String time,
String timeDay,
String timeNight,
) {
return _getImageBasedOnTime(
weather,
time,
timeDay,
timeNight,
_getDayNightImagePaths,
);
}
String getImageNowDaily(int? weather) {
return _getDailyImage(weather);
}
String getImageToday(
int weather,
String time,
String timeDay,
String timeNight,
) {
return _getImageBasedOnTime(
weather,
time,
timeDay,
timeNight,
_getTodayImagePaths,
);
}
String getImage7Day(int? weather) {
return _getDailyImage(weather, isDay: true);
}
String getText(int? weather) {
return _getWeatherText(weather);
}
String getImageNotification(
int weather,
String time,
String timeDay,
String timeNight,
) {
return _getImageBasedOnTime(
weather,
time,
timeDay,
timeNight,
_getNotificationImagePaths,
);
}
String _getImageBasedOnTime(
int weather,
String time,
String timeDay,
String timeNight,
Map<int, Map<bool, String>> imagePaths,
) {
final currentTime = DateTime.parse(time);
final day = DateTime.parse(timeDay);
final night = DateTime.parse(timeNight);
final dayTime = DateTime(
day.year,
day.month,
day.day,
day.hour,
day.minute,
);
final nightTime = DateTime(
night.year,
night.month,
night.day,
night.hour,
night.minute,
);
final isDayTime =
currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime);
return imagePaths[weather]?[isDayTime] ?? '';
}
String _getDailyImage(int? weather, {bool isDay = false}) {
switch (weather) {
case 0:
return '$assetImageRoot${isDay ? 'clear_day' : 'sun'}.png';
case 1:
case 2:
case 3:
return '$assetImageRoot${isDay ? 'cloudy_day' : 'cloud'}.png';
case 45:
case 48:
return '${assetImageRoot}fog${isDay ? '_day' : ''}.png';
case 51:
case 53:
case 55:
case 56:
case 57:
case 61:
case 63:
case 65:
case 66:
case 67:
case 80:
case 81:
case 82:
return '${assetImageRoot}rain${isDay ? '_day' : ''}.png';
case 71:
case 73:
case 75:
case 77:
case 85:
case 86:
return '${assetImageRoot}snow${isDay ? '_day' : ''}.png';
case 95:
case 96:
case 99:
return '${assetImageRoot}thunder${isDay ? '_day' : ''}.png';
default:
return '';
}
}
String _getWeatherText(int? weather) {
switch (weather) {
case 0:
return 'clear_sky'.tr;
case 1:
case 2:
return 'cloudy'.tr;
case 3:
return 'overcast'.tr;
case 45:
case 48:
return 'fog'.tr;
case 51:
case 53:
case 55:
return 'drizzle'.tr;
case 56:
case 57:
return 'drizzling_rain'.tr;
case 61:
case 63:
case 65:
return 'rain'.tr;
case 66:
case 67:
return 'freezing_rain'.tr;
case 80:
case 81:
case 82:
return 'heavy_rains'.tr;
case 71:
case 73:
case 75:
case 77:
case 85:
case 86:
return 'snow'.tr;
case 95:
case 96:
case 99:
return 'thunderstorm'.tr;
default:
return '';
}
}
final Map<int, Map<bool, String>> _getDayNightImagePaths = {
0: {
true: '${assetImageRoot}sun.png',
false: '${assetImageRoot}full-moon.png',
},
1: {true: '${assetImageRoot}cloud.png', false: '${assetImageRoot}moon.png'},
2: {true: '${assetImageRoot}cloud.png', false: '${assetImageRoot}moon.png'},
3: {true: '${assetImageRoot}cloud.png', false: '${assetImageRoot}moon.png'},
45: {
true: '${assetImageRoot}fog.png',
false: '${assetImageRoot}fog_moon.png',
},
48: {
true: '${assetImageRoot}fog.png',
false: '${assetImageRoot}fog_moon.png',
},
51: {true: '${assetImageRoot}rain.png', false: '${assetImageRoot}rain.png'},
53: {true: '${assetImageRoot}rain.png', false: '${assetImageRoot}rain.png'},
55: {true: '${assetImageRoot}rain.png', false: '${assetImageRoot}rain.png'},
56: {true: '${assetImageRoot}rain.png', false: '${assetImageRoot}rain.png'},
57: {true: '${assetImageRoot}rain.png', false: '${assetImageRoot}rain.png'},
61: {true: '${assetImageRoot}rain.png', false: '${assetImageRoot}rain.png'},
63: {true: '${assetImageRoot}rain.png', false: '${assetImageRoot}rain.png'},
65: {true: '${assetImageRoot}rain.png', false: '${assetImageRoot}rain.png'},
66: {true: '${assetImageRoot}rain.png', false: '${assetImageRoot}rain.png'},
67: {true: '${assetImageRoot}rain.png', false: '${assetImageRoot}rain.png'},
80: {
true: '${assetImageRoot}rain-fall.png',
false: '${assetImageRoot}rain-fall.png',
},
81: {
true: '${assetImageRoot}rain-fall.png',
false: '${assetImageRoot}rain-fall.png',
},
82: {
true: '${assetImageRoot}rain-fall.png',
false: '${assetImageRoot}rain-fall.png',
},
71: {true: '${assetImageRoot}snow.png', false: '${assetImageRoot}snow.png'},
73: {true: '${assetImageRoot}snow.png', false: '${assetImageRoot}snow.png'},
75: {true: '${assetImageRoot}snow.png', false: '${assetImageRoot}snow.png'},
77: {true: '${assetImageRoot}snow.png', false: '${assetImageRoot}snow.png'},
85: {true: '${assetImageRoot}snow.png', false: '${assetImageRoot}snow.png'},
86: {true: '${assetImageRoot}snow.png', false: '${assetImageRoot}snow.png'},
95: {
true: '${assetImageRoot}thunder.png',
false: '${assetImageRoot}thunder.png',
},
96: {
true: '${assetImageRoot}storm.png',
false: '${assetImageRoot}storm.png',
},
99: {
true: '${assetImageRoot}storm.png',
false: '${assetImageRoot}storm.png',
},
};
final Map<int, Map<bool, String>> _getTodayImagePaths = {
0: {
true: '${assetImageRoot}clear_day.png',
false: '${assetImageRoot}clear_night.png',
},
1: {
true: '${assetImageRoot}cloudy_day.png',
false: '${assetImageRoot}cloudy_night.png',
},
2: {
true: '${assetImageRoot}cloudy_day.png',
false: '${assetImageRoot}cloudy_night.png',
},
3: {
true: '${assetImageRoot}cloudy_day.png',
false: '${assetImageRoot}cloudy_night.png',
},
45: {
true: '${assetImageRoot}fog_day.png',
false: '${assetImageRoot}fog_night.png',
},
48: {
true: '${assetImageRoot}fog_day.png',
false: '${assetImageRoot}fog_night.png',
},
51: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
53: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
55: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
56: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
57: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
61: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
63: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
65: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
66: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
67: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
80: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
81: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
82: {
true: '${assetImageRoot}rain_day.png',
false: '${assetImageRoot}rain_night.png',
},
71: {
true: '${assetImageRoot}snow_day.png',
false: '${assetImageRoot}snow_night.png',
},
73: {
true: '${assetImageRoot}snow_day.png',
false: '${assetImageRoot}snow_night.png',
},
75: {
true: '${assetImageRoot}snow_day.png',
false: '${assetImageRoot}snow_night.png',
},
77: {
true: '${assetImageRoot}snow_day.png',
false: '${assetImageRoot}snow_night.png',
},
85: {
true: '${assetImageRoot}snow_day.png',
false: '${assetImageRoot}snow_night.png',
},
86: {
true: '${assetImageRoot}snow_day.png',
false: '${assetImageRoot}snow_night.png',
},
95: {
true: '${assetImageRoot}thunder_day.png',
false: '${assetImageRoot}thunder_night.png',
},
96: {
true: '${assetImageRoot}thunder_day.png',
false: '${assetImageRoot}thunder_night.png',
},
99: {
true: '${assetImageRoot}thunder_day.png',
false: '${assetImageRoot}thunder_night.png',
},
};
final Map<int, Map<bool, String>> _getNotificationImagePaths = {
0: {true: 'sun.png', false: 'full-moon.png'},
1: {true: 'cloud.png', false: 'moon.png'},
2: {true: 'cloud.png', false: 'moon.png'},
3: {true: 'cloud.png', false: 'moon.png'},
45: {true: 'fog.png', false: 'fog_moon.png'},
48: {true: 'fog.png', false: 'fog_moon.png'},
51: {true: 'rain.png', false: 'rain.png'},
53: {true: 'rain.png', false: 'rain.png'},
55: {true: 'rain.png', false: 'rain.png'},
56: {true: 'rain.png', false: 'rain.png'},
57: {true: 'rain.png', false: 'rain.png'},
61: {true: 'rain.png', false: 'rain.png'},
63: {true: 'rain.png', false: 'rain.png'},
65: {true: 'rain.png', false: 'rain.png'},
66: {true: 'rain.png', false: 'rain.png'},
67: {true: 'rain.png', false: 'rain.png'},
80: {true: 'rain-fall.png', false: 'rain-fall.png'},
81: {true: 'rain-fall.png', false: 'rain-fall.png'},
82: {true: 'rain-fall.png', false: 'rain-fall.png'},
71: {true: 'snow.png', false: 'snow.png'},
73: {true: 'snow.png', false: 'snow.png'},
75: {true: 'snow.png', false: 'snow.png'},
77: {true: 'snow.png', false: 'snow.png'},
85: {true: 'snow.png', false: 'snow.png'},
86: {true: 'snow.png', false: 'snow.png'},
95: {true: 'thunder.png', false: 'thunder.png'},
96: {true: 'storm.png', false: 'storm.png'},
99: {true: 'storm.png', false: 'storm.png'},
};
}

View file

@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gap/gap.dart';
import 'package:rain/app/ui/widgets/weather/status/status_data.dart';
class SunsetSunrise extends StatefulWidget {
const SunsetSunrise({
super.key,
required this.timeSunrise,
required this.timeSunset,
});
final String timeSunrise;
final String timeSunset;
@override
State<SunsetSunrise> createState() => _SunsetSunriseState();
}
class _SunsetSunriseState extends State<SunsetSunrise> {
final statusData = StatusData();
@override
Widget build(BuildContext context) {
final textTheme = context.textTheme;
final titleSmall = textTheme.titleSmall;
final titleLarge = textTheme.titleLarge;
return Card(
margin: const EdgeInsets.only(bottom: 15),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 20),
child: Row(
children: [
_buildSunTimeColumn(
context,
'sunrise'.tr,
statusData.getTimeFormat(widget.timeSunrise),
'assets/images/sunrise.png',
titleSmall,
titleLarge,
),
_buildSunTimeColumn(
context,
'sunset'.tr,
statusData.getTimeFormat(widget.timeSunset),
'assets/images/sunset.png',
titleSmall,
titleLarge,
),
],
),
),
);
}
Widget _buildSunTimeColumn(
BuildContext context,
String label,
String time,
String imagePath,
TextStyle? labelStyle,
TextStyle? timeStyle,
) {
return Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: labelStyle, overflow: TextOverflow.ellipsis),
const Gap(2),
Text(time, style: timeStyle),
],
),
),
const Gap(5),
Flexible(child: Image.asset(imagePath, scale: 10)),
],
),
);
}
}

View file

@ -0,0 +1,26 @@
import 'dart:ui';
extension HexColor on Color {
/// String is in the format "aabbcc" or "ffaabbcc" with an optional leading "#".
static Color fromHex(String hexString) {
final buffer = StringBuffer();
if (hexString.length == 6 || hexString.length == 7) buffer.write('ff');
buffer.write(hexString.replaceFirst('#', ''));
return Color(int.parse(buffer.toString(), radix: 16));
}
/// Prefixes a hash sign if [leadingHashSign] is set to `true` (default is `true`).
String toHex({bool leadingHashSign = true}) {
final argb = toARGB32(); // Get 32-bit integer representation
final a = (argb >> 24) & 0xFF;
final r = (argb >> 16) & 0xFF;
final g = (argb >> 8) & 0xFF;
final b = argb & 0xFF;
return '${leadingHashSign ? '#' : ''}'
'${a.toRadixString(16).padLeft(2, '0')}'
'${r.toRadixString(16).padLeft(2, '0')}'
'${g.toRadixString(16).padLeft(2, '0')}'
'${b.toRadixString(16).padLeft(2, '0')}';
}
}

View file

58
lib/app/utils/notification.dart Executable file
View file

@ -0,0 +1,58 @@
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:rain/app/controller/controller.dart';
import 'package:rain/main.dart';
import 'package:timezone/timezone.dart' as tz;
class NotificationShow {
static const String _channelId = 'Rain';
static const String _channelName = 'DARK NIGHT';
Future<void> showNotification(
int id,
String title,
String body,
DateTime date,
String icon,
) async {
try {
final imagePath = await _getLocalImagePath(icon);
final notificationDetails = await _buildNotificationDetails(imagePath);
final scheduledTime = _getScheduledTime(date);
await flutterLocalNotificationsPlugin.zonedSchedule(
id,
title,
body,
scheduledTime,
notificationDetails,
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
payload: imagePath,
);
} catch (e) {
print('Error showing notification: $e');
}
}
Future<String> _getLocalImagePath(String icon) async {
return await WeatherController().getLocalImagePath(icon);
}
Future<NotificationDetails> _buildNotificationDetails(
String imagePath,
) async {
final androidNotificationDetails = AndroidNotificationDetails(
_channelId,
_channelName,
priority: Priority.high,
importance: Importance.max,
playSound: false,
enableVibration: false,
largeIcon: FilePathAndroidBitmap(imagePath),
);
return NotificationDetails(android: androidNotificationDetails);
}
tz.TZDateTime _getScheduledTime(DateTime date) {
return tz.TZDateTime.from(date, tz.local);
}
}

View file

@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
final GlobalKey<ScaffoldMessengerState> globalKey =
GlobalKey<ScaffoldMessengerState>();
void showSnackBar({required String content, VoidCallback? onPressed}) {
globalKey.currentState?.showSnackBar(
SnackBar(
content: Text(content),
action:
onPressed != null
? SnackBarAction(label: 'settings'.tr, onPressed: onPressed)
: null,
),
);
}

View file

@ -1,32 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class MyTextButton extends StatelessWidget {
const MyTextButton({
super.key,
required this.buttonName,
required this.onPressed,
});
final String buttonName;
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 50,
width: double.infinity,
child: ElevatedButton(
style: ButtonStyle(
shadowColor: const WidgetStatePropertyAll(Colors.transparent),
backgroundColor: WidgetStatePropertyAll(
context.theme.colorScheme.secondaryContainer.withAlpha(80)),
),
onPressed: onPressed,
child: Text(
buttonName,
style: context.textTheme.titleMedium,
),
),
);
}
}

View file

@ -1,250 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:intl/intl.dart';
import 'package:rain/app/data/weather.dart';
import 'package:rain/app/widgets/desc/desc_container.dart';
import 'package:rain/app/widgets/desc/message.dart';
import 'package:rain/app/widgets/hourly/weather_hourly.dart';
import 'package:rain/app/widgets/now/weather_now.dart';
import 'package:rain/app/widgets/status/status_data.dart';
import 'package:rain/app/widgets/status/status_weather.dart';
import 'package:rain/app/widgets/sun_moon/sunset_sunrise.dart';
import 'package:rain/main.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
class InfoDailyCard extends StatefulWidget {
const InfoDailyCard({
super.key,
required this.weatherData,
required this.index,
});
final WeatherCard weatherData;
final int index;
@override
State<InfoDailyCard> createState() => _InfoDailyCardState();
}
class _InfoDailyCardState extends State<InfoDailyCard> {
final statusWeather = StatusWeather();
final statusData = StatusData();
final message = Message();
late PageController pageController;
int pageIndex = 0;
int hourOfDay = 0;
@override
void initState() {
pageController = PageController(initialPage: widget.index);
pageIndex = widget.index;
super.initState();
}
@override
void dispose() {
pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final weatherData = widget.weatherData;
final timeDaily = weatherData.timeDaily ?? [];
final weatherCodeDaily = weatherData.weathercodeDaily ?? [];
final textTheme = context.textTheme;
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
centerTitle: true,
leading: IconButton(
onPressed: () {
Get.back();
},
icon: const Icon(
IconsaxPlusLinear.arrow_left_3,
size: 20,
),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
title: Text(
DateFormat.MMMMEEEEd(locale.languageCode)
.format(timeDaily[pageIndex]),
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
),
body: SafeArea(
child: PageView.builder(
controller: pageController,
onPageChanged: (index) {
setState(() {
pageIndex = index;
hourOfDay = 0;
});
},
itemCount: timeDaily.length,
itemBuilder: (context, index) {
final indexedWeatherCodeDaily = weatherCodeDaily[index];
final temperature2MMin = weatherData.temperature2MMin?[index];
final temperature2MMax = weatherData.temperature2MMax?[index];
final apparentTemperatureMin =
weatherData.apparentTemperatureMin?[index];
final apparentTemperatureMax =
weatherData.apparentTemperatureMax?[index];
final uvIndexMax = weatherData.uvIndexMax?[index];
final windDirection10MDominant =
weatherData.winddirection10MDominant?[index];
final windSpeed10MMax = weatherData.windspeed10MMax?[index];
final windGusts10MMax = weatherData.windgusts10MMax?[index];
final precipitationProbabilityMax =
weatherData.precipitationProbabilityMax?[index];
final rainSum = weatherData.rainSum?[index];
final precipitationSum = weatherData.precipitationSum?[index];
final sunrise = weatherData.sunrise![index];
final sunset = weatherData.sunset![index];
final startIndex = index * 24;
return indexedWeatherCodeDaily == null
? null
: Container(
margin: const EdgeInsets.symmetric(horizontal: 10),
child: ListView(
children: [
WeatherNow(
weather:
weatherData.weathercode![startIndex + hourOfDay],
degree: weatherData
.temperature2M![startIndex + hourOfDay],
feels: weatherData
.apparentTemperature![startIndex + hourOfDay]!,
time: weatherData.time![startIndex + hourOfDay],
timeDay: sunrise,
timeNight: sunset,
tempMax: temperature2MMax!,
tempMin: temperature2MMin!,
),
Card(
margin: const EdgeInsets.only(bottom: 15),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
child: SizedBox(
height: 135,
child: ScrollablePositionedList.separated(
separatorBuilder:
(BuildContext context, int index) {
return const VerticalDivider(
width: 10,
indent: 40,
endIndent: 40,
);
},
scrollDirection: Axis.horizontal,
itemCount: 24,
itemBuilder: (ctx, i) {
int hourlyIndex = startIndex + i;
return GestureDetector(
onTap: () {
hourOfDay = i;
setState(() {});
},
child: Container(
margin: const EdgeInsets.symmetric(
vertical: 5),
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 5,
),
decoration: BoxDecoration(
color: i == hourOfDay
? context.theme.colorScheme
.secondaryContainer
: Colors.transparent,
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
),
child: WeatherHourly(
time: weatherData.time![hourlyIndex],
weather: weatherData
.weathercode![hourlyIndex],
degree: weatherData
.temperature2M![hourlyIndex],
timeDay: sunrise,
timeNight: sunset,
),
),
);
},
),
),
),
),
SunsetSunrise(
timeSunrise: sunrise,
timeSunset: sunset,
),
DescContainer(
humidity: weatherData
.relativehumidity2M?[startIndex + hourOfDay],
wind:
weatherData.windspeed10M?[startIndex + hourOfDay],
visibility:
weatherData.visibility?[startIndex + hourOfDay],
feels: weatherData
.apparentTemperature?[startIndex + hourOfDay],
evaporation: weatherData
.evapotranspiration?[startIndex + hourOfDay],
precipitation: weatherData
.precipitation?[startIndex + hourOfDay],
direction: weatherData
.winddirection10M?[startIndex + hourOfDay],
pressure: weatherData
.surfacePressure?[startIndex + hourOfDay],
rain: weatherData.rain?[startIndex + hourOfDay],
cloudcover:
weatherData.cloudcover?[startIndex + hourOfDay],
windgusts:
weatherData.windgusts10M?[startIndex + hourOfDay],
uvIndex: weatherData.uvIndex?[startIndex + hourOfDay],
dewpoint2M:
weatherData.dewpoint2M?[startIndex + hourOfDay],
precipitationProbability:
weatherData.precipitationProbability?[
startIndex + hourOfDay],
shortwaveRadiation: weatherData
.shortwaveRadiation?[startIndex + hourOfDay],
initiallyExpanded: true,
title: 'hourlyVariables'.tr,
),
DescContainer(
apparentTemperatureMin: apparentTemperatureMin,
apparentTemperatureMax: apparentTemperatureMax,
uvIndexMax: uvIndexMax,
windDirection10MDominant: windDirection10MDominant,
windSpeed10MMax: windSpeed10MMax,
windGusts10MMax: windGusts10MMax,
precipitationProbabilityMax:
precipitationProbabilityMax,
rainSum: rainSum,
precipitationSum: precipitationSum,
initiallyExpanded: true,
title: 'dailyVariables'.tr,
),
],
),
);
},
),
),
);
}
}

View file

@ -1,81 +0,0 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:rain/app/widgets/status/status_weather.dart';
import 'package:rain/app/widgets/status/status_data.dart';
import 'package:rain/main.dart';
class ListDailyCard extends StatefulWidget {
const ListDailyCard({
super.key,
required this.timeDaily,
required this.weathercodeDaily,
required this.temperature2MMax,
required this.temperature2MMin,
});
final DateTime timeDaily;
final int? weathercodeDaily;
final double? temperature2MMax;
final double? temperature2MMin;
@override
State<ListDailyCard> createState() => _ListDailyCardState();
}
class _ListDailyCardState extends State<ListDailyCard> {
final statusWeather = StatusWeather();
final statusData = StatusData();
@override
Widget build(BuildContext context) {
return widget.weathercodeDaily == null
? Container()
: Card(
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${statusData.getDegree(widget.temperature2MMin?.round())} / ${statusData.getDegree(widget.temperature2MMax?.round())}',
style: context.textTheme.titleLarge?.copyWith(
fontSize: 22,
fontWeight: FontWeight.w600,
),
),
const Gap(5),
Text(
DateFormat.MMMMEEEEd(locale.languageCode)
.format(widget.timeDaily),
style: context.textTheme.titleMedium?.copyWith(
color: Colors.grey,
fontWeight: FontWeight.w400,
),
),
const Gap(5),
Text(
statusWeather.getText(widget.weathercodeDaily),
style: context.textTheme.titleMedium?.copyWith(
color: Colors.grey,
fontWeight: FontWeight.w400,
),
),
],
),
),
const Gap(5),
Image.asset(
statusWeather.getImageNowDaily(widget.weathercodeDaily),
scale: 6.5,
),
],
),
),
);
}
}

View file

@ -1,144 +0,0 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:rain/app/data/weather.dart';
import 'package:rain/app/widgets/daily/info_daily_card.dart';
import 'package:rain/app/widgets/status/status_data.dart';
import 'package:rain/app/widgets/status/status_weather.dart';
import 'package:rain/main.dart';
class WeatherDaily extends StatefulWidget {
const WeatherDaily({
super.key,
required this.weatherData,
required this.onTap,
});
final WeatherCard weatherData;
final VoidCallback onTap;
@override
State<WeatherDaily> createState() => _WeatherDailyState();
}
class _WeatherDailyState extends State<WeatherDaily> {
final statusWeather = StatusWeather();
final statusData = StatusData();
@override
Widget build(BuildContext context) {
final splashColor = context.theme.colorScheme.primary.withOpacity(0.4);
const inkWellBorderRadius = BorderRadius.all(
Radius.circular(16),
);
final weatherData = widget.weatherData;
final weatherCodeDaily = weatherData.weathercodeDaily ?? [];
final textTheme = context.textTheme;
final labelLarge = textTheme.labelLarge;
return Card(
margin: const EdgeInsets.only(bottom: 15),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 5),
child: Column(
children: [
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: 7,
itemBuilder: (ctx, index) {
return InkWell(
splashColor: splashColor,
borderRadius: inkWellBorderRadius,
onTap: () => Get.to(
() => InfoDailyCard(
weatherData: weatherData,
index: index,
),
transition: Transition.downToUp,
),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
DateFormat.EEEE(locale.languageCode)
.format((weatherData.timeDaily ?? [])[index]),
style: labelLarge,
overflow: TextOverflow.ellipsis,
),
),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
statusWeather
.getImage7Day(weatherCodeDaily[index]),
scale: 3,
),
const Gap(5),
Expanded(
child: Text(
statusWeather
.getText(weatherCodeDaily[index]),
style: labelLarge,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
statusData.getDegree(
(weatherData.temperature2MMin ?? [])[index]
?.round()),
style: labelLarge,
),
Text(
' / ',
style: labelLarge,
),
Text(
statusData.getDegree(
(weatherData.temperature2MMax ?? [])[index]
?.round()),
style: labelLarge,
),
],
),
),
],
),
),
);
},
),
const Divider(),
InkWell(
splashColor: splashColor,
borderRadius: inkWellBorderRadius,
onTap: widget.onTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Text(
'weatherMore'.tr,
style: textTheme.titleMedium,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
),
);
}
}

View file

@ -1,71 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:rain/app/data/weather.dart';
import 'package:rain/app/widgets/daily/info_daily_card.dart';
import 'package:rain/app/widgets/daily/list_daily_card.dart';
class WeatherMore extends StatefulWidget {
const WeatherMore({
super.key,
required this.weatherData,
});
final WeatherCard weatherData;
@override
State<WeatherMore> createState() => _WeatherMoreState();
}
class _WeatherMoreState extends State<WeatherMore> {
@override
Widget build(BuildContext context) {
const transparent = Colors.transparent;
final weatherData = widget.weatherData;
final timeDaily = weatherData.timeDaily ?? [];
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
centerTitle: true,
leading: IconButton(
onPressed: () {
Get.back();
},
icon: const Icon(
IconsaxPlusLinear.arrow_left_3,
size: 20,
),
splashColor: transparent,
highlightColor: transparent,
),
title: Text(
'weatherMore'.tr,
style: context.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
),
body: SafeArea(
child: ListView.builder(
itemCount: timeDaily.length,
itemBuilder: (context, index) => GestureDetector(
onTap: () => Get.to(
() => InfoDailyCard(
weatherData: weatherData,
index: index,
),
transition: Transition.downToUp,
),
child: ListDailyCard(
timeDaily: timeDaily[index],
weathercodeDaily: weatherData.weathercodeDaily![index],
temperature2MMax: weatherData.temperature2MMax![index],
temperature2MMin: weatherData.temperature2MMin![index],
),
),
),
),
);
}
}

View file

@ -1,306 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:rain/app/widgets/desc/desc.dart';
import 'package:rain/app/widgets/desc/message.dart';
import 'package:rain/app/widgets/status/status_data.dart';
class DescContainer extends StatefulWidget {
const DescContainer({
super.key,
this.humidity,
this.wind,
this.visibility,
this.feels,
this.evaporation,
this.precipitation,
this.direction,
this.pressure,
this.rain,
this.cloudcover,
this.windgusts,
this.uvIndex,
this.dewpoint2M,
this.precipitationProbability,
this.shortwaveRadiation,
this.apparentTemperatureMin,
this.apparentTemperatureMax,
this.uvIndexMax,
this.windDirection10MDominant,
this.windSpeed10MMax,
this.windGusts10MMax,
this.precipitationProbabilityMax,
this.rainSum,
this.precipitationSum,
required this.initiallyExpanded,
required this.title,
});
final int? humidity;
final double? wind;
final double? visibility;
final double? feels;
final double? evaporation;
final double? precipitation;
final int? direction;
final double? pressure;
final double? rain;
final int? cloudcover;
final double? windgusts;
final double? uvIndex;
final double? dewpoint2M;
final int? precipitationProbability;
final double? shortwaveRadiation;
final double? apparentTemperatureMin;
final double? apparentTemperatureMax;
final double? uvIndexMax;
final int? windDirection10MDominant;
final double? windSpeed10MMax;
final double? windGusts10MMax;
final int? precipitationProbabilityMax;
final double? rainSum;
final double? precipitationSum;
final bool initiallyExpanded;
final String title;
@override
State<DescContainer> createState() => _DescContainerState();
}
class _DescContainerState extends State<DescContainer> {
final statusData = StatusData();
final message = Message();
@override
Widget build(BuildContext context) {
final dewpoint2M = widget.dewpoint2M?.round();
final feels = widget.feels;
final visibility = widget.visibility;
final direction = widget.direction;
final wind = widget.wind;
final windgusts = widget.windgusts;
final evaporation = widget.evaporation;
final precipitation = widget.precipitation;
final rain = widget.rain;
final precipitationProbability = widget.precipitationProbability;
final humidity = widget.humidity;
final cloudcover = widget.cloudcover;
final pressure = widget.pressure;
final uvIndex = widget.uvIndex;
final shortwaveRadiation = widget.shortwaveRadiation;
final apparentTemperatureMin = widget.apparentTemperatureMin;
final apparentTemperatureMax = widget.apparentTemperatureMax;
final uvIndexMax = widget.uvIndexMax;
final windDirection10MDominant = widget.windDirection10MDominant;
final windSpeed10MMax = widget.windSpeed10MMax;
final windGusts10MMax = widget.windGusts10MMax;
final precipitationProbabilityMax = widget.precipitationProbabilityMax;
final rainSum = widget.rainSum;
final precipitationSum = widget.precipitationSum;
final initiallyExpanded = widget.initiallyExpanded;
final title = widget.title;
return Card(
margin: const EdgeInsets.only(bottom: 15),
child: ExpansionTile(
shape: const Border(),
title: Text(
title,
style: context.textTheme.labelLarge,
),
initiallyExpanded: initiallyExpanded,
children: [
Padding(
padding: const EdgeInsets.only(top: 20, bottom: 5),
child: Wrap(
alignment: WrapAlignment.spaceEvenly,
spacing: 5,
children: [
apparentTemperatureMin == null
? Container()
: DescWeather(
imageName: 'assets/images/cold.png',
value: statusData
.getDegree(apparentTemperatureMin.round()),
desc: 'apparentTemperatureMin'.tr,
),
apparentTemperatureMax == null
? Container()
: DescWeather(
imageName: 'assets/images/hot.png',
value: statusData
.getDegree(apparentTemperatureMax.round()),
desc: 'apparentTemperatureMax'.tr,
),
uvIndexMax == null
? Container()
: DescWeather(
imageName: 'assets/images/uv.png',
value: '${uvIndexMax.round()}',
desc: 'uvIndex'.tr,
message: message.getUvIndex(uvIndexMax.round()),
),
windDirection10MDominant == null
? Container()
: DescWeather(
imageName: 'assets/images/windsock.png',
value: '$windDirection10MDominant°',
desc: 'direction'.tr,
message: message.getDirection(windDirection10MDominant),
),
windSpeed10MMax == null
? Container()
: DescWeather(
imageName: 'assets/images/wind.png',
value: statusData.getSpeed(windSpeed10MMax.round()),
desc: 'wind'.tr,
),
windGusts10MMax == null
? Container()
: DescWeather(
imageName: 'assets/images/windgusts.png',
value: statusData.getSpeed(windGusts10MMax.round()),
desc: 'windgusts'.tr,
),
precipitationProbabilityMax == null
? Container()
: DescWeather(
imageName:
'assets/images/precipitation_probability.png',
value: '$precipitationProbabilityMax%',
desc: 'precipitationProbability'.tr,
),
rainSum == null
? Container()
: DescWeather(
imageName: 'assets/images/water.png',
value: statusData.getPrecipitation(rainSum),
desc: 'rain'.tr,
),
precipitationSum == null
? Container()
: DescWeather(
imageName: 'assets/images/rainfall.png',
value: statusData.getPrecipitation(precipitationSum),
desc: 'precipitation'.tr,
),
dewpoint2M == null
? Container()
: DescWeather(
imageName: 'assets/images/dew.png',
value: statusData.getDegree(dewpoint2M.round()),
desc: 'dewpoint'.tr,
),
feels == null
? Container()
: DescWeather(
imageName: 'assets/images/temperature.png',
value: statusData.getDegree(feels.round()),
desc: 'feels'.tr,
),
visibility == null
? Container()
: DescWeather(
imageName: 'assets/images/fog.png',
value: statusData.getVisibility(visibility),
desc: 'visibility'.tr,
),
direction == null
? Container()
: DescWeather(
imageName: 'assets/images/windsock.png',
value: '$direction°',
desc: 'direction'.tr,
message: message.getDirection(direction),
),
wind == null
? Container()
: DescWeather(
imageName: 'assets/images/wind.png',
value: statusData.getSpeed(wind.round()),
desc: 'wind'.tr,
),
windgusts == null
? Container()
: DescWeather(
imageName: 'assets/images/windgusts.png',
value: statusData.getSpeed(windgusts.round()),
desc: 'windgusts'.tr,
),
evaporation == null
? Container()
: DescWeather(
imageName: 'assets/images/evaporation.png',
value: statusData.getPrecipitation(evaporation.abs()),
desc: 'evaporation'.tr,
),
precipitation == null
? Container()
: DescWeather(
imageName: 'assets/images/rainfall.png',
value: statusData.getPrecipitation(precipitation),
desc: 'precipitation'.tr,
),
rain == null
? Container()
: DescWeather(
imageName: 'assets/images/water.png',
value: statusData.getPrecipitation(rain),
desc: 'rain'.tr,
),
precipitationProbability == null
? Container()
: DescWeather(
imageName:
'assets/images/precipitation_probability.png',
value: '$precipitationProbability%',
desc: 'precipitationProbability'.tr,
),
humidity == null
? Container()
: DescWeather(
imageName: 'assets/images/humidity.png',
value: '$humidity%',
desc: 'humidity'.tr,
),
cloudcover == null
? Container()
: DescWeather(
imageName: 'assets/images/cloudy.png',
value: '$cloudcover%',
desc: 'cloudcover'.tr,
),
pressure == null
? Container()
: DescWeather(
imageName: 'assets/images/atmospheric.png',
value: statusData.getPressure(pressure.round()),
desc: 'pressure'.tr,
message: message.getPressure(pressure.round()),
),
uvIndex == null
? Container()
: DescWeather(
imageName: 'assets/images/uv.png',
value: '${uvIndex.round()}',
desc: 'uvIndex'.tr,
message: message.getUvIndex(uvIndex.round()),
),
shortwaveRadiation == null
? Container()
: DescWeather(
imageName: 'assets/images/shortwave_radiation.png',
value: '${shortwaveRadiation.round()} ${'W/m2'.tr}',
desc: 'shortwaveRadiation'.tr,
),
],
),
),
],
),
);
}
}

View file

@ -1,59 +0,0 @@
import 'package:get/get.dart';
class Message {
String getPressure(int? pressure) {
if (pressure != null) {
if (pressure < 1000) {
return 'low'.tr;
} else if (pressure > 1020) {
return 'high'.tr;
} else {
return 'normal'.tr;
}
} else {
return '';
}
}
String getUvIndex(int? uvIndex) {
if (uvIndex != null) {
if (uvIndex < 3) {
return 'uvLow'.tr;
} else if (uvIndex < 6) {
return 'uvAverage'.tr;
} else if (uvIndex < 8) {
return 'uvHigh'.tr;
} else if (uvIndex < 11) {
return 'uvVeryHigh'.tr;
} else {
return 'uvExtreme'.tr;
}
} else {
return '';
}
}
String getDirection(int? direction) {
if (direction != null) {
if (direction >= 337.5 || direction < 22.5) {
return 'north'.tr;
} else if (direction >= 22.5 && direction < 67.5) {
return 'northeast'.tr;
} else if (direction >= 67.5 && direction < 112.5) {
return 'east'.tr;
} else if (direction >= 112.5 && direction < 157.5) {
return 'southeast'.tr;
} else if (direction >= 157.5 && direction < 202.5) {
return 'south'.tr;
} else if (direction >= 202.5 && direction < 247.5) {
return 'southwest'.tr;
} else if (direction >= 247.5 && direction < 292.5) {
return 'west'.tr;
} else {
return 'northwest'.tr;
}
} else {
return '';
}
}
}

View file

@ -1,71 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:rain/app/widgets/status/status_data.dart';
import 'package:rain/app/widgets/status/status_weather.dart';
import 'package:rain/main.dart';
class WeatherHourly extends StatefulWidget {
const WeatherHourly({
super.key,
required this.time,
required this.weather,
required this.degree,
required this.timeDay,
required this.timeNight,
});
final String time;
final String timeDay;
final String timeNight;
final int weather;
final double degree;
@override
State<WeatherHourly> createState() => _WeatherHourlyState();
}
class _WeatherHourlyState extends State<WeatherHourly> {
final statusWeather = StatusWeather();
final statusData = StatusData();
@override
Widget build(BuildContext context) {
final textTheme = context.textTheme;
final time = widget.time;
return Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Column(
children: [
Text(
statusData.getTimeFormat(time),
style: textTheme.labelLarge,
),
Text(
DateFormat('E', locale.languageCode)
.format(DateTime.tryParse(time)!),
style: textTheme.labelLarge?.copyWith(
color: Colors.grey,
),
),
],
),
Image.asset(
statusWeather.getImageToday(
widget.weather,
time,
widget.timeDay,
widget.timeNight,
),
scale: 3,
),
Text(
statusData.getDegree(widget.degree.round()),
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
);
}
}

View file

@ -1,151 +0,0 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:rain/app/widgets/status/status_data.dart';
import 'package:rain/app/widgets/status/status_weather.dart';
import 'package:rain/main.dart';
class WeatherNow extends StatefulWidget {
const WeatherNow({
super.key,
required this.weather,
required this.degree,
required this.time,
required this.timeDay,
required this.timeNight,
required this.tempMax,
required this.tempMin,
required this.feels,
});
final String time;
final String timeDay;
final String timeNight;
final int weather;
final double degree;
final double tempMax;
final double tempMin;
final double feels;
@override
State<WeatherNow> createState() => _WeatherNowState();
}
class _WeatherNowState extends State<WeatherNow> {
final statusWeather = StatusWeather();
final statusData = StatusData();
@override
Widget build(BuildContext context) {
return largeElement
? Padding(
padding: const EdgeInsets.only(bottom: 15),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Gap(15),
Image(
image: AssetImage(statusWeather.getImageNow(widget.weather,
widget.time, widget.timeDay, widget.timeNight)),
fit: BoxFit.fill,
height: 200,
),
Text(
'${roundDegree ? widget.degree.round() : widget.degree}',
style: context.textTheme.displayLarge?.copyWith(
fontSize: 90,
fontWeight: FontWeight.w800,
shadows: const [
Shadow(
blurRadius: 15,
offset: Offset(5, 5),
)
]),
),
Text(
statusWeather.getText(widget.weather),
style: context.textTheme.titleLarge,
),
const Gap(5),
Text(
DateFormat.MMMMEEEEd(locale.languageCode).format(
DateTime.parse(widget.time),
),
style: context.textTheme.labelLarge?.copyWith(
color: Colors.grey,
),
),
],
),
)
: Card(
margin: const EdgeInsets.only(bottom: 15),
child: Padding(
padding: const EdgeInsets.only(
top: 18, bottom: 18, left: 25, right: 15),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
DateFormat.MMMMEEEEd(locale.languageCode).format(
DateTime.parse(widget.time),
),
style: context.textTheme.labelLarge?.copyWith(
color: Colors.grey,
),
),
const Gap(5),
Text(
statusWeather.getText(widget.weather),
style: context.textTheme.titleLarge
?.copyWith(fontSize: 20),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('feels'.tr,
style: context.textTheme.bodyMedium),
Text('', style: context.textTheme.bodyMedium),
Text(statusData.getDegree(widget.feels.round()),
style: context.textTheme.bodyMedium),
],
),
const Gap(30),
Text(
statusData.getDegree(roundDegree
? widget.degree.round()
: widget.degree),
style: context.textTheme.displayMedium?.copyWith(
fontWeight: FontWeight.w800,
),
),
const Gap(5),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(statusData.getDegree((widget.tempMin.round())),
style: context.textTheme.labelLarge),
Text(' / ', style: context.textTheme.labelLarge),
Text(statusData.getDegree((widget.tempMax.round())),
style: context.textTheme.labelLarge),
],
),
],
),
),
Image(
image: AssetImage(statusWeather.getImageNow(widget.weather,
widget.time, widget.timeDay, widget.timeNight)),
fit: BoxFit.fill,
height: 140,
),
],
),
),
);
}
}

View file

@ -1,87 +0,0 @@
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:rain/main.dart';
import 'package:timezone/timezone.dart';
class StatusData {
String getDegree(degree) {
switch (settings.degrees) {
case 'celsius':
return '$degree°C';
case 'fahrenheit':
return '$degree°F';
default:
return '$degree°C';
}
}
String getSpeed(int? speed) {
switch (settings.measurements) {
case 'metric':
return settings.wind == 'm/s'
? '${(speed! * (5 / 18)).toPrecision(1)} ${'m/s'.tr}'
: '$speed ${'kph'.tr}';
case 'imperial':
return '$speed ${'mph'.tr}';
default:
return '$speed ${'kph'.tr}';
}
}
String getPressure(int? pressure) {
return settings.pressure == 'mmHg'
? '${(pressure! * (3 / 4)).toPrecision(1)} ${'mmHg'.tr}'
: '$pressure ${'hPa'.tr}';
}
String getVisibility(double? length) {
if (length != null) {
switch (settings.measurements) {
case 'metric':
return '${length > 1000 ? (length / 1000).round() : (length / 1000).toStringAsFixed(2)} ${'km'.tr}';
case 'imperial':
return '${length > 5280 ? (length / 5280).round() : (length / 5280).toStringAsFixed(2)} ${'mi'.tr}';
default:
return '${length > 1000 ? (length / 1000).round() : (length / 1000).toStringAsFixed(2)} ${'km'.tr}';
}
} else {
return '';
}
}
String getPrecipitation(double? precipitation) {
switch (settings.measurements) {
case 'metric':
return '$precipitation ${'mm'.tr}';
case 'imperial':
return '$precipitation ${'inch'.tr}';
default:
return '$precipitation ${'mm'.tr}';
}
}
String getTimeFormat(String time) {
switch (settings.timeformat) {
case '12':
return DateFormat.jm(locale.languageCode)
.format(DateTime.tryParse(time)!);
case '24':
return DateFormat.Hm(locale.languageCode)
.format(DateTime.tryParse(time)!);
default:
return DateFormat.Hm(locale.languageCode)
.format(DateTime.tryParse(time)!);
}
}
String getTimeFormatTz(TZDateTime time) {
switch (settings.timeformat) {
case '12':
return DateFormat.jm(locale.languageCode).format(time);
case '24':
return DateFormat.Hm(locale.languageCode).format(time);
default:
return DateFormat.Hm(locale.languageCode).format(time);
}
}
}

View file

@ -1,340 +0,0 @@
import 'package:get/get.dart';
const assetImageRoot = 'assets/images/';
class StatusWeather {
String getImageNow(
int weather, String time, String timeDay, String timeNight) {
final currentTime = DateTime.parse(time);
final day = DateTime.parse(timeDay);
final night = DateTime.parse(timeNight);
final dayTime =
DateTime(day.year, day.month, day.day, day.hour, day.minute);
final nightTime =
DateTime(night.year, night.month, night.day, night.hour, night.minute);
switch (weather) {
case 0:
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) {
return '${assetImageRoot}sun.png';
} else {
return '${assetImageRoot}full-moon.png';
}
case 1:
case 2:
case 3:
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) {
return '${assetImageRoot}cloud.png';
} else {
return '${assetImageRoot}moon.png';
}
case 45:
case 48:
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) {
return '${assetImageRoot}fog.png';
} else {
return '${assetImageRoot}fog_moon.png';
}
case 51:
case 53:
case 55:
case 56:
case 57:
case 61:
case 63:
case 65:
case 66:
case 67:
return '${assetImageRoot}rain.png';
case 80:
case 81:
case 82:
return '${assetImageRoot}rain-fall.png';
case 71:
case 73:
case 75:
case 77:
case 85:
case 86:
return '${assetImageRoot}snow.png';
case 95:
return '${assetImageRoot}thunder.png';
case 96:
case 99:
return '${assetImageRoot}storm.png';
default:
return '';
}
}
String getImageNowDaily(int? weather) {
switch (weather) {
case 0:
return '${assetImageRoot}sun.png';
case 1:
case 2:
case 3:
return '${assetImageRoot}cloud.png';
case 45:
case 48:
return '${assetImageRoot}fog.png';
case 51:
case 53:
case 55:
case 56:
case 57:
case 61:
case 63:
case 65:
case 66:
case 67:
return '${assetImageRoot}rain.png';
case 80:
case 81:
case 82:
return '${assetImageRoot}rain-fall.png';
case 71:
case 73:
case 75:
case 77:
case 85:
case 86:
return '${assetImageRoot}snow.png';
case 95:
return '${assetImageRoot}thunder.png';
case 96:
case 99:
return '${assetImageRoot}storm.png';
default:
return '';
}
}
String getImageToday(
int weather, String time, String timeDay, String timeNight) {
final currentTime = DateTime.parse(time);
final day = DateTime.parse(timeDay);
final night = DateTime.parse(timeNight);
final dayTime =
DateTime(day.year, day.month, day.day, day.hour, day.minute);
final nightTime =
DateTime(night.year, night.month, night.day, night.hour, night.minute);
switch (weather) {
case 0:
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) {
return '${assetImageRoot}clear_day.png';
} else {
return '${assetImageRoot}clear_night.png';
}
case 1:
case 2:
case 3:
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) {
return '${assetImageRoot}cloudy_day.png';
} else {
return '${assetImageRoot}cloudy_night.png';
}
case 45:
case 48:
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) {
return '${assetImageRoot}fog_day.png';
} else {
return '${assetImageRoot}fog_night.png';
}
case 51:
case 53:
case 55:
case 56:
case 57:
case 61:
case 63:
case 65:
case 66:
case 67:
case 80:
case 81:
case 82:
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) {
return '${assetImageRoot}rain_day.png';
} else {
return '${assetImageRoot}rain_night.png';
}
case 71:
case 73:
case 75:
case 77:
case 85:
case 86:
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) {
return '${assetImageRoot}snow_day.png';
} else {
return '${assetImageRoot}snow_night.png';
}
case 95:
case 96:
case 99:
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) {
return '${assetImageRoot}thunder_day.png';
} else {
return '${assetImageRoot}thunder_night.png';
}
default:
return '';
}
}
String getImage7Day(int? weather) {
switch (weather) {
case 0:
return '${assetImageRoot}clear_day.png';
case 1:
case 2:
case 3:
return '${assetImageRoot}cloudy_day.png';
case 45:
case 48:
return '${assetImageRoot}fog_day.png';
case 51:
case 53:
case 55:
case 56:
case 57:
case 61:
case 63:
case 65:
case 66:
case 67:
case 80:
case 81:
case 82:
return '${assetImageRoot}rain_day.png';
case 71:
case 73:
case 75:
case 77:
case 85:
case 86:
return '${assetImageRoot}snow_day.png';
case 95:
case 96:
case 99:
return '${assetImageRoot}thunder_day.png';
default:
return '';
}
}
String getText(int? weather) {
switch (weather) {
case 0:
return 'clear_sky'.tr;
case 1:
case 2:
return 'cloudy'.tr;
case 3:
return 'overcast'.tr;
case 45:
case 48:
return 'fog'.tr;
case 51:
case 53:
case 55:
return 'drizzle'.tr;
case 56:
case 57:
return 'drizzling_rain'.tr;
case 61:
case 63:
case 65:
return 'rain'.tr;
case 66:
case 67:
return 'freezing_rain'.tr;
case 80:
case 81:
case 82:
return 'heavy_rains'.tr;
case 71:
case 73:
case 75:
case 77:
case 85:
case 86:
return 'snow'.tr;
case 95:
case 96:
case 99:
return 'thunderstorm'.tr;
default:
return '';
}
}
String getImageNotification(
int weather, String time, String timeDay, String timeNight) {
final currentTime = DateTime.parse(time);
final day = DateTime.parse(timeDay);
final night = DateTime.parse(timeNight);
final dayTime =
DateTime(day.year, day.month, day.day, day.hour, day.minute);
final nightTime =
DateTime(night.year, night.month, night.day, night.hour, night.minute);
switch (weather) {
case 0:
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) {
return 'sun.png';
} else {
return 'full-moon.png';
}
case 1:
case 2:
case 3:
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) {
return 'cloud.png';
} else {
return 'moon.png';
}
case 45:
case 48:
if (currentTime.isAfter(dayTime) && currentTime.isBefore(nightTime)) {
return 'fog.png';
} else {
return 'fog_moon.png';
}
case 51:
case 53:
case 55:
case 56:
case 57:
case 61:
case 63:
case 65:
case 66:
case 67:
return 'rain.png';
case 80:
case 81:
case 82:
return 'rain-fall.png';
case 71:
case 73:
case 75:
case 77:
case 85:
case 86:
return 'snow.png';
case 95:
return 'thunder.png';
case 96:
case 99:
return 'storm.png';
default:
return '';
}
}
}

View file

@ -1,102 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gap/gap.dart';
import 'package:rain/app/widgets/status/status_data.dart';
class SunsetSunrise extends StatefulWidget {
const SunsetSunrise({
super.key,
required this.timeSunrise,
required this.timeSunset,
});
final String timeSunrise;
final String timeSunset;
@override
State<SunsetSunrise> createState() => _SunsetSunriseState();
}
class _SunsetSunriseState extends State<SunsetSunrise> {
final statusData = StatusData();
@override
Widget build(BuildContext context) {
final textTheme = context.textTheme;
final titleSmall = textTheme.titleSmall;
final titleLarge = textTheme.titleLarge;
return Card(
margin: const EdgeInsets.only(bottom: 15),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 20),
child: Row(
children: [
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'sunrise'.tr,
style: titleSmall,
overflow: TextOverflow.ellipsis,
),
const Gap(2),
Text(
statusData.getTimeFormat(widget.timeSunrise),
style: titleLarge,
),
],
),
),
const Gap(5),
Flexible(
child: Image.asset(
'assets/images/sunrise.png',
scale: 10,
),
),
],
),
),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'sunset'.tr,
style: titleSmall,
overflow: TextOverflow.ellipsis,
),
const Gap(2),
Text(
statusData.getTimeFormat(widget.timeSunset),
style: titleLarge,
),
],
),
),
const Gap(5),
Flexible(
child: Image.asset(
'assets/images/sunset.png',
scale: 10,
),
),
],
),
),
],
),
),
);
}
}

352
lib/main.dart Normal file → Executable file
View file

@ -14,15 +14,14 @@ import 'package:internet_connection_checker_plus/internet_connection_checker_plu
import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart';
import 'package:rain/app/controller/controller.dart';
import 'package:rain/app/data/weather.dart';
import 'package:rain/app/modules/geolocation.dart';
import 'package:rain/app/modules/home.dart';
import 'package:rain/app/modules/onboarding.dart';
import 'package:rain/app/data/db.dart';
import 'package:rain/app/ui/geolocation.dart';
import 'package:rain/app/ui/home.dart';
import 'package:rain/app/ui/onboarding.dart';
import 'package:rain/theme/theme.dart';
import 'package:rain/theme/theme_controller.dart';
import 'package:rain/translation/translation.dart';
import 'package:rain/utils/device_info.dart';
import 'package:time_machine/time_machine.dart';
import 'package:rain/app/utils/device_info.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
import 'package:workmanager/workmanager.dart';
@ -30,10 +29,11 @@ import 'package:workmanager/workmanager.dart';
late Isar isar;
late Settings settings;
late LocationCache locationCache;
final ValueNotifier<Future<bool>> isOnline =
ValueNotifier(InternetConnection().hasInternetAccess);
final ValueNotifier<Future<bool>> isOnline = ValueNotifier(
InternetConnection().hasInternetAccess,
);
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
bool amoledTheme = false;
@ -47,36 +47,36 @@ String timeEnd = '21:00';
String widgetBackgroundColor = '';
String widgetTextColor = '';
final List appLanguages = [
{'name': 'বাংলা', 'locale': const Locale('bn', 'IN')},
{'name': 'Čeština', 'locale': const Locale('cs', 'CZ')},
{'name': 'Dansk', 'locale': const Locale('da', 'DK')},
{'name': 'Deutsch', 'locale': const Locale('de', 'DE')},
{'name': 'English', 'locale': const Locale('en', 'US')},
{'name': 'Español', 'locale': const Locale('es', 'ES')},
{'name': 'Français', 'locale': const Locale('fr', 'FR')},
// {'name': 'Gaeilge', 'locale': const Locale('ga', 'IE')},
{'name': 'हिन्दी', 'locale': const Locale('hi', 'IN')},
{'name': 'Magyar', 'locale': const Locale('hu', 'HU')},
{'name': 'Italiano', 'locale': const Locale('it', 'IT')},
{'name': '한국어', 'locale': const Locale('ko', 'KR')},
{'name': 'فارسی', 'locale': const Locale('fa', 'IR')},
{'name': 'ქართული', 'locale': const Locale('ka', 'GE')},
{'name': 'Nederlands', 'locale': const Locale('nl', 'NL')},
{'name': 'Polski', 'locale': const Locale('pl', 'PL')},
{'name': 'Português (Brasil)', 'locale': const Locale('pt', 'BR')},
{'name': 'Română', 'locale': const Locale('ro', 'RO')},
{'name': 'Русский', 'locale': const Locale('ru', 'RU')},
{'name': 'Slovenčina', 'locale': const Locale('sk', 'SK')},
{'name': 'Türkçe', 'locale': const Locale('tr', 'TR')},
{'name': 'اردو', 'locale': const Locale('ur', 'PK')},
{'name': '中文(简体)', 'locale': const Locale('zh', 'CN')},
{'name': '中文(繁體)', 'locale': const Locale('zh', 'TW')},
];
const String appGroupId = 'DARK NIGHT';
const String androidWidgetName = 'OreoWidget';
const List<Map<String, dynamic>> appLanguages = [
{'name': 'বাংলা', 'locale': Locale('bn', 'IN')},
{'name': 'Čeština', 'locale': Locale('cs', 'CZ')},
{'name': 'Dansk', 'locale': Locale('da', 'DK')},
{'name': 'Deutsch', 'locale': Locale('de', 'DE')},
{'name': 'English', 'locale': Locale('en', 'US')},
{'name': 'Español', 'locale': Locale('es', 'ES')},
{'name': 'Français', 'locale': Locale('fr', 'FR')},
// {'name': 'Gaeilge', 'locale': Locale('ga', 'IE')},
{'name': 'हिन्दी', 'locale': Locale('hi', 'IN')},
{'name': 'Magyar', 'locale': Locale('hu', 'HU')},
{'name': 'Italiano', 'locale': Locale('it', 'IT')},
{'name': '한국어', 'locale': Locale('ko', 'KR')},
{'name': 'فارسی', 'locale': Locale('fa', 'IR')},
{'name': 'ქართული', 'locale': Locale('ka', 'GE')},
{'name': 'Nederlands', 'locale': Locale('nl', 'NL')},
{'name': 'Polski', 'locale': Locale('pl', 'PL')},
{'name': 'Português (Brasil)', 'locale': Locale('pt', 'BR')},
{'name': 'Română', 'locale': Locale('ro', 'RO')},
{'name': 'Русский', 'locale': Locale('ru', 'RU')},
{'name': 'Slovenčina', 'locale': Locale('sk', 'SK')},
{'name': 'Türkçe', 'locale': Locale('tr', 'TR')},
{'name': 'اردو', 'locale': Locale('ur', 'PK')},
{'name': '中文(简体)', 'locale': Locale('zh', 'CN')},
{'name': '中文(繁體)', 'locale': Locale('zh', 'TW')},
];
@pragma('vm:entry-point')
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) {
@ -85,58 +85,40 @@ void callbackDispatcher() {
}
void main() async {
final String timeZoneName;
WidgetsFlutterBinding.ensureInitialized();
Connectivity()
.onConnectivityChanged
.listen((List<ConnectivityResult> result) {
result.contains(ConnectivityResult.none)
? isOnline.value = Future(() => false)
: isOnline.value = InternetConnection().hasInternetAccess;
});
DeviceFeature().init();
if (Platform.isAndroid) {
Workmanager().initialize(callbackDispatcher, isInDebugMode: kDebugMode);
await setOptimalDisplayMode();
}
if (Platform.isAndroid || Platform.isIOS) {
timeZoneName = await FlutterTimezone.getLocalTimezone();
} else {
timeZoneName = '${DateTimeZone.local}';
}
tz.initializeTimeZones();
tz.setLocalLocation(tz.getLocation(timeZoneName));
await isarInit();
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');
const DarwinInitializationSettings initializationSettingsDarwin =
DarwinInitializationSettings();
const LinuxInitializationSettings initializationSettingsLinux =
LinuxInitializationSettings(defaultActionName: 'Rain');
const InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsDarwin,
linux: initializationSettingsLinux,
);
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
await initializeApp();
runApp(const MyApp());
}
Future<void> setOptimalDisplayMode() async {
final List<DisplayMode> supported = await FlutterDisplayMode.supported;
final DisplayMode active = await FlutterDisplayMode.active;
final List<DisplayMode> sameResolution = supported
.where((DisplayMode m) =>
m.width == active.width && m.height == active.height)
.toList()
..sort((DisplayMode a, DisplayMode b) =>
b.refreshRate.compareTo(a.refreshRate));
final DisplayMode mostOptimalMode =
sameResolution.isNotEmpty ? sameResolution.first : active;
await FlutterDisplayMode.setPreferredMode(mostOptimalMode);
Future<void> initializeApp() async {
setupConnectivityListener();
await initializeTimeZone();
await initializeIsar();
await initializeNotifications();
if (Platform.isAndroid) {
await setOptimalDisplayMode();
Workmanager().initialize(callbackDispatcher, isInDebugMode: kDebugMode);
HomeWidget.setAppGroupId(appGroupId);
}
DeviceFeature().init();
}
Future<void> isarInit() async {
void setupConnectivityListener() {
Connectivity().onConnectivityChanged.listen((result) {
isOnline.value =
result.contains(ConnectivityResult.none)
? Future.value(false)
: InternetConnection().hasInternetAccess;
});
}
Future<void> initializeTimeZone() async {
final timeZoneName = await FlutterTimezone.getLocalTimezone();
tz.initializeTimeZones();
tz.setLocalLocation(tz.getLocation(timeZoneName));
}
Future<void> initializeIsar() async {
isar = await Isar.open([
SettingsSchema,
MainWeatherCacheSchema,
@ -158,6 +140,28 @@ Future<void> isarInit() async {
}
}
Future<void> initializeNotifications() async {
const initializationSettings = InitializationSettings(
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
iOS: DarwinInitializationSettings(),
linux: LinuxInitializationSettings(defaultActionName: 'Rain'),
);
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
}
Future<void> setOptimalDisplayMode() async {
final supported = await FlutterDisplayMode.supported;
final active = await FlutterDisplayMode.active;
final sameResolution =
supported
.where((m) => m.width == active.width && m.height == active.height)
.toList()
..sort((a, b) => b.refreshRate.compareTo(a.refreshRate));
final mostOptimalMode =
sameResolution.isNotEmpty ? sameResolution.first : active;
await FlutterDisplayMode.setPreferredMode(mostOptimalMode);
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@ -176,30 +180,14 @@ class MyApp extends StatefulWidget {
}) async {
final state = context.findAncestorStateOfType<_MyAppState>()!;
if (newAmoledTheme != null) {
state.changeAmoledTheme(newAmoledTheme);
}
if (newMaterialColor != null) {
state.changeMarerialTheme(newMaterialColor);
}
if (newRoundDegree != null) {
state.changeRoundDegree(newRoundDegree);
}
if (newLargeElement != null) {
state.changeLargeElement(newLargeElement);
}
if (newLocale != null) {
state.changeLocale(newLocale);
}
if (newTimeRange != null) {
state.changeTimeRange(newTimeRange);
}
if (newTimeStart != null) {
state.changeTimeStart(newTimeStart);
}
if (newTimeEnd != null) {
state.changeTimeEnd(newTimeEnd);
}
if (newAmoledTheme != null) state.changeAmoledTheme(newAmoledTheme);
if (newMaterialColor != null) state.changeMarerialTheme(newMaterialColor);
if (newRoundDegree != null) state.changeRoundDegree(newRoundDegree);
if (newLargeElement != null) state.changeLargeElement(newLargeElement);
if (newLocale != null) state.changeLocale(newLocale);
if (newTimeRange != null) state.changeTimeRange(newTimeRange);
if (newTimeStart != null) state.changeTimeStart(newTimeStart);
if (newTimeEnd != null) state.changeTimeEnd(newTimeEnd);
if (newWidgetBackgroundColor != null) {
state.changeWidgetBackgroundColor(newWidgetBackgroundColor);
}
@ -215,83 +203,41 @@ class MyApp extends StatefulWidget {
class _MyAppState extends State<MyApp> {
final themeController = Get.put(ThemeController());
void changeAmoledTheme(bool newAmoledTheme) {
setState(() {
amoledTheme = newAmoledTheme;
});
}
void changeMarerialTheme(bool newMaterialColor) {
setState(() {
materialColor = newMaterialColor;
});
}
void changeRoundDegree(bool newRoundDegree) {
setState(() {
roundDegree = newRoundDegree;
});
}
void changeLargeElement(bool newLargeElement) {
setState(() {
largeElement = newLargeElement;
});
}
void changeTimeRange(int newTimeRange) {
setState(() {
timeRange = newTimeRange;
});
}
void changeTimeStart(String newTimeStart) {
setState(() {
timeStart = newTimeStart;
});
}
void changeTimeEnd(String newTimeEnd) {
setState(() {
timeEnd = newTimeEnd;
});
}
void changeLocale(Locale newLocale) {
setState(() {
locale = newLocale;
});
}
void changeWidgetBackgroundColor(String newWidgetBackgroundColor) {
setState(() {
widgetBackgroundColor = newWidgetBackgroundColor;
});
}
void changeWidgetTextColor(String newWidgetTextColor) {
setState(() {
widgetTextColor = newWidgetTextColor;
});
}
void changeAmoledTheme(bool newAmoledTheme) =>
setState(() => amoledTheme = newAmoledTheme);
void changeMarerialTheme(bool newMaterialColor) =>
setState(() => materialColor = newMaterialColor);
void changeRoundDegree(bool newRoundDegree) =>
setState(() => roundDegree = newRoundDegree);
void changeLargeElement(bool newLargeElement) =>
setState(() => largeElement = newLargeElement);
void changeTimeRange(int newTimeRange) =>
setState(() => timeRange = newTimeRange);
void changeTimeStart(String newTimeStart) =>
setState(() => timeStart = newTimeStart);
void changeTimeEnd(String newTimeEnd) => setState(() => timeEnd = newTimeEnd);
void changeLocale(Locale newLocale) => setState(() => locale = newLocale);
void changeWidgetBackgroundColor(String newWidgetBackgroundColor) =>
setState(() => widgetBackgroundColor = newWidgetBackgroundColor);
void changeWidgetTextColor(String newWidgetTextColor) =>
setState(() => widgetTextColor = newWidgetTextColor);
@override
void initState() {
super.initState();
amoledTheme = settings.amoledTheme;
materialColor = settings.materialColor;
roundDegree = settings.roundDegree;
largeElement = settings.largeElement;
locale = Locale(
settings.language!.substring(0, 2), settings.language!.substring(3));
settings.language!.substring(0, 2),
settings.language!.substring(3),
);
timeRange = settings.timeRange ?? 1;
timeStart = settings.timeStart ?? '09:00';
timeEnd = settings.timeEnd ?? '21:00';
widgetBackgroundColor = settings.widgetBackgroundColor ?? '';
widgetTextColor = settings.widgetTextColor ?? '';
if (Platform.isAndroid) {
HomeWidget.setAppGroupId(appGroupId);
}
super.initState();
}
@override
@ -304,34 +250,65 @@ class _MyAppState extends State<MyApp> {
child: DynamicColorBuilder(
builder: (lightColorScheme, darkColorScheme) {
final lightMaterialTheme = lightTheme(
lightColorScheme?.surface, lightColorScheme, edgeToEdgeAvailable);
lightColorScheme?.surface,
lightColorScheme,
edgeToEdgeAvailable,
);
final darkMaterialTheme = darkTheme(
darkColorScheme?.surface, darkColorScheme, edgeToEdgeAvailable);
final darkMaterialThemeOled =
darkTheme(oledColor, darkColorScheme, edgeToEdgeAvailable);
darkColorScheme?.surface,
darkColorScheme,
edgeToEdgeAvailable,
);
final darkMaterialThemeOled = darkTheme(
oledColor,
darkColorScheme,
edgeToEdgeAvailable,
);
return GetMaterialApp(
themeMode: themeController.theme,
theme: materialColor
theme:
materialColor
? lightColorScheme != null
? lightMaterialTheme
: lightTheme(
lightColor, colorSchemeLight, edgeToEdgeAvailable)
: lightTheme(lightColor, colorSchemeLight, edgeToEdgeAvailable),
darkTheme: amoledTheme
lightColor,
colorSchemeLight,
edgeToEdgeAvailable,
)
: lightTheme(
lightColor,
colorSchemeLight,
edgeToEdgeAvailable,
),
darkTheme:
amoledTheme
? materialColor
? darkColorScheme != null
? darkMaterialThemeOled
: darkTheme(
oledColor, colorSchemeDark, edgeToEdgeAvailable)
: darkTheme(oledColor, colorSchemeDark, edgeToEdgeAvailable)
oledColor,
colorSchemeDark,
edgeToEdgeAvailable,
)
: darkTheme(
oledColor,
colorSchemeDark,
edgeToEdgeAvailable,
)
: materialColor
? darkColorScheme != null
? darkMaterialTheme
: darkTheme(
darkColor, colorSchemeDark, edgeToEdgeAvailable)
darkColor,
colorSchemeDark,
edgeToEdgeAvailable,
)
: darkTheme(
darkColor, colorSchemeDark, edgeToEdgeAvailable),
darkColor,
colorSchemeDark,
edgeToEdgeAvailable,
),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
@ -343,11 +320,12 @@ class _MyAppState extends State<MyApp> {
supportedLocales:
appLanguages.map((e) => e['locale'] as Locale).toList(),
debugShowCheckedModeBanner: false,
home: settings.onboard
? (locationCache.city == null) ||
(locationCache.district == null) ||
(locationCache.lat == null) ||
(locationCache.lon == null)
home:
settings.onboard
? (locationCache.city == null ||
locationCache.district == null ||
locationCache.lat == null ||
locationCache.lon == null)
? const SelectGeolocation(isStart: true)
: const HomePage()
: const OnBording(),

234
lib/theme/theme.dart Normal file → Executable file
View file

@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:dynamic_color/dynamic_color.dart';
final ThemeData baseLigth = ThemeData.light(useMaterial3: true);
final ThemeData baseLight = ThemeData.light(useMaterial3: true);
final ThemeData baseDark = ThemeData.dark(useMaterial3: true);
const Color lightColor = Colors.white;
@ -14,140 +14,146 @@ ColorScheme colorSchemeLight = ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
brightness: Brightness.light,
);
ColorScheme colorSchemeDark = ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
brightness: Brightness.dark,
);
ThemeData lightTheme(
Color? color, ColorScheme? colorScheme, bool edgeToEdgeAvailable) {
return baseLigth.copyWith(
Color? color,
ColorScheme? colorScheme,
bool edgeToEdgeAvailable,
) {
return _buildTheme(
baseTheme: baseLight,
brightness: Brightness.light,
colorScheme: colorScheme
?.copyWith(
brightness: Brightness.light,
surface: baseLigth.colorScheme.surface,
)
.harmonized(),
textTheme: GoogleFonts.ubuntuTextTheme(baseLigth.textTheme),
appBarTheme: AppBarTheme(
backgroundColor: color,
foregroundColor: baseLigth.colorScheme.onSurface,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
elevation: 0,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.dark,
statusBarColor: Colors.transparent,
systemStatusBarContrastEnforced: false,
systemNavigationBarContrastEnforced: false,
systemNavigationBarDividerColor: Colors.transparent,
systemNavigationBarIconBrightness: Brightness.dark,
systemNavigationBarColor:
edgeToEdgeAvailable ? Colors.transparent : colorScheme?.surface,
),
),
primaryColor: color,
canvasColor: color,
scaffoldBackgroundColor: color,
cardTheme: baseLigth.cardTheme.copyWith(
color: color,
surfaceTintColor:
color == oledColor ? Colors.transparent : colorScheme?.surfaceTint,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
shadowColor: Colors.transparent,
),
bottomSheetTheme: baseLigth.bottomSheetTheme.copyWith(
backgroundColor: color,
surfaceTintColor:
color == oledColor ? Colors.transparent : colorScheme?.surfaceTint,
),
navigationRailTheme: baseLigth.navigationRailTheme.copyWith(
backgroundColor: color,
),
navigationBarTheme: baseLigth.navigationBarTheme.copyWith(
backgroundColor: color,
surfaceTintColor:
color == oledColor ? Colors.transparent : colorScheme?.surfaceTint,
),
inputDecorationTheme: baseLigth.inputDecorationTheme.copyWith(
labelStyle: WidgetStateTextStyle.resolveWith(
(Set<WidgetState> states) {
return const TextStyle(fontSize: 14);
},
),
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
),
indicatorColor: Colors.black,
colorScheme: colorScheme,
edgeToEdgeAvailable: edgeToEdgeAvailable,
);
}
ThemeData darkTheme(
Color? color, ColorScheme? colorScheme, bool edgeToEdgeAvailable) {
return baseDark.copyWith(
Color? color,
ColorScheme? colorScheme,
bool edgeToEdgeAvailable,
) {
return _buildTheme(
baseTheme: baseDark,
brightness: Brightness.dark,
colorScheme: colorScheme
color: color,
colorScheme: colorScheme,
edgeToEdgeAvailable: edgeToEdgeAvailable,
);
}
ThemeData _buildTheme({
required ThemeData baseTheme,
required Brightness brightness,
required Color? color,
required ColorScheme? colorScheme,
required bool edgeToEdgeAvailable,
}) {
final harmonizedColorScheme =
colorScheme
?.copyWith(
brightness: Brightness.dark,
surface: baseDark.colorScheme.surface,
brightness: brightness,
surface: baseTheme.colorScheme.surface,
)
.harmonized(),
textTheme: GoogleFonts.ubuntuTextTheme(baseDark.textTheme),
appBarTheme: AppBarTheme(
backgroundColor: color,
foregroundColor: baseDark.colorScheme.onSurface,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
elevation: 0,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light,
statusBarColor: Colors.transparent,
systemStatusBarContrastEnforced: false,
systemNavigationBarContrastEnforced: false,
systemNavigationBarDividerColor: Colors.transparent,
systemNavigationBarIconBrightness: Brightness.light,
systemNavigationBarColor:
edgeToEdgeAvailable ? Colors.transparent : colorScheme?.surface,
),
.harmonized();
return baseTheme.copyWith(
brightness: brightness,
colorScheme: harmonizedColorScheme,
textTheme: GoogleFonts.ubuntuTextTheme(baseTheme.textTheme),
appBarTheme: _buildAppBarTheme(
color,
baseTheme.colorScheme.onSurface,
edgeToEdgeAvailable,
brightness,
harmonizedColorScheme,
),
primaryColor: color,
canvasColor: color,
scaffoldBackgroundColor: color,
cardTheme: baseDark.cardTheme.copyWith(
color: color,
surfaceTintColor:
color == oledColor ? Colors.transparent : colorScheme?.surfaceTint,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
cardTheme: _buildCardTheme(color, harmonizedColorScheme),
bottomSheetTheme: _buildBottomSheetTheme(color, harmonizedColorScheme),
navigationRailTheme: baseTheme.navigationRailTheme.copyWith(
backgroundColor: color,
),
navigationBarTheme: _buildNavigationBarTheme(color, harmonizedColorScheme),
inputDecorationTheme: _buildInputDecorationTheme(),
);
}
AppBarTheme _buildAppBarTheme(
Color? color,
Color? onSurfaceColor,
bool edgeToEdgeAvailable,
Brightness brightness,
ColorScheme? colorScheme,
) {
return AppBarTheme(
backgroundColor: color,
foregroundColor: onSurfaceColor,
shadowColor: Colors.transparent,
),
bottomSheetTheme: baseDark.bottomSheetTheme.copyWith(
backgroundColor: color,
surfaceTintColor:
color == oledColor ? Colors.transparent : colorScheme?.surfaceTint,
),
navigationRailTheme: baseDark.navigationRailTheme.copyWith(
backgroundColor: color,
),
navigationBarTheme: baseDark.navigationBarTheme.copyWith(
backgroundColor: color,
surfaceTintColor:
color == oledColor ? Colors.transparent : colorScheme?.surfaceTint,
),
inputDecorationTheme: baseDark.inputDecorationTheme.copyWith(
labelStyle: WidgetStateTextStyle.resolveWith(
(Set<WidgetState> states) {
return const TextStyle(fontSize: 14);
},
),
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
surfaceTintColor: Colors.transparent,
elevation: 0,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarIconBrightness:
brightness == Brightness.light ? Brightness.dark : Brightness.light,
statusBarColor: Colors.transparent,
systemStatusBarContrastEnforced: false,
systemNavigationBarContrastEnforced: false,
systemNavigationBarDividerColor: Colors.transparent,
systemNavigationBarIconBrightness:
brightness == Brightness.light ? Brightness.dark : Brightness.light,
systemNavigationBarColor:
edgeToEdgeAvailable ? Colors.transparent : colorScheme?.surface,
),
);
}
CardThemeData _buildCardTheme(Color? color, ColorScheme? colorScheme) {
return CardThemeData(
color: color,
surfaceTintColor:
color == oledColor ? Colors.transparent : colorScheme?.surfaceTint,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
shadowColor: Colors.transparent,
);
}
BottomSheetThemeData _buildBottomSheetTheme(
Color? color,
ColorScheme? colorScheme,
) {
return BottomSheetThemeData(
backgroundColor: color,
surfaceTintColor:
color == oledColor ? Colors.transparent : colorScheme?.surfaceTint,
);
}
NavigationBarThemeData _buildNavigationBarTheme(
Color? color,
ColorScheme? colorScheme,
) {
return NavigationBarThemeData(
backgroundColor: color,
surfaceTintColor:
color == oledColor ? Colors.transparent : colorScheme?.surfaceTint,
);
}
InputDecorationTheme _buildInputDecorationTheme() {
return InputDecorationTheme(
labelStyle: WidgetStateTextStyle.resolveWith((Set<WidgetState> states) {
return const TextStyle(fontSize: 14);
}),
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
);
}

33
lib/theme/theme_controller.dart Normal file → Executable file
View file

@ -1,31 +1,40 @@
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:rain/app/data/weather.dart';
import 'package:rain/app/data/db.dart';
import 'package:rain/main.dart';
class ThemeController extends GetxController {
ThemeMode get theme => settings.theme == 'system'
? ThemeMode.system
: settings.theme == 'dark'
? ThemeMode.dark
: ThemeMode.light;
ThemeMode get theme => _getThemeMode();
void saveOledTheme(bool isOled) {
settings.amoledTheme = isOled;
isar.writeTxnSync(() => isar.settings.putSync(settings));
_updateSetting((settings) => settings.amoledTheme = isOled);
}
void saveMaterialTheme(bool isMaterial) {
settings.materialColor = isMaterial;
isar.writeTxnSync(() => isar.settings.putSync(settings));
_updateSetting((settings) => settings.materialColor = isMaterial);
}
void saveTheme(String themeMode) {
settings.theme = themeMode;
isar.writeTxnSync(() => isar.settings.putSync(settings));
_updateSetting((settings) => settings.theme = themeMode);
}
void changeTheme(ThemeData theme) => Get.changeTheme(theme);
void changeThemeMode(ThemeMode themeMode) => Get.changeThemeMode(themeMode);
ThemeMode _getThemeMode() {
switch (settings.theme) {
case 'system':
return ThemeMode.system;
case 'dark':
return ThemeMode.dark;
default:
return ThemeMode.light;
}
}
void _updateSetting(void Function(Settings) update) {
update(settings);
isar.writeTxnSync(() => isar.settings.putSync(settings));
}
}

0
lib/translation/bn_in.dart Normal file → Executable file
View file

0
lib/translation/cs_cz.dart Normal file → Executable file
View file

0
lib/translation/da_dk.dart Normal file → Executable file
View file

0
lib/translation/de_de.dart Normal file → Executable file
View file

0
lib/translation/en_us.dart Normal file → Executable file
View file

6
lib/translation/es_es.dart Normal file → Executable file
View file

@ -44,8 +44,7 @@ class EsEs {
'hPa': 'hPa',
'settings': 'Ajustes',
'no_inter': 'Sin conexión a Internet',
'on_inter':
'Conéctate a Internet para obtener información meteorológica.',
'on_inter': 'Conéctate a Internet para obtener información meteorológica.',
'location': 'Ubicación',
'no_location':
'Activa la localización para obtener información meteorológica para tu ubicación actual.',
@ -136,8 +135,7 @@ class EsEs {
'map': 'Mapa',
'clearCacheStore': 'Borrar caché',
'deletedCacheStore': 'Borrando caché',
'deletedCacheStoreQuery':
'¿Estás seguro de que quieres borrar el caché?',
'deletedCacheStoreQuery': '¿Estás seguro de que quieres borrar el caché?',
'addWidget': 'Agregar widget',
'hideMap': 'Ocultar mapa',
};

0
lib/translation/fa_ir.dart Normal file → Executable file
View file

3
lib/translation/fr_fr.dart Normal file → Executable file
View file

@ -60,8 +60,7 @@ class FrFr {
'district': 'District',
'noWeatherCard': 'Ajouter une ville',
'deletedCardWeather': 'Supprimer une ville',
'deletedCardWeatherQuery':
'Êtes-vous sûr de vouloir supprimer la ville ?',
'deletedCardWeatherQuery': 'Êtes-vous sûr de vouloir supprimer la ville ?',
'delete': 'Supprimer',
'cancel': 'Annuler',
'time': 'Heure locale',

0
lib/translation/ga_ie.dart Normal file → Executable file
View file

0
lib/translation/hi_in.dart Normal file → Executable file
View file

3
lib/translation/hu_hu.dart Normal file → Executable file
View file

@ -44,8 +44,7 @@ class HuHu {
'hPa': 'hPa',
'settings': 'Beállítások',
'no_inter': 'Nincs internet',
'on_inter':
'Kapcsolja be az internetet az időjárási adatok lekéréséhez.',
'on_inter': 'Kapcsolja be az internetet az időjárási adatok lekéréséhez.',
'location': 'Hely',
'no_location':
'Engedélyezze a helyszolgáltatást az aktuális hely időjárásadatainak megszerzéséhez.',

6
lib/translation/it_it.dart Normal file → Executable file
View file

@ -44,8 +44,7 @@ class ItIt {
'hPa': 'hPa',
'settings': 'Imposta.',
'no_inter': 'Non c\'è connessione Internet',
'on_inter':
'Attiva la connessione Internet per avere dati meteorologici.',
'on_inter': 'Attiva la connessione Internet per avere dati meteorologici.',
'location': 'Posizione',
'no_location':
'Abilita il servizio di localizzazione per ottenere i dati meteo per la posizione corrente.',
@ -60,8 +59,7 @@ class ItIt {
'district': 'Regione',
'noWeatherCard': 'Aggiungi una città',
'deletedCardWeather': 'Rimozione della città',
'deletedCardWeatherQuery':
'Sei sicuro di voler rimuovere questa città?',
'deletedCardWeatherQuery': 'Sei sicuro di voler rimuovere questa città?',
'delete': 'Elimina',
'cancel': 'Annulla',
'time': 'Orario locale',

6
lib/translation/ka_ge.dart Normal file → Executable file
View file

@ -59,8 +59,7 @@ class KaGe {
'district': 'რაიონი',
'noWeatherCard': 'დაამატეთ ქალაქი',
'deletedCardWeather': 'ქალაქის წაშლა',
'deletedCardWeatherQuery':
'დარწმუნებული ხართ, რომ გსურთ ქალაქის წაშლა?',
'deletedCardWeatherQuery': 'დარწმუნებული ხართ, რომ გსურთ ქალაქის წაშლა?',
'delete': 'ამოღება',
'cancel': 'გაუქმება',
'time': 'დრო ქალაქში',
@ -135,8 +134,7 @@ class KaGe {
'map': 'რუკა',
'clearCacheStore': 'ქეშის გასუფთავება',
'deletedCacheStore': 'ქეშის გასუფთავება მიმდინარეობს',
'deletedCacheStoreQuery':
'დარწმუნებული ხართ, რომ გსურთ ქეშის გასუფთავება?',
'deletedCacheStoreQuery': 'დარწმუნებული ხართ, რომ გსურთ ქეშის გასუფთავება?',
'addWidget': 'ვიდჯეტის დამატება',
'hideMap': 'რუკის დამალვა',
};

0
lib/translation/ko_kr.dart Normal file → Executable file
View file

6
lib/translation/nl_nl.dart Normal file → Executable file
View file

@ -44,8 +44,7 @@ class NlNl {
'hPa': 'hPa',
'settings': 'Instellingen.',
'no_inter': 'Geen Internet',
'on_inter':
'Schakel Internet in om meteorologische gegevens te ontvangen.',
'on_inter': 'Schakel Internet in om meteorologische gegevens te ontvangen.',
'location': 'Locatie',
'no_location':
'Schakel de locatiedienst in om weer gegevens voor de huidige locatie te ontvangen.',
@ -60,8 +59,7 @@ class NlNl {
'district': 'District',
'noWeatherCard': 'Voeg een stad toe',
'deletedCardWeather': 'Verwijder een city',
'deletedCardWeatherQuery':
'Weet je zeker dat je de stad wilt verwijderen?',
'deletedCardWeatherQuery': 'Weet je zeker dat je de stad wilt verwijderen?',
'delete': 'Verwijder',
'cancel': 'Annuleer',
'time': 'Tijd in de stad',

3
lib/translation/pl_pl.dart Normal file → Executable file
View file

@ -134,8 +134,7 @@ class PlPl {
'map': 'Mapa',
'clearCacheStore': 'Wyczyść pamięć podręczną',
'deletedCacheStore': 'Czyszczenie pamięci podręcznej',
'deletedCacheStoreQuery':
'Czy na pewno chcesz wyczyścić pamięć podręczną?',
'deletedCacheStoreQuery': 'Czy na pewno chcesz wyczyścić pamięć podręczną?',
'addWidget': 'Dodaj widget',
'hideMap': 'Ukryj mapę',
};

0
lib/translation/pt_br.dart Normal file → Executable file
View file

0
lib/translation/ro_ro.dart Normal file → Executable file
View file

0
lib/translation/ru_ru.dart Normal file → Executable file
View file

0
lib/translation/sk_sk.dart Normal file → Executable file
View file

0
lib/translation/tr_tr.dart Normal file → Executable file
View file

0
lib/translation/translation.dart Normal file → Executable file
View file

0
lib/translation/ur_pk.dart Normal file → Executable file
View file

0
lib/translation/zh_ch.dart Normal file → Executable file
View file

0
lib/translation/zh_tw.dart Normal file → Executable file
View file

Some files were not shown because too many files have changed in this diff Show more