settings frontend done, just need to setup screenmodel
This commit is contained in:
parent
e132ceb55f
commit
0809535e16
30 changed files with 732 additions and 143 deletions
|
@ -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")
|
||||
}
|
||||
// Accompanist
|
||||
val accompanistVersion = "0.30.0"
|
||||
implementation("com.google.accompanist:accompanist-permissions:$accompanistVersion")
|
||||
}
|
||||
|
|
|
@ -14,11 +14,10 @@
|
|||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.WeatherApp">
|
||||
<activity
|
||||
android:name=".presentation.activity.MainActivity"
|
||||
android:name=".presentation.QWeatherActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -15,7 +15,7 @@ fun WeatherDataDto.toWeatherDataMap(): Map<Int, List<WeatherData>> {
|
|||
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],
|
||||
|
|
|
@ -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<Application>())
|
||||
// }
|
||||
viewModelOf(::WeatherViewModel)
|
||||
|
||||
factoryOf(::WeatherScreenModel)
|
||||
// factory { WeatherScreenModel(get(), get()) }
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Array<String>>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val weatherViewModel = getViewModel<WeatherViewModel>()
|
||||
|
||||
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 = { /* ... */ }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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 <reified E : Enum<E>> EnumRadioController(
|
||||
default: E,
|
||||
labelFactory: (E) -> String = { it.toString() },
|
||||
crossinline onChoiceSelected: (E) -> Unit
|
||||
) {
|
||||
var choice by remember { mutableStateOf(default) }
|
||||
val ctx = LocalContext.current
|
||||
|
||||
Column {
|
||||
enumValues<E>().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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
package com.henryhiles.qweather.presentation.components
|
||||
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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 <reified E : Enum<E>> 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))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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 <reified E : Enum<E>> 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<WeatherViewModel>()
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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 ->
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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<WeatherScreenModel>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -1,3 +1,20 @@
|
|||
<resources>
|
||||
<string name="app_name">QWeather</string>
|
||||
<string name="app_name" translatable="false">QWeather</string>
|
||||
<string name="tab_today">Today</string>
|
||||
<string name="tab_weekly">Weekly</string>
|
||||
<string name="tab_settings">Settings</string>
|
||||
|
||||
<string name="action_back">Back</string>
|
||||
<string name="action_confirm">Confirm</string>
|
||||
|
||||
<string name="appearance_theme">Theme</string>
|
||||
<string name="appearance_monet">Dynamic Theme</string>
|
||||
<string name="appearance_monet_description">Available on Android 12+</string>
|
||||
|
||||
<string name="settings_appearance">Appearance</string>
|
||||
<string name="settings_appearance_description">Theme, code style</string>
|
||||
|
||||
<string name="theme_system">System</string>
|
||||
<string name="theme_light">Light</string>
|
||||
<string name="theme_dark">Dark</string>
|
||||
</resources>
|
Reference in a new issue