Compare commits
	
		
			No commits in common. "d88bb723786e6074e05d540b9b28bccbc8e58234" and "16c9fdba515e15dd096d1666398bd8d1bc6132bd" have entirely different histories.
		
	
	
		
			d88bb72378
			...
			16c9fdba51
		
	
		|  | @ -1,15 +0,0 @@ | |||
| root = true | ||||
| 
 | ||||
| [*] | ||||
| charset = utf-8 | ||||
| indent_size = 2 | ||||
| indent_style = space | ||||
| insert_final_newline = true | ||||
| 
 | ||||
| [*.{kt,kts}] | ||||
| ij_kotlin_packages_to_use_import_on_demand = unset | ||||
| ij_kotlin_name_count_to_use_star_import = 999 | ||||
| ij_kotlin_name_count_to_use_star_import_for_members = 999 | ||||
| ij_kotlin_allow_trailing_comma = true | ||||
| ij_kotlin_allow_trailing_comma_on_call_site = true | ||||
| ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL | ||||
|  | @ -1,74 +1,74 @@ | |||
| plugins { | ||||
|   alias(libs.plugins.android.application) | ||||
|   alias(libs.plugins.kotlin.android) | ||||
|   alias(libs.plugins.kotlin.compose) | ||||
|   id("kotlin-parcelize") | ||||
|   id("org.jlleitschuh.gradle.ktlint") version "11.5.1" | ||||
|     alias(libs.plugins.android.application) | ||||
|     alias(libs.plugins.kotlin.android) | ||||
|     alias(libs.plugins.kotlin.compose) | ||||
|     id("kotlin-parcelize") | ||||
| } | ||||
| 
 | ||||
| android { | ||||
|   namespace = "com.module.breeze" | ||||
|   compileSdk = 35 | ||||
|     namespace = "com.module.breeze" | ||||
|     compileSdk = 35 | ||||
| 
 | ||||
|   defaultConfig { | ||||
|     applicationId = "com.module.breeze" | ||||
|     minSdk = 25 | ||||
|     targetSdk = 35 | ||||
|     versionCode = 1 | ||||
|     versionName = "1.0" | ||||
|     defaultConfig { | ||||
|         applicationId = "com.module.breeze" | ||||
|         minSdk = 25 | ||||
|         targetSdk = 35 | ||||
|         versionCode = 1 | ||||
|         versionName = "1.0" | ||||
| 
 | ||||
|     testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" | ||||
|   } | ||||
| 
 | ||||
|   buildTypes { | ||||
|     release { | ||||
|       isMinifyEnabled = false | ||||
|       proguardFiles( | ||||
|         getDefaultProguardFile("proguard-android-optimize.txt"), | ||||
|         "proguard-rules.pro", | ||||
|       ) | ||||
|         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" | ||||
|     } | ||||
| 
 | ||||
|     buildTypes { | ||||
|         release { | ||||
|             isMinifyEnabled = false | ||||
|             proguardFiles( | ||||
|                 getDefaultProguardFile("proguard-android-optimize.txt"), | ||||
|                 "proguard-rules.pro" | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|     compileOptions { | ||||
|         sourceCompatibility = JavaVersion.VERSION_11 | ||||
|         targetCompatibility = JavaVersion.VERSION_11 | ||||
|     } | ||||
|     kotlinOptions { | ||||
|         jvmTarget = "11" | ||||
|     } | ||||
|     buildFeatures { | ||||
|         compose = true | ||||
|     } | ||||
|   } | ||||
|   compileOptions { | ||||
|     sourceCompatibility = JavaVersion.VERSION_11 | ||||
|     targetCompatibility = JavaVersion.VERSION_11 | ||||
|   } | ||||
|   kotlinOptions { | ||||
|     jvmTarget = "11" | ||||
|   } | ||||
|   buildFeatures { | ||||
|     compose = true | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| dependencies { | ||||
| 
 | ||||
|   implementation(libs.androidx.core.ktx) | ||||
|   implementation(libs.androidx.lifecycle.runtime.ktx) | ||||
|   implementation(libs.androidx.activity.compose) | ||||
|   implementation(platform(libs.androidx.compose.bom)) | ||||
|   implementation(libs.androidx.ui) | ||||
|   implementation(libs.androidx.ui.graphics) | ||||
|   implementation(libs.androidx.ui.tooling.preview) | ||||
|   implementation(libs.androidx.material3) | ||||
|   implementation("androidx.compose.material:material-icons-extended:1.7.8") | ||||
|   implementation(libs.androidx.appcompat) | ||||
|   implementation(libs.androidx.preference) | ||||
|   implementation(libs.material) | ||||
|   testImplementation(libs.junit) | ||||
|   androidTestImplementation(libs.androidx.junit) | ||||
|   androidTestImplementation(libs.androidx.espresso.core) | ||||
|   androidTestImplementation(platform(libs.androidx.compose.bom)) | ||||
|   androidTestImplementation(libs.androidx.ui.test.junit4) | ||||
|   debugImplementation(libs.androidx.ui.tooling) | ||||
|   debugImplementation(libs.androidx.ui.test.manifest) | ||||
|     implementation(libs.androidx.core.ktx) | ||||
|     implementation(libs.androidx.lifecycle.runtime.ktx) | ||||
|     implementation(libs.androidx.activity.compose) | ||||
|     implementation(platform(libs.androidx.compose.bom)) | ||||
|     implementation(libs.androidx.ui) | ||||
|     implementation(libs.androidx.ui.graphics) | ||||
|     implementation(libs.androidx.ui.tooling.preview) | ||||
|     implementation(libs.androidx.material3) | ||||
|     implementation("androidx.compose.material:material-icons-extended:1.7.8") | ||||
|     implementation(libs.androidx.appcompat) | ||||
|     implementation(libs.androidx.preference) | ||||
|     implementation(libs.material) | ||||
|     testImplementation(libs.junit) | ||||
|     androidTestImplementation(libs.androidx.junit) | ||||
|     androidTestImplementation(libs.androidx.espresso.core) | ||||
|     androidTestImplementation(platform(libs.androidx.compose.bom)) | ||||
|     androidTestImplementation(libs.androidx.ui.test.junit4) | ||||
|     debugImplementation(libs.androidx.ui.tooling) | ||||
|     debugImplementation(libs.androidx.ui.test.manifest) | ||||
| 
 | ||||
|   implementation("androidx.datastore:datastore-preferences:1.0.0") | ||||
|   implementation("androidx.datastore:datastore:1.0.0") | ||||
|   implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") // For JSON serialization | ||||
|     implementation("androidx.datastore:datastore-preferences:1.0.0") | ||||
|     implementation("androidx.datastore:datastore:1.0.0") | ||||
|     implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") // For JSON serialization | ||||
| 
 | ||||
|     implementation("com.squareup.retrofit2:retrofit:2.9.0") | ||||
|     implementation("com.squareup.retrofit2:converter-gson:2.9.0") | ||||
|     implementation("com.squareup.okhttp3:logging-interceptor:4.9.1") | ||||
|     implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") | ||||
| 
 | ||||
|   implementation("com.squareup.retrofit2:retrofit:2.9.0") | ||||
|   implementation("com.squareup.retrofit2:converter-gson:2.9.0") | ||||
|   implementation("com.squareup.okhttp3:logging-interceptor:4.9.1") | ||||
|   implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") | ||||
| } | ||||
|  | @ -0,0 +1,24 @@ | |||
| package com.module.breeze | ||||
| 
 | ||||
| import androidx.test.platform.app.InstrumentationRegistry | ||||
| import androidx.test.ext.junit.runners.AndroidJUnit4 | ||||
| 
 | ||||
| import org.junit.Test | ||||
| import org.junit.runner.RunWith | ||||
| 
 | ||||
| import org.junit.Assert.* | ||||
| 
 | ||||
| /** | ||||
|  * Instrumented test, which will execute on an Android device. | ||||
|  * | ||||
|  * See [testing documentation](http://d.android.com/tools/testing). | ||||
|  */ | ||||
| @RunWith(AndroidJUnit4::class) | ||||
| class ExampleInstrumentedTest { | ||||
|     @Test | ||||
|     fun useAppContext() { | ||||
|         // Context of the app under test. | ||||
|         val appContext = InstrumentationRegistry.getInstrumentation().targetContext | ||||
|         assertEquals("com.module.breeze", appContext.packageName) | ||||
|     } | ||||
| } | ||||
|  | @ -11,36 +11,35 @@ import androidx.core.content.ContextCompat | |||
| 
 | ||||
| // AI | ||||
| class GpsRepository(private val context: Context) : LocationListener { | ||||
|   private var locationManager: LocationManager? = null | ||||
|   private var onLocationChanged: ((Location) -> Unit)? = null | ||||
|     private var locationManager: LocationManager? = null | ||||
|     private var onLocationChanged: ((Location) -> Unit)? = null | ||||
| 
 | ||||
|   fun startListening(onLocationChanged: (Location) -> Unit) { | ||||
|     this.onLocationChanged = onLocationChanged | ||||
|     locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager | ||||
|     fun startListening(onLocationChanged: (Location) -> Unit) { | ||||
|         this.onLocationChanged = onLocationChanged | ||||
|         locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager | ||||
| 
 | ||||
|     if (ContextCompat.checkSelfPermission( | ||||
|         context, | ||||
|         Manifest.permission.ACCESS_FINE_LOCATION, | ||||
|       ) == PackageManager.PERMISSION_GRANTED | ||||
|     ) { | ||||
|       locationManager?.requestLocationUpdates( | ||||
|         LocationManager.GPS_PROVIDER, | ||||
|         1000L, | ||||
|         1f, | ||||
|         this, | ||||
|       ) | ||||
|         if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) | ||||
|             == | ||||
|             PackageManager.PERMISSION_GRANTED | ||||
|         ) { | ||||
|             locationManager?.requestLocationUpdates( | ||||
|                 LocationManager.GPS_PROVIDER, | ||||
|                 1000L, | ||||
|                 1f, | ||||
|                 this | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   fun stopListening() { | ||||
|     locationManager?.removeUpdates(this) | ||||
|   } | ||||
|     fun stopListening() { | ||||
|         locationManager?.removeUpdates(this) | ||||
|     } | ||||
| 
 | ||||
|   override fun onLocationChanged(location: Location) { | ||||
|     onLocationChanged?.invoke(location) | ||||
|   } | ||||
|     override fun onLocationChanged(location: Location) { | ||||
|         onLocationChanged?.invoke(location) | ||||
|     } | ||||
| 
 | ||||
|   override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {} | ||||
|   override fun onProviderEnabled(provider: String) {} | ||||
|   override fun onProviderDisabled(provider: String) {} | ||||
|     override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {} | ||||
|     override fun onProviderEnabled(provider: String) {} | ||||
|     override fun onProviderDisabled(provider: String) {} | ||||
| } | ||||
|  | @ -1,17 +1,12 @@ | |||
| package com.module.breeze | ||||
| 
 | ||||
| import android.Manifest | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import android.content.pm.PackageManager | ||||
| import android.location.Location | ||||
| import android.os.Build | ||||
| import android.os.Bundle | ||||
| import androidx.activity.ComponentActivity | ||||
| import androidx.activity.compose.rememberLauncherForActivityResult | ||||
| import androidx.activity.compose.setContent | ||||
| import androidx.activity.enableEdgeToEdge | ||||
| import androidx.activity.result.contract.ActivityResultContracts | ||||
| import androidx.annotation.RequiresApi | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Column | ||||
|  | @ -36,7 +31,6 @@ import androidx.compose.material3.Icon | |||
| import androidx.compose.material3.Scaffold | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.DisposableEffect | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
|  | @ -51,231 +45,247 @@ import androidx.compose.ui.text.font.FontWeight | |||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.unit.sp | ||||
| import androidx.core.content.ContextCompat | ||||
| import com.module.breeze.ui.theme.BreezeTheme | ||||
| import kotlinx.coroutines.runBlocking | ||||
| import java.text.DecimalFormat | ||||
| import java.time.LocalDate | ||||
| import kotlin.math.roundToInt | ||||
| import android.Manifest | ||||
| import android.content.pm.PackageManager | ||||
| import android.location.Location | ||||
| import androidx.activity.compose.rememberLauncherForActivityResult | ||||
| import androidx.activity.result.contract.ActivityResultContracts | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.material3.Button | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.DisposableEffect | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.core.content.ContextCompat | ||||
| 
 | ||||
| 
 | ||||
| val fontSizeCurrentTemp = 48.sp | ||||
| val fontSizeUpper = 16.sp | ||||
| val fontSizeTitle = 22.sp | ||||
| val breezeFontWeight = FontWeight.Bold | ||||
| val iconStyle = Icons.Outlined | ||||
| val iconStyle = Icons.Outlined; | ||||
| val textColor = Color(0, 0, 0) | ||||
| val numberFormat = DecimalFormat("00") | ||||
| val apiKey = "8a6090c4308455152cd8c677b802883b" | ||||
| 
 | ||||
| class MainActivity : ComponentActivity() { | ||||
|   override fun onCreate(savedInstanceState: Bundle?) { | ||||
|     super.onCreate(savedInstanceState) | ||||
|     enableEdgeToEdge() | ||||
|     setContent { | ||||
|       BreezeTheme { | ||||
|         Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> | ||||
|           WeatherInfo( | ||||
|             modifier = Modifier.padding(innerPadding), | ||||
|           ) | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         enableEdgeToEdge() | ||||
|         setContent { | ||||
|             BreezeTheme { | ||||
|                 Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> | ||||
|                     WeatherInfo( | ||||
|                         modifier = Modifier.padding(innerPadding) | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   companion object { | ||||
|     fun openSettings(ctx: Context) { | ||||
|       var intent = Intent(ctx, SettingsActivity::class.java) | ||||
|       ctx.startActivity(intent) | ||||
|     companion object { | ||||
|         fun openSettings(ctx: Context) { | ||||
|             var intent = Intent(ctx, SettingsActivity::class.java) | ||||
|             ctx.startActivity(intent) | ||||
|         } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.O) | ||||
| fun getTodaysLocation(ctx: Context): String { | ||||
|   var dayOfTheWeek = LocalDate.now().dayOfWeek.value - 1 | ||||
|   var plz = "Loading ..." | ||||
|   runBlocking { | ||||
|     plz = getLocations(ctx)[dayOfTheWeek] | ||||
|   } | ||||
|   return plz | ||||
|     var dayOfTheWeek = LocalDate.now().dayOfWeek.value - 1 | ||||
|     var plz = "Loading ..." | ||||
|     runBlocking { | ||||
|         plz = getLocations(ctx)[dayOfTheWeek] | ||||
|     } | ||||
|     return plz | ||||
| } | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.O) | ||||
| fun fetchWeather(ctx: Context): ForecastSummary { | ||||
|   var todayPlz = getTodaysLocation(ctx) | ||||
|     var todayPlz = getTodaysLocation(ctx) | ||||
| 
 | ||||
|   var data: ForecastSummary | ||||
|   runBlocking { | ||||
|     val forecastResponse = RetrofitClient.instance.getWeatherForecast("$todayPlz,ch", apiKey) | ||||
|     val todaysForecast = getTodaysForecast(forecastResponse) | ||||
|     var data: ForecastSummary | ||||
|     runBlocking { | ||||
|         val forecastResponse = RetrofitClient.instance.getWeatherForecast("${todayPlz},ch", apiKey) | ||||
|         val todaysForecast = getTodaysForecast(forecastResponse) | ||||
| 
 | ||||
|     data = todaysForecast | ||||
|   } | ||||
|   return data | ||||
|         data = todaysForecast | ||||
|         println("Today's Forecast for ${todayPlz}:") | ||||
|         println("Min Temperature: ${todaysForecast.minTemp}°C") | ||||
|         println("Max Temperature: ${todaysForecast.maxTemp}°C") | ||||
|         println("Rain: ${if (todaysForecast.hasRain) "Yes" else "No"}") | ||||
|         println("Snow: ${if (todaysForecast.hasSnow) "Yes" else "No"}") | ||||
|         println("Strong Winds: ${if (todaysForecast.hasStrongWinds) "Yes" else "No"}") | ||||
|     } | ||||
|     return data | ||||
| } | ||||
| 
 | ||||
| fun fetchCurrentTemp(ctx: Context, lat: Double, lon: Double): WeatherResponse { | ||||
|   var data: WeatherResponse | ||||
|   runBlocking { | ||||
|     data = RetrofitClient.instance.getCurrentWeather(lat, lon, apiKey) | ||||
|   } | ||||
|   return data | ||||
|     var data: WeatherResponse | ||||
|     runBlocking { | ||||
|         data = RetrofitClient.instance.getCurrentWeather(lat, lon, apiKey) | ||||
|     } | ||||
|     return data | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| @Composable | ||||
| fun WeatherInfo(modifier: Modifier = Modifier) { | ||||
|   val ctx = LocalContext.current | ||||
|   var forecast by remember { mutableStateOf(ForecastSummary(0.0, 0.0, false, false, false)) } | ||||
|   var currentTemp by remember { | ||||
|     mutableStateOf( | ||||
|       WeatherResponse(Main(0.0, 0.0, 0.0, 0.0, 0, 0), emptyList(), 0L), | ||||
|     ) | ||||
|   } | ||||
|   val locationHelper = remember { GpsRepository(ctx) } | ||||
| 
 | ||||
|   var location by remember { mutableStateOf<Location?>(null) } | ||||
| 
 | ||||
|   val locationPermission = ContextCompat.checkSelfPermission( | ||||
|     ctx, | ||||
|     Manifest.permission.ACCESS_FINE_LOCATION, | ||||
|   ) | ||||
|   var hasLocationPermission by remember { | ||||
|     mutableStateOf(locationPermission == PackageManager.PERMISSION_GRANTED) | ||||
|   } | ||||
| 
 | ||||
|   val permissionLauncher = rememberLauncherForActivityResult( | ||||
|     ActivityResultContracts.RequestPermission(), | ||||
|   ) { isGranted -> hasLocationPermission = isGranted } | ||||
| 
 | ||||
|   DisposableEffect(key1 = locationHelper, key2 = hasLocationPermission) { | ||||
|     if (hasLocationPermission) { | ||||
|       locationHelper.startListening { newLocation -> | ||||
|         location = newLocation | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     onDispose { | ||||
|       locationHelper.stopListening() | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|     forecast = fetchWeather(ctx) | ||||
|   } | ||||
| 
 | ||||
|   Navigation( | ||||
|     iconStyle.Settings, | ||||
|     "Settings Icon", | ||||
|     onClick = { MainActivity.openSettings(ctx) }, | ||||
|   ) | ||||
| 
 | ||||
|   Column( | ||||
|     modifier = Modifier.fillMaxSize(), | ||||
|     verticalArrangement = Arrangement.Center, | ||||
|     horizontalAlignment = Alignment.CenterHorizontally, | ||||
|   ) { | ||||
|     Row(verticalAlignment = Alignment.Bottom) { | ||||
|       Icon( | ||||
|         imageVector = iconStyle.Map, | ||||
|         contentDescription = "Location Icon", | ||||
|         modifier = Modifier | ||||
|           .padding(end = 16.dp) | ||||
|           .size(32.dp), | ||||
|       ) | ||||
|       Icon( | ||||
|         imageVector = iconStyle.WaterDrop, | ||||
|         contentDescription = "Rain Icon", | ||||
|         modifier = Modifier.alpha(if (forecast.hasRain) 1F else 0.2F), | ||||
|       ) | ||||
|       Icon( | ||||
|         imageVector = iconStyle.AcUnit, | ||||
|         contentDescription = "Snow Icon", | ||||
|         modifier = Modifier.alpha(if (forecast.hasSnow) 1F else 0.2F), | ||||
|       ) | ||||
| 
 | ||||
|       Icon( | ||||
|         imageVector = iconStyle.Air, | ||||
|         contentDescription = "Wind Icon", | ||||
|         modifier = Modifier.alpha(if (forecast.hasStrongWinds) 1F else 0.2F), | ||||
|       ) | ||||
|     } | ||||
|     Row { | ||||
|       Icon( | ||||
|         imageVector = iconStyle.ArrowDownward, | ||||
|         contentDescription = "Arrow down Icon", | ||||
|       ) | ||||
|       Text( | ||||
|         text = numberFormat.format(forecast.minTemp.roundToInt()) + "°", | ||||
|         fontSize = fontSizeUpper, | ||||
|         fontWeight = breezeFontWeight, | ||||
|       ) | ||||
|       Icon( | ||||
|         imageVector = iconStyle.ArrowUpward, | ||||
|         contentDescription = "Arrow up Icon", | ||||
|       ) | ||||
|       Text( | ||||
|         text = numberFormat.format(forecast.maxTemp.roundToInt()) + "°", | ||||
|         fontSize = fontSizeUpper, | ||||
|         fontWeight = breezeFontWeight, | ||||
|       ) | ||||
|     } | ||||
|     if (hasLocationPermission) { | ||||
|       location?.let { | ||||
|         currentTemp = fetchCurrentTemp(ctx, it.latitude, it.latitude) | ||||
|       } | ||||
|       Text( | ||||
|         text = numberFormat.format(currentTemp.main.temp.roundToInt()) + "°", | ||||
|         fontSize = fontSizeCurrentTemp, | ||||
|         fontWeight = breezeFontWeight, | ||||
|       ) | ||||
|     } else { | ||||
|       Button( | ||||
|         onClick = { | ||||
|           permissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) | ||||
|         }, | ||||
|         colors = ButtonDefaults.buttonColors( | ||||
|           containerColor = Color(0, 0, 0, 20), | ||||
|           contentColor = textColor, | ||||
|         ), | ||||
|       ) { | ||||
|         Text( | ||||
|           text = "Grant Permission", | ||||
|           fontWeight = breezeFontWeight, | ||||
|     val ctx = LocalContext.current | ||||
|     var forecast by remember { mutableStateOf(ForecastSummary(0.0, 0.0, false, false, false)) } | ||||
|     var currentTemp by remember { | ||||
|         mutableStateOf( | ||||
|             WeatherResponse(Main(0.0, 0.0, 0.0, 0.0, 0, 0), emptyList(), 0L) | ||||
|         ) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|     val locationHelper = remember { GpsRepository(ctx) } | ||||
| 
 | ||||
|     var location by remember { mutableStateOf<Location?>(null) } | ||||
| 
 | ||||
|     val locationPermission = ContextCompat.checkSelfPermission( | ||||
|         ctx, Manifest.permission.ACCESS_FINE_LOCATION | ||||
|     ) | ||||
|     var hasLocationPermission by remember { | ||||
|         mutableStateOf(locationPermission == PackageManager.PERMISSION_GRANTED) | ||||
|     } | ||||
| 
 | ||||
|     val permissionLauncher = rememberLauncherForActivityResult( | ||||
|         ActivityResultContracts.RequestPermission() | ||||
|     ) { isGranted -> hasLocationPermission = isGranted } | ||||
| 
 | ||||
| 
 | ||||
|     DisposableEffect(key1 = locationHelper, key2 = hasLocationPermission) { | ||||
|         if (hasLocationPermission) { | ||||
|             locationHelper.startListening { newLocation -> | ||||
|                 location = newLocation | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         onDispose { | ||||
|             locationHelper.stopListening() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|         forecast = fetchWeather(ctx) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     Navigation( | ||||
|         iconStyle.Settings, "Settings Icon", onClick = { MainActivity.openSettings(ctx) }) | ||||
| 
 | ||||
|     Column( | ||||
|         modifier = Modifier.fillMaxSize(), | ||||
|         verticalArrangement = Arrangement.Center, | ||||
|         horizontalAlignment = Alignment.CenterHorizontally, | ||||
|     ) { | ||||
|         Row(verticalAlignment = Alignment.Bottom) { | ||||
|             Icon( | ||||
|                 imageVector = iconStyle.Map, | ||||
|                 contentDescription = "Location Icon", | ||||
|                 modifier = Modifier | ||||
|                     .padding(end = 16.dp) | ||||
|                     .size(32.dp) | ||||
|             ) | ||||
|             Icon( | ||||
|                 imageVector = iconStyle.WaterDrop, | ||||
|                 contentDescription = "Rain Icon", | ||||
|                 modifier = Modifier.alpha(if (forecast.hasRain) 1F else 0.2F) | ||||
|             ) | ||||
|             Icon( | ||||
|                 imageVector = iconStyle.AcUnit, | ||||
|                 contentDescription = "Snow Icon", | ||||
|                 modifier = Modifier.alpha(if (forecast.hasSnow) 1F else 0.2F) | ||||
|             ) | ||||
| 
 | ||||
|             Icon( | ||||
|                 imageVector = iconStyle.Air, | ||||
|                 contentDescription = "Wind Icon", | ||||
|                 modifier = Modifier.alpha(if (forecast.hasStrongWinds) 1F else 0.2F) | ||||
|             ) | ||||
|         } | ||||
|         Row { | ||||
|             Icon( | ||||
|                 imageVector = iconStyle.ArrowDownward, contentDescription = "Arrow down Icon" | ||||
|             ) | ||||
|             Text( | ||||
|                 text = numberFormat.format(forecast.minTemp.roundToInt()) + "°", | ||||
|                 fontSize = fontSizeUpper, | ||||
|                 fontWeight = breezeFontWeight | ||||
|             ) | ||||
|             Icon( | ||||
|                 imageVector = iconStyle.ArrowUpward, contentDescription = "Arrow up Icon" | ||||
|             ) | ||||
|             Text( | ||||
|                 text = numberFormat.format(forecast.maxTemp.roundToInt()) + "°", | ||||
|                 fontSize = fontSizeUpper, | ||||
|                 fontWeight = breezeFontWeight | ||||
|             ) | ||||
|         } | ||||
|         if (hasLocationPermission) { | ||||
|             location?.let { | ||||
|                 currentTemp = fetchCurrentTemp(ctx, it.latitude, it.latitude) | ||||
|             } | ||||
|             Text( | ||||
|                 text = numberFormat.format(currentTemp.main.temp.roundToInt()) + "°", | ||||
|                 fontSize = fontSizeCurrentTemp, | ||||
|                 fontWeight = breezeFontWeight, | ||||
|             ) | ||||
|         } else { | ||||
|             Button( | ||||
|                 onClick = { | ||||
|                     permissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) | ||||
|                 }, | ||||
|                 colors = ButtonDefaults.buttonColors( | ||||
|                     containerColor = Color(0, 0, 0, 20), contentColor = textColor | ||||
|                 ), | ||||
|             ) { | ||||
|                 Text( | ||||
|                     text = "Grant Permission", | ||||
|                     fontWeight = breezeFontWeight, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Preview(showBackground = true) | ||||
| @Composable | ||||
| fun GreetingPreview() { | ||||
|   BreezeTheme { | ||||
|     WeatherInfo() | ||||
|   } | ||||
|     BreezeTheme { | ||||
|         WeatherInfo() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| @Composable | ||||
| fun Navigation(icon: ImageVector, iconDescription: String, onClick: () -> Unit = {}) { | ||||
|   Row( | ||||
|     horizontalArrangement = Arrangement.End, | ||||
|     modifier = Modifier.fillMaxWidth(), | ||||
|   ) { | ||||
|     Button( | ||||
|       onClick = onClick, | ||||
|       colors = ButtonDefaults.buttonColors( | ||||
|         containerColor = Color(255, 0, 0, 0), | ||||
|         contentColor = textColor, | ||||
|       ), | ||||
|       shape = CircleShape, | ||||
|       contentPadding = PaddingValues(0.dp), | ||||
|       modifier = Modifier.padding(0.dp, 40.dp, 28.dp, 0.dp), | ||||
|     Row( | ||||
|         horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth() | ||||
|     ) { | ||||
|       Icon( | ||||
|         imageVector = icon, | ||||
|         contentDescription = iconDescription, | ||||
|         modifier = Modifier.size(40.dp), | ||||
|       ) | ||||
|         Button( | ||||
|             onClick = onClick, | ||||
|             colors = ButtonDefaults.buttonColors( | ||||
|                 containerColor = Color(255, 0, 0, 0), contentColor = textColor | ||||
|             ), | ||||
|             shape = CircleShape, | ||||
|             contentPadding = PaddingValues(0.dp), | ||||
|             modifier = Modifier.padding(0.dp, 40.dp, 28.dp, 0.dp) | ||||
|         ) { | ||||
|             Icon( | ||||
|                 imageVector = icon, | ||||
|                 contentDescription = iconDescription, | ||||
|                 modifier = Modifier.size(40.dp) | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| package com.module.breeze | ||||
| 
 | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import android.os.Bundle | ||||
|  | @ -44,161 +45,165 @@ import kotlinx.serialization.encodeToString | |||
| import kotlinx.serialization.json.Json | ||||
| 
 | ||||
| var plzArrayStatus: List<String> = listOf( | ||||
|   "Loading...", | ||||
|   "Loading...", | ||||
|   "Loading...", | ||||
|   "Loading...", | ||||
|   "Loading...", | ||||
|   "Loading...", | ||||
|   "Loading...", | ||||
|     "Loading...", | ||||
|     "Loading...", | ||||
|     "Loading...", | ||||
|     "Loading...", | ||||
|     "Loading...", | ||||
|     "Loading...", | ||||
|     "Loading...", | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| // ### Start ChatGPT + Fixing | ||||
| val SHARED_PLZ_KEY = stringPreferencesKey("shared-plz") | ||||
| val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings") | ||||
| 
 | ||||
| suspend fun setLocations(context: Context, array: List<String>) { | ||||
|   context.dataStore.edit { preferences -> | ||||
|     preferences[SHARED_PLZ_KEY] = Json.encodeToString(array) | ||||
|   } | ||||
|     context.dataStore.edit { preferences -> | ||||
|         preferences[SHARED_PLZ_KEY] = Json.encodeToString(array) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| suspend fun getLocations(context: Context): List<String> { | ||||
|   val preferences = context.dataStore.data.first() | ||||
|   return preferences[SHARED_PLZ_KEY]?.let { Json.decodeFromString(it) } ?: emptyList() | ||||
|     val preferences = context.dataStore.data.first() | ||||
|     return preferences[SHARED_PLZ_KEY]?.let { Json.decodeFromString(it) } ?: emptyList() | ||||
| } | ||||
| // ### End ChatGPT | ||||
| 
 | ||||
| suspend fun getPLZ(index: Int, ctx: Context): String { | ||||
|   var arr = getLocations(ctx) | ||||
|   if (arr.isEmpty() || arr.size != 7) { | ||||
|     arr = listOf("8005", "8005", "8005", "8400", "8400", "8500", "8500") | ||||
|   } | ||||
|   plzArrayStatus = arr | ||||
|   return arr[index] | ||||
|     var arr = getLocations(ctx) | ||||
|     if (arr.isEmpty() || arr.size != 7) { | ||||
|         arr = listOf("8005", "8005", "8005", "8400", "8400", "8500", "8500") | ||||
|     } | ||||
|     plzArrayStatus = arr | ||||
|     return arr[index] | ||||
| } | ||||
| 
 | ||||
| suspend fun setPLZ(index: Int, value: String, ctx: Context) { | ||||
|   var arr = plzArrayStatus.toMutableList() | ||||
|   arr[index] = value | ||||
|   setLocations(ctx, arr) | ||||
|     var arr = plzArrayStatus.toMutableList() | ||||
|     arr[index] = value | ||||
|     setLocations(ctx, arr) | ||||
| } | ||||
| 
 | ||||
| class SettingsActivity : ComponentActivity() { | ||||
|   override fun onCreate(savedInstanceState: Bundle?) { | ||||
|     super.onCreate(savedInstanceState) | ||||
|     enableEdgeToEdge() | ||||
|     setContent { | ||||
|       BreezeTheme { | ||||
|         Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> | ||||
|           Settings( | ||||
|             modifier = Modifier.padding(innerPadding), | ||||
|           ) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   companion object { | ||||
|     fun openHome(ctx: Context) { | ||||
|       var intent = Intent(ctx, MainActivity::class.java) | ||||
|       ctx.startActivity(intent) | ||||
| class SettingsActivity : ComponentActivity() { | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         enableEdgeToEdge() | ||||
|         setContent { | ||||
|             BreezeTheme { | ||||
|                 Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> | ||||
|                     Settings( | ||||
|                         modifier = Modifier.padding(innerPadding) | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         fun openHome(ctx: Context) { | ||||
|             var intent = Intent(ctx, MainActivity::class.java) | ||||
|             ctx.startActivity(intent) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun Settings(modifier: Modifier = Modifier) { | ||||
|   val ctx = LocalContext.current | ||||
|     val ctx = LocalContext.current | ||||
| 
 | ||||
|   Navigation( | ||||
|     iconStyle.Home, | ||||
|     "Home Icon", | ||||
|     onClick = { SettingsActivity.openHome(ctx) }, | ||||
|   ) | ||||
|   Column( | ||||
|     modifier = Modifier.fillMaxSize(), | ||||
|     verticalArrangement = Arrangement.Center, | ||||
|     horizontalAlignment = Alignment.CenterHorizontally, | ||||
|   ) { | ||||
|     Row( | ||||
|       verticalAlignment = Alignment.CenterVertically, | ||||
|       modifier = Modifier.padding(16.dp), | ||||
|     ) { | ||||
|       Icon( | ||||
|         imageVector = iconStyle.Map, | ||||
|         contentDescription = "Configured Locations Icon", | ||||
|         modifier = Modifier.size(40.dp), | ||||
|       ) | ||||
|       Text( | ||||
|         text = "Configured Locations", | ||||
|         fontWeight = breezeFontWeight, | ||||
|         fontSize = fontSizeTitle, | ||||
|       ) | ||||
|     Navigation(iconStyle.Home, "Home Icon", onClick = { | ||||
|         SettingsActivity.openHome(ctx) | ||||
|     }) | ||||
|     Column( | ||||
|         modifier = Modifier.fillMaxSize(), | ||||
|         verticalArrangement = Arrangement.Center, | ||||
|         horizontalAlignment = Alignment.CenterHorizontally | ||||
|     ) | ||||
|     { | ||||
|         Row( | ||||
|             verticalAlignment = Alignment.CenterVertically, | ||||
|             modifier = Modifier.padding(16.dp) | ||||
|         ) | ||||
|         { | ||||
|             Icon( | ||||
|                 imageVector = iconStyle.Map, | ||||
|                 contentDescription = "Configured Locations Icon", | ||||
|                 modifier = Modifier.size(40.dp) | ||||
|             ) | ||||
|             Text( | ||||
|                 text = "Configured Locations", | ||||
|                 fontWeight = breezeFontWeight, | ||||
|                 fontSize = fontSizeTitle | ||||
|             ) | ||||
|         } | ||||
|         ConfiguredLocationDay("Monday", 0, ctx) | ||||
|         ConfiguredLocationDay("Tuesday", 1, ctx) | ||||
|         ConfiguredLocationDay("Wednesday", 2, ctx) | ||||
|         ConfiguredLocationDay("Thursday", 3, ctx) | ||||
|         ConfiguredLocationDay("Friday", 4, ctx) | ||||
|         ConfiguredLocationDay("Saturday", 5, ctx) | ||||
|         ConfiguredLocationDay("Sunday", 6, ctx) | ||||
|     } | ||||
|     ConfiguredLocationDay("Monday", 0, ctx) | ||||
|     ConfiguredLocationDay("Tuesday", 1, ctx) | ||||
|     ConfiguredLocationDay("Wednesday", 2, ctx) | ||||
|     ConfiguredLocationDay("Thursday", 3, ctx) | ||||
|     ConfiguredLocationDay("Friday", 4, ctx) | ||||
|     ConfiguredLocationDay("Saturday", 5, ctx) | ||||
|     ConfiguredLocationDay("Sunday", 6, ctx) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @Preview(showBackground = true) | ||||
| @Composable | ||||
| fun GreetingPreview2() { | ||||
|   BreezeTheme { | ||||
|     Settings() | ||||
|   } | ||||
|     BreezeTheme { | ||||
|         Settings() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Composable | ||||
| fun ConfiguredLocationDay(dayName: String = "day", index: Int, ctx: Context) { | ||||
|   var plz by remember { mutableStateOf("") } | ||||
|   var isLoadingGet by remember { mutableStateOf(false) } | ||||
|   var isLoadingSet by remember { mutableStateOf(false) } | ||||
|     var plz by remember { mutableStateOf("") } | ||||
|     var isLoadingGet by remember { mutableStateOf(false) } | ||||
|     var isLoadingSet by remember { mutableStateOf(false) } | ||||
| 
 | ||||
|   LaunchedEffect(index) { | ||||
|     isLoadingGet = true | ||||
|     plz = getPLZ(index, ctx) | ||||
|     isLoadingGet = false | ||||
|   } | ||||
| 
 | ||||
|   LaunchedEffect(plz) { | ||||
|     if (plz.length == 4) { | ||||
|       isLoadingSet = true | ||||
|       setPLZ(index, plz, ctx) | ||||
|       isLoadingSet = false | ||||
|     LaunchedEffect(index) { | ||||
|         isLoadingGet = true | ||||
|         plz = getPLZ(index, ctx) | ||||
|         isLoadingGet = false | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   if (isLoadingSet || isLoadingGet) { | ||||
|     CircularProgressIndicator() | ||||
|   } else { | ||||
|     Row( | ||||
|       verticalAlignment = Alignment.CenterVertically, | ||||
|       modifier = Modifier.padding(0.dp, 4.dp), | ||||
|     ) { | ||||
|       Text( | ||||
|         text = dayName, | ||||
|         fontWeight = breezeFontWeight, | ||||
|         modifier = Modifier.width(100.dp), | ||||
|       ) | ||||
|       TextField( | ||||
|         value = plz, | ||||
|         onValueChange = { newPlz -> | ||||
|           var cutNewPlz = newPlz | ||||
|           if (newPlz.length > 4) { | ||||
|             cutNewPlz = newPlz.substring(0, 4) | ||||
|           } | ||||
|           plz = cutNewPlz | ||||
|         }, | ||||
|         label = { Text("PLZ") }, | ||||
|         keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), | ||||
|         modifier = Modifier.width(110.dp), | ||||
|       ) | ||||
|     LaunchedEffect(plz) { | ||||
|         if (plz.length == 4) { | ||||
|             isLoadingSet = true | ||||
|             setPLZ(index, plz, ctx) | ||||
|             isLoadingSet = false | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if (isLoadingSet || isLoadingGet) { | ||||
|         CircularProgressIndicator() | ||||
|     } else { | ||||
|         Row( | ||||
|             verticalAlignment = Alignment.CenterVertically, | ||||
|             modifier = Modifier.padding(0.dp, 4.dp) | ||||
|         ) { | ||||
|             Text( | ||||
|                 text = dayName, | ||||
|                 fontWeight = breezeFontWeight, | ||||
|                 modifier = Modifier.width(100.dp) | ||||
|             ) | ||||
|             TextField( | ||||
|                 value = plz, | ||||
|                 onValueChange = { newPlz -> | ||||
|                     var cutNewPlz = newPlz | ||||
|                     if (newPlz.length > 4) { | ||||
|                         cutNewPlz = newPlz.substring(0, 4) | ||||
|                     } | ||||
|                     plz = cutNewPlz | ||||
|                 }, | ||||
|                 label = { Text("PLZ") }, | ||||
|                 keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), | ||||
|                 modifier = Modifier.width(110.dp) | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -1,129 +1,134 @@ | |||
| package com.module.breeze | ||||
| 
 | ||||
| import okhttp3.OkHttpClient | ||||
| import okhttp3.logging.HttpLoggingInterceptor | ||||
| import retrofit2.Retrofit | ||||
| import retrofit2.converter.gson.GsonConverterFactory | ||||
| import retrofit2.http.GET | ||||
| import retrofit2.http.Query | ||||
| import okhttp3.OkHttpClient | ||||
| import okhttp3.logging.HttpLoggingInterceptor | ||||
| import java.util.Date | ||||
| 
 | ||||
| // Whole File DeepSeek | ||||
| // Data classes for API responses | ||||
| data class WeatherResponse( | ||||
|   val main: Main, | ||||
|   val weather: List<Weather>, | ||||
|   val dt: Long, | ||||
|     val main: Main, | ||||
|     val weather: List<Weather>, | ||||
|     val dt: Long | ||||
| ) | ||||
| 
 | ||||
| data class Main( | ||||
|   val temp: Double, | ||||
|   val feels_like: Double, | ||||
|   val temp_min: Double, | ||||
|   val temp_max: Double, | ||||
|   val pressure: Int, | ||||
|   val humidity: Int, | ||||
|     val temp: Double, | ||||
|     val feels_like: Double, | ||||
|     val temp_min: Double, | ||||
|     val temp_max: Double, | ||||
|     val pressure: Int, | ||||
|     val humidity: Int | ||||
| ) | ||||
| 
 | ||||
| data class Weather( | ||||
|   val id: Int, | ||||
|   val main: String, | ||||
|   val description: String, | ||||
|   val icon: String, | ||||
|     val id: Int, | ||||
|     val main: String, | ||||
|     val description: String, | ||||
|     val icon: String | ||||
| ) | ||||
| 
 | ||||
| data class ForecastResponse( | ||||
|   val list: List<Forecast>, | ||||
|     val list: List<Forecast> | ||||
| ) | ||||
| 
 | ||||
| data class Forecast( | ||||
|   val dt: Long, | ||||
|   val main: Main, | ||||
|   val weather: List<Weather>, | ||||
|   val wind: Wind, | ||||
|   val rain: Rain?, | ||||
|   val snow: Snow?, | ||||
|     val dt: Long, | ||||
|     val main: Main, | ||||
|     val weather: List<Weather>, | ||||
|     val wind: Wind, | ||||
|     val rain: Rain?, | ||||
|     val snow: Snow? | ||||
| ) | ||||
| 
 | ||||
| data class Wind( | ||||
|   val speed: Double, | ||||
|   val deg: Int, | ||||
|     val speed: Double, | ||||
|     val deg: Int | ||||
| ) | ||||
| 
 | ||||
| data class Rain( | ||||
|   val `3h`: Double?, | ||||
|     val `3h`: Double? | ||||
| ) | ||||
| 
 | ||||
| data class Snow( | ||||
|   val `3h`: Double?, | ||||
|     val `3h`: Double? | ||||
| ) | ||||
| 
 | ||||
| // Retrofit API interface | ||||
| interface WeatherApiService { | ||||
|   @GET("weather") | ||||
|   suspend fun getCurrentWeather( | ||||
|     @Query("lat") lat: Double, | ||||
|     @Query("lon") lon: Double, | ||||
|     @Query("appid") apiKey: String, | ||||
|     @Query("units") units: String = "metric", | ||||
|   ): WeatherResponse | ||||
|     @GET("weather") | ||||
|     suspend fun getCurrentWeather( | ||||
|         @Query("lat") lat: Double, | ||||
|         @Query("lon") lon: Double, | ||||
|         @Query("appid") apiKey: String, | ||||
|         @Query("units") units: String = "metric" | ||||
|     ): WeatherResponse | ||||
| 
 | ||||
|   @GET("forecast") | ||||
|   suspend fun getWeatherForecast( | ||||
|     @Query("zip") zip: String, | ||||
|     @Query("appid") apiKey: String, | ||||
|     @Query("units") units: String = "metric", | ||||
|   ): ForecastResponse | ||||
|     @GET("forecast") | ||||
|     suspend fun getWeatherForecast( | ||||
|         @Query("zip") zip: String, | ||||
|         @Query("appid") apiKey: String, | ||||
|         @Query("units") units: String = "metric" | ||||
|     ): ForecastResponse | ||||
| } | ||||
| 
 | ||||
| // Retrofit client setup | ||||
| object RetrofitClient { | ||||
|   private const val BASE_URL = "https://api.openweathermap.org/data/2.5/" | ||||
|     private const val BASE_URL = "https://api.openweathermap.org/data/2.5/" | ||||
| 
 | ||||
|   private val logging = HttpLoggingInterceptor().apply { | ||||
|     level = HttpLoggingInterceptor.Level.BODY | ||||
|   } | ||||
|     private val logging = HttpLoggingInterceptor().apply { | ||||
|         level = HttpLoggingInterceptor.Level.BODY | ||||
|     } | ||||
| 
 | ||||
|   private val httpClient = OkHttpClient.Builder().addInterceptor(logging).build() | ||||
|     private val httpClient = OkHttpClient.Builder() | ||||
|         .addInterceptor(logging) | ||||
|         .build() | ||||
| 
 | ||||
|   val instance: WeatherApiService by lazy { | ||||
|     Retrofit.Builder().baseUrl(BASE_URL).client(httpClient) | ||||
|       .addConverterFactory(GsonConverterFactory.create()).build() | ||||
|       .create(WeatherApiService::class.java) | ||||
|   } | ||||
|     val instance: WeatherApiService by lazy { | ||||
|         Retrofit.Builder() | ||||
|             .baseUrl(BASE_URL) | ||||
|             .client(httpClient) | ||||
|             .addConverterFactory(GsonConverterFactory.create()) | ||||
|             .build() | ||||
|             .create(WeatherApiService::class.java) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // Helper function to filter today's forecast | ||||
| fun getTodaysForecast(forecastResponse: ForecastResponse): ForecastSummary { | ||||
|   val today = Date().time / 1000 // Current timestamp in seconds | ||||
|   val tomorrow = today + 86400 // 24 hours later | ||||
|     val today = Date().time / 1000 // Current timestamp in seconds | ||||
|     val tomorrow = today + 86400 // 24 hours later | ||||
| 
 | ||||
|   // Filter forecasts for today | ||||
|   val todaysForecasts = forecastResponse.list.filter { it.dt in today..tomorrow } | ||||
|     // Filter forecasts for today | ||||
|     val todaysForecasts = forecastResponse.list.filter { it.dt in today..tomorrow } | ||||
| 
 | ||||
|   // Extract min and max temperatures | ||||
|   val minTemp = todaysForecasts.minOfOrNull { it.main.temp_min } ?: 0.0 | ||||
|   val maxTemp = todaysForecasts.maxOfOrNull { it.main.temp_max } ?: 0.0 | ||||
|     // Extract min and max temperatures | ||||
|     val minTemp = todaysForecasts.minOfOrNull { it.main.temp_min } ?: 0.0 | ||||
|     val maxTemp = todaysForecasts.maxOfOrNull { it.main.temp_max } ?: 0.0 | ||||
| 
 | ||||
|   // Check for rain, snow, or strong winds | ||||
|   val hasRain = todaysForecasts.any { it.rain != null } | ||||
|   val hasSnow = todaysForecasts.any { it.snow != null } | ||||
|   val hasStrongWinds = todaysForecasts.any { it.wind.speed > 10.0 } // Wind speed > 10 m/s | ||||
|     // Check for rain, snow, or strong winds | ||||
|     val hasRain = todaysForecasts.any { it.rain != null } | ||||
|     val hasSnow = todaysForecasts.any { it.snow != null } | ||||
|     val hasStrongWinds = todaysForecasts.any { it.wind.speed > 10.0 } // Wind speed > 10 m/s | ||||
| 
 | ||||
|   return ForecastSummary( | ||||
|     minTemp = minTemp, | ||||
|     maxTemp = maxTemp, | ||||
|     hasRain = hasRain, | ||||
|     hasSnow = hasSnow, | ||||
|     hasStrongWinds = hasStrongWinds, | ||||
|   ) | ||||
|     return ForecastSummary( | ||||
|         minTemp = minTemp, | ||||
|         maxTemp = maxTemp, | ||||
|         hasRain = hasRain, | ||||
|         hasSnow = hasSnow, | ||||
|         hasStrongWinds = hasStrongWinds | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| // Data class for today's forecast summary | ||||
| data class ForecastSummary( | ||||
|   val minTemp: Double, | ||||
|   val maxTemp: Double, | ||||
|   val hasRain: Boolean, | ||||
|   val hasSnow: Boolean, | ||||
|   val hasStrongWinds: Boolean, | ||||
|     val minTemp: Double, | ||||
|     val maxTemp: Double, | ||||
|     val hasRain: Boolean, | ||||
|     val hasSnow: Boolean, | ||||
|     val hasStrongWinds: Boolean | ||||
| ) | ||||
|  | @ -1,5 +1,6 @@ | |||
| package com.module.breeze.ui.theme | ||||
| 
 | ||||
| import android.app.Activity | ||||
| import android.os.Build | ||||
| import androidx.compose.foundation.isSystemInDarkTheme | ||||
| import androidx.compose.material3.MaterialTheme | ||||
|  | @ -11,47 +12,47 @@ import androidx.compose.runtime.Composable | |||
| import androidx.compose.ui.platform.LocalContext | ||||
| 
 | ||||
| private val DarkColorScheme = darkColorScheme( | ||||
|   primary = Purple80, | ||||
|   secondary = PurpleGrey80, | ||||
|   tertiary = Pink80, | ||||
|     primary = Purple80, | ||||
|     secondary = PurpleGrey80, | ||||
|     tertiary = Pink80 | ||||
| ) | ||||
| 
 | ||||
| private val LightColorScheme = lightColorScheme( | ||||
|   primary = Purple40, | ||||
|   secondary = PurpleGrey40, | ||||
|   tertiary = Pink40, | ||||
|     primary = Purple40, | ||||
|     secondary = PurpleGrey40, | ||||
|     tertiary = Pink40 | ||||
| 
 | ||||
|   /* Other default colors to override | ||||
|   background = Color(0xFFFFFBFE), | ||||
|   surface = Color(0xFFFFFBFE), | ||||
|   onPrimary = Color.White, | ||||
|   onSecondary = Color.White, | ||||
|   onTertiary = Color.White, | ||||
|   onBackground = Color(0xFF1C1B1F), | ||||
|   onSurface = Color(0xFF1C1B1F), | ||||
|   */ | ||||
|     /* Other default colors to override | ||||
|     background = Color(0xFFFFFBFE), | ||||
|     surface = Color(0xFFFFFBFE), | ||||
|     onPrimary = Color.White, | ||||
|     onSecondary = Color.White, | ||||
|     onTertiary = Color.White, | ||||
|     onBackground = Color(0xFF1C1B1F), | ||||
|     onSurface = Color(0xFF1C1B1F), | ||||
|     */ | ||||
| ) | ||||
| 
 | ||||
| @Composable | ||||
| fun BreezeTheme( | ||||
|   darkTheme: Boolean = isSystemInDarkTheme(), | ||||
|   // Dynamic color is available on Android 12+ | ||||
|   dynamicColor: Boolean = true, | ||||
|   content: @Composable () -> Unit, | ||||
|     darkTheme: Boolean = isSystemInDarkTheme(), | ||||
|     // Dynamic color is available on Android 12+ | ||||
|     dynamicColor: Boolean = true, | ||||
|     content: @Composable () -> Unit | ||||
| ) { | ||||
|   val colorScheme = when { | ||||
|     dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { | ||||
|       val context = LocalContext.current | ||||
|       if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) | ||||
|     val colorScheme = when { | ||||
|         dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { | ||||
|             val context = LocalContext.current | ||||
|             if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) | ||||
|         } | ||||
| 
 | ||||
|         darkTheme -> LightColorScheme | ||||
|         else -> LightColorScheme | ||||
|     } | ||||
| 
 | ||||
|     darkTheme -> LightColorScheme | ||||
|     else -> LightColorScheme | ||||
|   } | ||||
| 
 | ||||
|   MaterialTheme( | ||||
|     colorScheme = colorScheme, | ||||
|     typography = Typography, | ||||
|     content = content, | ||||
|   ) | ||||
|     MaterialTheme( | ||||
|         colorScheme = colorScheme, | ||||
|         typography = Typography, | ||||
|         content = content | ||||
|     ) | ||||
| } | ||||
|  | @ -8,27 +8,27 @@ import androidx.compose.ui.unit.sp | |||
| 
 | ||||
| // Set of Material typography styles to start with | ||||
| val Typography = Typography( | ||||
|   bodyLarge = TextStyle( | ||||
|     fontFamily = FontFamily.Default, | ||||
|     fontWeight = FontWeight.Normal, | ||||
|     fontSize = 16.sp, | ||||
|     lineHeight = 24.sp, | ||||
|     letterSpacing = 0.5.sp, | ||||
|   ), | ||||
|   /* Other default text styles to override | ||||
|   titleLarge = TextStyle( | ||||
|       fontFamily = FontFamily.Default, | ||||
|       fontWeight = FontWeight.Normal, | ||||
|       fontSize = 22.sp, | ||||
|       lineHeight = 28.sp, | ||||
|       letterSpacing = 0.sp | ||||
|   ), | ||||
|   labelSmall = TextStyle( | ||||
|       fontFamily = FontFamily.Default, | ||||
|       fontWeight = FontWeight.Medium, | ||||
|       fontSize = 11.sp, | ||||
|       lineHeight = 16.sp, | ||||
|       letterSpacing = 0.5.sp | ||||
|   ) | ||||
|   */ | ||||
|     bodyLarge = TextStyle( | ||||
|         fontFamily = FontFamily.Default, | ||||
|         fontWeight = FontWeight.Normal, | ||||
|         fontSize = 16.sp, | ||||
|         lineHeight = 24.sp, | ||||
|         letterSpacing = 0.5.sp | ||||
|     ) | ||||
|     /* Other default text styles to override | ||||
|     titleLarge = TextStyle( | ||||
|         fontFamily = FontFamily.Default, | ||||
|         fontWeight = FontWeight.Normal, | ||||
|         fontSize = 22.sp, | ||||
|         lineHeight = 28.sp, | ||||
|         letterSpacing = 0.sp | ||||
|     ), | ||||
|     labelSmall = TextStyle( | ||||
|         fontFamily = FontFamily.Default, | ||||
|         fontWeight = FontWeight.Medium, | ||||
|         fontSize = 11.sp, | ||||
|         lineHeight = 16.sp, | ||||
|         letterSpacing = 0.5.sp | ||||
|     ) | ||||
|     */ | ||||
| ) | ||||
|  | @ -0,0 +1,17 @@ | |||
| package com.module.breeze | ||||
| 
 | ||||
| import org.junit.Test | ||||
| 
 | ||||
| import org.junit.Assert.* | ||||
| 
 | ||||
| /** | ||||
|  * Example local unit test, which will execute on the development machine (host). | ||||
|  * | ||||
|  * See [testing documentation](http://d.android.com/tools/testing). | ||||
|  */ | ||||
| class ExampleUnitTest { | ||||
|     @Test | ||||
|     fun addition_isCorrect() { | ||||
|         assertEquals(4, 2 + 2) | ||||
|     } | ||||
| } | ||||
		Loading…
	
		Reference in New Issue