diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index b268ef3..99486d0 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -4,6 +4,14 @@ <selectionStates> <SelectionState runConfigName="app"> <option name="selectionMode" value="DROPDOWN" /> + <DropdownSelection timestamp="2025-03-23T17:25:15.945565980Z"> + <Target type="DEFAULT_BOOT"> + <handle> + <DeviceId pluginId="LocalEmulator" identifier="path=/home/lorenz/.android/avd/Medium_Phone_API_35.avd" /> + </handle> + </Target> + </DropdownSelection> + <DialogSelection /> </SelectionState> </selectionStates> </component> diff --git a/app/build.gradle.kts b/app/build.gradle.kts index eea99f0..4132139 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -66,4 +66,9 @@ dependencies { 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") + } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5a8640b..804be05 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> + <uses-permission android:name="android.permission.INTERNET" /> + <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" diff --git a/app/src/main/java/com/module/breeze/MainActivity.kt b/app/src/main/java/com/module/breeze/MainActivity.kt index 9e45657..e9ae634 100644 --- a/app/src/main/java/com/module/breeze/MainActivity.kt +++ b/app/src/main/java/com/module/breeze/MainActivity.kt @@ -2,11 +2,12 @@ package com.module.breeze import android.content.Context import android.content.Intent +import android.os.Build import android.os.Bundle -import android.provider.CalendarContract import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -15,7 +16,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AcUnit @@ -25,31 +25,20 @@ import androidx.compose.material.icons.outlined.ArrowUpward import androidx.compose.material.icons.outlined.Map import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.WaterDrop -import androidx.compose.material.icons.rounded.AcUnit -import androidx.compose.material.icons.rounded.Air -import androidx.compose.material.icons.rounded.ArrowDownward -import androidx.compose.material.icons.rounded.ArrowDropDown -import androidx.compose.material.icons.rounded.ArrowUpward -import androidx.compose.material.icons.rounded.KeyboardArrowDown -import androidx.compose.material.icons.rounded.KeyboardArrowUp -import androidx.compose.material.icons.rounded.LocationOn -import androidx.compose.material.icons.rounded.Map -import androidx.compose.material.icons.rounded.Settings -import androidx.compose.material.icons.rounded.WaterDrop -import androidx.compose.material.icons.sharp.KeyboardArrowDown -import androidx.compose.material.icons.twotone.AccountCircle import androidx.compose.material3.Button -import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight @@ -57,14 +46,20 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.module.breeze.ui.theme.BreezeTheme +import kotlinx.coroutines.runBlocking +import java.text.DecimalFormat +import java.time.LocalDate +import kotlin.math.roundToInt val fontSizeCurrentTemp = 48.sp val fontSizeUpper = 16.sp -val fontSizeTitle = 24.sp +val fontSizeTitle = 22.sp val breezeFontWeight = FontWeight.Bold 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?) { @@ -89,14 +84,44 @@ class MainActivity : ComponentActivity() { } } +@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 +} + +@RequiresApi(Build.VERSION_CODES.O) +fun fetchWeather(ctx: Context): ForecastSummary { + var todayPlz = getTodaysLocation(ctx) + + var data: ForecastSummary + runBlocking { + val forecastResponse = RetrofitClient.instance.getWeatherForecast("${todayPlz},ch", apiKey) + val todaysForecast = getTodaysForecast(forecastResponse) + + 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 +} + @Composable fun WeatherInfo(modifier: Modifier = Modifier) { val ctx = LocalContext.current + var forecast by remember { mutableStateOf(ForecastSummary(0.0, 0.0, false, false, false)) } - - var displayRain = false; - var displaySnow = false; - var displayWind = false; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + forecast = fetchWeather(ctx) + } Navigation( iconStyle.Settings, @@ -121,18 +146,18 @@ fun WeatherInfo(modifier: Modifier = Modifier) { Icon( imageVector = iconStyle.WaterDrop, contentDescription = "Rain Icon", - modifier = Modifier.alpha(if (displayRain) 1F else 0.2F) + modifier = Modifier.alpha(if (forecast.hasRain) 1F else 0.2F) ) Icon( imageVector = iconStyle.AcUnit, contentDescription = "Snow Icon", - modifier = Modifier.alpha(if (displaySnow) 1F else 0.2F) + modifier = Modifier.alpha(if (forecast.hasSnow) 1F else 0.2F) ) Icon( imageVector = iconStyle.Air, contentDescription = "Wind Icon", - modifier = Modifier.alpha(if (displayWind) 1F else 0.2F) + modifier = Modifier.alpha(if (forecast.hasStrongWinds) 1F else 0.2F) ) } Row { @@ -141,7 +166,7 @@ fun WeatherInfo(modifier: Modifier = Modifier) { contentDescription = "Arrow down Icon" ) Text( - text = "18°", + text = numberFormat.format(forecast.minTemp.roundToInt()), fontSize = fontSizeUpper, fontWeight = breezeFontWeight ) @@ -150,7 +175,7 @@ fun WeatherInfo(modifier: Modifier = Modifier) { contentDescription = "Arrow up Icon" ) Text( - text = "23°", + text = numberFormat.format(forecast.maxTemp.roundToInt()), fontSize = fontSizeUpper, fontWeight = breezeFontWeight ) diff --git a/app/src/main/java/com/module/breeze/SettingsActivity.kt b/app/src/main/java/com/module/breeze/SettingsActivity.kt index 54e3599..db91969 100644 --- a/app/src/main/java/com/module/breeze/SettingsActivity.kt +++ b/app/src/main/java/com/module/breeze/SettingsActivity.kt @@ -34,7 +34,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit @@ -60,20 +59,20 @@ var plzArrayStatus: List<String> = listOf( val SHARED_PLZ_KEY = stringPreferencesKey("shared-plz") val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings") -suspend fun saveArray(context: Context, array: List<String>) { +suspend fun setLocations(context: Context, array: List<String>) { context.dataStore.edit { preferences -> preferences[SHARED_PLZ_KEY] = Json.encodeToString(array) } } -suspend fun getArray(context: Context): List<String> { +suspend fun getLocations(context: Context): List<String> { 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 = getArray(ctx) + var arr = getLocations(ctx) if (arr.isEmpty() || arr.size != 7) { arr = listOf("8005", "8005", "8005", "8400", "8400", "8500", "8500") } @@ -84,7 +83,7 @@ suspend fun getPLZ(index: Int, ctx: Context): String { suspend fun setPLZ(index: Int, value: String, ctx: Context) { var arr = plzArrayStatus.toMutableList() arr[index] = value - saveArray(ctx, arr) + setLocations(ctx, arr) } @@ -139,7 +138,7 @@ fun Settings(modifier: Modifier = Modifier) { Text( text = "Configured Locations", fontWeight = breezeFontWeight, - fontSize = 22.sp + fontSize = fontSizeTitle ) } ConfiguredLocationDay("Monday", 0, ctx) diff --git a/app/src/main/java/com/module/breeze/WheatherRepository.kt b/app/src/main/java/com/module/breeze/WheatherRepository.kt new file mode 100644 index 0000000..7831128 --- /dev/null +++ b/app/src/main/java/com/module/breeze/WheatherRepository.kt @@ -0,0 +1,134 @@ +package com.module.breeze + +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 +) + +data class Main( + 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 +) + +data class ForecastResponse( + 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? +) + +data class Wind( + val speed: Double, + val deg: Int +) + +data class Rain( + val `3h`: Double? +) + +data class Snow( + 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("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 val logging = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + 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) + } +} + +// 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 + + // 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 + + // 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 + ) +} + +// 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 +) \ No newline at end of file