diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cfa6faa..dd5cfe1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -57,25 +57,25 @@ android { } dependencies { + val composeVersion = "1.4.0" implementation("androidx.core:core-ktx:1.9.0") - implementation("androidx.compose.ui:ui:1.4.0") + implementation("androidx.compose.ui:ui:$composeVersion") implementation("androidx.compose.material3:material3:1.1.0-beta01") - implementation("androidx.compose.ui:ui-tooling-preview:1.4.0") + implementation("androidx.compose.material:material-icons-extended:$composeVersion") + implementation("androidx.compose.ui:ui-tooling-preview:$composeVersion") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1") implementation("androidx.activity:activity-compose:1.7.0") implementation("androidx.core:core-ktx:1.9.0") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1") - testImplementation("junit:junit:4.13.2") - androidTestImplementation("androidx.test.ext:junit:1.1.5") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") - androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.4.0") - debugImplementation("androidx.compose.ui:ui-tooling:1.4.0") + debugImplementation("androidx.compose.ui:ui-tooling:$composeVersion") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.2") // Voyager val voyagerVersion = "1.0.0-rc04" implementation("cafe.adriel.voyager:voyager-navigator:$voyagerVersion") + implementation("cafe.adriel.voyager:voyager-tab-navigator:$voyagerVersion") implementation("cafe.adriel.voyager:voyager-transitions:$voyagerVersion") implementation("cafe.adriel.voyager:voyager-androidx:$voyagerVersion") implementation("cafe.adriel.voyager:voyager-koin:$voyagerVersion") @@ -92,5 +92,7 @@ dependencies { implementation("com.squareup.retrofit2:converter-moshi:2.9.0") implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3") - implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1") -} \ No newline at end of file + // Accompanist + val accompanistVersion = "0.30.0" + implementation("com.google.accompanist:accompanist-permissions:$accompanistVersion") +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fff1a58..95f9c98 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,11 +14,10 @@ android:supportsRtl="true" android:theme="@style/Theme.WeatherApp"> - diff --git a/app/src/main/java/com/henryhiles/qweather/data/location/DefaultLocationTracker.kt b/app/src/main/java/com/henryhiles/qweather/data/location/DefaultLocationTracker.kt index 02f0184..2787021 100644 --- a/app/src/main/java/com/henryhiles/qweather/data/location/DefaultLocationTracker.kt +++ b/app/src/main/java/com/henryhiles/qweather/data/location/DefaultLocationTracker.kt @@ -18,11 +18,6 @@ class DefaultLocationTracker constructor( Manifest.permission.ACCESS_FINE_LOCATION ) == PackageManager.PERMISSION_GRANTED - val hasAccessCoarseLocationPermission = ContextCompat.checkSelfPermission( - application, - Manifest.permission.ACCESS_COARSE_LOCATION - ) == PackageManager.PERMISSION_GRANTED - val locationManager = application.getSystemService(Context.LOCATION_SERVICE) as LocationManager val isGpsEnabled = locationManager.isProviderEnabled( diff --git a/app/src/main/java/com/henryhiles/qweather/data/mappers/WeatherMappers.kt b/app/src/main/java/com/henryhiles/qweather/data/mappers/WeatherMappers.kt index 62b05de..b7f6aa8 100644 --- a/app/src/main/java/com/henryhiles/qweather/data/mappers/WeatherMappers.kt +++ b/app/src/main/java/com/henryhiles/qweather/data/mappers/WeatherMappers.kt @@ -15,7 +15,7 @@ fun WeatherDataDto.toWeatherDataMap(): Map> { IndexedWeatherData( index = index, data = WeatherData( time = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME), - temperatureCelsius = temperatures[index], + temperatureCelsius = temperatures[index].toInt(), pressure = pressures[index], windSpeed = windSpeeds[index], humidity = humidities[index], diff --git a/app/src/main/java/com/henryhiles/qweather/di/AppModule.kt b/app/src/main/java/com/henryhiles/qweather/di/AppModule.kt index cefda7b..4c2e87f 100644 --- a/app/src/main/java/com/henryhiles/qweather/di/AppModule.kt +++ b/app/src/main/java/com/henryhiles/qweather/di/AppModule.kt @@ -1,8 +1,8 @@ package com.henryhiles.qweather.di import com.henryhiles.qweather.data.remote.WeatherApi -import com.henryhiles.qweather.presentation.viewmodel.WeatherViewModel -import org.koin.androidx.viewmodel.dsl.viewModelOf +import com.henryhiles.qweather.presentation.screenmodel.WeatherScreenModel +import org.koin.core.module.dsl.factoryOf import org.koin.core.module.dsl.singleOf import org.koin.dsl.module import retrofit2.Retrofit @@ -36,5 +36,7 @@ val appModule = module { // single { // LocationServices.getFusedLocationProviderClient(get()) // } - viewModelOf(::WeatherViewModel) + + factoryOf(::WeatherScreenModel) +// factory { WeatherScreenModel(get(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/domain/weather/WeatherData.kt b/app/src/main/java/com/henryhiles/qweather/domain/weather/WeatherData.kt index 6471ec6..c59c18f 100644 --- a/app/src/main/java/com/henryhiles/qweather/domain/weather/WeatherData.kt +++ b/app/src/main/java/com/henryhiles/qweather/domain/weather/WeatherData.kt @@ -4,7 +4,7 @@ import java.time.LocalDateTime data class WeatherData( val time: LocalDateTime, - val temperatureCelsius: Double, + val temperatureCelsius: Int, val pressure: Double, val windSpeed: Double, val humidity: Double, diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/QWeatherActivity.kt b/app/src/main/java/com/henryhiles/qweather/presentation/QWeatherActivity.kt new file mode 100644 index 0000000..a2704e6 --- /dev/null +++ b/app/src/main/java/com/henryhiles/qweather/presentation/QWeatherActivity.kt @@ -0,0 +1,27 @@ +package com.henryhiles.qweather.presentation + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.material3.Surface +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.transitions.SlideTransition +import com.henryhiles.qweather.presentation.screen.MainScreen +import com.henryhiles.qweather.presentation.ui.theme.WeatherAppTheme + +class QWeatherActivity : ComponentActivity() { + @OptIn(ExperimentalAnimationApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + WeatherAppTheme { + Surface { + Navigator(screen = MainScreen()) { + SlideTransition(it) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/activity/MainActivity.kt b/app/src/main/java/com/henryhiles/qweather/presentation/activity/MainActivity.kt deleted file mode 100644 index cb208c9..0000000 --- a/app/src/main/java/com/henryhiles/qweather/presentation/activity/MainActivity.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.henryhiles.qweather.presentation.activity - -import android.Manifest -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface -import androidx.compose.ui.Modifier -import cafe.adriel.voyager.navigator.CurrentScreen -import cafe.adriel.voyager.navigator.Navigator -import com.henryhiles.qweather.presentation.screen.TodayScreen -import com.henryhiles.qweather.presentation.ui.theme.WeatherAppTheme -import com.henryhiles.qweather.presentation.viewmodel.WeatherViewModel -import org.koin.androidx.viewmodel.ext.android.getViewModel - -class MainActivity : ComponentActivity() { - private lateinit var permissionLauncher: ActivityResultLauncher> - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val weatherViewModel = getViewModel() - - permissionLauncher = - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { weatherViewModel.loadWeatherInfo() } - permissionLauncher.launch( - arrayOf( - Manifest.permission.ACCESS_FINE_LOCATION, - ) - ) - setContent { - WeatherAppTheme { - Surface { - Navigator(TodayScreen()) { navigator -> - Scaffold( - topBar = { /* ... */ }, - content = { padding -> Box(modifier = Modifier.padding(padding)) { CurrentScreen() } }, - bottomBar = { /* ... */ } - ) - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/components/BackButton.kt b/app/src/main/java/com/henryhiles/qweather/presentation/components/BackButton.kt new file mode 100644 index 0000000..3f5d410 --- /dev/null +++ b/app/src/main/java/com/henryhiles/qweather/presentation/components/BackButton.kt @@ -0,0 +1,21 @@ +package com.henryhiles.qweather.presentation.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import cafe.adriel.voyager.navigator.LocalNavigator +import com.henryhiles.qweather.R + +@Composable +fun BackButton() { + val nav = LocalNavigator.current + + if (nav?.canPop == true) { + IconButton(onClick = { nav.pop() }) { + Icon(Icons.Filled.ArrowBack, stringResource(R.string.action_back)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/components/LargeToolbar.kt b/app/src/main/java/com/henryhiles/qweather/presentation/components/LargeToolbar.kt new file mode 100644 index 0000000..f899b95 --- /dev/null +++ b/app/src/main/java/com/henryhiles/qweather/presentation/components/LargeToolbar.kt @@ -0,0 +1,18 @@ +package com.henryhiles.qweather.presentation.components + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.* +import androidx.compose.runtime.Composable + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun LargeToolbar( + title: String, + actions: @Composable RowScope.() -> Unit = {}, +) { + LargeTopAppBar( + title = { Text(text = title) }, + navigationIcon = { BackButton() }, + actions = actions, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/components/RadioController.kt b/app/src/main/java/com/henryhiles/qweather/presentation/components/RadioController.kt new file mode 100644 index 0000000..5827c72 --- /dev/null +++ b/app/src/main/java/com/henryhiles/qweather/presentation/components/RadioController.kt @@ -0,0 +1,47 @@ +package com.henryhiles.qweather.presentation.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext + +@Composable +inline fun > EnumRadioController( + default: E, + labelFactory: (E) -> String = { it.toString() }, + crossinline onChoiceSelected: (E) -> Unit +) { + var choice by remember { mutableStateOf(default) } + val ctx = LocalContext.current + + Column { + enumValues().forEach { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + choice = it + onChoiceSelected(it) + }, + verticalAlignment = Alignment.CenterVertically + ) { + Text(labelFactory(it)) + Spacer(Modifier.weight(1f)) + RadioButton( + selected = it == choice, + onClick = { + choice = it + onChoiceSelected(it) + }) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/components/WeatherDay.kt b/app/src/main/java/com/henryhiles/qweather/presentation/components/WeatherDay.kt deleted file mode 100644 index 4704f72..0000000 --- a/app/src/main/java/com/henryhiles/qweather/presentation/components/WeatherDay.kt +++ /dev/null @@ -1,2 +0,0 @@ -package com.henryhiles.qweather.presentation.components - diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/components/settings/SettingsCategory.kt b/app/src/main/java/com/henryhiles/qweather/presentation/components/settings/SettingsCategory.kt new file mode 100644 index 0000000..ac92fb1 --- /dev/null +++ b/app/src/main/java/com/henryhiles/qweather/presentation/components/settings/SettingsCategory.kt @@ -0,0 +1,35 @@ +package com.henryhiles.qweather.presentation.components.settings + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.LocalNavigator + +@Composable +fun SettingsCategory( + icon: ImageVector, + text: String, + subtext: String, + destination: (() -> Screen)? = null +) { + val screen = destination?.invoke() + val nav = LocalNavigator.current + + Box( + modifier = Modifier + .clickable { + screen?.let { nav?.push(it) } + } + ) { + SettingItem( + icon = { Icon(icon, null) }, + text = { Text(text) }, + secondaryText = { Text(subtext) } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/components/settings/SettingsChoiceDialog.kt b/app/src/main/java/com/henryhiles/qweather/presentation/components/settings/SettingsChoiceDialog.kt new file mode 100644 index 0000000..1ab86f2 --- /dev/null +++ b/app/src/main/java/com/henryhiles/qweather/presentation/components/settings/SettingsChoiceDialog.kt @@ -0,0 +1,50 @@ +package com.henryhiles.qweather.presentation.components.settings + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.res.stringResource +import com.henryhiles.qweather.R +import com.henryhiles.qweather.presentation.components.EnumRadioController + +@Composable +inline fun > SettingsChoiceDialog( + visible: Boolean = false, + default: E, + noinline title: @Composable () -> Unit, + crossinline labelFactory: (E) -> String = { it.toString() }, + noinline onRequestClose: () -> Unit = {}, + crossinline description: @Composable () -> Unit = {}, + noinline onChoice: (E) -> Unit = {}, +) { + + var choice by remember { mutableStateOf(default) } + + AnimatedVisibility( + visible = visible, + enter = slideInVertically(), + exit = slideOutVertically() + ) { + AlertDialog( + onDismissRequest = { onRequestClose() }, + title = title, + text = { + description() + EnumRadioController( + default, + labelFactory + ) { choice = it } + }, + confirmButton = { + FilledTonalButton(onClick = { onChoice(choice) }) { + Text(text = stringResource(id = R.string.action_confirm)) + } + } + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/components/settings/SettingsItem.kt b/app/src/main/java/com/henryhiles/qweather/presentation/components/settings/SettingsItem.kt new file mode 100644 index 0000000..427342c --- /dev/null +++ b/app/src/main/java/com/henryhiles/qweather/presentation/components/settings/SettingsItem.kt @@ -0,0 +1,58 @@ +package com.henryhiles.qweather.presentation.components.settings + +import androidx.compose.foundation.layout.* + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun SettingItem( + modifier: Modifier = Modifier, + icon: @Composable (() -> Unit)? = null, + text: @Composable () -> Unit, + secondaryText: @Composable (() -> Unit) = { }, + trailing: @Composable (() -> Unit) = { }, +) { + Row( + modifier = modifier + .heightIn(min = 64.dp) + .fillMaxWidth() + .padding(horizontal = 18.dp, vertical = 14.dp), + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (icon != null) Box(modifier = Modifier.padding(8.dp)) { + icon() + } + + Column( + verticalArrangement = Arrangement.spacedBy(5.dp) + ) { + ProvideTextStyle( + MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.Normal, + fontSize = 19.sp + ) + ) { + text() + } + ProvideTextStyle( + MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurface.copy(0.6f) + ) + ) { + secondaryText() + } + } + + Spacer(Modifier.weight(1f, true)) + + trailing() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/components/settings/SettingsItemChoice.kt b/app/src/main/java/com/henryhiles/qweather/presentation/components/settings/SettingsItemChoice.kt new file mode 100644 index 0000000..a87041d --- /dev/null +++ b/app/src/main/java/com/henryhiles/qweather/presentation/components/settings/SettingsItemChoice.kt @@ -0,0 +1,44 @@ +package com.henryhiles.qweather.presentation.components.settings + +import androidx.compose.foundation.clickable +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier + +@Composable +inline fun > SettingsItemChoice( + label: String, + title: String = label, + disabled: Boolean = false, + pref: E, + crossinline labelFactory: (E) -> String = { it.toString() }, + crossinline onPrefChange: (E) -> Unit, +) { + val choiceLabel = labelFactory(pref) + var opened by remember { + mutableStateOf(false) + } + + SettingItem( + modifier = Modifier.clickable { opened = true }, + text = { Text(text = label) }, + ) { + SettingsChoiceDialog( + visible = opened, + title = { Text(title) }, + default = pref, + labelFactory = labelFactory, + onRequestClose = { + opened = false + }, + onChoice = { + opened = false + onPrefChange(it) + } + ) + FilledTonalButton(onClick = { opened = true }, enabled = !disabled) { + Text(choiceLabel) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/components/settings/SettingsSwitch.kt b/app/src/main/java/com/henryhiles/qweather/presentation/components/settings/SettingsSwitch.kt new file mode 100644 index 0000000..e2cdd6d --- /dev/null +++ b/app/src/main/java/com/henryhiles/qweather/presentation/components/settings/SettingsSwitch.kt @@ -0,0 +1,32 @@ +package com.henryhiles.qweather.presentation.components.settings + +import androidx.compose.foundation.clickable +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun SettingsSwitch( + label: String, + secondaryLabel: String? = null, + disabled: Boolean = false, + pref: Boolean, + onPrefChange: (Boolean) -> Unit, +) { + SettingItem( + modifier = Modifier.clickable(enabled = !disabled) { onPrefChange(!pref) }, + text = { Text(text = label) }, + secondaryText = { + secondaryLabel?.let { + Text(text = it) + } + } + ) { + Switch( + checked = pref, + enabled = !disabled, + onCheckedChange = { onPrefChange(!pref) } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/components/WeatherCard.kt b/app/src/main/java/com/henryhiles/qweather/presentation/components/weather/WeatherCard.kt similarity index 97% rename from app/src/main/java/com/henryhiles/qweather/presentation/components/WeatherCard.kt rename to app/src/main/java/com/henryhiles/qweather/presentation/components/weather/WeatherCard.kt index 7a7e9e2..5d9a277 100644 --- a/app/src/main/java/com/henryhiles/qweather/presentation/components/WeatherCard.kt +++ b/app/src/main/java/com/henryhiles/qweather/presentation/components/weather/WeatherCard.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.henryhiles.qweather.R -import com.henryhiles.qweather.presentation.viewmodel.WeatherState +import com.henryhiles.qweather.presentation.screenmodel.WeatherState import java.time.format.DateTimeFormatter import kotlin.math.roundToInt @@ -26,7 +26,6 @@ fun WeatherCard(state: WeatherState, modifier: Modifier = Modifier) { val formattedTime = remember(it) { it.time.format(DateTimeFormatter.ofPattern("HH:mm")) } - Card( shape = RoundedCornerShape(8.dp), modifier = modifier.padding(16.dp) diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/components/WeatherDataDisplay.kt b/app/src/main/java/com/henryhiles/qweather/presentation/components/weather/WeatherDataDisplay.kt similarity index 100% rename from app/src/main/java/com/henryhiles/qweather/presentation/components/WeatherDataDisplay.kt rename to app/src/main/java/com/henryhiles/qweather/presentation/components/weather/WeatherDataDisplay.kt diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/components/WeatherForecast.kt b/app/src/main/java/com/henryhiles/qweather/presentation/components/weather/WeatherForecast.kt similarity index 95% rename from app/src/main/java/com/henryhiles/qweather/presentation/components/WeatherForecast.kt rename to app/src/main/java/com/henryhiles/qweather/presentation/components/weather/WeatherForecast.kt index 7df9fbf..dc0b561 100644 --- a/app/src/main/java/com/henryhiles/qweather/presentation/components/WeatherForecast.kt +++ b/app/src/main/java/com/henryhiles/qweather/presentation/components/weather/WeatherForecast.kt @@ -10,7 +10,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.henryhiles.qweather.presentation.viewmodel.WeatherState +import com.henryhiles.qweather.presentation.screenmodel.WeatherState import java.time.LocalDateTime @Composable diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/components/WeatherHour.kt b/app/src/main/java/com/henryhiles/qweather/presentation/components/weather/WeatherHour.kt similarity index 100% rename from app/src/main/java/com/henryhiles/qweather/presentation/components/WeatherHour.kt rename to app/src/main/java/com/henryhiles/qweather/presentation/components/weather/WeatherHour.kt diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/screen/AppearanceSettingsScreen.kt b/app/src/main/java/com/henryhiles/qweather/presentation/screen/AppearanceSettingsScreen.kt new file mode 100644 index 0000000..48c079e --- /dev/null +++ b/app/src/main/java/com/henryhiles/qweather/presentation/screen/AppearanceSettingsScreen.kt @@ -0,0 +1,65 @@ +package com.henryhiles.qweather.presentation.screen + +import android.os.Build +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.koin.getScreenModel +import com.henryhiles.qweather.R +import com.henryhiles.qweather.presentation.components.LargeToolbar +import com.henryhiles.qweather.presentation.components.settings.SettingsItemChoice +import com.henryhiles.qweather.presentation.components.settings.SettingsSwitch +import com.henryhiles.qweather.presentation.screenmodel.AppearanceSettingsScreenModel + +class AppearanceSettingsScreen : Screen { + + @Composable + override fun Content() = Screen() + + @Composable + private fun Screen( + screenModel: AppearanceSettingsScreenModel = getScreenModel() + ) { + val ctx = LocalContext.current + + Scaffold(topBar = { Toolbar() }) { pv -> + Column( + modifier = Modifier + .padding(pv) + .verticalScroll(rememberScrollState()) + ) { + + SettingsSwitch( + label = stringResource(R.string.appearance_monet), + secondaryLabel = stringResource(R.string.appearance_monet_description), + pref = screenModel.prefs.monet, + disabled = Build.VERSION.SDK_INT < Build.VERSION_CODES.S + ) { screenModel.prefs.monet = it } + + SettingsItemChoice( + label = stringResource(R.string.appearance_theme), + pref = screenModel.prefs.theme, + labelFactory = { ctx.getString(it.label) } + ) { screenModel.prefs.theme = it } + + } + } + } + + @Composable + private fun Toolbar( + ) { + LargeToolbar( + title = stringResource(R.string.settings_appearance), + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/screen/MainScreen.kt b/app/src/main/java/com/henryhiles/qweather/presentation/screen/MainScreen.kt new file mode 100644 index 0000000..20b3ded --- /dev/null +++ b/app/src/main/java/com/henryhiles/qweather/presentation/screen/MainScreen.kt @@ -0,0 +1,67 @@ +package com.henryhiles.qweather.presentation.screen + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DateRange +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.CurrentScreen +import cafe.adriel.voyager.navigator.tab.TabNavigator +import com.henryhiles.qweather.R +import com.henryhiles.qweather.presentation.tabs.SettingsTab +import com.henryhiles.qweather.presentation.tabs.TodayTab +import com.henryhiles.qweather.presentation.tabs.WeekTab + +class MainScreen : Screen { + @Composable + override fun Content() { + TabNavigator(tab = TodayTab) { navigator -> + Scaffold( + bottomBar = { + NavigationBar { + NavigationBarItem( + selected = navigator.current == TodayTab, + onClick = { navigator.current = TodayTab }, + label = { Text(text = stringResource(id = R.string.tab_today)) }, + icon = { + Icon( + imageVector = Icons.Default.Home, + contentDescription = stringResource(id = R.string.tab_today) + ) + }) + NavigationBarItem( + selected = navigator.current == WeekTab, + onClick = { navigator.current = WeekTab }, + label = { Text(text = "Weekly") }, + icon = { + Icon( + imageVector = Icons.Default.DateRange, + contentDescription = "Weekly" + ) + }) + NavigationBarItem( + selected = navigator.current == SettingsTab, + onClick = { navigator.current = SettingsTab }, + label = { Text(text = "Settings") }, + icon = { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = "Settings" + ) + }) + } + } + ) { padding -> + Box(modifier = Modifier.padding(padding)) { + CurrentScreen() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/screen/TodayScreen.kt b/app/src/main/java/com/henryhiles/qweather/presentation/screen/TodayScreen.kt deleted file mode 100644 index 119a634..0000000 --- a/app/src/main/java/com/henryhiles/qweather/presentation/screen/TodayScreen.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.henryhiles.qweather.presentation.screen - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.androidx.AndroidScreen -import com.henryhiles.qweather.presentation.components.WeatherCard -import com.henryhiles.qweather.presentation.components.WeatherForecast -import com.henryhiles.qweather.presentation.viewmodel.WeatherViewModel -import org.koin.androidx.compose.getViewModel - -class TodayScreen : AndroidScreen() { - @OptIn(ExperimentalMaterial3Api::class) - @Composable - override fun Content() { - val weatherViewModel = getViewModel() - Box(modifier = Modifier.fillMaxSize()) { - Column( - modifier = Modifier - .fillMaxSize() - ) { - WeatherCard(state = weatherViewModel.state) - Spacer(modifier = Modifier.height(16.dp)) - WeatherForecast(state = weatherViewModel.state) - } - if (weatherViewModel.state.isLoading) CircularProgressIndicator( - modifier = Modifier.align( - Alignment.Center - ) - ) - weatherViewModel.state.error?.let { - AlertDialog(onDismissRequest = {}) { - Surface( - shape = MaterialTheme.shapes.large - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = "An error occurred", - style = MaterialTheme.typography.headlineSmall, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) - SelectionContainer { - Text( - text = it, - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(16.dp) - ) - } - - } - } - } - - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/screenmodel/AppearanceSettingsScreenModel.kt b/app/src/main/java/com/henryhiles/qweather/presentation/screenmodel/AppearanceSettingsScreenModel.kt new file mode 100644 index 0000000..7a12166 --- /dev/null +++ b/app/src/main/java/com/henryhiles/qweather/presentation/screenmodel/AppearanceSettingsScreenModel.kt @@ -0,0 +1,20 @@ +package com.henryhiles.qweather.presentation.screenmodel + +import android.content.Context +import android.os.Build +import androidx.annotation.StringRes +import cafe.adriel.voyager.core.model.ScreenModel +import com.henryhiles.qweather.R + +class AppearanceSettingsScreenModel(context: Context) : ScreenModel { + + var theme by enumPreference("theme", Theme.SYSTEM) + var monet by booleanPreference("monet", Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + +} + +enum class Theme(@StringRes val label: Int) { + SYSTEM(R.string.theme_system), + LIGHT(R.string.theme_light), + DARK(R.string.theme_dark); +} \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/viewmodel/WeatherViewModel.kt b/app/src/main/java/com/henryhiles/qweather/presentation/screenmodel/WeatherScreenModel.kt similarity index 88% rename from app/src/main/java/com/henryhiles/qweather/presentation/viewmodel/WeatherViewModel.kt rename to app/src/main/java/com/henryhiles/qweather/presentation/screenmodel/WeatherScreenModel.kt index 1600ff3..2061e89 100644 --- a/app/src/main/java/com/henryhiles/qweather/presentation/viewmodel/WeatherViewModel.kt +++ b/app/src/main/java/com/henryhiles/qweather/presentation/screenmodel/WeatherScreenModel.kt @@ -1,10 +1,10 @@ -package com.henryhiles.qweather.presentation.viewmodel +package com.henryhiles.qweather.presentation.screenmodel import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.coroutineScope import com.henryhiles.qweather.domain.location.LocationTracker import com.henryhiles.qweather.domain.repository.WeatherRepository import com.henryhiles.qweather.domain.util.Resource @@ -17,15 +17,15 @@ data class WeatherState( val error: String? = null ) -class WeatherViewModel constructor( +class WeatherScreenModel constructor( private val repository: WeatherRepository, private val locationTracker: LocationTracker, -) : ViewModel() { +) : ScreenModel { var state by mutableStateOf(WeatherState()) private set fun loadWeatherInfo() { - viewModelScope.launch { + coroutineScope.launch { state = state.copy(isLoading = true, error = null) val currentLocation = locationTracker.getCurrentLocation() currentLocation?.let { location -> diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/tabs/SettingsTab.kt b/app/src/main/java/com/henryhiles/qweather/presentation/tabs/SettingsTab.kt new file mode 100644 index 0000000..cad3236 --- /dev/null +++ b/app/src/main/java/com/henryhiles/qweather/presentation/tabs/SettingsTab.kt @@ -0,0 +1,66 @@ +package com.henryhiles.qweather.presentation.tabs + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.outlined.Palette +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.stringResource +import cafe.adriel.voyager.navigator.tab.Tab +import cafe.adriel.voyager.navigator.tab.TabOptions +import com.henryhiles.qweather.R +import com.henryhiles.qweather.presentation.components.LargeToolbar +import com.henryhiles.qweather.presentation.components.settings.SettingsCategory +import com.henryhiles.qweather.presentation.screen.AppearanceSettingsScreen + +object SettingsTab : Tab { + override val options: TabOptions + @Composable + get() { + val title = stringResource(R.string.tab_settings) + val icon = rememberVectorPainter(Icons.Default.Settings) + + return remember { + TabOptions( + index = 0u, + title = title, + icon = icon + ) + } + } + + @Composable + override fun Content() { + Scaffold( + topBar = { Toolbar() }, + ) { + Column( + modifier = Modifier + .padding(it) + .verticalScroll(rememberScrollState()) + ) { + + SettingsCategory( + icon = Icons.Outlined.Palette, + text = stringResource(R.string.settings_appearance), + subtext = stringResource(R.string.settings_appearance_description), + destination = ::AppearanceSettingsScreen + ) + } + } + } + + @Composable + private fun Toolbar() { + LargeToolbar( + title = stringResource(R.string.tab_settings), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/tabs/TodayTab.kt b/app/src/main/java/com/henryhiles/qweather/presentation/tabs/TodayTab.kt new file mode 100644 index 0000000..bf0b2a9 --- /dev/null +++ b/app/src/main/java/com/henryhiles/qweather/presentation/tabs/TodayTab.kt @@ -0,0 +1,103 @@ +package com.henryhiles.qweather.presentation.tabs + +import android.Manifest +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Home +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.koin.getScreenModel +import cafe.adriel.voyager.navigator.tab.Tab +import cafe.adriel.voyager.navigator.tab.TabOptions +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberPermissionState +import com.henryhiles.qweather.R +import com.henryhiles.qweather.presentation.components.WeatherCard +import com.henryhiles.qweather.presentation.components.WeatherForecast +import com.henryhiles.qweather.presentation.screenmodel.WeatherScreenModel + +object TodayTab : Tab { + override val options: TabOptions + @Composable + get() { + val title = stringResource(R.string.tab_today) + val icon = rememberVectorPainter(Icons.Default.Home) + + return remember { + TabOptions( + index = 0u, + title = title, + icon = icon + ) + } + } + + @OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) + @Composable + override fun Content() { + val weatherViewModel = getScreenModel() + + val permissionsState = rememberPermissionState( + Manifest.permission.ACCESS_FINE_LOCATION, + ) { + weatherViewModel.loadWeatherInfo() + } + + LaunchedEffect(key1 = true) { + permissionsState.launchPermissionRequest() + } + + Box(modifier = Modifier.fillMaxSize()) { + when { + weatherViewModel.state.isLoading -> { + CircularProgressIndicator( + modifier = Modifier.align( + Alignment.Center + ) + ) + } + weatherViewModel.state.error != null -> { + AlertDialog(onDismissRequest = {}) { + Surface( + shape = MaterialTheme.shapes.large + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = "An error occurred", + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + SelectionContainer { + Text( + text = weatherViewModel.state.error!!, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(16.dp) + ) + } + } + } + } + } + else -> { + Column( + modifier = Modifier + .fillMaxSize() + ) { + WeatherCard(state = weatherViewModel.state) + Spacer(modifier = Modifier.height(16.dp)) + WeatherForecast(state = weatherViewModel.state) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/henryhiles/qweather/presentation/tabs/WeekTab.kt b/app/src/main/java/com/henryhiles/qweather/presentation/tabs/WeekTab.kt new file mode 100644 index 0000000..34c6e1d --- /dev/null +++ b/app/src/main/java/com/henryhiles/qweather/presentation/tabs/WeekTab.kt @@ -0,0 +1,34 @@ +package com.henryhiles.qweather.presentation.tabs + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DateRange +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.stringResource +import cafe.adriel.voyager.navigator.tab.Tab +import cafe.adriel.voyager.navigator.tab.TabOptions +import com.henryhiles.qweather.R + +object WeekTab : Tab { + override val options: TabOptions + @Composable + get() { + val title = stringResource(R.string.tab_weekly) + val icon = rememberVectorPainter(Icons.Default.DateRange) + + return remember { + TabOptions( + index = 0u, + title = title, + icon = icon + ) + } + } + + @Composable + override fun Content() { + Text(text = "Week Screen") + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8d12d2e..ff2081b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,20 @@ - QWeather + QWeather + Today + Weekly + Settings + + Back + Confirm + + Theme + Dynamic Theme + Available on Android 12+ + + Appearance + Theme, code style + + System + Light + Dark \ No newline at end of file