Compare commits

...

80 commits
v1.3.0 ... 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
Yoshi
07142e25a7 Hide map 2024-08-30 22:31:42 +03:00
Yoshi
55e813749a Fix widget 2024-08-29 22:00:08 +03:00
Yoshi
1d7cbbd1bf Fix map and add new button 2024-08-28 21:52:42 +03:00
Yoshi
1ea344c69a Settings widget 2024-08-26 21:27:15 +03:00
Yoshi
5cd6e2e6c8 Delete 2gis 2024-08-23 07:15:37 +03:00
Yoshi
c0c15bb811 Fix ExpandableFab 2024-08-22 23:34:56 +03:00
Yoshi
d5a34ea11b Fix map 2024-08-22 22:49:37 +03:00
Yoshi
f174c0f336 Add buttons to the map 2024-08-21 23:20:50 +03:00
Yoshi
d0791279a2 Fix animation markers 2024-08-21 21:55:29 +03:00
Yoshi
88a7bafffb Add clear cache 2024-08-20 22:19:02 +03:00
Yoshi
27e276a092 Add search the map 2024-08-18 18:35:30 +03:00
Yoshi
383fb24881 Fix markers 2024-08-18 14:08:54 +03:00
Yoshi
8ed047a1aa Add markers 2024-08-15 22:39:51 +03:00
Yoshi
6c7da7b28d Add map 2024-08-12 21:03:35 +03:00
Yoshi
b2e843c5d9 Update dependencies 2024-08-09 22:19:37 +03:00
Yoshi
91e1757a20 Replacing SizedBox with Gap 2024-08-04 11:48:25 +03:00
Yoshi
8bce420296 Fix UI 2024-08-03 18:55:26 +03:00
Yoshi
2cc70f9221 Add mmHg and m/s 2024-08-02 22:52:33 +03:00
Yoshi
dd57938153 Fix anim new add form && Add feels to the top tile 2024-07-29 22:37:08 +03:00
Yoshi
fc246ac5a2 Add new form create_card_weather 2024-07-24 23:07:35 +03:00
Yoshi
33ceb30885 Fix api code 2024-07-13 07:00:58 +03:00
Yoshi
46e1546e5b Two weather display options 2024-07-10 21:59:36 +03:00
Yoshi
dd3339bc3b issue #185 2024-07-09 23:03:40 +03:00
Yoshi
9a7858f279 Fix widget 2024-07-07 22:26:29 +03:00
Yoshi
6d0d0efe36 Fix refresh 2024-07-07 15:01:58 +03:00
Yoshi
aa125fcdc1 Add search cards 2024-07-07 14:49:45 +03:00
Yoshi
0d565752d5 Fix redesign 2024-06-24 20:39:57 +03:00
Yoshi
6646f657d2 Redesign 2024-06-23 23:23:43 +03:00
Yoshi
647d620d45
Merge pull request #182 from Regu-Miabyss/patch-1
Update zh_tw.dart
2024-06-15 08:51:52 +03:00
Regu-Miabyss
b459a50f60
Update zh_tw.dart
Chinese (Traditional) translation fix
2024-06-15 08:12:12 +08:00
Yoshi
da64975e68 issue #179 2024-06-08 18:03:59 +03:00
Yoshi
58dbbe52ce Add locale ko_KR 2024-05-31 10:40:10 +03:00
Yoshi
275bbba853
Merge pull request #181 from tsyqax/main
Add Korean Translation
2024-05-31 10:32:51 +03:00
tsyqax6
ceb27da369
Translation (Start But End) 2024-05-31 12:34:23 +09:00
tsyqax6
b8600ab472
Create ko_kr.dart 2024-05-31 12:09:44 +09:00
Yoshi
3cfd4f8248 Fix flutter_native_splash 2024-05-26 19:09:28 +03:00
Yoshi
8eae7203f0 Update dependencies 2024-05-18 21:58:53 +03:00
Yoshi
42f7a2590e
Merge pull request #180 from jash13desai/main
Some UI Fixes
2024-05-14 11:54:04 +03:00
jash-desai
4b1536579d minor changes 2024-05-11 23:01:43 +05:30
jash-desai
f3400916d9 fixed horizontal padding in validator text in MyTextForm widget 2024-05-11 22:49:11 +05:30
jash-desai
696634c08f settings_full label text in bottom navbar fixed 2024-05-11 22:31:43 +05:30
Yoshi
a00abd4f3a Fix bug 2024-04-10 21:25:23 +03:00
Yoshi
1342079b6b Update dependencies 2024-04-05 22:52:00 +03:00
Yoshi
4d310776ce Add locale zh_TW 2024-03-11 23:42:29 +03:00
Yoshi
cb7367c04f
Merge pull request #173 from Regu-Miabyss/main
I made the Chinese(Traditional) translate.
2024-03-11 23:38:05 +03:00
Regu-Miabyss
51f72b4e7b
Update main.dart 2024-03-10 17:22:53 +08:00
Regu-Miabyss
400614a8f1
Add Chinese Traditional translation 2024-03-10 17:20:47 +08:00
Regu-Miabyss
1e11b6b775
Added Chinese Traditional translation 2024-03-10 17:19:04 +08:00
Yoshi
902bc45207 Add locale fa_IR 2024-03-03 16:11:30 +03:00
Yoshi
8275ec8f9e
Merge pull request #170 from behdanisohrab/main
Added Persian Translation
2024-03-02 17:13:47 +03:00
Sohrab Behdani
441a5ee680 Added Persian Translation 2024-03-02 10:44:42 +03:30
Yoshi
a46986f050 Update dependencies 2024-02-15 22:55:14 +03:00
Yoshi
fa5fbb8ce6 Add da_DK locale and fix error 2024-02-13 22:32:28 +03:00
Yoshi
cda285ea15
Merge pull request #168 from HJMVR/main
Danish translations
2024-02-13 22:05:22 +03:00
Yoshi
f08c459bc1
Update da_dk.dart 2024-02-13 22:04:57 +03:00
Hans-Jørgen Martinus Vork Rosenwein
1f945c4dfd
last update 2024-02-13 12:43:24 +01:00
Hans-Jørgen Martinus Vork Rosenwein
c5b55a291b
more danish 2024-02-13 12:34:29 +01:00
Hans-Jørgen Martinus Vork Rosenwein
013493c0dd
More danish updates 2024-02-13 12:21:08 +01:00
Hans-Jørgen Martinus Vork Rosenwein
58373c7c9c
UPdate 2024-02-13 11:26:30 +01:00
Hans-Jørgen Martinus Vork Rosenwein
504a77f6ad
Merge branch 'darkmoonight:main' into main 2024-02-13 11:21:43 +01:00
Hans-Jørgen Martinus Vork Rosenwein
0f29d2bdad
Update da_dk.dart 2024-02-12 20:04:55 +01:00
Hans-Jørgen Martinus Vork Rosenwein
265ba143ab
Create da_dk.dart 2024-02-12 10:23:56 +01:00
149 changed files with 11768 additions and 8157 deletions

3
.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
@ -41,4 +42,4 @@ app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
/android/app/release

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,15 +1,15 @@
<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'>
<p align='center'>
<a href='https://github.com/darkmoonight/Rain/stargazers'><img alt='Stars' src='https://img.shields.io/github/stars/darkmoonight/Rain?color=abc0d3'/></a>
<a href='https://github.com/darkmoonight/Rain/forks'><img alt='Forks' src='https://img.shields.io/github/forks/darkmoonight/Rain?color=abc0d3'/></a>
<a href='https://github.com/darkmoonight/Rain/releases'><img alt='GitHub release' src='https://img.shields.io/github/v/release/darkmoonight/Rain?color=abc0d3'/></a>
<a href='https://github.com/darkmoonight/Rain/blob/main/LICENSE'><img alt='License' src='https://img.shields.io/github/license/darkmoonight/Rain?color=abc0d3'/></a>
</p>
<p align='center'>
<a href='https://github.com/darkmoonight/Rain/stargazers'><img alt='Stars' src='https://img.shields.io/github/stars/darkmoonight/Rain?color=abc0d3'/></a>
<a href='https://github.com/darkmoonight/Rain/forks'><img alt='Forks' src='https://img.shields.io/github/forks/darkmoonight/Rain?color=abc0d3'/></a>
<a href='https://github.com/darkmoonight/Rain/releases'><img alt='GitHub release' src='https://img.shields.io/github/v/release/darkmoonight/Rain?color=abc0d3'/></a>
<a href='https://github.com/darkmoonight/Rain/blob/main/LICENSE'><img alt='License' src='https://img.shields.io/github/license/darkmoonight/Rain?color=abc0d3'/></a>
</p>
</p>
<p align='center'> Tired of unpredictable weather? Rain's got you covered! Get ready for any forecast. 🌦️ </p>
@ -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,62 +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']
release {
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.0.0"
implementation("androidx.core:core-remoteviews:1.1.0")
implementation("com.google.android.material:material:1.12.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

@ -11,16 +11,6 @@
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:extractNativeLibs="true">
<receiver
android:name=".OreoWidget"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/oreo_widget_info" />
</receiver>
<activity
android:name=".MainActivity"
android:exported="true"
@ -28,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"
@ -36,7 +27,32 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="es.antonborri.home_widget.action.LAUNCH" />
</intent-filter>
</activity>
<receiver
android:name=".OreoWidget"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/oreo_widget_info" />
</receiver>
<receiver android:name="es.antonborri.home_widget.HomeWidgetBackgroundReceiver"
android:exported="true">
<intent-filter>
<action android:name="es.antonborri.home_widget.action.BACKGROUND" />
</intent-filter>
</receiver>
<service android:name="es.antonborri.home_widget.HomeWidgetBackgroundService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true" />
<meta-data
android:name="flutterEmbedding"
android:value="2" />
@ -52,4 +68,4 @@
</intent-filter>
</receiver>
</application>
</manifest>
</manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Before After
Before After

View file

@ -3,6 +3,7 @@
style="@style/Widget.Android.AppWidget.Container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSecondaryContainer"
android:theme="@style/Theme.Android.AppWidgetContainer"
android:id="@+id/widget_day_oreo">
@ -27,7 +28,7 @@
android:shadowRadius="1"
android:textSize="@dimen/widget_large_title_text_size"
tools:ignore="ObsoleteLayoutParam"
tools:text="Saturday, September 30 " />
tools:text="Saturday, September 30 " />
<ImageView
android:id="@+id/widget_day_icon"

View file

@ -3,6 +3,7 @@
style="@style/Widget.Android.AppWidget.Container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSecondaryContainer"
android:theme="@style/Theme.Android.AppWidgetContainer"
android:id="@+id/widget_day_oreo">
@ -27,7 +28,7 @@
android:shadowRadius="1"
android:textSize="@dimen/widget_large_title_text_size"
tools:ignore="ObsoleteLayoutParam"
tools:text="Saturday, September 30 " />
tools:text="Saturday, September 30 " />
<ImageView
android:id="@+id/widget_day_icon"

View file

@ -3,6 +3,7 @@
style="@style/Widget.Android.AppWidget.Container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSecondaryContainer"
android:theme="@style/Theme.Android.AppWidgetContainer"
android:id="@+id/widget_day_oreo">
@ -32,8 +33,8 @@
android:shadowRadius="1"
android:textAppearance="@android:style/TextAppearance.Material.Large"
android:textSize="@dimen/widget_title_text_size"
tools:text="21°"
tools:ignore="ObsoleteLayoutParam" />
tools:ignore="ObsoleteLayoutParam"
tools:text="21°" />
</LinearLayout>

View file

@ -3,6 +3,7 @@
style="@style/Widget.Android.AppWidget.Container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSecondaryContainer"
android:theme="@style/Theme.Android.AppWidgetContainer"
android:id="@+id/widget_day_oreo">

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<style name="LaunchTheme" parent="@style/Theme.Material3.DynamicColors.Dark.NoActionBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
@ -14,7 +14,7 @@
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<style name="NormalTheme" parent="@style/Theme.Material3.DynamicColors.Dark.NoActionBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View file

@ -3,7 +3,7 @@
<!--
Having themes.xml for night-v31 because of the priority order of the resource qualifiers.
-->
<style name="Theme.Android.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault.DayNight">
<style name="Theme.Android.AppWidgetContainerParent" parent="@style/Theme.Material3.DynamicColors.DayNight">
<item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item>
<item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item>
</style>

View file

@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<style name="LaunchTheme" parent="@style/Theme.Material3.DynamicColors.Dark.NoActionBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:forceDarkAllowed" tools:targetApi="q">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowDrawsSystemBarBackgrounds" >false</item>
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="o_mr1">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
@ -16,7 +16,7 @@
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<style name="NormalTheme" parent="@style/Theme.Material3.DynamicColors.Dark.NoActionBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<style name="LaunchTheme" parent="@style/Theme.Material3.DynamicColors.DayNight.NoActionBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
@ -14,17 +14,15 @@
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<style name="NormalTheme" parent="@style/Theme.Material3.DynamicColors.DayNight.NoActionBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
<item name="android:id">@android:id/background</item>
<item name="android:padding">?attr/appWidgetPadding</item>
<item name="android:background">@drawable/app_widget_background</item>
<item name="android:clipToOutline">true</item>
</style>
<style name="Widget.Android.AppWidget.InnerView" parent="android:Widget">
<item name="android:padding">?attr/appWidgetPadding</item>
<item name="android:background">@drawable/app_widget_inner_view_background</item>

View file

@ -5,7 +5,7 @@
and @android:dimen/system_app_widget_internal_padding requires API level 31
-->
<style name="Theme.Android.AppWidgetContainerParent"
parent="@android:style/Theme.DeviceDefault.DayNight">
parent="@style/Theme.Material3.DynamicColors.DayNight">
<item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item>
<item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item>
</style>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_widget_description">Oreo Widget</string>
<string name="app_widget_description">Rain Widget</string>
<string name="date_format_widget_oreo_style">EEE, d MMM │</string>
<string name="date_format_widget_oreo_big_style">EEEE, d MMM │</string>
</resources>

View file

@ -2,14 +2,14 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode
setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<style name="LaunchTheme" parent="@style/Theme.Material3.DynamicColors.DayNight.NoActionBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed" tools:ignore="NewApi">false</item>
<item name="android:forceDarkAllowed" tools:targetApi="q">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode" tools:ignore="NewApi">shortEdges</item>
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="o_mr1">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
@ -17,18 +17,15 @@
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<style name="NormalTheme" parent="@style/Theme.Material3.DynamicColors.DayNight.NoActionBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
<item name="android:id">@android:id/background</item>
<item name="android:background">?android:attr/colorBackground</item>
<item name="android:background">?attr/colorSurface</item>
</style>
<style name="Widget.Android.AppWidget.InnerView" parent="android:Widget">
<item name="android:background">?android:attr/colorBackground</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
<item name="android:background">?attr/colorSurface</item>
<item name="android:textColor">?attr/itemTextColor</item>
</style>
</resources>
</resources>

View file

@ -1,5 +1,5 @@
<resources>
<style name="Theme.Android.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault">
<style name="Theme.Android.AppWidgetContainerParent" parent="@style/Theme.Material3.DynamicColors.DayNight">
<!-- Radius of the outer bound of widgets to make the rounded corners -->
<item name="appWidgetRadius">16dp</item>
<!--

View file

@ -1,15 +1,3 @@
buildscript {
ext.kotlin_version = '1.9.22'
repositories {
google()
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
@ -20,6 +8,8 @@ allprojects {
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}

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,25 +5,21 @@ 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()
mavenCentral()
gradlePluginPortal()
}
plugins {
id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '7.4.2' 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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Before After
Before After

View file

@ -25,11 +25,11 @@
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSLocationWhenInUseUsageDescription</key>
<string>The Rain App requires access to the device&apos;s location.</string>
<string>The Rain App requires access to the device's location.</string>
<key>NSLocationTemporaryUsageDescriptionDictionary</key>
<dict>
<key>TemporaryPreciseAccuracy</key>
<string>The Rain App requires temporary access to the device&apos;s precise location.</string>
<string>The Rain App requires temporary access to the device's precise location.</string>
</dict>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>

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.
}
}

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

@ -1,69 +1,38 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:rain/app/api/city.dart';
import 'package:rain/app/api/weather.dart';
import 'package:rain/app/data/weather.dart';
import 'package:rain/app/api/city_api.dart';
import 'package:rain/app/api/weather_api.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();
Future<MainWeatherCache> getWeatherData(double? lat, double? lon) async {
String url =
'latitude=$lat&longitude=$lon&hourly=temperature_2m,relativehumidity_2m,apparent_temperature,precipitation,rain,weathercode,surface_pressure,visibility,evapotranspiration,windspeed_10m,winddirection_10m,windgusts_10m,cloudcover,uv_index,dewpoint_2m,precipitation_probability,shortwave_radiation&daily=weathercode,temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min,sunrise,sunset,precipitation_sum,precipitation_probability_max,windspeed_10m_max,windgusts_10m_max,uv_index_max,rain_sum,winddirection_10m_dominant&forecast_days=12&timezone=auto';
String urlWeather;
settings.measurements == 'imperial' && settings.degrees == 'fahrenheit'
? urlWeather =
'$url&temperature_unit=fahrenheit&windspeed_unit=mph&precipitation_unit=inch'
: settings.measurements == 'imperial'
? urlWeather = '$url&windspeed_unit=mph&precipitation_unit=inch'
: settings.degrees == 'fahrenheit'
? urlWeather = '$url&temperature_unit=fahrenheit'
: urlWeather = url;
static const String _weatherParams =
'hourly=temperature_2m,relativehumidity_2m,apparent_temperature,precipitation,rain,weathercode,surface_pressure,visibility,evapotranspiration,windspeed_10m,winddirection_10m,windgusts_10m,cloudcover,uv_index,dewpoint_2m,precipitation_probability,shortwave_radiation'
'&daily=weathercode,temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min,sunrise,sunset,precipitation_sum,precipitation_probability_max,windspeed_10m_max,windgusts_10m_max,uv_index_max,rain_sum,winddirection_10m_dominant'
'&forecast_days=12&timezone=auto';
String _buildWeatherUrl(double lat, double lon) {
String url = 'latitude=$lat&longitude=$lon&$_weatherParams';
if (settings.measurements == 'imperial') {
url += '&windspeed_unit=mph&precipitation_unit=inch';
}
if (settings.degrees == 'fahrenheit') {
url += '&temperature_unit=fahrenheit';
}
return url;
}
Future<MainWeatherCache> getWeatherData(double lat, double lon) async {
final String urlWeather = _buildWeatherUrl(lat, lon);
try {
Response response = await dio.get(urlWeather);
WeatherDataApi weatherData = WeatherDataApi.fromJson(response.data);
return MainWeatherCache(
time: weatherData.hourly.time,
temperature2M: weatherData.hourly.temperature2M,
relativehumidity2M: weatherData.hourly.relativeHumidity2M,
apparentTemperature: weatherData.hourly.apparentTemperature,
precipitation: weatherData.hourly.precipitation,
rain: weatherData.hourly.rain,
weathercode: weatherData.hourly.weatherCode,
surfacePressure: weatherData.hourly.surfacePressure,
visibility: weatherData.hourly.visibility,
evapotranspiration: weatherData.hourly.evapotranspiration,
windspeed10M: weatherData.hourly.windSpeed10M,
winddirection10M: weatherData.hourly.windDirection10M,
windgusts10M: weatherData.hourly.windGusts10M,
cloudcover: weatherData.hourly.cloudCover,
uvIndex: weatherData.hourly.uvIndex,
dewpoint2M: weatherData.hourly.dewpoint2M,
precipitationProbability: weatherData.hourly.precipitationProbability,
shortwaveRadiation: weatherData.hourly.shortwaveRadiation,
timeDaily: weatherData.daily.time,
weathercodeDaily: weatherData.daily.weatherCode,
temperature2MMax: weatherData.daily.temperature2MMax,
temperature2MMin: weatherData.daily.temperature2MMin,
apparentTemperatureMax: weatherData.daily.apparentTemperatureMax,
apparentTemperatureMin: weatherData.daily.apparentTemperatureMin,
sunrise: weatherData.daily.sunrise,
sunset: weatherData.daily.sunset,
precipitationSum: weatherData.daily.precipitationSum,
precipitationProbabilityMax:
weatherData.daily.precipitationProbabilityMax,
windspeed10MMax: weatherData.daily.windSpeed10MMax,
windgusts10MMax: weatherData.daily.windGusts10MMax,
uvIndexMax: weatherData.daily.uvIndexMax,
rainSum: weatherData.daily.rainSum,
winddirection10MDominant: weatherData.daily.windDirection10MDominant,
timezone: weatherData.timezone,
timestamp: DateTime.now(),
);
return _mapWeatherDataToCache(weatherData);
} on DioException catch (e) {
if (kDebugMode) {
print(e);
@ -72,63 +41,24 @@ class WeatherAPI {
}
}
Future<WeatherCard> getWeatherCard(double? lat, double? lon, String city,
String district, String timezone) async {
String url =
'latitude=$lat&longitude=$lon&hourly=temperature_2m,relativehumidity_2m,apparent_temperature,precipitation,rain,weathercode,surface_pressure,visibility,evapotranspiration,windspeed_10m,winddirection_10m,windgusts_10m,cloudcover,uv_index,dewpoint_2m,precipitation_probability,shortwave_radiation&daily=weathercode,temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min,sunrise,sunset,precipitation_sum,precipitation_probability_max,windspeed_10m_max,windgusts_10m_max,uv_index_max,rain_sum,winddirection_10m_dominant&forecast_days=12&timezone=auto';
String urlWeather;
settings.measurements == 'imperial' && settings.degrees == 'fahrenheit'
? urlWeather =
'$url&temperature_unit=fahrenheit&windspeed_unit=mph&precipitation_unit=inch'
: settings.measurements == 'imperial'
? urlWeather = '$url&windspeed_unit=mph&precipitation_unit=inch'
: settings.degrees == 'fahrenheit'
? urlWeather = '$url&temperature_unit=fahrenheit'
: urlWeather = url;
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 WeatherCard(
time: weatherData.hourly.time,
temperature2M: weatherData.hourly.temperature2M,
relativehumidity2M: weatherData.hourly.relativeHumidity2M,
apparentTemperature: weatherData.hourly.apparentTemperature,
precipitation: weatherData.hourly.precipitation,
rain: weatherData.hourly.rain,
weathercode: weatherData.hourly.weatherCode,
surfacePressure: weatherData.hourly.surfacePressure,
visibility: weatherData.hourly.visibility,
evapotranspiration: weatherData.hourly.evapotranspiration,
windspeed10M: weatherData.hourly.windSpeed10M,
winddirection10M: weatherData.hourly.windDirection10M,
windgusts10M: weatherData.hourly.windGusts10M,
cloudcover: weatherData.hourly.cloudCover,
uvIndex: weatherData.hourly.uvIndex,
dewpoint2M: weatherData.hourly.dewpoint2M,
precipitationProbability: weatherData.hourly.precipitationProbability,
shortwaveRadiation: weatherData.hourly.shortwaveRadiation,
timeDaily: weatherData.daily.time,
weathercodeDaily: weatherData.daily.weatherCode,
temperature2MMax: weatherData.daily.temperature2MMax,
temperature2MMin: weatherData.daily.temperature2MMin,
apparentTemperatureMax: weatherData.daily.apparentTemperatureMax,
apparentTemperatureMin: weatherData.daily.apparentTemperatureMin,
sunrise: weatherData.daily.sunrise,
sunset: weatherData.daily.sunset,
precipitationSum: weatherData.daily.precipitationSum,
precipitationProbabilityMax:
weatherData.daily.precipitationProbabilityMax,
windspeed10MMax: weatherData.daily.windSpeed10MMax,
windgusts10MMax: weatherData.daily.windGusts10MMax,
uvIndexMax: weatherData.daily.uvIndexMax,
rainSum: weatherData.daily.rainSum,
winddirection10MDominant: weatherData.daily.windDirection10MDominant,
lat: lat,
lon: lon,
city: city,
district: district,
timezone: timezone,
timestamp: DateTime.now(),
return _mapWeatherDataToCard(
weatherData,
lat,
lon,
city,
district,
timezone,
);
} on DioException catch (e) {
if (kDebugMode) {
@ -139,7 +69,7 @@ class WeatherAPI {
}
Future<Iterable<Result>> getCity(String query, Locale? locale) async {
final url =
final String url =
'https://geocoding-api.open-meteo.com/v1/search?name=$query&count=5&language=${locale?.languageCode}&format=json';
try {
Response response = await dioLocation.get(url);
@ -163,4 +93,97 @@ class WeatherAPI {
rethrow;
}
}
MainWeatherCache _mapWeatherDataToCache(WeatherDataApi weatherData) {
return MainWeatherCache(
time: weatherData.hourly.time,
temperature2M: weatherData.hourly.temperature2M,
relativehumidity2M: weatherData.hourly.relativeHumidity2M,
apparentTemperature: weatherData.hourly.apparentTemperature,
precipitation: weatherData.hourly.precipitation,
rain: weatherData.hourly.rain,
weathercode: weatherData.hourly.weatherCode,
surfacePressure: weatherData.hourly.surfacePressure,
visibility: weatherData.hourly.visibility,
evapotranspiration: weatherData.hourly.evapotranspiration,
windspeed10M: weatherData.hourly.windSpeed10M,
winddirection10M: weatherData.hourly.windDirection10M,
windgusts10M: weatherData.hourly.windGusts10M,
cloudcover: weatherData.hourly.cloudCover,
uvIndex: weatherData.hourly.uvIndex,
dewpoint2M: weatherData.hourly.dewpoint2M,
precipitationProbability: weatherData.hourly.precipitationProbability,
shortwaveRadiation: weatherData.hourly.shortwaveRadiation,
timeDaily: weatherData.daily.time,
weathercodeDaily: weatherData.daily.weatherCode,
temperature2MMax: weatherData.daily.temperature2MMax,
temperature2MMin: weatherData.daily.temperature2MMin,
apparentTemperatureMax: weatherData.daily.apparentTemperatureMax,
apparentTemperatureMin: weatherData.daily.apparentTemperatureMin,
sunrise: weatherData.daily.sunrise,
sunset: weatherData.daily.sunset,
precipitationSum: weatherData.daily.precipitationSum,
precipitationProbabilityMax:
weatherData.daily.precipitationProbabilityMax,
windspeed10MMax: weatherData.daily.windSpeed10MMax,
windgusts10MMax: weatherData.daily.windGusts10MMax,
uvIndexMax: weatherData.daily.uvIndexMax,
rainSum: weatherData.daily.rainSum,
winddirection10MDominant: weatherData.daily.windDirection10MDominant,
timezone: weatherData.timezone,
timestamp: DateTime.now(),
);
}
WeatherCard _mapWeatherDataToCard(
WeatherDataApi weatherData,
double lat,
double lon,
String city,
String district,
String timezone,
) {
return WeatherCard(
time: weatherData.hourly.time,
temperature2M: weatherData.hourly.temperature2M,
relativehumidity2M: weatherData.hourly.relativeHumidity2M,
apparentTemperature: weatherData.hourly.apparentTemperature,
precipitation: weatherData.hourly.precipitation,
rain: weatherData.hourly.rain,
weathercode: weatherData.hourly.weatherCode,
surfacePressure: weatherData.hourly.surfacePressure,
visibility: weatherData.hourly.visibility,
evapotranspiration: weatherData.hourly.evapotranspiration,
windspeed10M: weatherData.hourly.windSpeed10M,
winddirection10M: weatherData.hourly.windDirection10M,
windgusts10M: weatherData.hourly.windGusts10M,
cloudcover: weatherData.hourly.cloudCover,
uvIndex: weatherData.hourly.uvIndex,
dewpoint2M: weatherData.hourly.dewpoint2M,
precipitationProbability: weatherData.hourly.precipitationProbability,
shortwaveRadiation: weatherData.hourly.shortwaveRadiation,
timeDaily: weatherData.daily.time,
weathercodeDaily: weatherData.daily.weatherCode,
temperature2MMax: weatherData.daily.temperature2MMax,
temperature2MMin: weatherData.daily.temperature2MMin,
apparentTemperatureMax: weatherData.daily.apparentTemperatureMax,
apparentTemperatureMin: weatherData.daily.apparentTemperatureMin,
sunrise: weatherData.daily.sunrise,
sunset: weatherData.daily.sunset,
precipitationSum: weatherData.daily.precipitationSum,
precipitationProbabilityMax:
weatherData.daily.precipitationProbabilityMax,
windspeed10MMax: weatherData.daily.windSpeed10MMax,
windgusts10MMax: weatherData.daily.windGusts10MMax,
uvIndexMax: weatherData.daily.uvIndexMax,
rainSum: weatherData.daily.rainSum,
winddirection10MDominant: weatherData.daily.windDirection10MDominant,
lat: lat,
lon: lon,
city: city,
district: district,
timezone: timezone,
timestamp: DateTime.now(),
);
}
}

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

@ -1,15 +1,14 @@
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))),
);
);
}
class Result {
@ -26,9 +25,9 @@ class Result {
double longitude;
factory Result.fromJson(Map<String, dynamic> json) => Result(
admin1: json['admin1'] ?? '',
name: json['name'],
latitude: json['latitude'],
longitude: json['longitude'],
);
admin1: json['admin1'] ?? '',
name: json['name'],
latitude: json['latitude'],
longitude: json['longitude'],
);
}

View file

@ -2,8 +2,8 @@ import 'package:freezed_annotation/freezed_annotation.dart';
//ignore_for_file: invalid_annotation_target
part 'weather.freezed.dart';
part 'weather.g.dart';
part 'weather_api.freezed.dart';
part 'weather_api.g.dart';
@freezed
class WeatherDataApi with _$WeatherDataApi {

View file

@ -3,7 +3,7 @@
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'weather.dart';
part of 'weather_api.dart';
// **************************************************************************
// FreezedGenerator
@ -12,7 +12,7 @@ part of 'weather.dart';
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
WeatherDataApi _$WeatherDataApiFromJson(Map<String, dynamic> json) {
return _WeatherDataApi.fromJson(json);
@ -24,8 +24,12 @@ mixin _$WeatherDataApi {
Daily get daily => throw _privateConstructorUsedError;
String get timezone => throw _privateConstructorUsedError;
/// Serializes this WeatherDataApi to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of WeatherDataApi
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$WeatherDataApiCopyWith<WeatherDataApi> get copyWith =>
throw _privateConstructorUsedError;
}
@ -52,6 +56,8 @@ class _$WeatherDataApiCopyWithImpl<$Res, $Val extends WeatherDataApi>
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of WeatherDataApi
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -75,6 +81,8 @@ class _$WeatherDataApiCopyWithImpl<$Res, $Val extends WeatherDataApi>
) as $Val);
}
/// Create a copy of WeatherDataApi
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$HourlyCopyWith<$Res> get hourly {
@ -83,6 +91,8 @@ class _$WeatherDataApiCopyWithImpl<$Res, $Val extends WeatherDataApi>
});
}
/// Create a copy of WeatherDataApi
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$DailyCopyWith<$Res> get daily {
@ -116,6 +126,8 @@ class __$$WeatherDataApiImplCopyWithImpl<$Res>
_$WeatherDataApiImpl _value, $Res Function(_$WeatherDataApiImpl) _then)
: super(_value, _then);
/// Create a copy of WeatherDataApi
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -172,11 +184,13 @@ class _$WeatherDataApiImpl implements _WeatherDataApi {
other.timezone == timezone));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, hourly, daily, timezone);
@JsonKey(ignore: true)
/// Create a copy of WeatherDataApi
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$WeatherDataApiImplCopyWith<_$WeatherDataApiImpl> get copyWith =>
@ -206,8 +220,11 @@ abstract class _WeatherDataApi implements WeatherDataApi {
Daily get daily;
@override
String get timezone;
/// Create a copy of WeatherDataApi
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$WeatherDataApiImplCopyWith<_$WeatherDataApiImpl> get copyWith =>
throw _privateConstructorUsedError;
}
@ -251,8 +268,12 @@ mixin _$Hourly {
@JsonKey(name: 'shortwave_radiation')
List<double?>? get shortwaveRadiation => throw _privateConstructorUsedError;
/// Serializes this Hourly to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of Hourly
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$HourlyCopyWith<Hourly> get copyWith => throw _privateConstructorUsedError;
}
@ -293,6 +314,8 @@ class _$HourlyCopyWithImpl<$Res, $Val extends Hourly>
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of Hourly
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -429,6 +452,8 @@ class __$$HourlyImplCopyWithImpl<$Res>
_$HourlyImpl _value, $Res Function(_$HourlyImpl) _then)
: super(_value, _then);
/// Create a copy of Hourly
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -820,7 +845,7 @@ class _$HourlyImpl implements _Hourly {
.equals(other._shortwaveRadiation, _shortwaveRadiation));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
@ -843,7 +868,9 @@ class _$HourlyImpl implements _Hourly {
const DeepCollectionEquality().hash(_precipitationProbability),
const DeepCollectionEquality().hash(_shortwaveRadiation));
@JsonKey(ignore: true)
/// Create a copy of Hourly
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$HourlyImplCopyWith<_$HourlyImpl> get copyWith =>
@ -933,8 +960,11 @@ abstract class _Hourly implements Hourly {
@override
@JsonKey(name: 'shortwave_radiation')
List<double?>? get shortwaveRadiation;
/// Create a copy of Hourly
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$HourlyImplCopyWith<_$HourlyImpl> get copyWith =>
throw _privateConstructorUsedError;
}
@ -978,8 +1008,12 @@ mixin _$Daily {
List<int?>? get windDirection10MDominant =>
throw _privateConstructorUsedError;
/// Serializes this Daily to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of Daily
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$DailyCopyWith<Daily> get copyWith => throw _privateConstructorUsedError;
}
@ -1020,6 +1054,8 @@ class _$DailyCopyWithImpl<$Res, $Val extends Daily>
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of Daily
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -1141,6 +1177,8 @@ class __$$DailyImplCopyWithImpl<$Res>
_$DailyImpl _value, $Res Function(_$DailyImpl) _then)
: super(_value, _then);
/// Create a copy of Daily
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -1478,7 +1516,7 @@ class _$DailyImpl implements _Daily {
other._windDirection10MDominant, _windDirection10MDominant));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
@ -1498,7 +1536,9 @@ class _$DailyImpl implements _Daily {
const DeepCollectionEquality().hash(_rainSum),
const DeepCollectionEquality().hash(_windDirection10MDominant));
@JsonKey(ignore: true)
/// Create a copy of Daily
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$DailyImplCopyWith<_$DailyImpl> get copyWith =>
@ -1581,8 +1621,11 @@ abstract class _Daily implements Daily {
@override
@JsonKey(name: 'winddirection_10m_dominant')
List<int?>? get windDirection10MDominant;
/// Create a copy of Daily
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$DailyImplCopyWith<_$DailyImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'weather.dart';
part of 'weather_api.dart';
// **************************************************************************
// JsonSerializableGenerator
@ -24,7 +24,7 @@ Map<String, dynamic> _$$WeatherDataApiImplToJson(
_$HourlyImpl _$$HourlyImplFromJson(Map<String, dynamic> json) => _$HourlyImpl(
time: (json['time'] as List<dynamic>?)?.map((e) => e as String).toList(),
weatherCode: (json['weathercode'] as List<dynamic>?)
?.map((e) => e as int)
?.map((e) => (e as num).toInt())
.toList(),
temperature2M: (json['temperature_2m'] as List<dynamic>?)
?.map((e) => (e as num).toDouble())
@ -39,7 +39,7 @@ _$HourlyImpl _$$HourlyImplFromJson(Map<String, dynamic> json) => _$HourlyImpl(
?.map((e) => (e as num?)?.toDouble())
.toList(),
relativeHumidity2M: (json['relativehumidity_2m'] as List<dynamic>?)
?.map((e) => e as int?)
?.map((e) => (e as num?)?.toInt())
.toList(),
surfacePressure: (json['surface_pressure'] as List<dynamic>?)
?.map((e) => (e as num?)?.toDouble())
@ -54,13 +54,13 @@ _$HourlyImpl _$$HourlyImplFromJson(Map<String, dynamic> json) => _$HourlyImpl(
?.map((e) => (e as num?)?.toDouble())
.toList(),
windDirection10M: (json['winddirection_10m'] as List<dynamic>?)
?.map((e) => e as int?)
?.map((e) => (e as num?)?.toInt())
.toList(),
windGusts10M: (json['windgusts_10m'] as List<dynamic>?)
?.map((e) => (e as num?)?.toDouble())
.toList(),
cloudCover: (json['cloudcover'] as List<dynamic>?)
?.map((e) => e as int?)
?.map((e) => (e as num?)?.toInt())
.toList(),
uvIndex: (json['uv_index'] as List<dynamic>?)
?.map((e) => (e as num?)?.toDouble())
@ -70,7 +70,7 @@ _$HourlyImpl _$$HourlyImplFromJson(Map<String, dynamic> json) => _$HourlyImpl(
.toList(),
precipitationProbability:
(json['precipitation_probability'] as List<dynamic>?)
?.map((e) => e as int?)
?.map((e) => (e as num?)?.toInt())
.toList(),
shortwaveRadiation: (json['shortwave_radiation'] as List<dynamic>?)
?.map((e) => (e as num?)?.toDouble())
@ -102,7 +102,7 @@ Map<String, dynamic> _$$HourlyImplToJson(_$HourlyImpl instance) =>
_$DailyImpl _$$DailyImplFromJson(Map<String, dynamic> json) => _$DailyImpl(
time: _dateTimeFromJson(json['time'] as List?),
weatherCode: (json['weathercode'] as List<dynamic>?)
?.map((e) => e as int?)
?.map((e) => (e as num?)?.toInt())
.toList(),
temperature2MMax: (json['temperature_2m_max'] as List<dynamic>?)
?.map((e) => (e as num?)?.toDouble())
@ -127,7 +127,7 @@ _$DailyImpl _$$DailyImplFromJson(Map<String, dynamic> json) => _$DailyImpl(
(json['sunset'] as List<dynamic>?)?.map((e) => e as String).toList(),
precipitationProbabilityMax:
(json['precipitation_probability_max'] as List<dynamic>?)
?.map((e) => e as int?)
?.map((e) => (e as num?)?.toInt())
.toList(),
windSpeed10MMax: (json['windspeed_10m_max'] as List<dynamic>?)
?.map((e) => (e as num?)?.toDouble())
@ -143,7 +143,7 @@ _$DailyImpl _$$DailyImplFromJson(Map<String, dynamic> json) => _$DailyImpl(
.toList(),
windDirection10MDominant:
(json['winddirection_10m_dominant'] as List<dynamic>?)
?.map((e) => e as int?)
?.map((e) => (e as num?)?.toInt())
.toList(),
);

437
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,16 +10,17 @@ 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;
import 'package:timezone/standalone.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
import 'package:url_launcher/url_launcher.dart';
import 'package:workmanager/workmanager.dart';
class WeatherController extends GetxController {
@ -52,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) {
@ -70,35 +69,36 @@ 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(
desiredAccuracy: LocationAccuracy.high);
return await Geolocator.getCurrentPosition();
}
Future<void> setLocation() async {
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 (!isOnline) {
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;
if (!isOnline) {
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 {
if (!isOnline) {
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,23 +243,17 @@ 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);
}
});
}
Future<void> deleteCache() async {
if (!isOnline) {
if (!(await isOnline.value)) {
return;
}
@ -261,41 +263,49 @@ class WeatherController extends GetxController {
.timestampLessThan(cacheExpiry)
.deleteAllSync();
});
if ((isar.mainWeatherCaches.where().findAllSync()).isEmpty) {
if (isar.mainWeatherCaches.where().findAllSync().isEmpty) {
await flutterLocalNotificationsPlugin.cancelAll();
}
}
Future<void> deleteAll(bool changeCity) async {
if (!isOnline) {
if (!(await isOnline.value)) {
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 {
if (!isOnline) {
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,120 +313,88 @@ 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()
.timestampLessThan(cacheExpiry)
.sortByIndex()
.findAllSync();
.filter()
.timestampLessThan(cacheExpiry)
.sortByIndex()
.findAllSync();
if (!isOnline || 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();
});
}
}
void _updateWeatherCard(WeatherCard oldCard, WeatherCard updatedCard) {
oldCard
..time = updatedCard.time
..weathercode = updatedCard.weathercode
..temperature2M = updatedCard.temperature2M
..apparentTemperature = updatedCard.apparentTemperature
..relativehumidity2M = updatedCard.relativehumidity2M
..precipitation = updatedCard.precipitation
..rain = updatedCard.rain
..surfacePressure = updatedCard.surfacePressure
..visibility = updatedCard.visibility
..evapotranspiration = updatedCard.evapotranspiration
..windspeed10M = updatedCard.windspeed10M
..winddirection10M = updatedCard.winddirection10M
..windgusts10M = updatedCard.windgusts10M
..cloudcover = updatedCard.cloudcover
..uvIndex = updatedCard.uvIndex
..dewpoint2M = updatedCard.dewpoint2M
..precipitationProbability = updatedCard.precipitationProbability
..shortwaveRadiation = updatedCard.shortwaveRadiation
..timeDaily = updatedCard.timeDaily
..weathercodeDaily = updatedCard.weathercodeDaily
..temperature2MMax = updatedCard.temperature2MMax
..temperature2MMin = updatedCard.temperature2MMin
..apparentTemperatureMax = updatedCard.apparentTemperatureMax
..apparentTemperatureMin = updatedCard.apparentTemperatureMin
..sunrise = updatedCard.sunrise
..sunset = updatedCard.sunset
..precipitationSum = updatedCard.precipitationSum
..precipitationProbabilityMax = updatedCard.precipitationProbabilityMax
..windspeed10MMax = updatedCard.windspeed10MMax
..windgusts10MMax = updatedCard.windgusts10MMax
..uvIndexMax = updatedCard.uvIndexMax
..rainSum = updatedCard.rainSum
..winddirection10MDominant = updatedCard.winddirection10MDominant
..timestamp = DateTime.now();
isar.weatherCards.putSync(oldCard);
}
Future<void> updateCard(WeatherCard weatherCard) async {
if (!isOnline) {
if (!(await isOnline.value)) {
return;
}
final updatedCard = await WeatherAPI().getWeatherCard(
weatherCard.lat,
weatherCard.lon,
weatherCard.lat!,
weatherCard.lon!,
weatherCard.city!,
weatherCard.district!,
weatherCard.timezone!,
);
isar.writeTxnSync(() {
weatherCard
..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(weatherCard);
_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,21 +433,21 @@ 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);
return imagePath;
}
void notlification(MainWeatherCache mainWeatherCache) async {
DateTime now = DateTime.now();
int startHour = timeConvert(timeStart).hour;
int endHour = timeConvert(timeEnd).hour;
void notification(MainWeatherCache mainWeatherCache) async {
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) {
notlification(_mainWeather.value);
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,29 +534,39 @@ 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(
'weather_icon',
await getLocalImagePath(
StatusWeather().getImageNotification(
mainWeatherCache.weathercode![hour],
mainWeatherCache.time![hour],
mainWeatherCache.sunrise![day],
mainWeatherCache.sunset![day],
))),
),
),
),
HomeWidget.saveWidgetData(
'weather_degree',
'${mainWeatherCache.temperature2M?[hour].round()}°',
),
HomeWidget.updateWidget(androidName: androidWidgetName),
]).then((value) {
return !value.contains(false);
});
]);
return !results.contains(false);
}
void urlLauncher(String uri) async {
final url = Uri.parse(uri);
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
throw Exception('Could not launch $url');
}
}
}

209
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 {
@ -12,10 +12,14 @@ class Settings {
bool materialColor = false;
bool amoledTheme = false;
bool roundDegree = false;
bool largeElement = false;
bool hideMap = false;
String? widgetBackgroundColor;
String? widgetTextColor;
String measurements = 'metric';
String degrees = 'celsius';
String wind = 'kph';
String pressure = 'hPa';
String timeformat = '24';
String? language;
int? timeRange;
@ -101,43 +105,43 @@ class MainWeatherCache {
});
Map<String, dynamic> toJson() => {
'id': id,
'time': time,
'weathercode': weathercode,
'temperature2M': temperature2M,
'apparentTemperature': apparentTemperature,
'relativehumidity2M': relativehumidity2M,
'precipitation': precipitation,
'rain': rain,
'surfacePressure': surfacePressure,
'visibility': visibility,
'evapotranspiration': evapotranspiration,
'windspeed10M': windspeed10M,
'winddirection10M': winddirection10M,
'windgusts10M': windgusts10M,
'cloudcover': cloudcover,
'uvIndex': uvIndex,
'dewpoint2M': dewpoint2M,
'precipitationProbability': precipitationProbability,
'shortwaveRadiation': shortwaveRadiation,
'timeDaily': timeDaily,
'weathercodeDaily': weathercodeDaily,
'temperature2MMax': temperature2MMax,
'temperature2MMin': temperature2MMin,
'apparentTemperatureMax': apparentTemperatureMax,
'apparentTemperatureMin': apparentTemperatureMin,
'sunrise': sunrise,
'sunset': sunset,
'precipitationSum': precipitationSum,
'precipitationProbabilityMax': precipitationProbabilityMax,
'windspeed10MMax': windspeed10MMax,
'windgusts10MMax': windgusts10MMax,
'uvIndexMax': uvIndexMax,
'rainSum': rainSum,
'winddirection10MDominant': winddirection10MDominant,
'timezone': timezone,
'timestamp': timestamp,
};
'id': id,
'time': time,
'weathercode': weathercode,
'temperature2M': temperature2M,
'apparentTemperature': apparentTemperature,
'relativehumidity2M': relativehumidity2M,
'precipitation': precipitation,
'rain': rain,
'surfacePressure': surfacePressure,
'visibility': visibility,
'evapotranspiration': evapotranspiration,
'windspeed10M': windspeed10M,
'winddirection10M': winddirection10M,
'windgusts10M': windgusts10M,
'cloudcover': cloudcover,
'uvIndex': uvIndex,
'dewpoint2M': dewpoint2M,
'precipitationProbability': precipitationProbability,
'shortwaveRadiation': shortwaveRadiation,
'timeDaily': timeDaily,
'weathercodeDaily': weathercodeDaily,
'temperature2MMax': temperature2MMax,
'temperature2MMin': temperature2MMin,
'apparentTemperatureMax': apparentTemperatureMax,
'apparentTemperatureMin': apparentTemperatureMin,
'sunrise': sunrise,
'sunset': sunset,
'precipitationSum': precipitationSum,
'precipitationProbabilityMax': precipitationProbabilityMax,
'windspeed10MMax': windspeed10MMax,
'windgusts10MMax': windgusts10MMax,
'uvIndexMax': uvIndexMax,
'rainSum': rainSum,
'winddirection10MDominant': winddirection10MDominant,
'timezone': timezone,
'timestamp': timestamp,
};
}
@collection
@ -148,12 +152,15 @@ 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,
'lat': lat,
'lon': lon,
'city': city,
'district': district,
};
}
@collection
@ -244,56 +251,57 @@ class WeatherCard {
});
Map<String, dynamic> toJson() => {
'id': id,
'time': time,
'weathercode': weathercode,
'temperature2M': temperature2M,
'apparentTemperature': apparentTemperature,
'relativehumidity2M': relativehumidity2M,
'precipitation': precipitation,
'rain': rain,
'surfacePressure': surfacePressure,
'visibility': visibility,
'evapotranspiration': evapotranspiration,
'windspeed10M': windspeed10M,
'winddirection10M': winddirection10M,
'windgusts10M': windgusts10M,
'cloudcover': cloudcover,
'uvIndex': uvIndex,
'dewpoint2M': dewpoint2M,
'precipitationProbability': precipitationProbability,
'shortwaveRadiation': shortwaveRadiation,
'timeDaily': timeDaily,
'weathercodeDaily': weathercodeDaily,
'temperature2MMax': temperature2MMax,
'temperature2MMin': temperature2MMin,
'apparentTemperatureMax': apparentTemperatureMax,
'apparentTemperatureMin': apparentTemperatureMin,
'sunrise': sunrise,
'sunset': sunset,
'precipitationSum': precipitationSum,
'precipitationProbabilityMax': precipitationProbabilityMax,
'windspeed10MMax': windspeed10MMax,
'windgusts10MMax': windgusts10MMax,
'uvIndexMax': uvIndexMax,
'rainSum': rainSum,
'winddirection10MDominant': winddirection10MDominant,
'timezone': timezone,
'timestamp': timestamp,
'lat': lat,
'lon': lon,
'city': city,
'district': district,
'index': index,
};
'id': id,
'time': time,
'weathercode': weathercode,
'temperature2M': temperature2M,
'apparentTemperature': apparentTemperature,
'relativehumidity2M': relativehumidity2M,
'precipitation': precipitation,
'rain': rain,
'surfacePressure': surfacePressure,
'visibility': visibility,
'evapotranspiration': evapotranspiration,
'windspeed10M': windspeed10M,
'winddirection10M': winddirection10M,
'windgusts10M': windgusts10M,
'cloudcover': cloudcover,
'uvIndex': uvIndex,
'dewpoint2M': dewpoint2M,
'precipitationProbability': precipitationProbability,
'shortwaveRadiation': shortwaveRadiation,
'timeDaily': timeDaily,
'weathercodeDaily': weathercodeDaily,
'temperature2MMax': temperature2MMax,
'temperature2MMin': temperature2MMin,
'apparentTemperatureMax': apparentTemperatureMax,
'apparentTemperatureMin': apparentTemperatureMin,
'sunrise': sunrise,
'sunset': sunset,
'precipitationSum': precipitationSum,
'precipitationProbabilityMax': precipitationProbabilityMax,
'windspeed10MMax': windspeed10MMax,
'windgusts10MMax': windgusts10MMax,
'uvIndexMax': uvIndexMax,
'rainSum': rainSum,
'winddirection10MDominant': winddirection10MDominant,
'timezone': timezone,
'timestamp': timestamp,
'lat': lat,
'lon': lon,
'city': city,
'district': district,
'index': index,
};
factory WeatherCard.fromJson(Map<String, dynamic> json) {
return 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'] ?? []),
@ -306,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'],

576
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
@ -27,75 +27,95 @@ const SettingsSchema = CollectionSchema(
name: r'degrees',
type: IsarType.string,
),
r'language': PropertySchema(
r'hideMap': PropertySchema(
id: 2,
name: r'hideMap',
type: IsarType.bool,
),
r'language': PropertySchema(
id: 3,
name: r'language',
type: IsarType.string,
),
r'largeElement': PropertySchema(
id: 4,
name: r'largeElement',
type: IsarType.bool,
),
r'location': PropertySchema(
id: 3,
id: 5,
name: r'location',
type: IsarType.bool,
),
r'materialColor': PropertySchema(
id: 4,
id: 6,
name: r'materialColor',
type: IsarType.bool,
),
r'measurements': PropertySchema(
id: 5,
id: 7,
name: r'measurements',
type: IsarType.string,
),
r'notifications': PropertySchema(
id: 6,
id: 8,
name: r'notifications',
type: IsarType.bool,
),
r'onboard': PropertySchema(
id: 7,
id: 9,
name: r'onboard',
type: IsarType.bool,
),
r'pressure': PropertySchema(
id: 10,
name: r'pressure',
type: IsarType.string,
),
r'roundDegree': PropertySchema(
id: 8,
id: 11,
name: r'roundDegree',
type: IsarType.bool,
),
r'theme': PropertySchema(
id: 9,
id: 12,
name: r'theme',
type: IsarType.string,
),
r'timeEnd': PropertySchema(
id: 10,
id: 13,
name: r'timeEnd',
type: IsarType.string,
),
r'timeRange': PropertySchema(
id: 11,
id: 14,
name: r'timeRange',
type: IsarType.long,
),
r'timeStart': PropertySchema(
id: 12,
id: 15,
name: r'timeStart',
type: IsarType.string,
),
r'timeformat': PropertySchema(
id: 13,
id: 16,
name: r'timeformat',
type: IsarType.string,
),
r'widgetBackgroundColor': PropertySchema(
id: 14,
id: 17,
name: r'widgetBackgroundColor',
type: IsarType.string,
),
r'widgetTextColor': PropertySchema(
id: 15,
id: 18,
name: r'widgetTextColor',
type: IsarType.string,
),
r'wind': PropertySchema(
id: 19,
name: r'wind',
type: IsarType.string,
)
},
estimateSize: _settingsEstimateSize,
@ -109,7 +129,7 @@ const SettingsSchema = CollectionSchema(
getId: _settingsGetId,
getLinks: _settingsGetLinks,
attach: _settingsAttach,
version: '3.1.0+1',
version: '3.1.8',
);
int _settingsEstimateSize(
@ -126,6 +146,7 @@ int _settingsEstimateSize(
}
}
bytesCount += 3 + object.measurements.length * 3;
bytesCount += 3 + object.pressure.length * 3;
{
final value = object.theme;
if (value != null) {
@ -157,6 +178,7 @@ int _settingsEstimateSize(
bytesCount += 3 + value.length * 3;
}
}
bytesCount += 3 + object.wind.length * 3;
return bytesCount;
}
@ -168,20 +190,24 @@ void _settingsSerialize(
) {
writer.writeBool(offsets[0], object.amoledTheme);
writer.writeString(offsets[1], object.degrees);
writer.writeString(offsets[2], object.language);
writer.writeBool(offsets[3], object.location);
writer.writeBool(offsets[4], object.materialColor);
writer.writeString(offsets[5], object.measurements);
writer.writeBool(offsets[6], object.notifications);
writer.writeBool(offsets[7], object.onboard);
writer.writeBool(offsets[8], object.roundDegree);
writer.writeString(offsets[9], object.theme);
writer.writeString(offsets[10], object.timeEnd);
writer.writeLong(offsets[11], object.timeRange);
writer.writeString(offsets[12], object.timeStart);
writer.writeString(offsets[13], object.timeformat);
writer.writeString(offsets[14], object.widgetBackgroundColor);
writer.writeString(offsets[15], object.widgetTextColor);
writer.writeBool(offsets[2], object.hideMap);
writer.writeString(offsets[3], object.language);
writer.writeBool(offsets[4], object.largeElement);
writer.writeBool(offsets[5], object.location);
writer.writeBool(offsets[6], object.materialColor);
writer.writeString(offsets[7], object.measurements);
writer.writeBool(offsets[8], object.notifications);
writer.writeBool(offsets[9], object.onboard);
writer.writeString(offsets[10], object.pressure);
writer.writeBool(offsets[11], object.roundDegree);
writer.writeString(offsets[12], object.theme);
writer.writeString(offsets[13], object.timeEnd);
writer.writeLong(offsets[14], object.timeRange);
writer.writeString(offsets[15], object.timeStart);
writer.writeString(offsets[16], object.timeformat);
writer.writeString(offsets[17], object.widgetBackgroundColor);
writer.writeString(offsets[18], object.widgetTextColor);
writer.writeString(offsets[19], object.wind);
}
Settings _settingsDeserialize(
@ -193,21 +219,25 @@ Settings _settingsDeserialize(
final object = Settings();
object.amoledTheme = reader.readBool(offsets[0]);
object.degrees = reader.readString(offsets[1]);
object.hideMap = reader.readBool(offsets[2]);
object.id = id;
object.language = reader.readStringOrNull(offsets[2]);
object.location = reader.readBool(offsets[3]);
object.materialColor = reader.readBool(offsets[4]);
object.measurements = reader.readString(offsets[5]);
object.notifications = reader.readBool(offsets[6]);
object.onboard = reader.readBool(offsets[7]);
object.roundDegree = reader.readBool(offsets[8]);
object.theme = reader.readStringOrNull(offsets[9]);
object.timeEnd = reader.readStringOrNull(offsets[10]);
object.timeRange = reader.readLongOrNull(offsets[11]);
object.timeStart = reader.readStringOrNull(offsets[12]);
object.timeformat = reader.readString(offsets[13]);
object.widgetBackgroundColor = reader.readStringOrNull(offsets[14]);
object.widgetTextColor = reader.readStringOrNull(offsets[15]);
object.language = reader.readStringOrNull(offsets[3]);
object.largeElement = reader.readBool(offsets[4]);
object.location = reader.readBool(offsets[5]);
object.materialColor = reader.readBool(offsets[6]);
object.measurements = reader.readString(offsets[7]);
object.notifications = reader.readBool(offsets[8]);
object.onboard = reader.readBool(offsets[9]);
object.pressure = reader.readString(offsets[10]);
object.roundDegree = reader.readBool(offsets[11]);
object.theme = reader.readStringOrNull(offsets[12]);
object.timeEnd = reader.readStringOrNull(offsets[13]);
object.timeRange = reader.readLongOrNull(offsets[14]);
object.timeStart = reader.readStringOrNull(offsets[15]);
object.timeformat = reader.readString(offsets[16]);
object.widgetBackgroundColor = reader.readStringOrNull(offsets[17]);
object.widgetTextColor = reader.readStringOrNull(offsets[18]);
object.wind = reader.readString(offsets[19]);
return object;
}
@ -223,33 +253,41 @@ P _settingsDeserializeProp<P>(
case 1:
return (reader.readString(offset)) as P;
case 2:
return (reader.readStringOrNull(offset)) as P;
case 3:
return (reader.readBool(offset)) as P;
case 3:
return (reader.readStringOrNull(offset)) as P;
case 4:
return (reader.readBool(offset)) as P;
case 5:
return (reader.readString(offset)) as P;
return (reader.readBool(offset)) as P;
case 6:
return (reader.readBool(offset)) as P;
case 7:
return (reader.readBool(offset)) as P;
return (reader.readString(offset)) as P;
case 8:
return (reader.readBool(offset)) as P;
case 9:
return (reader.readStringOrNull(offset)) as P;
return (reader.readBool(offset)) as P;
case 10:
return (reader.readStringOrNull(offset)) as P;
return (reader.readString(offset)) as P;
case 11:
return (reader.readLongOrNull(offset)) as P;
return (reader.readBool(offset)) as P;
case 12:
return (reader.readStringOrNull(offset)) as P;
case 13:
return (reader.readString(offset)) as P;
case 14:
return (reader.readStringOrNull(offset)) as P;
case 14:
return (reader.readLongOrNull(offset)) as P;
case 15:
return (reader.readStringOrNull(offset)) as P;
case 16:
return (reader.readString(offset)) as P;
case 17:
return (reader.readStringOrNull(offset)) as P;
case 18:
return (reader.readStringOrNull(offset)) as P;
case 19:
return (reader.readString(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
@ -484,6 +522,16 @@ extension SettingsQueryFilter
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> hideMapEqualTo(
bool value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'hideMap',
value: value,
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> idEqualTo(Id value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
@ -682,6 +730,16 @@ extension SettingsQueryFilter
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> largeElementEqualTo(
bool value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'largeElement',
value: value,
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> locationEqualTo(
bool value) {
return QueryBuilder.apply(this, (query) {
@ -856,6 +914,136 @@ extension SettingsQueryFilter
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> pressureEqualTo(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'pressure',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> pressureGreaterThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'pressure',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> pressureLessThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'pressure',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> pressureBetween(
String lower,
String upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'pressure',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> pressureStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'pressure',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> pressureEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'pressure',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> pressureContains(
String value,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'pressure',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> pressureMatches(
String pattern,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'pressure',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> pressureIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'pressure',
value: '',
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> pressureIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'pressure',
value: '',
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> roundDegreeEqualTo(
bool value) {
return QueryBuilder.apply(this, (query) {
@ -1813,6 +2001,136 @@ extension SettingsQueryFilter
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> windEqualTo(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'wind',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> windGreaterThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'wind',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> windLessThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'wind',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> windBetween(
String lower,
String upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'wind',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> windStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'wind',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> windEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'wind',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> windContains(
String value,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'wind',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> windMatches(
String pattern,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'wind',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> windIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'wind',
value: '',
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> windIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'wind',
value: '',
));
});
}
}
extension SettingsQueryObject
@ -1846,6 +2164,18 @@ extension SettingsQuerySortBy on QueryBuilder<Settings, Settings, QSortBy> {
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> sortByHideMap() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'hideMap', Sort.asc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> sortByHideMapDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'hideMap', Sort.desc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> sortByLanguage() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'language', Sort.asc);
@ -1858,6 +2188,18 @@ extension SettingsQuerySortBy on QueryBuilder<Settings, Settings, QSortBy> {
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> sortByLargeElement() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'largeElement', Sort.asc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> sortByLargeElementDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'largeElement', Sort.desc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> sortByLocation() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'location', Sort.asc);
@ -1918,6 +2260,18 @@ extension SettingsQuerySortBy on QueryBuilder<Settings, Settings, QSortBy> {
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> sortByPressure() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'pressure', Sort.asc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> sortByPressureDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'pressure', Sort.desc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> sortByRoundDegree() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'roundDegree', Sort.asc);
@ -2014,6 +2368,18 @@ extension SettingsQuerySortBy on QueryBuilder<Settings, Settings, QSortBy> {
return query.addSortBy(r'widgetTextColor', Sort.desc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> sortByWind() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'wind', Sort.asc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> sortByWindDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'wind', Sort.desc);
});
}
}
extension SettingsQuerySortThenBy
@ -2042,6 +2408,18 @@ extension SettingsQuerySortThenBy
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> thenByHideMap() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'hideMap', Sort.asc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> thenByHideMapDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'hideMap', Sort.desc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
@ -2066,6 +2444,18 @@ extension SettingsQuerySortThenBy
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> thenByLargeElement() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'largeElement', Sort.asc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> thenByLargeElementDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'largeElement', Sort.desc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> thenByLocation() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'location', Sort.asc);
@ -2126,6 +2516,18 @@ extension SettingsQuerySortThenBy
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> thenByPressure() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'pressure', Sort.asc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> thenByPressureDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'pressure', Sort.desc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> thenByRoundDegree() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'roundDegree', Sort.asc);
@ -2222,6 +2624,18 @@ extension SettingsQuerySortThenBy
return query.addSortBy(r'widgetTextColor', Sort.desc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> thenByWind() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'wind', Sort.asc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> thenByWindDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'wind', Sort.desc);
});
}
}
extension SettingsQueryWhereDistinct
@ -2239,6 +2653,12 @@ extension SettingsQueryWhereDistinct
});
}
QueryBuilder<Settings, Settings, QDistinct> distinctByHideMap() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'hideMap');
});
}
QueryBuilder<Settings, Settings, QDistinct> distinctByLanguage(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
@ -2246,6 +2666,12 @@ extension SettingsQueryWhereDistinct
});
}
QueryBuilder<Settings, Settings, QDistinct> distinctByLargeElement() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'largeElement');
});
}
QueryBuilder<Settings, Settings, QDistinct> distinctByLocation() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'location');
@ -2277,6 +2703,13 @@ extension SettingsQueryWhereDistinct
});
}
QueryBuilder<Settings, Settings, QDistinct> distinctByPressure(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'pressure', caseSensitive: caseSensitive);
});
}
QueryBuilder<Settings, Settings, QDistinct> distinctByRoundDegree() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'roundDegree');
@ -2332,6 +2765,13 @@ extension SettingsQueryWhereDistinct
caseSensitive: caseSensitive);
});
}
QueryBuilder<Settings, Settings, QDistinct> distinctByWind(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'wind', caseSensitive: caseSensitive);
});
}
}
extension SettingsQueryProperty
@ -2354,12 +2794,24 @@ extension SettingsQueryProperty
});
}
QueryBuilder<Settings, bool, QQueryOperations> hideMapProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'hideMap');
});
}
QueryBuilder<Settings, String?, QQueryOperations> languageProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'language');
});
}
QueryBuilder<Settings, bool, QQueryOperations> largeElementProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'largeElement');
});
}
QueryBuilder<Settings, bool, QQueryOperations> locationProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'location');
@ -2390,6 +2842,12 @@ extension SettingsQueryProperty
});
}
QueryBuilder<Settings, String, QQueryOperations> pressureProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'pressure');
});
}
QueryBuilder<Settings, bool, QQueryOperations> roundDegreeProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'roundDegree');
@ -2438,6 +2896,12 @@ extension SettingsQueryProperty
return query.addPropertyName(r'widgetTextColor');
});
}
QueryBuilder<Settings, String, QQueryOperations> windProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'wind');
});
}
}
// coverage:ignore-file
@ -2638,7 +3102,7 @@ const MainWeatherCacheSchema = CollectionSchema(
getId: _mainWeatherCacheGetId,
getLinks: _mainWeatherCacheGetLinks,
attach: _mainWeatherCacheAttach,
version: '3.1.0+1',
version: '3.1.8',
);
int _mainWeatherCacheEstimateSize(
@ -10361,7 +10825,7 @@ const LocationCacheSchema = CollectionSchema(
getId: _locationCacheGetId,
getLinks: _locationCacheGetLinks,
attach: _locationCacheAttach,
version: '3.1.0+1',
version: '3.1.8',
);
int _locationCacheEstimateSize(
@ -11460,7 +11924,7 @@ const WeatherCardSchema = CollectionSchema(
getId: _weatherCardGetId,
getLinks: _weatherCardGetLinks,
attach: _weatherCardAttach,
version: '3.1.0+1',
version: '3.1.8',
);
int _weatherCardEstimateSize(

View file

@ -1,187 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:iconsax/iconsax.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(
Iconsax.arrow_left_1,
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],
timeDay: weatherCard.sunrise![dayNow],
timeNight: weatherCard.sunset![dayNow],
),
Card(
margin: const EdgeInsets.symmetric(vertical: 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.primaryContainer
: 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],
),
WeatherDaily(
weatherData: weatherCard,
onTap: () => Get.to(
() => WeatherMore(
weatherData: weatherCard,
),
transition: Transition.downToUp,
),
),
],
),
),
),
),
);
}
}

View file

@ -1,133 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:iconsax/iconsax.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 ListWeatherCard extends StatefulWidget {
const ListWeatherCard({super.key});
@override
State<ListWeatherCard> createState() => _ListWeatherCardState();
}
class _ListWeatherCardState extends State<ListWeatherCard> {
final weatherController = Get.put(WeatherController());
@override
Widget build(BuildContext context) {
final textTheme = context.textTheme;
final titleMedium = textTheme.titleMedium;
return RefreshIndicator(
onRefresh: () async {
await weatherController.updateCacheCard(true);
setState(() {});
},
child: 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,
),
),
),
],
),
),
)
: ReorderableListView(
onReorder: (oldIndex, newIndex) =>
weatherController.reorder(oldIndex, newIndex),
children: [
...weatherController.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(
Iconsax.trush_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,245 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:iconsax/iconsax.dart';
import 'package:rain/app/api/api.dart';
import 'package:rain/app/api/city.dart';
import 'package:rain/app/controller/controller.dart';
import 'package:rain/app/widgets/text_form.dart';
import 'package:rain/main.dart';
class CreateWeatherCard extends StatefulWidget {
const CreateWeatherCard({super.key});
@override
State<CreateWeatherCard> createState() => _CreateWeatherCardState();
}
class _CreateWeatherCardState extends State<CreateWeatherCard> {
bool isLoading = false;
final formKey = 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();
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(() {});
}
@override
Widget build(BuildContext context) {
const kTextFieldElevation = 4.0;
return 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.fromLTRB(10, 10, 10, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: () {
Get.back();
},
icon: const Icon(
Iconsax.close_square,
size: 18,
),
),
Text(
'create'.tr,
style: context.textTheme.titleLarge?.copyWith(
fontSize: 20,
),
textAlign: TextAlign.center,
),
IconButton(
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();
}
},
icon: const Icon(
Iconsax.tick_square,
size: 18,
),
),
],
),
),
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(Iconsax.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(Iconsax.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(Iconsax.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(Icons.location_city_rounded),
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(Iconsax.global),
margin: const EdgeInsets.only(left: 10, right: 10, top: 10),
),
const SizedBox(height: 20),
],
),
),
if (isLoading)
const Center(
child: CircularProgressIndicator(),
),
],
),
),
);
}
}

View file

@ -1,124 +0,0 @@
import 'package:flutter/material.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 SizedBox(width: 7),
Text(
statusWeather.getText(widget.weather[weatherController
.getTime(widget.time, widget.timezone)]),
style: context.textTheme.titleMedium?.copyWith(
color: Colors.grey,
fontWeight: FontWeight.w400,
),
),
],
),
const SizedBox(height: 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 SizedBox(height: 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 SizedBox(width: 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,405 +0,0 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:iconsax/iconsax.dart';
import 'package:rain/app/api/api.dart';
import 'package:rain/app/api/city.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();
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(() {});
}
@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(
Iconsax.arrow_left_1,
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: [
Image.asset(
'assets/icons/Search.png',
scale: 7,
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 5, horizontal: 10),
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(
Iconsax.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,
),
),
);
},
),
),
),
);
},
),
),
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 (!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(
Iconsax.location,
),
),
),
),
],
),
MyTextForm(
elevation: kTextFieldElevation,
controller: _controllerLat,
labelText: 'lat'.tr,
type: TextInputType.number,
icon: const Icon(Iconsax.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(Iconsax.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(Icons.location_city_rounded),
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(Iconsax.global),
margin: const EdgeInsets.only(
left: 10, right: 10, top: 10),
),
const SizedBox(height: 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,277 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:iconsax/iconsax.dart';
import 'package:isar/isar.dart';
import 'package:rain/app/api/api.dart';
import 'package:rain/app/api/city.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.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 pages = [
const WeatherPage(),
const ListWeatherCard(),
const SettingsPage(),
];
@override
void initState() {
getData();
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;
});
});
super.initState();
}
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;
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(
Iconsax.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: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
),
);
},
),
1 => Text(
'cities'.tr,
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
2 => Text(
'settings_full'.tr,
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
int() => null,
},
actions: switch (tabIndex) {
0 => [
IconButton(
onPressed: () {
if (visible) {
_controller.clear();
_focusNode.unfocus();
visible = false;
} else {
visible = true;
}
setState(() {});
},
icon: Icon(
visible ? Icons.close : Iconsax.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(Iconsax.cloud_sunny),
selectedIcon: const Icon(Iconsax.cloud_sunny5),
label: 'name'.tr,
),
NavigationDestination(
icon: const Icon(Iconsax.map_1),
selectedIcon: const Icon(Iconsax.map5),
label: 'cities'.tr,
),
NavigationDestination(
icon: const Icon(Iconsax.category),
selectedIcon: const Icon(Iconsax.category5),
label: 'settings'.tr,
),
],
),
floatingActionButton: tabIndex == 1
? FloatingActionButton(
onPressed: () => showModalBottomSheet(
context: context,
isScrollControlled: true,
enableDrag: false,
builder: (BuildContext context) =>
const CreateWeatherCard(),
),
child: const Icon(
Iconsax.add,
),
)
: null,
),
),
);
}
}

View file

@ -1,171 +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: 350,
),
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];
return ListView(
children: [
WeatherNow(
time: mainWeather.time![hourOfDay],
weather: mainWeather.weathercode![hourOfDay],
degree: mainWeather.temperature2M![hourOfDay],
timeDay: sunrise,
timeNight: sunset,
),
Card(
margin: const EdgeInsets.symmetric(vertical: 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.primaryContainer
: 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],
),
WeatherDaily(
weatherData: weatherCard,
onTap: () => Get.to(
() => WeatherMore(
weatherData: weatherCard,
),
transition: Transition.downToUp,
),
)
],
);
}),
),
);
}
}

View file

@ -1,183 +0,0 @@
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:get/get.dart';
class OnBording extends StatefulWidget {
const OnBording({super.key});
@override
State<OnBording> createState() => _OnBordingState();
}
class _OnBordingState extends State<OnBording> {
late PageController pageController;
int pageIndex = 0;
@override
void initState() {
pageController = PageController(initialPage: 0);
super.initState();
}
@override
void dispose() {
pageController.dispose();
super.dispose();
}
void onBoardHome() {
settings.onboard = true;
isar.writeTxnSync(() => isar.settings.putSync(settings));
Get.off(() => const SelectGeolocation(isStart: true),
transition: Transition.downToUp);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
Expanded(
child: PageView.builder(
controller: pageController,
itemCount: data.length,
onPageChanged: (index) {
setState(() {
pageIndex = index;
});
},
itemBuilder: (context, index) => OnboardContent(
image: data[index].image,
title: data[index].title,
description: data[index].description,
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
...List.generate(
data.length,
(index) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: DotIndicator(isActive: index == pageIndex),
)),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: MyTextButton(
buttonName:
pageIndex == data.length - 1 ? 'start'.tr : 'next'.tr,
onPressed: () {
pageIndex == data.length - 1
? onBoardHome()
: pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.ease,
);
},
),
)
],
),
),
);
}
}
class DotIndicator extends StatelessWidget {
const DotIndicator({
super.key,
this.isActive = false,
});
final bool isActive;
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
height: 8,
width: 8,
decoration: BoxDecoration(
color: isActive
? context.theme.colorScheme.secondary
: context.theme.colorScheme.secondaryContainer,
shape: BoxShape.circle,
),
);
}
}
class Onboard {
final String image, title, description;
Onboard({
required this.image,
required this.title,
required this.description,
});
}
final List<Onboard> data = [
Onboard(
image: 'assets/icons/Rain.png',
title: 'name'.tr,
description: 'description'.tr),
Onboard(
image: 'assets/icons/Design.png',
title: 'name2'.tr,
description: 'description2'.tr),
Onboard(
image: 'assets/icons/Team.png',
title: 'name3'.tr,
description: 'description3'.tr),
];
class OnboardContent extends StatelessWidget {
const OnboardContent({
super.key,
required this.image,
required this.title,
required this.description,
});
final String image, title, description;
@override
Widget build(BuildContext context) {
return Column(
children: [
Flexible(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
image,
scale: 5,
),
Text(
title,
style: context.textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(height: 10),
SizedBox(
width: 300,
child: Text(
description,
style: context.textTheme.labelLarge?.copyWith(fontSize: 14),
textAlign: TextAlign.center,
),
),
],
),
),
],
);
}
}

View file

@ -1,922 +0,0 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:iconsax/iconsax.dart';
import 'package:intl/intl.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:rain/app/controller/controller.dart';
import 'package:rain/app/data/weather.dart';
import 'package:rain/app/modules/settings/widgets/setting_card.dart';
import 'package:rain/main.dart';
import 'package:rain/theme/theme_controller.dart';
import 'package:rain/utils/color_converter.dart';
import 'package:url_launcher/url_launcher.dart';
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
@override
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
final themeController = Get.put(ThemeController());
final weatherController = Get.put(WeatherController());
String? appVersion;
String? colorBackground;
String? colorText;
Future<void> infoVersion() async {
final packageInfo = await PackageInfo.fromPlatform();
setState(() {
appVersion = packageInfo.version;
});
}
void urlLauncher(String uri) async {
final Uri url = Uri.parse(uri);
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
throw Exception('Could not launch $url');
}
}
@override
void initState() {
infoVersion();
super.initState();
}
updateLanguage(Locale locale) {
settings.language = '$locale';
isar.writeTxnSync(() => isar.settings.putSync(settings));
Get.updateLocale(locale);
Get.back();
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SettingCard(
icon: const Icon(Iconsax.brush_1),
text: 'appearance'.tr,
onPressed: () {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, setState) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 15),
child: Text(
'appearance'.tr,
style: context.textTheme.titleLarge?.copyWith(
fontSize: 20,
),
),
),
SettingCard(
elevation: 4,
icon: const Icon(Iconsax.moon),
text: 'theme'.tr,
dropdown: true,
dropdownName: settings.theme?.tr,
dropdownList: <String>[
'system'.tr,
'dark'.tr,
'light'.tr
],
dropdownCange: (String? newValue) {
final newThemeMode = newValue?.tr;
final darkTheme = 'dark'.tr;
final systemTheme = 'system'.tr;
ThemeMode themeMode =
newThemeMode == systemTheme
? ThemeMode.system
: newThemeMode == darkTheme
? ThemeMode.dark
: ThemeMode.light;
String theme = newThemeMode == systemTheme
? 'system'
: newThemeMode == darkTheme
? 'dark'
: 'light';
themeController.saveTheme(theme);
themeController.changeThemeMode(themeMode);
setState(() {});
},
),
SettingCard(
elevation: 4,
icon: const Icon(Iconsax.mobile),
text: 'amoledTheme'.tr,
switcher: true,
value: settings.amoledTheme,
onChange: (value) {
themeController.saveOledTheme(value);
MyApp.updateAppState(context,
newAmoledTheme: value);
},
),
SettingCard(
elevation: 4,
icon: const Icon(Iconsax.colorfilter),
text: 'materialColor'.tr,
switcher: true,
value: settings.materialColor,
onChange: (value) {
themeController.saveMaterialTheme(value);
MyApp.updateAppState(context,
newMaterialColor: value);
},
),
const SizedBox(height: 10),
],
),
);
},
);
},
);
},
),
SettingCard(
icon: const Icon(Iconsax.code),
text: 'functions'.tr,
onPressed: () {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, setState) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 15),
child: Text(
'functions'.tr,
style: context.textTheme.titleLarge?.copyWith(
fontSize: 20,
),
),
),
SettingCard(
elevation: 4,
icon: const Icon(Iconsax.map_1),
text: 'location'.tr,
switcher: true,
value: settings.location,
onChange: (value) async {
if (value) {
bool serviceEnabled = await Geolocator
.isLocationServiceEnabled();
if (!serviceEnabled) {
if (!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;
}
weatherController.getCurrentLocation();
}
isar.writeTxnSync(() {
settings.location = value;
isar.settings.putSync(settings);
});
setState(() {});
},
),
SettingCard(
elevation: 4,
icon: const Icon(Iconsax.notification_1),
text: 'notifications'.tr,
switcher: true,
value: settings.notifications,
onChange: (value) async {
final resultExact =
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.requestExactAlarmsPermission();
final result = Platform.isIOS
? await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions()
: await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.requestNotificationsPermission();
if (result != null && resultExact != null) {
isar.writeTxnSync(() {
settings.notifications = value;
isar.settings.putSync(settings);
});
if (value) {
weatherController.notlification(
weatherController.mainWeather);
} else {
flutterLocalNotificationsPlugin.cancelAll();
}
setState(() {});
}
},
),
SettingCard(
elevation: 4,
icon: const Icon(Iconsax.notification_status),
text: 'timeRange'.tr,
dropdown: true,
dropdownName: '$timeRange',
dropdownList: const <String>[
'1',
'2',
'3',
'4',
'5',
],
dropdownCange: (String? newValue) {
isar.writeTxnSync(() {
settings.timeRange = int.parse(newValue!);
isar.settings.putSync(settings);
});
MyApp.updateAppState(context,
newTimeRange: int.parse(newValue!));
if (settings.notifications) {
flutterLocalNotificationsPlugin.cancelAll();
weatherController.notlification(
weatherController.mainWeather);
}
},
),
SettingCard(
elevation: 4,
icon: const Icon(Iconsax.timer_start),
text: 'timeStart'.tr,
info: true,
infoSettings: true,
infoWidget: _TextInfo(
info: settings.timeformat == '12'
? DateFormat.jm().format(DateFormat.Hm()
.parse(weatherController
.timeConvert(timeStart)
.format(context)))
: DateFormat.Hm().format(DateFormat.Hm()
.parse(weatherController
.timeConvert(timeStart)
.format(context))),
),
onPressed: () async {
final TimeOfDay? timeStartPicker =
await showTimePicker(
context: context,
initialTime:
weatherController.timeConvert(timeStart),
builder: (context, child) {
final Widget mediaQueryWrapper = MediaQuery(
data: MediaQuery.of(context).copyWith(
alwaysUse24HourFormat:
settings.timeformat == '12'
? false
: true,
),
child: child!,
);
return mediaQueryWrapper;
},
);
if (timeStartPicker != null) {
isar.writeTxnSync(() {
settings.timeStart =
timeStartPicker.format(context);
isar.settings.putSync(settings);
});
if (!mounted) return;
MyApp.updateAppState(context,
newTimeStart:
timeStartPicker.format(context));
if (settings.notifications) {
flutterLocalNotificationsPlugin.cancelAll();
weatherController.notlification(
weatherController.mainWeather);
}
}
},
),
SettingCard(
elevation: 4,
icon: const Icon(Iconsax.timer_pause),
text: 'timeEnd'.tr,
info: true,
infoSettings: true,
infoWidget: _TextInfo(
info: settings.timeformat == '12'
? DateFormat.jm().format(DateFormat.Hm()
.parse(weatherController
.timeConvert(timeEnd)
.format(context)))
: DateFormat.Hm().format(DateFormat.Hm()
.parse(weatherController
.timeConvert(timeEnd)
.format(context))),
),
onPressed: () async {
final TimeOfDay? timeEndPicker =
await showTimePicker(
context: context,
initialTime:
weatherController.timeConvert(timeEnd),
builder: (context, child) {
final Widget mediaQueryWrapper = MediaQuery(
data: MediaQuery.of(context).copyWith(
alwaysUse24HourFormat:
settings.timeformat == '12'
? false
: true,
),
child: child!,
);
return mediaQueryWrapper;
},
);
if (timeEndPicker != null) {
isar.writeTxnSync(() {
settings.timeEnd =
timeEndPicker.format(context);
isar.settings.putSync(settings);
});
if (!mounted) return;
MyApp.updateAppState(context,
newTimeEnd:
timeEndPicker.format(context));
if (settings.notifications) {
flutterLocalNotificationsPlugin.cancelAll();
weatherController.notlification(
weatherController.mainWeather);
}
}
},
),
const SizedBox(height: 10),
],
),
);
},
);
},
);
},
),
SettingCard(
icon: const Icon(Iconsax.d_square),
text: 'data'.tr,
onPressed: () {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, setState) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 15),
child: Text(
'data'.tr,
style: context.textTheme.titleLarge?.copyWith(
fontSize: 20,
),
),
),
SettingCard(
elevation: 4,
icon: const Icon(Iconsax.cloud_notif),
text: 'roundDegree'.tr,
switcher: true,
value: settings.roundDegree,
onChange: (value) {
settings.roundDegree = value;
isar.writeTxnSync(
() => isar.settings.putSync(settings));
MyApp.updateAppState(context,
newRoundDegree: value);
setState(() {});
},
),
SettingCard(
elevation: 4,
icon: const Icon(Iconsax.sun_1),
text: 'degrees'.tr,
dropdown: true,
dropdownName: settings.degrees.tr,
dropdownList: <String>[
'celsius'.tr,
'fahrenheit'.tr
],
dropdownCange: (String? newValue) async {
isar.writeTxnSync(() {
settings.degrees = newValue == 'celsius'.tr
? 'celsius'
: 'fahrenheit';
isar.settings.putSync(settings);
});
await weatherController.deleteAll(false);
await weatherController.setLocation();
await weatherController.updateCacheCard(true);
setState(() {});
},
),
SettingCard(
elevation: 4,
icon: const Icon(Iconsax.rulerpen),
text: 'measurements'.tr,
dropdown: true,
dropdownName: settings.measurements.tr,
dropdownList: <String>[
'metric'.tr,
'imperial'.tr
],
dropdownCange: (String? newValue) async {
isar.writeTxnSync(() {
settings.measurements =
newValue == 'metric'.tr
? 'metric'
: 'imperial';
isar.settings.putSync(settings);
});
await weatherController.deleteAll(false);
await weatherController.setLocation();
await weatherController.updateCacheCard(true);
setState(() {});
},
),
SettingCard(
elevation: 4,
icon: const Icon(Iconsax.clock),
text: 'timeformat'.tr,
dropdown: true,
dropdownName: settings.timeformat.tr,
dropdownList: <String>['12'.tr, '24'.tr],
dropdownCange: (String? newValue) {
isar.writeTxnSync(() {
settings.timeformat =
newValue == '12'.tr ? '12' : '24';
isar.settings.putSync(settings);
});
setState(() {});
},
),
const SizedBox(height: 10),
],
),
);
},
);
},
);
},
),
SettingCard(
icon: const Icon(Iconsax.setting_3),
text: 'widget'.tr,
onPressed: () {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, setState) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 15),
child: Text(
'widget'.tr,
style: context.textTheme.titleLarge?.copyWith(
fontSize: 20,
),
),
),
SettingCard(
elevation: 4,
icon: const Icon(Iconsax.bucket_square),
text: 'widgetBackground'.tr,
info: true,
infoWidget: CircleAvatar(
backgroundColor: context.theme.indicatorColor,
radius: 11,
child: CircleAvatar(
backgroundColor: widgetBackgroundColor.isEmpty
? context.theme.primaryColor
: HexColor.fromHex(widgetBackgroundColor),
radius: 10,
),
),
onPressed: () {
colorBackground = null;
showDialog(
context: context,
builder: (context) => Dialog(
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: 15),
child: Text(
'widgetBackground'.tr,
style: context
.textTheme.titleMedium
?.copyWith(fontSize: 18),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15),
child: Theme(
data: context.theme.copyWith(
inputDecorationTheme:
InputDecorationTheme(
border: OutlineInputBorder(
borderRadius:
BorderRadius.circular(
8),
),
),
),
child: ColorPicker(
pickerColor: widgetBackgroundColor
.isEmpty
? context.theme.primaryColor
: HexColor.fromHex(
widgetBackgroundColor),
onColorChanged: (pickedColor) {
colorBackground =
pickedColor.toHex();
},
hexInputBar: true,
labelTypes: const [],
pickerAreaHeightPercent: 0.7,
pickerAreaBorderRadius:
BorderRadius.circular(20),
),
),
),
IconButton(
icon: const Icon(
Iconsax.tick_square,
),
onPressed: () {
if (colorBackground == null) {
return;
}
weatherController
.updateWidgetBackgroundColor(
colorBackground!);
MyApp.updateAppState(context,
newWidgetBackgroundColor:
colorBackground);
Get.back();
},
),
],
),
),
),
);
},
),
SettingCard(
elevation: 4,
icon: const Icon(Iconsax.text_block),
text: 'widgetText'.tr,
info: true,
infoWidget: CircleAvatar(
backgroundColor: context.theme.indicatorColor,
radius: 11,
child: CircleAvatar(
backgroundColor: widgetTextColor.isEmpty
? context.theme.primaryColor
: HexColor.fromHex(widgetTextColor),
radius: 10,
),
),
onPressed: () {
colorText = null;
showDialog(
context: context,
builder: (context) => Dialog(
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: 15),
child: Text(
'widgetText'.tr,
style: context
.textTheme.titleMedium
?.copyWith(fontSize: 18),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15),
child: Theme(
data: context.theme.copyWith(
inputDecorationTheme:
InputDecorationTheme(
border: OutlineInputBorder(
borderRadius:
BorderRadius.circular(
8),
),
),
),
child: ColorPicker(
pickerColor: widgetTextColor
.isEmpty
? context.theme.primaryColor
: HexColor.fromHex(
widgetTextColor),
onColorChanged: (pickedColor) {
colorText =
pickedColor.toHex();
},
hexInputBar: true,
labelTypes: const [],
pickerAreaHeightPercent: 0.7,
pickerAreaBorderRadius:
BorderRadius.circular(20),
),
),
),
IconButton(
icon: const Icon(
Iconsax.tick_square,
),
onPressed: () {
if (colorText == null) return;
weatherController
.updateWidgetTextColor(
colorText!);
MyApp.updateAppState(context,
newWidgetTextColor:
colorText);
Get.back();
},
),
],
),
),
),
);
},
),
const SizedBox(height: 10),
],
),
);
},
);
},
);
},
),
SettingCard(
icon: const Icon(Iconsax.language_square),
text: 'language'.tr,
info: true,
infoSettings: true,
infoWidget: _TextInfo(
info: appLanguages.firstWhere(
(element) => (element['locale'] == locale),
orElse: () => appLanguages.first)['name'],
),
onPressed: () {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, setState) {
return ListView(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 15),
child: Text(
'language'.tr,
style: context.textTheme.titleLarge?.copyWith(
fontSize: 20,
),
textAlign: TextAlign.center,
),
),
ListView.builder(
shrinkWrap: true,
physics: const BouncingScrollPhysics(),
itemCount: appLanguages.length,
itemBuilder: (context, index) {
return Card(
elevation: 4,
margin: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
child: ListTile(
title: Text(
appLanguages[index]['name'],
style: context.textTheme.labelLarge,
textAlign: TextAlign.center,
),
onTap: () {
MyApp.updateAppState(context,
newLocale: appLanguages[index]
['locale']);
updateLanguage(
appLanguages[index]['locale']);
},
),
);
},
),
const SizedBox(height: 10),
],
);
},
);
},
);
},
),
SettingCard(
icon: const Icon(Iconsax.dollar_square),
text: 'support'.tr,
onPressed: () {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, setState) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 15),
child: Text(
'support'.tr,
style: context.textTheme.titleLarge?.copyWith(
fontSize: 20,
),
),
),
SettingCard(
elevation: 4,
icon: const Icon(Iconsax.card),
text: 'DonationAlerts',
onPressed: () => urlLauncher(
'https://www.donationalerts.com/r/darkmoonight'),
),
SettingCard(
elevation: 4,
icon: const Icon(Iconsax.wallet),
text: 'ЮMoney',
onPressed: () => urlLauncher(
'https://yoomoney.ru/to/4100117672775961'),
),
const SizedBox(height: 10),
],
),
);
},
);
},
);
},
),
SettingCard(
icon: const Icon(Iconsax.document),
text: 'license'.tr,
onPressed: () => Get.to(
LicensePage(
applicationIcon: Container(
width: 100,
height: 100,
margin: const EdgeInsets.symmetric(vertical: 5),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(20)),
image: DecorationImage(
image: AssetImage('assets/icons/icon.png'),
),
),
),
applicationName: 'Rain',
applicationVersion: appVersion,
),
transition: Transition.downToUp,
),
),
SettingCard(
icon: const Icon(Iconsax.hierarchy_square_2),
text: 'version'.tr,
info: true,
infoWidget: _TextInfo(
info: '$appVersion',
),
),
SettingCard(
icon: Image.asset(
'assets/images/github.png',
scale: 20,
),
text: '${'project'.tr} GitHub',
onPressed: () =>
urlLauncher('https://github.com/darkmoonight/Rain'),
),
const SizedBox(height: 10),
],
),
);
}
}
class _TextInfo extends StatelessWidget {
const _TextInfo({required this.info});
final String info;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: 5),
child: Text(
info,
style: context.textTheme.bodyMedium,
overflow: TextOverflow.visible,
),
);
}
}

View file

@ -1,94 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:iconsax/iconsax.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(16),
),
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>(
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(
Iconsax.arrow_right_3,
size: 18,
),
],
)
: infoWidget!
: const Icon(
Iconsax.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,
),
);
}
}

477
lib/app/ui/map/view/map.dart Executable file
View file

@ -0,0 +1,477 @@
import 'dart:io';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:flutter/material.dart';
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
import 'package:flutter_map/flutter_map.dart';
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/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 MapPage extends StatefulWidget {
const MapPage({super.key});
@override
State<MapPage> createState() => _MapPageState();
}
class _MapPageState extends State<MapPage> with TickerProviderStateMixin {
late final AnimatedMapController _animatedMapController =
AnimatedMapController(vsync: this);
final weatherController = Get.put(WeatherController());
final statusWeather = StatusWeather();
final statusData = StatusData();
final Future<CacheStore> _cacheStoreFuture = _getCacheStore();
final GlobalKey<ExpandableFabState> _fabKey = GlobalKey<ExpandableFabState>();
final bool _isDarkMode = Get.theme.brightness == Brightness.dark;
WeatherCard? _selectedWeatherCard;
bool _isCardVisible = false;
late final AnimationController _animationController;
late final Animation<Offset> _offsetAnimation;
static const _useTransformerId = 'useTransformerId';
final bool _useTransformer = true;
final _focusNode = FocusNode();
late final TextEditingController _controllerSearch = TextEditingController();
static Future<CacheStore> _getCacheStore() async {
final dir = await getTemporaryDirectory();
return FileCacheStore('${dir.path}${Platform.pathSeparator}MapTiles');
}
@override
void initState() {
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_offsetAnimation = Tween<Offset>(
begin: const Offset(0.0, 1.0),
end: Offset.zero,
).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
super.initState();
}
@override
void dispose() {
_animatedMapController.dispose();
_controllerSearch.dispose();
_animationController.dispose();
super.dispose();
}
void _resetMapOrientation({LatLng? center, double? zoom}) {
_animatedMapController.animateTo(
customId: _useTransformer ? _useTransformerId : null,
dest: center,
zoom: zoom,
rotation: 0,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
}
void _onMarkerTap(WeatherCard weatherCard) {
setState(() {
_selectedWeatherCard = weatherCard;
});
_animationController.forward();
_isCardVisible = true;
if (_fabKey.currentState?.isOpen == true) {
_fabKey.currentState?.toggle();
}
}
void _hideCard() {
_animationController.reverse().then((_) {
setState(() {
_isCardVisible = false;
_selectedWeatherCard = null;
});
});
_focusNode.unfocus();
}
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),
scale: 18,
),
const MaxGap(5),
Text(
statusData.getDegree(
roundDegree ? temperature2M.round() : temperature2M,
),
style: context.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
);
}
Marker _buildMainLocationMarker(
WeatherCard weatherCard,
int hourOfDay,
int dayOfNow,
) {
return Marker(
height: 50,
width: 100,
point: LatLng(weatherCard.lat!, weatherCard.lon!),
child: GestureDetector(
onTap: () => _onMarkerTap(weatherCard),
child: _buildStyleMarkers(
weatherCard.weathercode![hourOfDay],
weatherCard.time![hourOfDay],
weatherCard.sunrise![dayOfNow],
weatherCard.sunset![dayOfNow],
weatherCard.temperature2M![hourOfDay],
),
),
);
}
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: _buildStyleMarkers(
weatherCardList.weathercode![hourOfDay],
weatherCardList.time![hourOfDay],
weatherCardList.sunrise![dayOfNow],
weatherCardList.sunset![dayOfNow],
weatherCardList.temperature2M![hourOfDay],
),
),
);
}
Widget _buildMapTileLayer(CacheStore cacheStore) {
return TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.darkmoonight.rain',
tileProvider: CachedTileProvider(
store: cacheStore,
maxStale: const Duration(days: 30),
),
);
}
Widget _buildWeatherCard() {
return _isCardVisible && _selectedWeatherCard != null
? SlideTransition(
position: _offsetAnimation,
child: GestureDetector(
onTap:
() => Get.to(
() => PlaceInfo(weatherCard: _selectedWeatherCard!),
transition: Transition.downToUp,
),
child: PlaceCard(
time: _selectedWeatherCard!.time!,
timeDaily: _selectedWeatherCard!.timeDaily!,
timeDay: _selectedWeatherCard!.sunrise!,
timeNight: _selectedWeatherCard!.sunset!,
weather: _selectedWeatherCard!.weathercode!,
degree: _selectedWeatherCard!.temperature2M!,
district: _selectedWeatherCard!.district!,
city: _selectedWeatherCard!.city!,
timezone: _selectedWeatherCard!.timezone!,
),
),
)
: 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;
final mainWeather = weatherController.mainWeather;
final hourOfDay = weatherController.hourOfDay.value;
final dayOfNow = weatherController.dayOfNow.value;
return Scaffold(
body: FutureBuilder<CacheStore>(
future: _cacheStoreFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}
final cacheStore = snapshot.data!;
return Stack(
children: [
FlutterMap(
mapController: _animatedMapController.mapController,
options: MapOptions(
backgroundColor: context.theme.colorScheme.surface,
initialCenter: LatLng(mainLocation.lat!, mainLocation.lon!),
initialZoom: 8,
interactionOptions: const InteractionOptions(
flags: InteractiveFlag.all & ~InteractiveFlag.rotate,
),
cameraConstraint: CameraConstraint.contain(
bounds: LatLngBounds(
const LatLng(-90, -180),
const LatLng(90, 180),
),
),
onTap: (_, __) => _hideCard(),
onLongPress:
(tapPosition, point) => showModalBottomSheet(
context: context,
isScrollControlled: true,
enableDrag: false,
builder:
(BuildContext context) => CreatePlace(
latitude: '${point.latitude}',
longitude: '${point.longitude}',
),
),
),
children: [
if (_isDarkMode)
ColorFiltered(
colorFilter: const 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
]),
child: _buildMapTileLayer(cacheStore),
)
else
_buildMapTileLayer(cacheStore),
RichAttributionWidget(
animationConfig: const ScaleRAWA(),
alignment: AttributionAlignment.bottomLeft,
attributions: [
TextSourceAttribution(
'OpenStreetMap contributors',
onTap:
() => weatherController.urlLauncher(
'https://openstreetmap.org/copyright',
),
),
],
),
Obx(() {
final mainMarker = _buildMainLocationMarker(
WeatherCard.fromJson({
...mainWeather.toJson(),
...mainLocation.toJson(),
}),
hourOfDay,
dayOfNow,
);
final cardMarkers =
weatherController.weatherCards
.map(
(weatherCardList) =>
_buildCardMarker(weatherCardList),
)
.toList();
return MarkerLayer(markers: [mainMarker, ...cardMarkers]);
}),
ExpandableFab(
key: _fabKey,
pos: ExpandableFabPos.right,
type: ExpandableFabType.up,
distance: 70,
openButtonBuilder: RotateFloatingActionButtonBuilder(
child: const Icon(IconsaxPlusLinear.menu),
fabSize: ExpandableFabSize.regular,
),
closeButtonBuilder: DefaultFloatingActionButtonBuilder(
child: const Icon(Icons.close),
fabSize: ExpandableFabSize.regular,
),
children: [
FloatingActionButton(
heroTag: null,
child: const Icon(IconsaxPlusLinear.home_2),
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,
),
),
FloatingActionButton(
heroTag: null,
child: const Icon(IconsaxPlusLinear.search_zoom_in),
onPressed:
() => _animatedMapController.animatedZoomIn(
customId:
_useTransformer ? _useTransformerId : null,
),
),
],
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: _buildWeatherCard(),
),
],
),
_buildSearchField(),
],
);
},
),
floatingActionButtonLocation: ExpandableFab.location,
);
}
}

198
lib/app/ui/onboarding.dart Executable file
View file

@ -0,0 +1,198 @@
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 {
const OnBording({super.key});
@override
State<OnBording> createState() => _OnBordingState();
}
class _OnBordingState extends State<OnBording> {
late PageController pageController;
int pageIndex = 0;
@override
void initState() {
super.initState();
pageController = PageController(initialPage: 0);
}
@override
void dispose() {
pageController.dispose();
super.dispose();
}
void onBoardHome() {
settings.onboard = true;
isar.writeTxnSync(() => isar.settings.putSync(settings));
Get.off(
() => const SelectGeolocation(isStart: true),
transition: Transition.downToUp,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: SafeArea(
child: Column(
children: [
_buildPageView(),
_buildDotIndicators(),
_buildActionButton(),
],
),
),
);
}
Widget _buildPageView() {
return Expanded(
child: PageView.builder(
controller: pageController,
itemCount: data.length,
onPageChanged: (index) {
setState(() {
pageIndex = index;
});
},
itemBuilder: (context, index) => OnboardContent(
image: data[index].image,
title: data[index].title,
description: data[index].description,
),
),
);
}
Widget _buildDotIndicators() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
data.length,
(index) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: DotIndicator(isActive: index == pageIndex),
),
),
);
}
Widget _buildActionButton() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: MyTextButton(
buttonName: pageIndex == data.length - 1 ? 'start'.tr : 'next'.tr,
onPressed: () {
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});
final bool isActive;
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
height: 8,
width: 8,
decoration: BoxDecoration(
color: isActive
? context.theme.colorScheme.secondary
: context.theme.colorScheme.secondaryContainer,
shape: BoxShape.circle,
),
);
}
}
class Onboard {
final String image, title, description;
Onboard({
required this.image,
required this.title,
required this.description,
});
}
final List<Onboard> data = [
Onboard(
image: 'assets/icons/Rain.png',
title: 'name'.tr,
description: 'description'.tr,
),
Onboard(
image: 'assets/icons/Design.png',
title: 'name2'.tr,
description: 'description2'.tr,
),
Onboard(
image: 'assets/icons/Team.png',
title: 'name3'.tr,
description: 'description3'.tr,
),
];
class OnboardContent extends StatelessWidget {
const OnboardContent({
super.key,
required this.image,
required this.title,
required this.description,
});
final String image, title, description;
@override
Widget build(BuildContext context) {
return Column(
children: [
Flexible(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(image, scale: 5),
Text(
title,
style: context.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const Gap(10),
SizedBox(
width: 300,
child: Text(
description,
style: context.textTheme.labelLarge?.copyWith(fontSize: 14),
textAlign: TextAlign.center,
),
),
],
),
),
],
);
}
}

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

@ -13,7 +13,9 @@ class MyTextForm extends StatelessWidget {
this.validator,
this.elevation,
this.focusNode,
this.onChanged,
});
final String labelText;
final TextInputType type;
final Icon icon;
@ -23,24 +25,35 @@ class MyTextForm extends StatelessWidget {
final String? Function(String?)? validator;
final double? elevation;
final FocusNode? focusNode;
final Function(String)? onChanged;
@override
Widget build(BuildContext context) {
return Card(
elevation: elevation,
margin: margin,
child: TextFormField(
focusNode: focusNode,
controller: controller,
keyboardType: type,
style: context.textTheme.labelLarge,
decoration: InputDecoration(
prefixIcon: icon,
suffixIcon: iconButton,
labelText: labelText,
),
validator: validator,
),
child: _buildTextFormField(context),
);
}
Widget _buildTextFormField(BuildContext context) {
return TextFormField(
focusNode: focusNode,
controller: controller,
keyboardType: type,
style: context.textTheme.labelLarge,
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,
);
}
}

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

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
class DescWeather extends StatefulWidget {
@ -9,6 +10,7 @@ class DescWeather extends StatefulWidget {
required this.desc,
this.message = '',
});
final String imageName;
final String value;
final String desc;
@ -25,36 +27,42 @@ 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(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
widget.imageName,
scale: 20,
),
const SizedBox(height: 5),
Text(
widget.value,
style: textTheme.labelLarge,
),
Expanded(
child: Text(
widget.desc,
style: textTheme.bodySmall,
overflow: hide ? TextOverflow.ellipsis : TextOverflow.visible,
textAlign: TextAlign.center,
),
),
],
),
child: _buildContent(textTheme),
),
),
);
}
void _toggleDescriptionVisibility() {
setState(() => hide = !hide);
}
Widget _buildContent(TextTheme textTheme) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(widget.imageName, scale: 20),
const Gap(5),
Text(
widget.value,
style: textTheme.labelLarge,
overflow: TextOverflow.ellipsis,
),
Expanded(
child: Text(
widget.desc,
style: textTheme.bodySmall,
overflow: hide ? TextOverflow.ellipsis : TextOverflow.visible,
textAlign: TextAlign.center,
),
),
],
);
}
}

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')}';
}
}

31
lib/app/utils/device_info.dart Executable file
View file

@ -0,0 +1,31 @@
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
class DeviceFeature {
DeviceFeature._internal();
static final DeviceFeature _singleton = DeviceFeature._internal();
factory DeviceFeature() {
return _singleton;
}
final _deviceInfoPlugin = DeviceInfoPlugin();
AndroidDeviceInfo? _androidDeviceInfo;
Future<void> init() async {
try {
_androidDeviceInfo = await _deviceInfoPlugin.androidInfo;
} catch (e) {
if (kDebugMode) {
print('Error initializing device info: $e');
}
}
}
bool isEdgeToEdgeAvailable() {
return _androidDeviceInfo != null &&
_androidDeviceInfo!.version.sdkInt > 28;
}
}

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,27 +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(
onPressed: onPressed,
child: Text(
buttonName,
style: context.textTheme.titleMedium,
),
),
);
}
}

View file

@ -1,250 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_glow/flutter_glow.dart';
import 'package:get/get.dart';
import 'package:iconsax/iconsax.dart';
import 'package:intl/intl.dart';
import 'package:rain/app/data/weather.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';
import 'package:rain/app/widgets/status/status_weather.dart';
import 'package:rain/app/widgets/sun_moon/sunset_sunrise.dart';
import 'package:rain/main.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;
@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(
Iconsax.arrow_left_1,
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;
});
},
itemCount: timeDaily.length,
itemBuilder: (context, index) {
final indexedWeatherCodeDaily = weatherCodeDaily[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];
return indexedWeatherCodeDaily == null
? null
: Container(
margin: const EdgeInsets.symmetric(horizontal: 10),
child: ListView(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 15),
Image(
image: AssetImage(
statusWeather
.getImageNowDaily(indexedWeatherCodeDaily),
),
fit: BoxFit.fill,
height: 200,
),
const SizedBox(height: 10),
GlowText(
'${weatherData.temperature2MMin![index]?.round()} / ${weatherData.temperature2MMax![index]?.round()}',
style: textTheme.titleLarge?.copyWith(
fontSize: 35,
fontWeight: FontWeight.w800,
),
blurRadius: 4,
),
const SizedBox(height: 5),
Text(
statusWeather.getText(indexedWeatherCodeDaily),
style: textTheme.titleLarge,
),
const SizedBox(height: 5),
Text(
DateFormat.MMMMEEEEd(locale.languageCode)
.format(timeDaily[index]),
style: textTheme.labelLarge?.copyWith(
color: Colors.grey,
fontSize: 16,
),
),
],
),
Padding(
padding: const EdgeInsets.only(top: 15),
child: SunsetSunrise(
timeSunrise: weatherData.sunrise![index],
timeSunset: weatherData.sunset![index],
),
),
Card(
margin: const EdgeInsets.only(bottom: 15),
child: Padding(
padding: const EdgeInsets.only(top: 20, bottom: 5),
child: Wrap(
alignment: WrapAlignment.spaceEvenly,
spacing: 5,
children: [
apparentTemperatureMin == null
? const Offstage()
: DescWeather(
imageName: 'assets/images/cold.png',
value: statusData.getDegree(
apparentTemperatureMin.round()),
desc: 'apparentTemperatureMin'.tr,
),
apparentTemperatureMax == null
? const Offstage()
: DescWeather(
imageName: 'assets/images/hot.png',
value: statusData.getDegree(
apparentTemperatureMax.round()),
desc: 'apparentTemperatureMax'.tr,
),
uvIndexMax == null
? const Offstage()
: DescWeather(
imageName: 'assets/images/uv.png',
value: '${uvIndexMax.round()}',
desc: 'uvIndex'.tr,
message: message
.getUvIndex(uvIndexMax.round()),
),
windDirection10MDominant == null
? const Offstage()
: DescWeather(
imageName: 'assets/images/windsock.png',
value: '$windDirection10MDominant°',
desc: 'direction'.tr,
message: message.getDirection(
windDirection10MDominant),
),
windSpeed10MMax == null
? const Offstage()
: DescWeather(
imageName: 'assets/images/wind.png',
value: statusData
.getSpeed(windSpeed10MMax.round()),
desc: 'wind'.tr,
),
windGusts10MMax == null
? const Offstage()
: DescWeather(
imageName:
'assets/images/windgusts.png',
value: statusData
.getSpeed(windGusts10MMax.round()),
desc: 'windgusts'.tr,
),
precipitationProbabilityMax == null
? const Offstage()
: DescWeather(
imageName:
'assets/images/precipitation_probability.png',
value: '$precipitationProbabilityMax%',
desc: 'precipitationProbability'.tr,
),
rainSum == null
? const Offstage()
: DescWeather(
imageName: 'assets/images/water.png',
value: statusData
.getPrecipitation(rainSum),
desc: 'rain'.tr,
),
precipitationSum == null
? const Offstage()
: DescWeather(
imageName: 'assets/images/rainfall.png',
value: statusData
.getPrecipitation(precipitationSum),
desc: 'precipitation'.tr,
),
],
),
),
),
],
),
);
},
),
),
);
}
}

View file

@ -1,80 +0,0 @@
import 'package:flutter/material.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 SizedBox(height: 5),
Text(
DateFormat.MMMMEEEEd(locale.languageCode)
.format(widget.timeDaily),
style: context.textTheme.titleMedium?.copyWith(
color: Colors.grey,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 5),
Text(
statusWeather.getText(widget.weathercodeDaily),
style: context.textTheme.titleMedium?.copyWith(
color: Colors.grey,
fontWeight: FontWeight.w400,
),
),
],
),
),
const SizedBox(width: 5),
Image.asset(
statusWeather.getImageNowDaily(widget.weathercodeDaily),
scale: 6.5,
),
],
),
),
);
}
}

View file

@ -1,146 +0,0 @@
import 'package:flutter/material.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;
final bodyMediumGrey = textTheme.bodyMedium?.copyWith(
color: Colors.grey,
);
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 SizedBox(width: 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: bodyMediumGrey,
),
Text(
statusData.getDegree(
(weatherData.temperature2MMax ?? [])[index]
?.round()),
style: bodyMediumGrey,
),
],
),
),
],
),
),
);
},
),
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/iconsax.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(
Iconsax.arrow_left_1,
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,189 +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,
required this.humidity,
required this.wind,
required this.visibility,
required this.feels,
required this.evaporation,
required this.precipitation,
required this.direction,
required this.pressure,
required this.rain,
required this.cloudcover,
required this.windgusts,
required this.uvIndex,
required this.dewpoint2M,
required this.precipitationProbability,
required this.shortwaveRadiation,
});
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;
@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;
return Card(
margin: const EdgeInsets.only(bottom: 15),
child: Padding(
padding: const EdgeInsets.only(top: 22, bottom: 5),
child: Wrap(
alignment: WrapAlignment.spaceEvenly,
spacing: 5,
children: [
dewpoint2M == null
? const Offstage()
: DescWeather(
imageName: 'assets/images/dew.png',
value: statusData.getDegree(dewpoint2M.round()),
desc: 'dewpoint'.tr,
),
feels == null
? const Offstage()
: DescWeather(
imageName: 'assets/images/temperature.png',
value: statusData.getDegree(feels.round()),
desc: 'feels'.tr,
),
visibility == null
? const Offstage()
: DescWeather(
imageName: 'assets/images/fog.png',
value: statusData.getVisibility(visibility),
desc: 'visibility'.tr,
),
direction == null
? const Offstage()
: DescWeather(
imageName: 'assets/images/windsock.png',
value: '$direction°',
desc: 'direction'.tr,
message: message.getDirection(direction),
),
wind == null
? const Offstage()
: DescWeather(
imageName: 'assets/images/wind.png',
value: statusData.getSpeed(wind.round()),
desc: 'wind'.tr,
),
windgusts == null
? const Offstage()
: DescWeather(
imageName: 'assets/images/windgusts.png',
value: statusData.getSpeed(windgusts.round()),
desc: 'windgusts'.tr,
),
evaporation == null
? const Offstage()
: DescWeather(
imageName: 'assets/images/evaporation.png',
value: statusData.getPrecipitation(evaporation.abs()),
desc: 'evaporation'.tr,
),
precipitation == null
? const Offstage()
: DescWeather(
imageName: 'assets/images/rainfall.png',
value: statusData.getPrecipitation(precipitation),
desc: 'precipitation'.tr,
),
rain == null
? const Offstage()
: DescWeather(
imageName: 'assets/images/water.png',
value: statusData.getPrecipitation(rain),
desc: 'rain'.tr,
),
precipitationProbability == null
? const Offstage()
: DescWeather(
imageName: 'assets/images/precipitation_probability.png',
value: '$precipitationProbability%',
desc: 'precipitationProbability'.tr,
),
humidity == null
? const Offstage()
: DescWeather(
imageName: 'assets/images/humidity.png',
value: '$humidity%',
desc: 'humidity'.tr,
),
cloudcover == null
? const Offstage()
: DescWeather(
imageName: 'assets/images/cloudy.png',
value: '$cloudcover%',
desc: 'cloudcover'.tr,
),
pressure == null
? const Offstage()
: DescWeather(
imageName: 'assets/images/atmospheric.png',
value: '${pressure.round()} ${'hPa'.tr}',
desc: 'pressure'.tr,
message: message.getPressure(pressure.round()),
),
uvIndex == null
? const Offstage()
: DescWeather(
imageName: 'assets/images/uv.png',
value: '${uvIndex.round()}',
desc: 'uvIndex'.tr,
message: message.getUvIndex(uvIndex.round()),
),
shortwaveRadiation == null
? const Offstage()
: 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,65 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_glow/flutter_glow.dart';
import 'package:get/get.dart';
import 'package:intl/intl.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,
});
final String time;
final String timeDay;
final String timeNight;
final int weather;
final double degree;
@override
State<WeatherNow> createState() => _WeatherNowState();
}
class _WeatherNowState extends State<WeatherNow> {
final statusWeather = StatusWeather();
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 15),
Image(
image: AssetImage(statusWeather.getImageNow(
widget.weather, widget.time, widget.timeDay, widget.timeNight)),
fit: BoxFit.fill,
height: 200,
),
GlowText(
'${roundDegree ? widget.degree.round() : widget.degree}',
style: context.textTheme.displayLarge?.copyWith(
fontSize: 90,
fontWeight: FontWeight.w800,
),
),
Text(
statusWeather.getText(widget.weather),
style: context.textTheme.titleLarge,
),
const SizedBox(height: 5),
Text(
DateFormat.MMMMEEEEd(locale.languageCode).format(
DateTime.parse(widget.time),
),
style: context.textTheme.labelLarge?.copyWith(
color: Colors.grey,
),
),
],
);
}
}

View file

@ -1,76 +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(int? 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 '$speed ${'kph'.tr}';
case 'imperial':
return '$speed ${'mph'.tr}';
default:
return '$speed ${'kph'.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().format(DateTime.tryParse(time)!);
case '24':
return DateFormat.Hm().format(DateTime.tryParse(time)!);
default:
return DateFormat.Hm().format(DateTime.tryParse(time)!);
}
}
String getTimeFormatTz(TZDateTime time) {
switch (settings.timeformat) {
case '12':
return DateFormat.jm().format(time);
case '24':
return DateFormat.Hm().format(time);
default:
return DateFormat.Hm().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,104 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.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) {
const crossAxisCenterAlignment = CrossAxisAlignment.center;
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(
crossAxisAlignment: crossAxisCenterAlignment,
children: [
Expanded(
child: Column(
crossAxisAlignment: crossAxisCenterAlignment,
children: [
Text(
'sunrise'.tr,
style: titleSmall,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
const SizedBox(height: 2),
Text(
statusData.getTimeFormat(widget.timeSunrise),
style: titleLarge,
),
],
),
),
const SizedBox(width: 5),
Flexible(
child: Image.asset(
'assets/images/sunrise.png',
scale: 10,
),
),
],
),
),
Expanded(
child: Row(
crossAxisAlignment: crossAxisCenterAlignment,
children: [
Expanded(
child: Column(
crossAxisAlignment: crossAxisCenterAlignment,
children: [
Text(
'sunset'.tr,
style: titleSmall,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
const SizedBox(height: 2),
Text(
statusData.getTimeFormat(widget.timeSunset),
style: titleLarge,
),
],
),
),
const SizedBox(width: 5),
Flexible(
child: Image.asset(
'assets/images/sunset.png',
scale: 10,
),
),
],
),
),
],
),
),
);
}
}

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

@ -10,32 +10,36 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:get/get.dart';
import 'package:home_widget/home_widget.dart';
import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart';
import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart';
import 'package:rain/app/controller/controller.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:time_machine/time_machine.dart';
import 'package:rain/theme/theme_controller.dart';
import 'package:rain/translation/translation.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';
import 'app/data/weather.dart';
import 'theme/theme_controller.dart';
import 'translation/translation.dart';
late Isar isar;
late Settings settings;
late LocationCache locationCache;
bool isOnline = false;
final ValueNotifier<Future<bool>> isOnline = ValueNotifier(
InternetConnection().hasInternetAccess,
);
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
bool amoledTheme = false;
bool materialColor = false;
bool roundDegree = false;
bool largeElement = false;
Locale locale = const Locale('en', 'US');
int timeRange = 1;
String timeStart = '09:00';
@ -43,32 +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': '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('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')},
];
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) {
@ -77,55 +85,40 @@ void callbackDispatcher() {
}
void main() async {
final String timeZoneName;
WidgetsFlutterBinding.ensureInitialized();
Connectivity().onConnectivityChanged.listen((ConnectivityResult result) {
result == ConnectivityResult.none ? isOnline = false : isOnline = true;
});
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(systemNavigationBarColor: Colors.black));
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,
@ -147,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});
@ -155,6 +170,7 @@ class MyApp extends StatefulWidget {
bool? newAmoledTheme,
bool? newMaterialColor,
bool? newRoundDegree,
bool? newLargeElement,
Locale? newLocale,
int? newTimeRange,
String? newTimeStart,
@ -164,27 +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 (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);
}
@ -200,127 +203,136 @@ 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 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
Widget build(BuildContext context) {
return DynamicColorBuilder(
builder: (lightColorScheme, darkColorScheme) {
final lightMaterialTheme =
lightTheme(lightColorScheme?.surface, lightColorScheme);
final darkMaterialTheme =
darkTheme(darkColorScheme?.surface, darkColorScheme);
final darkMaterialThemeOled = darkTheme(oledColor, darkColorScheme);
final edgeToEdgeAvailable = DeviceFeature().isEdgeToEdgeAvailable();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
return GetMaterialApp(
themeMode: themeController.theme,
theme: materialColor
? lightColorScheme != null
? lightMaterialTheme
: lightTheme(lightColor, colorSchemeLight)
: lightTheme(lightColor, colorSchemeLight),
darkTheme: amoledTheme
? materialColor
? darkColorScheme != null
? darkMaterialThemeOled
: darkTheme(oledColor, colorSchemeDark)
: darkTheme(oledColor, colorSchemeDark)
: materialColor
? darkColorScheme != null
? darkMaterialTheme
: darkTheme(darkColor, colorSchemeDark)
: darkTheme(darkColor, colorSchemeDark),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
translations: Translation(),
locale: locale,
fallbackLocale: const Locale('en', 'US'),
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)
? const SelectGeolocation(isStart: true)
: const HomePage()
: const OnBording(),
);
},
return GestureDetector(
onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
child: DynamicColorBuilder(
builder: (lightColorScheme, darkColorScheme) {
final lightMaterialTheme = lightTheme(
lightColorScheme?.surface,
lightColorScheme,
edgeToEdgeAvailable,
);
final darkMaterialTheme = darkTheme(
darkColorScheme?.surface,
darkColorScheme,
edgeToEdgeAvailable,
);
final darkMaterialThemeOled = darkTheme(
oledColor,
darkColorScheme,
edgeToEdgeAvailable,
);
return GetMaterialApp(
themeMode: themeController.theme,
theme:
materialColor
? lightColorScheme != null
? lightMaterialTheme
: lightTheme(
lightColor,
colorSchemeLight,
edgeToEdgeAvailable,
)
: lightTheme(
lightColor,
colorSchemeLight,
edgeToEdgeAvailable,
),
darkTheme:
amoledTheme
? materialColor
? darkColorScheme != null
? darkMaterialThemeOled
: darkTheme(
oledColor,
colorSchemeDark,
edgeToEdgeAvailable,
)
: darkTheme(
oledColor,
colorSchemeDark,
edgeToEdgeAvailable,
)
: materialColor
? darkColorScheme != null
? darkMaterialTheme
: darkTheme(
darkColor,
colorSchemeDark,
edgeToEdgeAvailable,
)
: darkTheme(
darkColor,
colorSchemeDark,
edgeToEdgeAvailable,
),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
translations: Translation(),
locale: locale,
fallbackLocale: const Locale('en', 'US'),
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)
? const SelectGeolocation(isStart: true)
: const HomePage()
: const OnBording(),
title: 'Rain',
);
},
),
);
}
}

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

@ -1,8 +1,9 @@
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
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;
@ -13,108 +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) {
return baseLigth.copyWith(
ThemeData lightTheme(
Color? color,
ColorScheme? colorScheme,
bool edgeToEdgeAvailable,
) {
return _buildTheme(
baseTheme: baseLight,
brightness: Brightness.light,
colorScheme: colorScheme
?.copyWith(
brightness: Brightness.light,
background: color,
surface: baseLigth.colorScheme.background,
)
.harmonized(),
textTheme: GoogleFonts.ubuntuTextTheme(baseLigth.textTheme),
appBarTheme: AppBarTheme(
backgroundColor: color,
foregroundColor: baseLigth.colorScheme.onSurface,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
elevation: 0,
),
primaryColor: color,
canvasColor: color,
scaffoldBackgroundColor: color,
cardTheme: baseLigth.cardTheme.copyWith(
color: color,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
shadowColor: Colors.transparent,
),
bottomSheetTheme: baseLigth.bottomSheetTheme.copyWith(
backgroundColor: color,
),
navigationRailTheme: baseLigth.navigationRailTheme.copyWith(
backgroundColor: color,
),
navigationBarTheme: baseLigth.navigationBarTheme.copyWith(
backgroundColor: color,
),
inputDecorationTheme: baseLigth.inputDecorationTheme.copyWith(
labelStyle: MaterialStateTextStyle.resolveWith(
(Set<MaterialState> states) {
return const TextStyle(fontSize: 14);
},
),
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
),
indicatorColor: Colors.black,
color: color,
colorScheme: colorScheme,
edgeToEdgeAvailable: edgeToEdgeAvailable,
);
}
ThemeData darkTheme(Color? color, ColorScheme? colorScheme) {
return baseDark.copyWith(
ThemeData darkTheme(
Color? color,
ColorScheme? colorScheme,
bool edgeToEdgeAvailable,
) {
return _buildTheme(
baseTheme: baseDark,
brightness: Brightness.dark,
colorScheme: colorScheme
?.copyWith(
brightness: Brightness.dark,
background: color,
surface: baseDark.colorScheme.background,
)
.harmonized(),
textTheme: GoogleFonts.ubuntuTextTheme(baseDark.textTheme),
appBarTheme: AppBarTheme(
backgroundColor: color,
foregroundColor: baseDark.colorScheme.onSurface,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
elevation: 0,
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,
surface: baseTheme.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,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
shadowColor: Colors.transparent,
),
bottomSheetTheme: baseDark.bottomSheetTheme.copyWith(
cardTheme: _buildCardTheme(color, harmonizedColorScheme),
bottomSheetTheme: _buildBottomSheetTheme(color, harmonizedColorScheme),
navigationRailTheme: baseTheme.navigationRailTheme.copyWith(
backgroundColor: color,
),
navigationRailTheme: baseDark.navigationRailTheme.copyWith(
backgroundColor: color,
),
navigationBarTheme: baseDark.navigationBarTheme.copyWith(
backgroundColor: color,
),
inputDecorationTheme: baseDark.inputDecorationTheme.copyWith(
labelStyle: MaterialStateTextStyle.resolveWith(
(Set<MaterialState> states) {
return const TextStyle(fontSize: 14);
},
),
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
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,
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));
}
}

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

@ -1,128 +1,141 @@
class BnIn {
Map<String, String> get messages => {
'start': 'শুরু করুন',
'description':
'প্রতি ঘণ্টায়, দিনে এবং সপ্তাহের জন্য প্রতিষ্ঠানের জন্য সক্রিয় পূর্বাভাস সহ আবহাওয়া অ্যাপ্লিকেশন।',
'name': 'আবহাওয়া',
'name2': 'সুবিধাজনক ডিজাইন',
'name3': 'আমাদের সাথে যোগাযোগ করুন',
'description2':
'সমস্ত নেভিগেশনটি এমনভাবে তৈরি করা হয়েছে যাতে আপনি অ্যাপ্লিকেশনে সর্বোত্তম সুবিধায় এবং দ্রুত ইন্টারঅ্যাক্ট করতে পারেন।',
'description3':
'আপনার যদি কোনও সমস্যা হয়, অনুগ্রহ করে ইমেল বা অ্যাপ্লিকেশন পর্যালোচনার মাধ্যমে আমাদের সাথে যোগাযোগ করুন।',
'next': 'পরবর্তী',
'search': 'অনুসন্ধান...',
'loading': 'লোড হচ্ছে...',
'searchCity': 'আপনার শহর খুঁজুন',
'humidity': 'আর্দ্ধমন্দ',
'wind': 'বায়ু',
'visibility': 'দৃশ্যতা',
'feels': 'অনুভব',
'evaporation': 'অবপাত ও প্রবাহ',
'precipitation': 'বৃষ্টিপাত',
'direction': 'দিশা',
'pressure': 'চাপ',
'rain': 'বৃষ্টি',
'clear_sky': 'পরিষ্কার আকাশ',
'cloudy': 'মেঘলা',
'overcast': 'মেঘাচ্ছন্ন',
'fog': 'কুয়াশা',
'drizzle': 'বৃষ্টি বৃষ্টি',
'freezing_drizzle': 'শীতল বৃষ্টি',
'freezing_rain': 'শীতল বৃষ্টি',
'rain_showers': 'বৃষ্টির বৃষ্টি',
'snow': 'তুষার',
'thunderstorm': 'বজ্রপাত',
'kph': 'কিমি/ঘণ্টা',
'mph': 'মাইল/ঘণ্টা',
'mi': 'মাইল',
'km': 'কিমি',
'inch': 'ইঞ্চ',
'mm': 'মিমি',
'hPa': 'হেক্টোপাস্কল',
'settings': 'সেটিংস',
'no_inter': 'ইন্টারনেট নেই',
'on_inter': 'মেটিয়োরোলজিক তথ্য পেতে ইন্টারনেট চালু করুন।',
'location': 'অবস্থান',
'no_location':
'বর্তমান অবস্থানের জন্য আবহাওয়া ডেটা পেতে অবস্থান সেবা সক্রিয় করুন।',
'theme': 'থিম',
'low': 'নিম্ন',
'high': 'উচ্চ',
'normal': 'সাধারণ',
'lat': 'অক্ষাংশ',
'lon': 'দ্রাঘিমাংশ',
'create': 'তৈরি করুন',
'city': 'শহর',
'district': 'জেলা',
'noWeatherCard': 'একটি শহর যোগ করুন',
'deletedCardWeather': 'একটি শহর মুছে ফেলা হচ্ছে',
'deletedCardWeatherQuery': 'আপনি কি নিশ্চিত যে আপনি শহরটি মুছতে চান?',
'delete': 'মুছে ফেলুন',
'cancel': 'বাতিল করুন',
'time': 'শহরে সময়',
'validateName': 'দয়া করে নাম লিখুন',
'measurements': 'মাপনের সিস্টেম',
'degrees': 'ডিগ্রি',
'celsius': 'সেলসিয়াস',
'fahrenheit': 'ফারেনহাইট',
'imperial': 'ইমপেরিয়াল',
'metric': 'মেট্রিক',
'validateValue': 'দয়া করে একটি মান লিখুন',
'validateNumber': 'দয়া করে একটি বৈধ সংখ্যা লিখুন',
'validate90': 'মান -৯০ থেকে ৯০ মধ্যে হতে হবে',
'validate180': 'মান -১৮০ থেকে ১৮০ মধ্যে হতে হবে',
'notifications': 'বিজ্ঞপ্তি',
'sunrise': 'সূর্যোদয়',
'sunset': 'সূর্যাস্ত',
'timeformat': 'সময় বিন্যাস',
'12': '১২-ঘণ্টা',
'24': '২৪-ঘণ্টা',
'cloudcover': 'মেঘপর্দা',
'uvIndex': 'আল্ট্রাভায়োলেট-সূচী',
'materialColor': 'গতিবিধির রঙ',
'uvLow': 'নিম্ন',
'uvAverage': 'মধ্যম',
'uvHigh': 'উচ্চ',
'uvVeryHigh': 'অত্যন্ত উচ্চ',
'uvExtreme': 'একাধিক',
'weatherMore': '১২-দিনের আবহাওয়া পূর্বানুমান',
'windgusts': 'ঝংকার',
'north': 'উত্তর',
'northeast': 'উত্তরপূর্ব',
'east': 'পূর্ব',
'southeast': 'দক্ষিণপূর্ব',
'south': 'দক্ষিণ',
'southwest': 'দক্ষিণপশ্চিম',
'west': 'পশ্চিম',
'northwest': 'উত্তরপশ্চিম',
'project': 'প্রকল্প',
'version': 'অ্যাপ্লিকেশন সংস্করণ',
'precipitationProbability': 'বৃষ্টিপাতের সম্ভাবনা',
'apparentTemperatureMin':
'ন্যায্য ন্যায্য তাপমাত্রা ন্যায্য ন্যায্য ন্যায্য ন্যায্য ন্যায্য ন্যায্য ন্যায্য',
'apparentTemperatureMax': 'সর্বাধিক ন্যায্য তাপমাত্রা',
'amoledTheme': 'এমোলেড-থিম',
'appearance': 'উপস্থিতি',
'functions': 'কার্য',
'data': 'ডেটা',
'language': 'ভাষা',
'timeRange': 'সময় পরিস্থিতি (ঘণ্টায়)',
'timeStart': 'শুরুর সময়',
'timeEnd': 'শেষ সময়',
'support': 'সাহায্য',
'system': 'সিস্টেম',
'dark': 'ডার্ক',
'light': 'আলো',
'license': 'লাইসেন্স',
'widget': 'উইজেট',
'widgetBackground': 'উইজেট পেশা',
'widgetText': 'উইজেট টেক্সট',
'dewpoint': 'তুষার বিন্দু',
'shortwaveRadiation': 'সংক্ষেপণ তরঙ্গ প্রকৃতি',
'W/m2': 'ডব্লিউ/মিটার বর্গ',
'roundDegree': 'ডিগ্রি রাউন্ড করুন',
'settings_full': 'সেটিংস',
'cities': 'শহর',
};
'start': 'শুরু করুন',
'description':
'প্রতি ঘণ্টায়, দিনে এবং সপ্তাহের জন্য প্রতিষ্ঠানের জন্য সক্রিয় পূর্বাভাস সহ আবহাওয়া অ্যাপ্লিকেশন।',
'name': 'আবহাওয়া',
'name2': 'সুবিধাজনক ডিজাইন',
'name3': 'আমাদের সাথে যোগাযোগ করুন',
'description2':
'সমস্ত নেভিগেশনটি এমনভাবে তৈরি করা হয়েছে যাতে আপনি অ্যাপ্লিকেশনে সর্বোত্তম সুবিধায় এবং দ্রুত ইন্টারঅ্যাক্ট করতে পারেন।',
'description3':
'আপনার যদি কোনও সমস্যা হয়, অনুগ্রহ করে ইমেল বা অ্যাপ্লিকেশন পর্যালোচনার মাধ্যমে আমাদের সাথে যোগাযোগ করুন।',
'next': 'পরবর্তী',
'search': 'অনুসন্ধান...',
'loading': 'লোড হচ্ছে...',
'searchCity': 'আপনার শহর খুঁজুন',
'humidity': 'আর্দ্ধমন্দ',
'wind': 'বায়ু',
'visibility': 'দৃশ্যতা',
'feels': 'অনুভব',
'evaporation': 'অবপাত ও প্রবাহ',
'precipitation': 'বৃষ্টিপাত',
'direction': 'দিশা',
'pressure': 'চাপ',
'rain': 'বৃষ্টি',
'clear_sky': 'পরিষ্কার আকাশ',
'cloudy': 'মেঘলা',
'overcast': 'মেঘাচ্ছন্ন',
'fog': 'কুয়াশা',
'drizzle': 'বৃষ্টি বৃষ্টি',
'freezing_drizzle': 'শীতল বৃষ্টি',
'freezing_rain': 'শীতল বৃষ্টি',
'rain_showers': 'বৃষ্টির বৃষ্টি',
'snow': 'তুষার',
'thunderstorm': 'বজ্রপাত',
'kph': 'কিমি/ঘণ্টা',
'mph': 'মাইল/ঘণ্টা',
'm/s': 'মি/সে',
'mmHg': 'মিমি Hg',
'mi': 'মাইল',
'km': 'কিমি',
'inch': 'ইঞ্চ',
'mm': 'মিমি',
'hPa': 'হেক্টোপাস্কল',
'settings': 'সেটিংস',
'no_inter': 'ইন্টারনেট নেই',
'on_inter': 'মেটিয়োরোলজিক তথ্য পেতে ইন্টারনেট চালু করুন।',
'location': 'অবস্থান',
'no_location':
'বর্তমান অবস্থানের জন্য আবহাওয়া ডেটা পেতে অবস্থান সেবা সক্রিয় করুন।',
'theme': 'থিম',
'low': 'নিম্ন',
'high': 'উচ্চ',
'normal': 'সাধারণ',
'lat': 'অক্ষাংশ',
'lon': 'দ্রাঘিমাংশ',
'create': 'তৈরি করুন',
'city': 'শহর',
'district': 'জেলা',
'noWeatherCard': 'একটি শহর যোগ করুন',
'deletedCardWeather': 'একটি শহর মুছে ফেলা হচ্ছে',
'deletedCardWeatherQuery': 'আপনি কি নিশ্চিত যে আপনি শহরটি মুছতে চান?',
'delete': 'মুছে ফেলুন',
'cancel': 'বাতিল করুন',
'time': 'শহরে সময়',
'validateName': 'দয়া করে নাম লিখুন',
'measurements': 'মাপনের সিস্টেম',
'degrees': 'ডিগ্রি',
'celsius': 'সেলসিয়াস',
'fahrenheit': 'ফারেনহাইট',
'imperial': 'ইমপেরিয়াল',
'metric': 'মেট্রিক',
'validateValue': 'দয়া করে একটি মান লিখুন',
'validateNumber': 'দয়া করে একটি বৈধ সংখ্যা লিখুন',
'validate90': 'মান -৯০ থেকে ৯০ মধ্যে হতে হবে',
'validate180': 'মান -১৮০ থেকে ১৮০ মধ্যে হতে হবে',
'notifications': 'বিজ্ঞপ্তি',
'sunrise': 'সূর্যোদয়',
'sunset': 'সূর্যাস্ত',
'timeformat': 'সময় বিন্যাস',
'12': '১২-ঘণ্টা',
'24': '২৪-ঘণ্টা',
'cloudcover': 'মেঘপর্দা',
'uvIndex': 'আল্ট্রাভায়োলেট-সূচী',
'materialColor': 'গতিবিধির রঙ',
'uvLow': 'নিম্ন',
'uvAverage': 'মধ্যম',
'uvHigh': 'উচ্চ',
'uvVeryHigh': 'অত্যন্ত উচ্চ',
'uvExtreme': 'একাধিক',
'weatherMore': '১২-দিনের আবহাওয়া পূর্বানুমান',
'windgusts': 'ঝংকার',
'north': 'উত্তর',
'northeast': 'উত্তরপূর্ব',
'east': 'পূর্ব',
'southeast': 'দক্ষিণপূর্ব',
'south': 'দক্ষিণ',
'southwest': 'দক্ষিণপশ্চিম',
'west': 'পশ্চিম',
'northwest': 'উত্তরপশ্চিম',
'project': 'প্রকল্প',
'version': 'অ্যাপ্লিকেশন সংস্করণ',
'precipitationProbability': 'বৃষ্টিপাতের সম্ভাবনা',
'apparentTemperatureMin':
'ন্যায্য ন্যায্য তাপমাত্রা ন্যায্য ন্যায্য ন্যায্য ন্যায্য ন্যায্য ন্যায্য ন্যায্য',
'apparentTemperatureMax': 'সর্বাধিক ন্যায্য তাপমাত্রা',
'amoledTheme': 'এমোলেড-থিম',
'appearance': 'উপস্থিতি',
'functions': 'কার্য',
'data': 'ডেটা',
'language': 'ভাষা',
'timeRange': 'সময় পরিস্থিতি (ঘণ্টায়)',
'timeStart': 'শুরুর সময়',
'timeEnd': 'শেষ সময়',
'support': 'সাহায্য',
'system': 'সিস্টেম',
'dark': 'ডার্ক',
'light': 'আলো',
'license': 'লাইসেন্স',
'widget': 'উইজেট',
'widgetBackground': 'উইজেট পেশা',
'widgetText': 'উইজেট টেক্সট',
'dewpoint': 'তুষার বিন্দু',
'shortwaveRadiation': 'সংক্ষেপণ তরঙ্গ প্রকৃতি',
'W/m2': 'ডব্লিউ/মিটার বর্গ',
'roundDegree': 'ডিগ্রি রাউন্ড করুন',
'settings_full': 'সেটিংস',
'cities': 'শহর',
'groups': 'আমাদের দলগুলি',
'openMeteo': 'Open-Meteo থেকে ডেটা (CC-BY 4.0)',
'hourlyVariables': 'ঘণ্টায় আবহাওয়ার পরিবর্তনশীল',
'dailyVariables': 'দৈনিক আবহাওয়ার পরিবর্তনশীল',
'largeElement': 'বড় আবহাওয়া ডিসপ্লে',
'map': 'মানচিত্র',
'clearCacheStore': 'ক্যাশ পরিষ্কার করুন',
'deletedCacheStore': 'ক্যাশ পরিষ্কার করা হচ্ছে',
'deletedCacheStoreQuery': 'আপনি কি সত্যিই ক্যাশ পরিষ্কার করতে চান?',
'addWidget': 'উইজেট যোগ করুন',
'hideMap': 'মানচিত্র লুকান',
};
}

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

@ -1,128 +1,141 @@
class CsCz {
Map<String, String> get messages => {
'start': 'Začít',
'description':
'Aplikace počasí s aktuálním předpovědí na každou hodinu, den a týden pro libovolné místo.',
'name': 'Počasí',
'name2': 'Pohodlný design',
'name3': 'Kontaktujte nás',
'description2':
'Celá navigace je navržena tak, aby bylo možné s aplikací co nejpohodlněji a nejrychleji interagovat.',
'description3':
'Pokud narazíte na nějaké potíže, kontaktujte nás prosím e-mailem nebo v recenzích aplikace.',
'next': 'Další',
'search': 'Hledat...',
'loading': 'Načítá se...',
'searchCity': 'Najděte své místo',
'humidity': 'Vlhkost',
'wind': 'Vítr',
'visibility': 'Viditelnost',
'feels': 'Pocitová teplota',
'evaporation': 'Evapotranspirace',
'precipitation': 'Srážky',
'direction': 'Směr',
'pressure': 'Tlak',
'rain': 'Déšť',
'clear_sky': 'Jasno',
'cloudy': 'Oblačno',
'overcast': 'Zataženo',
'fog': 'Mlha',
'drizzle': 'Mrholení',
'drizzling_rain': 'Mrznúce mrholení',
'freezing_rain': 'Mrazivý déšť',
'heavy_rains': 'Přeháňky',
'snow': 'Sníh',
'thunderstorm': 'Bouřka',
'kph': 'km/h',
'mph': 'mph',
'mi': 'mi',
'km': 'km',
'inch': 'inch',
'mm': 'mm',
'hPa': 'hPa',
'settings': 'Nast.',
'no_inter': 'Žádný internet',
'on_inter': 'Připojte se k internetu a získejte meteorologické údaje.',
'location': 'Poloha',
'no_location':
'Chcete-li získat údaje o počasí pro aktuální polohu, povolte službu určování polohy.',
'theme': 'Téma',
'low': 'Nízký',
'high': 'Vysoký',
'normal': 'Normální',
'lat': 'Zeměpisná šířka',
'lon': 'Zemepisná délka',
'create': 'Vytvořit',
'city': 'Místo',
'district': 'Okres',
'noWeatherCard': 'Přidat město',
'deletedCardWeather': 'Vymazat město',
'deletedCardWeatherQuery': 'Opravdu chcete odstranit město?',
'delete': 'Odstranit',
'cancel': 'Zrušit',
'time': 'Čas ve městě',
'validateName': 'Prosím zadejte název',
'measurements': 'Jednotky měření',
'degrees': 'Stupně',
'celsius': 'Celzius',
'fahrenheit': 'Fahrenheit',
'imperial': 'Imperiální',
'metric': 'Metrické',
'validateValue': 'Zadejte hodnotu',
'validateNumber': 'Zadejte platné číslo',
'validate90': 'Hodnota musí být mezi -90 a 90',
'validate180': 'Hodnota musí být mezi -180 a 180',
'notifications': 'Notifikace',
'sunrise': 'Východ slunce',
'sunset': 'Západ slunce',
'timeformat': 'Formát času',
'12': '12-hodinový',
'24': '24-hodinový',
'cloudcover': 'Oblačnost',
'uvIndex': 'UV-index',
'materialColor': 'Dynamické Barvy',
'uvLow': 'Nízký',
'uvAverage': 'Mírný',
'uvHigh': 'Vysoký',
'uvVeryHigh': 'Velmi vysoký',
'uvExtreme': 'Extrémní',
'weatherMore': 'Předpověď počasí na 12 dní',
'windgusts': 'Nárazy větru',
'north': 'Sever',
'northeast': 'Severo-Východ',
'east': 'Východ',
'southeast': 'Juhovýchod',
'south': 'Juž',
'southwest': 'Juhozápad',
'west': 'Západ',
'northwest': 'Severo-Západ',
'project': 'Projekt na',
'version': 'Verzia aplikace',
'precipitationProbability': 'Pravděpodobnost srážek',
'apparentTemperatureMin': 'Minimální pocitová teplota',
'apparentTemperatureMax': 'Maximální pocitová teplota',
'amoledTheme': 'AMOLED-téma',
'appearance': 'Vzhled',
'functions': 'Funkce',
'data': 'Data',
'language': 'Jazyk',
'timeRange': 'Frekvence (v hodinách)',
'timeStart': 'Čas začátku',
'timeEnd': 'Čas ukončení',
'support': 'Podpora',
'system': 'Systém',
'dark': 'Tmavá',
'light': 'Světlá',
'license': 'Licence',
'widget': 'Widget',
'widgetBackground': 'Pozadí widgetu',
'widgetText': 'Text widgetu',
'dewpoint': 'Rosný bod',
'shortwaveRadiation': 'Krátká vlnová radiace',
'roundDegree': 'Zaokrouhlit stupně',
'settings_full': 'Nastavení',
'cities': 'Města',
'searchMethod': 'Použijte hledání nebo geolokaci',
'done': 'Hotovo',
};
'start': 'Začít',
'description':
'Aplikace počasí s aktuálním předpovědí na každou hodinu, den a týden pro libovolné místo.',
'name': 'Počasí',
'name2': 'Pohodlný design',
'name3': 'Kontaktujte nás',
'description2':
'Celá navigace je navržena tak, aby bylo možné s aplikací co nejpohodlněji a nejrychleji interagovat.',
'description3':
'Pokud narazíte na nějaké potíže, kontaktujte nás prosím e-mailem nebo v recenzích aplikace.',
'next': 'Další',
'search': 'Hledat...',
'loading': 'Načítá se...',
'searchCity': 'Najděte své místo',
'humidity': 'Vlhkost',
'wind': 'Vítr',
'visibility': 'Viditelnost',
'feels': 'Pocitová teplota',
'evaporation': 'Evapotranspirace',
'precipitation': 'Srážky',
'direction': 'Směr',
'pressure': 'Tlak',
'rain': 'Déšť',
'clear_sky': 'Jasno',
'cloudy': 'Oblačno',
'overcast': 'Zataženo',
'fog': 'Mlha',
'drizzle': 'Mrholení',
'drizzling_rain': 'Mrznúce mrholení',
'freezing_rain': 'Mrazivý déšť',
'heavy_rains': 'Přeháňky',
'snow': 'Sníh',
'thunderstorm': 'Bouřka',
'kph': 'km/h',
'mph': 'mph',
'm/s': 'm/s',
'mmHg': 'mmHg',
'mi': 'mi',
'km': 'km',
'inch': 'inch',
'mm': 'mm',
'hPa': 'hPa',
'settings': 'Nast.',
'no_inter': 'Žádný internet',
'on_inter': 'Připojte se k internetu a získejte meteorologické údaje.',
'location': 'Poloha',
'no_location':
'Chcete-li získat údaje o počasí pro aktuální polohu, povolte službu určování polohy.',
'theme': 'Téma',
'low': 'Nízký',
'high': 'Vysoký',
'normal': 'Normální',
'lat': 'Zeměpisná šířka',
'lon': 'Zemepisná délka',
'create': 'Vytvořit',
'city': 'Místo',
'district': 'Okres',
'noWeatherCard': 'Přidat město',
'deletedCardWeather': 'Vymazat město',
'deletedCardWeatherQuery': 'Opravdu chcete odstranit město?',
'delete': 'Odstranit',
'cancel': 'Zrušit',
'time': 'Čas ve městě',
'validateName': 'Prosím zadejte název',
'measurements': 'Jednotky měření',
'degrees': 'Stupně',
'celsius': 'Celzius',
'fahrenheit': 'Fahrenheit',
'imperial': 'Imperiální',
'metric': 'Metrické',
'validateValue': 'Zadejte hodnotu',
'validateNumber': 'Zadejte platné číslo',
'validate90': 'Hodnota musí být mezi -90 a 90',
'validate180': 'Hodnota musí být mezi -180 a 180',
'notifications': 'Notifikace',
'sunrise': 'Východ slunce',
'sunset': 'Západ slunce',
'timeformat': 'Formát času',
'12': '12-hodinový',
'24': '24-hodinový',
'cloudcover': 'Oblačnost',
'uvIndex': 'UV-index',
'materialColor': 'Dynamické Barvy',
'uvLow': 'Nízký',
'uvAverage': 'Mírný',
'uvHigh': 'Vysoký',
'uvVeryHigh': 'Velmi vysoký',
'uvExtreme': 'Extrémní',
'weatherMore': 'Předpověď počasí na 12 dní',
'windgusts': 'Nárazy větru',
'north': 'Sever',
'northeast': 'Severo-Východ',
'east': 'Východ',
'southeast': 'Juhovýchod',
'south': 'Juž',
'southwest': 'Juhozápad',
'west': 'Západ',
'northwest': 'Severo-Západ',
'project': 'Projekt na',
'version': 'Verzia aplikace',
'precipitationProbability': 'Pravděpodobnost srážek',
'apparentTemperatureMin': 'Minimální pocitová teplota',
'apparentTemperatureMax': 'Maximální pocitová teplota',
'amoledTheme': 'AMOLED-téma',
'appearance': 'Vzhled',
'functions': 'Funkce',
'data': 'Data',
'language': 'Jazyk',
'timeRange': 'Frekvence (v hodinách)',
'timeStart': 'Čas začátku',
'timeEnd': 'Čas ukončení',
'support': 'Podpora',
'system': 'Systém',
'dark': 'Tmavá',
'light': 'Světlá',
'license': 'Licence',
'widget': 'Widget',
'widgetBackground': 'Pozadí widgetu',
'widgetText': 'Text widgetu',
'dewpoint': 'Rosný bod',
'shortwaveRadiation': 'Krátká vlnová radiace',
'roundDegree': 'Zaokrouhlit stupně',
'settings_full': 'Nastavení',
'cities': 'Města',
'searchMethod': 'Použijte hledání nebo geolokaci',
'done': 'Hotovo',
'groups': 'Naše skupiny',
'openMeteo': 'Data z Open-Meteo (CC-BY 4.0)',
'hourlyVariables': 'Hodinové meteorologické proměnné',
'dailyVariables': 'Denní meteorologické proměnné',
'largeElement': 'Velké zobrazení počasí',
'map': 'Mapa',
'clearCacheStore': 'Vymazat mezipaměť',
'deletedCacheStore': 'Čištění mezipaměti',
'deletedCacheStoreQuery': 'Opravdu chcete vymazat mezipaměť?',
'addWidget': 'Přidat widget',
'hideMap': 'Skrýt mapu',
};
}

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