From 3a50c3d5a11acc357ac232c6dd5100ded515671d Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 7 May 2024 13:44:07 +0200 Subject: [PATCH 01/10] Move composable and create previews --- .../icsdroid/ui/screen/EditCalendarScreen.kt | 272 ++++++++++++++++++ .../icsdroid/ui/views/EditCalendarActivity.kt | 172 +---------- .../views/SubscriptionSettingsComposable.kt | 27 +- 3 files changed, 309 insertions(+), 162 deletions(-) create mode 100644 app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt b/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt new file mode 100644 index 00000000..a80d4931 --- /dev/null +++ b/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt @@ -0,0 +1,272 @@ +package at.bitfire.icsdroid.ui.screen + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +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.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +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.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import at.bitfire.icsdroid.R +import at.bitfire.icsdroid.model.CredentialsModel +import at.bitfire.icsdroid.model.SubscriptionSettingsModel +import at.bitfire.icsdroid.ui.partials.ExtendedTopAppBar +import at.bitfire.icsdroid.ui.partials.GenericAlertDialog +import at.bitfire.icsdroid.ui.theme.AppTheme +import at.bitfire.icsdroid.ui.views.LoginCredentialsComposable +import at.bitfire.icsdroid.ui.views.SubscriptionSettingsComposable + +@Composable +fun EditCalendarScreen( + subscriptionSettingsModel: SubscriptionSettingsModel, + credentialsModel: CredentialsModel, + inputValid: Boolean, + modelsDirty: Boolean, + onDelete: () -> Unit, + onSave: () -> Unit, + onShare: () -> Unit, + onExit: () -> Unit +) { + EditCalendarScreen( + credentialsModel.uiState.username, + credentialsModel.uiState.password, + credentialsModel.uiState.requiresAuth, + + credentialsModel::setRequiresAuth, + credentialsModel::setUsername, + credentialsModel::setPassword, + + subscriptionSettingsModel.uiState.supportsAuthentication, + subscriptionSettingsModel.uiState.url, + subscriptionSettingsModel.uiState.title, + subscriptionSettingsModel.uiState.color, + subscriptionSettingsModel.uiState.ignoreAlerts, + subscriptionSettingsModel.uiState.defaultAlarmMinutes, + subscriptionSettingsModel.uiState.defaultAllDayAlarmMinutes, + subscriptionSettingsModel.uiState.ignoreDescription, + + subscriptionSettingsModel::setTitle, + subscriptionSettingsModel::setColor, + subscriptionSettingsModel::setIgnoreAlerts, + subscriptionSettingsModel::setDefaultAlarmMinutes, + subscriptionSettingsModel::setDefaultAllDayAlarmMinutes, + subscriptionSettingsModel::setIgnoreDescription, + + inputValid, + modelsDirty, + + onDelete, + onSave, + onShare, + onExit + ) +} + +@Composable +private fun EditCalendarScreen( + username: String?, + password: String?, + requiresAuth: Boolean, + + setRequiresAuth: (Boolean) -> Unit, + setUsername: (String) -> Unit, + setPassword: (String) -> Unit, + + supportsAuthentication: Boolean, + url: String?, + title: String?, + color: Int?, + ignoreAlerts: Boolean, + defaultAlarmMinutes: Long?, + defaultAllDayAlarmMinutes: Long?, + ignoreDescription: Boolean, + + setTitle: (String) -> Unit, + setColor: (Int) -> Unit, + setIgnoreAlerts: (Boolean) -> Unit, + setDefaultAlarmMinutes: (String) -> Unit, + setDefaultAllDayAlarmMinutes: (String) -> Unit, + setIgnoreDescription: (Boolean) -> Unit, + + inputValid: Boolean, + modelsDirty: Boolean, + + onDelete: () -> Unit, + onSave: () -> Unit, + onShare: () -> Unit, + onExit: () -> Unit +) { + Scaffold( + topBar = { AppBarComposable( + inputValid, + modelsDirty, + onDelete, + onSave, + onShare, + onExit + )} + ) { paddingValues -> + Column( + Modifier + .verticalScroll(rememberScrollState()) + .padding(paddingValues) + .padding(16.dp) + ) { + SubscriptionSettingsComposable( + url = url, + title = title, + titleChanged = setTitle, + color = color, + colorChanged = setColor, + ignoreAlerts = ignoreAlerts, + ignoreAlertsChanged = setIgnoreAlerts, + defaultAlarmMinutes = defaultAlarmMinutes, + defaultAlarmMinutesChanged = setDefaultAlarmMinutes, + defaultAllDayAlarmMinutes = defaultAllDayAlarmMinutes, + defaultAllDayAlarmMinutesChanged = setDefaultAllDayAlarmMinutes, + ignoreDescription = ignoreDescription, + onIgnoreDescriptionChanged = setIgnoreDescription, + isCreating = false, + modifier = Modifier.fillMaxWidth() + ) + AnimatedVisibility(visible = supportsAuthentication) { + LoginCredentialsComposable( + requiresAuth, + username, + password, + onRequiresAuthChange = setRequiresAuth, + onUsernameChange = setUsername, + onPasswordChange = setPassword + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun AppBarComposable( + valid: Boolean, + modelsDirty: Boolean, + onDelete: () -> Unit, + onSave: () -> Unit, + onShare: () -> Unit, + onExit: () -> Unit +) { + var openDeleteDialog by remember { mutableStateOf(false) } + if (openDeleteDialog) + GenericAlertDialog( + content = { Text(stringResource(R.string.edit_calendar_really_delete)) }, + confirmButton = stringResource(R.string.edit_calendar_delete) to { + onDelete() + openDeleteDialog = false + }, + dismissButton = stringResource(R.string.edit_calendar_cancel) to { + openDeleteDialog = false + }, + ) { openDeleteDialog = false } + var openSaveDismissDialog by remember { mutableStateOf(false) } + if (openSaveDismissDialog) { + GenericAlertDialog( + content = { Text(text = if (valid) + stringResource(R.string.edit_calendar_unsaved_changes) + else + stringResource(R.string.edit_calendar_need_valid_credentials) + ) }, + confirmButton = if (valid) stringResource(R.string.edit_calendar_save) to { + onSave() + openSaveDismissDialog = false + } else stringResource(R.string.edit_calendar_edit) to { + openSaveDismissDialog = false + }, + dismissButton = stringResource(R.string.edit_calendar_dismiss) to onExit + ) { openSaveDismissDialog = false } + } + ExtendedTopAppBar( + navigationIcon = { + IconButton( + onClick = { + if (modelsDirty) + openSaveDismissDialog = true + else + onExit() + } + ) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, null) + } + }, + title = { Text(text = stringResource(R.string.activity_edit_calendar)) }, + actions = { + IconButton(onClick = { onShare() }) { + Icon( + Icons.Filled.Share, + stringResource(R.string.edit_calendar_send_url) + ) + } + IconButton(onClick = { openDeleteDialog = true }) { + Icon(Icons.Filled.Delete, stringResource(R.string.edit_calendar_delete)) + } + AnimatedVisibility(visible = valid && modelsDirty) { + IconButton(onClick = { onSave() }) { + Icon(Icons.Filled.Check, stringResource(R.string.edit_calendar_save)) + } + } + } + ) +} + +@Preview +@Composable +fun EditCalendarScreen_Preview() { + AppTheme { + EditCalendarScreen( + username = "username", + password = "password", + requiresAuth = true, + setRequiresAuth = {}, + setUsername = {}, + setPassword = {}, + + supportsAuthentication = true, + url = "url", + title = "title", + color = 0, + ignoreAlerts = true, + defaultAlarmMinutes = 5L, + defaultAllDayAlarmMinutes = 10L, + ignoreDescription = false, + setTitle = {}, + setColor = {}, + setIgnoreAlerts = {}, + setDefaultAlarmMinutes = {}, + setDefaultAllDayAlarmMinutes = {}, + setIgnoreDescription = {}, + + inputValid = true, + modelsDirty = true, + + onDelete = {}, + onSave = {}, + onShare = {}, + onExit = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/views/EditCalendarActivity.kt b/app/src/main/java/at/bitfire/icsdroid/ui/views/EditCalendarActivity.kt index b3ac8361..abac43dd 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/views/EditCalendarActivity.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/views/EditCalendarActivity.kt @@ -5,45 +5,16 @@ package at.bitfire.icsdroid.ui.views import android.os.Bundle -import android.util.Log import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -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.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Share -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import androidx.core.app.ShareCompat import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.coroutineScope import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.viewModelScope -import at.bitfire.icsdroid.Constants import at.bitfire.icsdroid.R import at.bitfire.icsdroid.db.dao.SubscriptionsDao import at.bitfire.icsdroid.db.entity.Credential @@ -51,12 +22,8 @@ import at.bitfire.icsdroid.db.entity.Subscription import at.bitfire.icsdroid.model.CredentialsModel import at.bitfire.icsdroid.model.EditSubscriptionModel import at.bitfire.icsdroid.model.SubscriptionSettingsModel -import at.bitfire.icsdroid.ui.partials.ExtendedTopAppBar -import at.bitfire.icsdroid.ui.partials.GenericAlertDialog +import at.bitfire.icsdroid.ui.screen.EditCalendarScreen import at.bitfire.icsdroid.ui.theme.setContentThemed -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch class EditCalendarActivity: AppCompatActivity() { @@ -133,7 +100,16 @@ class EditCalendarActivity: AppCompatActivity() { finish() } - EditCalendarComposable() + EditCalendarScreen( + subscriptionSettingsModel, + credentialsModel, + inputValid, + modelsDirty, + { onDelete() }, + { onSave() }, + { onShare() }, + { finish() } + ) } } @@ -188,130 +164,4 @@ class EditCalendarActivity: AppCompatActivity() { } } - /* Composables */ - - @Composable - private fun EditCalendarComposable() { - val url = subscriptionSettingsModel.uiState.url - val title = subscriptionSettingsModel.uiState.title - val color = subscriptionSettingsModel.uiState.color - val ignoreAlerts = subscriptionSettingsModel.uiState.ignoreAlerts - val defaultAlarmMinutes = subscriptionSettingsModel.uiState.defaultAlarmMinutes - val defaultAllDayAlarmMinutes = subscriptionSettingsModel.uiState.defaultAllDayAlarmMinutes - val ignoreDescription = subscriptionSettingsModel.uiState.ignoreDescription - Scaffold( - topBar = { AppBarComposable(inputValid, modelsDirty) } - ) { paddingValues -> - Column( - Modifier - .verticalScroll(rememberScrollState()) - .padding(paddingValues) - .padding(16.dp) - ) { - SubscriptionSettingsComposable( - url = url, - title = title, - titleChanged = subscriptionSettingsModel::setTitle, - color = color, - colorChanged = subscriptionSettingsModel::setColor, - ignoreAlerts = ignoreAlerts, - ignoreAlertsChanged = subscriptionSettingsModel::setIgnoreAlerts, - defaultAlarmMinutes = defaultAlarmMinutes, - defaultAlarmMinutesChanged = subscriptionSettingsModel::setDefaultAlarmMinutes, - defaultAllDayAlarmMinutes = defaultAllDayAlarmMinutes, - defaultAllDayAlarmMinutesChanged = subscriptionSettingsModel::setDefaultAllDayAlarmMinutes, - ignoreDescription = ignoreDescription, - onIgnoreDescriptionChanged = subscriptionSettingsModel::setIgnoreDescription, - isCreating = false, - modifier = Modifier.fillMaxWidth() - ) - val supportsAuthentication = subscriptionSettingsModel.uiState.supportsAuthentication - val requiresAuth: Boolean = credentialsModel.uiState.requiresAuth - val username: String? = credentialsModel.uiState.username - val password: String? = credentialsModel.uiState.password - AnimatedVisibility(visible = supportsAuthentication) { - LoginCredentialsComposable( - requiresAuth, - username, - password, - onRequiresAuthChange = credentialsModel::setRequiresAuth, - onUsernameChange = credentialsModel::setUsername, - onPasswordChange = credentialsModel::setPassword - ) - } - } - } - } - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - private fun AppBarComposable(valid: Boolean, modelsDirty: Boolean) { - var openDeleteDialog by remember { mutableStateOf(false) } - if (openDeleteDialog) - GenericAlertDialog( - content = { Text(stringResource(R.string.edit_calendar_really_delete)) }, - confirmButton = stringResource(R.string.edit_calendar_delete) to { - onDelete() - openDeleteDialog = false - }, - dismissButton = stringResource(R.string.edit_calendar_cancel) to { - openDeleteDialog = false - }, - ) { openDeleteDialog = false } - var openSaveDismissDialog by remember { mutableStateOf(false) } - if (openSaveDismissDialog) { - GenericAlertDialog( - content = { Text(text = if (valid) - stringResource(R.string.edit_calendar_unsaved_changes) - else - stringResource(R.string.edit_calendar_need_valid_credentials) - ) }, - confirmButton = if (valid) stringResource(R.string.edit_calendar_save) to { - onSave() - openSaveDismissDialog = false - } else stringResource(R.string.edit_calendar_edit) to { - openSaveDismissDialog = false - }, - dismissButton = stringResource(R.string.edit_calendar_dismiss) to ::finish - ) { openSaveDismissDialog = false } - } - ExtendedTopAppBar( - navigationIcon = { - IconButton( - onClick = { - if (modelsDirty) - openSaveDismissDialog = true - else - finish() - } - ) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, null) - } - }, - title = { Text(text = stringResource(R.string.activity_edit_calendar)) }, - actions = { - IconButton(onClick = { onShare() }) { - Icon( - Icons.Filled.Share, - stringResource(R.string.edit_calendar_send_url) - ) - } - IconButton(onClick = { openDeleteDialog = true }) { - Icon(Icons.Filled.Delete, stringResource(R.string.edit_calendar_delete)) - } - AnimatedVisibility(visible = valid && modelsDirty) { - IconButton(onClick = { onSave() }) { - Icon(Icons.Filled.Check, stringResource(R.string.edit_calendar_save)) - } - } - } - ) - } - - @Preview - @Composable - fun TopBarComposable_Preview() { - AppBarComposable(valid = true, modelsDirty = true) - } - } \ No newline at end of file diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/views/SubscriptionSettingsComposable.kt b/app/src/main/java/at/bitfire/icsdroid/ui/views/SubscriptionSettingsComposable.kt index be648bc8..25c50ca2 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/views/SubscriptionSettingsComposable.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/views/SubscriptionSettingsComposable.kt @@ -30,11 +30,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import at.bitfire.icsdroid.R import at.bitfire.icsdroid.calendar.LocalCalendar import at.bitfire.icsdroid.ui.partials.ColorPickerDialog import at.bitfire.icsdroid.ui.partials.SwitchSetting +import at.bitfire.icsdroid.ui.theme.AppTheme @Composable fun SubscriptionSettingsComposable( @@ -184,8 +186,31 @@ fun SubscriptionSettingsComposable( SwitchSetting( title = stringResource(R.string.add_calendar_description_title), description = stringResource(R.string.add_calendar_description_summary), - checked = ignoreDescription ?: false, + checked = ignoreDescription, onCheckedChange = onIgnoreDescriptionChanged ) } +} + +@Preview +@Composable +fun SubscriptionSettingsComposable_Preview() { + AppTheme { + SubscriptionSettingsComposable( + url = "url", + title = "title", + titleChanged = {}, + color = 0, + colorChanged = {}, + ignoreAlerts = true, + ignoreAlertsChanged = {}, + defaultAlarmMinutes = 20L, + defaultAlarmMinutesChanged = {}, + defaultAllDayAlarmMinutes = 10L, + defaultAllDayAlarmMinutesChanged = {}, + ignoreDescription = false, + onIgnoreDescriptionChanged = {}, + isCreating = true + ) + } } \ No newline at end of file From b92aabe11a65c6e9aa7e6553194eb68aab72823d Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Wed, 8 May 2024 10:20:07 +0200 Subject: [PATCH 02/10] Create composables using models for clarity --- .../icsdroid/ui/screen/EditCalendarScreen.kt | 91 ++----------------- .../ui/views/CredentialsComposable.kt | 15 +++ .../views/SubscriptionSettingsComposable.kt | 27 ++++++ 3 files changed, 52 insertions(+), 81 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt b/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt index a80d4931..02f081df 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel import at.bitfire.icsdroid.R import at.bitfire.icsdroid.model.CredentialsModel import at.bitfire.icsdroid.model.SubscriptionSettingsModel @@ -46,29 +47,10 @@ fun EditCalendarScreen( onExit: () -> Unit ) { EditCalendarScreen( - credentialsModel.uiState.username, - credentialsModel.uiState.password, - credentialsModel.uiState.requiresAuth, - - credentialsModel::setRequiresAuth, - credentialsModel::setUsername, - credentialsModel::setPassword, - + subscriptionSettingsModel, subscriptionSettingsModel.uiState.supportsAuthentication, - subscriptionSettingsModel.uiState.url, - subscriptionSettingsModel.uiState.title, - subscriptionSettingsModel.uiState.color, - subscriptionSettingsModel.uiState.ignoreAlerts, - subscriptionSettingsModel.uiState.defaultAlarmMinutes, - subscriptionSettingsModel.uiState.defaultAllDayAlarmMinutes, - subscriptionSettingsModel.uiState.ignoreDescription, - subscriptionSettingsModel::setTitle, - subscriptionSettingsModel::setColor, - subscriptionSettingsModel::setIgnoreAlerts, - subscriptionSettingsModel::setDefaultAlarmMinutes, - subscriptionSettingsModel::setDefaultAllDayAlarmMinutes, - subscriptionSettingsModel::setIgnoreDescription, + credentialsModel, inputValid, modelsDirty, @@ -82,29 +64,10 @@ fun EditCalendarScreen( @Composable private fun EditCalendarScreen( - username: String?, - password: String?, - requiresAuth: Boolean, - - setRequiresAuth: (Boolean) -> Unit, - setUsername: (String) -> Unit, - setPassword: (String) -> Unit, - + subscriptionSettingsModel: SubscriptionSettingsModel, supportsAuthentication: Boolean, - url: String?, - title: String?, - color: Int?, - ignoreAlerts: Boolean, - defaultAlarmMinutes: Long?, - defaultAllDayAlarmMinutes: Long?, - ignoreDescription: Boolean, - setTitle: (String) -> Unit, - setColor: (Int) -> Unit, - setIgnoreAlerts: (Boolean) -> Unit, - setDefaultAlarmMinutes: (String) -> Unit, - setDefaultAllDayAlarmMinutes: (String) -> Unit, - setIgnoreDescription: (Boolean) -> Unit, + credentialsModel: CredentialsModel, inputValid: Boolean, modelsDirty: Boolean, @@ -131,30 +94,13 @@ private fun EditCalendarScreen( .padding(16.dp) ) { SubscriptionSettingsComposable( - url = url, - title = title, - titleChanged = setTitle, - color = color, - colorChanged = setColor, - ignoreAlerts = ignoreAlerts, - ignoreAlertsChanged = setIgnoreAlerts, - defaultAlarmMinutes = defaultAlarmMinutes, - defaultAlarmMinutesChanged = setDefaultAlarmMinutes, - defaultAllDayAlarmMinutes = defaultAllDayAlarmMinutes, - defaultAllDayAlarmMinutesChanged = setDefaultAllDayAlarmMinutes, - ignoreDescription = ignoreDescription, - onIgnoreDescriptionChanged = setIgnoreDescription, + subscriptionSettingsModel, isCreating = false, modifier = Modifier.fillMaxWidth() ) AnimatedVisibility(visible = supportsAuthentication) { LoginCredentialsComposable( - requiresAuth, - username, - password, - onRequiresAuthChange = setRequiresAuth, - onUsernameChange = setUsername, - onPasswordChange = setPassword + credentialsModel ) } } @@ -238,27 +184,10 @@ private fun AppBarComposable( fun EditCalendarScreen_Preview() { AppTheme { EditCalendarScreen( - username = "username", - password = "password", - requiresAuth = true, - setRequiresAuth = {}, - setUsername = {}, - setPassword = {}, - + subscriptionSettingsModel = viewModel(), supportsAuthentication = true, - url = "url", - title = "title", - color = 0, - ignoreAlerts = true, - defaultAlarmMinutes = 5L, - defaultAllDayAlarmMinutes = 10L, - ignoreDescription = false, - setTitle = {}, - setColor = {}, - setIgnoreAlerts = {}, - setDefaultAlarmMinutes = {}, - setDefaultAllDayAlarmMinutes = {}, - setIgnoreDescription = {}, + + credentialsModel = viewModel(), inputValid = true, modelsDirty = true, diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/views/CredentialsComposable.kt b/app/src/main/java/at/bitfire/icsdroid/ui/views/CredentialsComposable.kt index 3fd1b463..9c0239e1 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/views/CredentialsComposable.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/views/CredentialsComposable.kt @@ -33,6 +33,21 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import at.bitfire.icsdroid.R +import at.bitfire.icsdroid.model.CredentialsModel + +@Composable +fun LoginCredentialsComposable( + model: CredentialsModel +) { + LoginCredentialsComposable( + model.uiState.requiresAuth, + model.uiState.username, + model.uiState.password, + onRequiresAuthChange = model::setRequiresAuth, + onUsernameChange = model::setUsername, + onPasswordChange = model::setPassword + ) +} @Composable fun LoginCredentialsComposable( diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/views/SubscriptionSettingsComposable.kt b/app/src/main/java/at/bitfire/icsdroid/ui/views/SubscriptionSettingsComposable.kt index 25c50ca2..dc0d4560 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/views/SubscriptionSettingsComposable.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/views/SubscriptionSettingsComposable.kt @@ -32,12 +32,39 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel import at.bitfire.icsdroid.R import at.bitfire.icsdroid.calendar.LocalCalendar +import at.bitfire.icsdroid.model.SubscriptionSettingsModel import at.bitfire.icsdroid.ui.partials.ColorPickerDialog import at.bitfire.icsdroid.ui.partials.SwitchSetting import at.bitfire.icsdroid.ui.theme.AppTheme +@Composable +fun SubscriptionSettingsComposable( + model: SubscriptionSettingsModel = viewModel(), + isCreating: Boolean = false, + modifier: Modifier = Modifier +) { + SubscriptionSettingsComposable( + url = model.uiState.url, + title = model.uiState.title, + titleChanged = model::setTitle, + color = model.uiState.color, + colorChanged = model::setColor, + ignoreAlerts = model.uiState.ignoreAlerts, + ignoreAlertsChanged = model::setIgnoreAlerts, + defaultAlarmMinutes = model.uiState.defaultAlarmMinutes, + defaultAlarmMinutesChanged = model::setDefaultAlarmMinutes, + defaultAllDayAlarmMinutes = model.uiState.defaultAllDayAlarmMinutes, + defaultAllDayAlarmMinutesChanged = model::setDefaultAllDayAlarmMinutes, + ignoreDescription = model.uiState.ignoreDescription, + onIgnoreDescriptionChanged = model::setIgnoreDescription, + isCreating = isCreating, + modifier = modifier + ) +} + @Composable fun SubscriptionSettingsComposable( url: String?, From 644d3c2e576b1ffb0da269cff1fba775441f2d46 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Wed, 8 May 2024 10:20:21 +0200 Subject: [PATCH 03/10] Create composables using models for clarity --- .../java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt | 4 ++-- .../icsdroid/ui/views/SubscriptionSettingsComposable.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt b/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt index 02f081df..dc5b61cd 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt @@ -94,9 +94,9 @@ private fun EditCalendarScreen( .padding(16.dp) ) { SubscriptionSettingsComposable( + modifier = Modifier.fillMaxWidth(), subscriptionSettingsModel, - isCreating = false, - modifier = Modifier.fillMaxWidth() + isCreating = false ) AnimatedVisibility(visible = supportsAuthentication) { LoginCredentialsComposable( diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/views/SubscriptionSettingsComposable.kt b/app/src/main/java/at/bitfire/icsdroid/ui/views/SubscriptionSettingsComposable.kt index dc0d4560..947905a9 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/views/SubscriptionSettingsComposable.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/views/SubscriptionSettingsComposable.kt @@ -42,9 +42,9 @@ import at.bitfire.icsdroid.ui.theme.AppTheme @Composable fun SubscriptionSettingsComposable( + modifier: Modifier = Modifier, model: SubscriptionSettingsModel = viewModel(), - isCreating: Boolean = false, - modifier: Modifier = Modifier + isCreating: Boolean = false ) { SubscriptionSettingsComposable( url = model.uiState.url, From ab4ff744ef4fab24ae6a9985e00dc3bd5e0a2c32 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Wed, 8 May 2024 13:39:18 +0200 Subject: [PATCH 04/10] Move computed properties to new EditCalendarModel --- .../icsdroid/model/EditCalendarModel.kt | 96 ++++++++++++++++ .../icsdroid/ui/screen/EditCalendarScreen.kt | 68 ++---------- .../icsdroid/ui/views/EditCalendarActivity.kt | 103 ++---------------- 3 files changed, 117 insertions(+), 150 deletions(-) create mode 100644 app/src/main/java/at/bitfire/icsdroid/model/EditCalendarModel.kt diff --git a/app/src/main/java/at/bitfire/icsdroid/model/EditCalendarModel.kt b/app/src/main/java/at/bitfire/icsdroid/model/EditCalendarModel.kt new file mode 100644 index 00000000..eb116e0e --- /dev/null +++ b/app/src/main/java/at/bitfire/icsdroid/model/EditCalendarModel.kt @@ -0,0 +1,96 @@ +package at.bitfire.icsdroid.model + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.bitfire.icsdroid.db.dao.SubscriptionsDao +import at.bitfire.icsdroid.db.entity.Credential +import at.bitfire.icsdroid.db.entity.Subscription +import kotlinx.coroutines.launch + +class EditCalendarModel( + val editSubscriptionModel: EditSubscriptionModel, + val subscriptionSettingsModel: SubscriptionSettingsModel, + val credentialsModel: CredentialsModel +): ViewModel() { + + private var initialSubscription: Subscription? = null + private var initialCredential: Credential? = null + private var initialRequiresAuthValue: Boolean? = null + + init { + // Initialise view models and save their initial state + viewModelScope.launch { + editSubscriptionModel.subscriptionWithCredential.collect { data -> + if (data != null) + onSubscriptionLoaded(data) + } + } + } + + val inputValid: Boolean + @Composable + get() = remember(subscriptionSettingsModel.uiState, credentialsModel.uiState) { + val title = subscriptionSettingsModel.uiState.title + val requiresAuth = credentialsModel.uiState.requiresAuth + val username = credentialsModel.uiState.username + val password = credentialsModel.uiState.password + + val titleOK = !title.isNullOrBlank() + val authOK = if (requiresAuth) + !username.isNullOrBlank() && !password.isNullOrBlank() + else + true + titleOK && authOK + } + + val modelsDirty: Boolean + @Composable + get() = remember(subscriptionSettingsModel.uiState, credentialsModel.uiState) { + val requiresAuth = credentialsModel.uiState.requiresAuth + + val credentialsDirty = initialRequiresAuthValue != requiresAuth || + initialCredential?.let { !credentialsModel.equalsCredential(it) } ?: false + val subscriptionsDirty = initialSubscription?.let { + !subscriptionSettingsModel.equalsSubscription(it) + } ?: false + + credentialsDirty || subscriptionsDirty + } + + /** + * Initialise view models and remember their initial state + */ + fun onSubscriptionLoaded(subscriptionWithCredential: SubscriptionsDao.SubscriptionWithCredential) { + val subscription = subscriptionWithCredential.subscription + + subscriptionSettingsModel.setUrl(subscription.url.toString()) + subscription.displayName.let { + subscriptionSettingsModel.setTitle(it) + } + subscription.color.let(subscriptionSettingsModel::setColor) + subscription.ignoreEmbeddedAlerts.let(subscriptionSettingsModel::setIgnoreAlerts) + subscription.defaultAlarmMinutes?.toString().let(subscriptionSettingsModel::setDefaultAlarmMinutes) + subscription.defaultAllDayAlarmMinutes?.toString()?.let(subscriptionSettingsModel::setDefaultAllDayAlarmMinutes) + subscription.ignoreDescription.let(subscriptionSettingsModel::setIgnoreDescription) + + val credential = subscriptionWithCredential.credential + val requiresAuth = credential != null + credentialsModel.setRequiresAuth(requiresAuth) + + if (credential != null) { + credential.username.let(credentialsModel::setUsername) + credential.password.let(credentialsModel::setPassword) + } + + // Save state, before user makes changes + initialSubscription = subscription + initialCredential = credential + initialRequiresAuthValue = credentialsModel.uiState.requiresAuth + } + + fun onSave() = editSubscriptionModel.updateSubscription(subscriptionSettingsModel, credentialsModel) + + fun onDelete() = editSubscriptionModel.removeSubscription() +} diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt b/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt index dc5b61cd..c7de362a 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt @@ -27,8 +27,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import at.bitfire.icsdroid.R -import at.bitfire.icsdroid.model.CredentialsModel -import at.bitfire.icsdroid.model.SubscriptionSettingsModel +import at.bitfire.icsdroid.model.EditCalendarModel import at.bitfire.icsdroid.ui.partials.ExtendedTopAppBar import at.bitfire.icsdroid.ui.partials.GenericAlertDialog import at.bitfire.icsdroid.ui.theme.AppTheme @@ -37,52 +36,16 @@ import at.bitfire.icsdroid.ui.views.SubscriptionSettingsComposable @Composable fun EditCalendarScreen( - subscriptionSettingsModel: SubscriptionSettingsModel, - credentialsModel: CredentialsModel, - inputValid: Boolean, - modelsDirty: Boolean, - onDelete: () -> Unit, - onSave: () -> Unit, - onShare: () -> Unit, - onExit: () -> Unit -) { - EditCalendarScreen( - subscriptionSettingsModel, - subscriptionSettingsModel.uiState.supportsAuthentication, - - credentialsModel, - - inputValid, - modelsDirty, - - onDelete, - onSave, - onShare, - onExit - ) -} - -@Composable -private fun EditCalendarScreen( - subscriptionSettingsModel: SubscriptionSettingsModel, - supportsAuthentication: Boolean, - - credentialsModel: CredentialsModel, - - inputValid: Boolean, - modelsDirty: Boolean, - - onDelete: () -> Unit, - onSave: () -> Unit, + editCalendarModel: EditCalendarModel = viewModel(), onShare: () -> Unit, onExit: () -> Unit ) { Scaffold( topBar = { AppBarComposable( - inputValid, - modelsDirty, - onDelete, - onSave, + editCalendarModel.inputValid, + editCalendarModel.modelsDirty, + editCalendarModel::onDelete, + editCalendarModel::onSave, onShare, onExit )} @@ -95,12 +58,14 @@ private fun EditCalendarScreen( ) { SubscriptionSettingsComposable( modifier = Modifier.fillMaxWidth(), - subscriptionSettingsModel, + editCalendarModel.subscriptionSettingsModel, isCreating = false ) - AnimatedVisibility(visible = supportsAuthentication) { + AnimatedVisibility( + visible = editCalendarModel.subscriptionSettingsModel.uiState.supportsAuthentication + ) { LoginCredentialsComposable( - credentialsModel + editCalendarModel.credentialsModel ) } } @@ -184,16 +149,7 @@ private fun AppBarComposable( fun EditCalendarScreen_Preview() { AppTheme { EditCalendarScreen( - subscriptionSettingsModel = viewModel(), - supportsAuthentication = true, - - credentialsModel = viewModel(), - - inputValid = true, - modelsDirty = true, - - onDelete = {}, - onSave = {}, + editCalendarModel = viewModel(), onShare = {}, onExit = {} ) diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/views/EditCalendarActivity.kt b/app/src/main/java/at/bitfire/icsdroid/ui/views/EditCalendarActivity.kt index abac43dd..d42191c8 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/views/EditCalendarActivity.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/views/EditCalendarActivity.kt @@ -8,18 +8,13 @@ import android.os.Bundle import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.core.app.ShareCompat import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import at.bitfire.icsdroid.R -import at.bitfire.icsdroid.db.dao.SubscriptionsDao -import at.bitfire.icsdroid.db.entity.Credential -import at.bitfire.icsdroid.db.entity.Subscription import at.bitfire.icsdroid.model.CredentialsModel +import at.bitfire.icsdroid.model.EditCalendarModel import at.bitfire.icsdroid.model.EditSubscriptionModel import at.bitfire.icsdroid.model.SubscriptionSettingsModel import at.bitfire.icsdroid.ui.screen.EditCalendarScreen @@ -33,45 +28,9 @@ class EditCalendarActivity: AppCompatActivity() { } private val subscriptionSettingsModel by viewModels() - private var initialSubscription: Subscription? = null private val credentialsModel by viewModels() - private var initialCredential: Credential? = null - private var initialRequiresAuthValue: Boolean? = null - // Whether user made changes are legal - private val inputValid: Boolean - @Composable - get() = remember(subscriptionSettingsModel.uiState, credentialsModel.uiState) { - val title = subscriptionSettingsModel.uiState.title - val requiresAuth = credentialsModel.uiState.requiresAuth - val username = credentialsModel.uiState.username - val password = credentialsModel.uiState.password - - val titleOK = !title.isNullOrBlank() - val authOK = if (requiresAuth) - !username.isNullOrBlank() && !password.isNullOrBlank() - else - true - titleOK && authOK - } - - // Whether unsaved changes exist - private val modelsDirty: Boolean - @Composable - get() = remember(subscriptionSettingsModel.uiState, credentialsModel.uiState) { - val requiresAuth = credentialsModel.uiState.requiresAuth - - val credentialsDirty = initialRequiresAuthValue != requiresAuth || - initialCredential?.let { !credentialsModel.equalsCredential(it) } ?: false - val subscriptionsDirty = initialSubscription?.let { - !subscriptionSettingsModel.equalsSubscription(it) - } ?: false - - credentialsDirty || subscriptionsDirty - } - - - private val model by viewModels { + private val editSubscriptionModel by viewModels { object: ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { @@ -84,16 +43,8 @@ class EditCalendarActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - // Initialise view models and save their initial state - lifecycleScope.launch { - model.subscriptionWithCredential.flowWithLifecycle(lifecycle).collect { data -> - if (data != null) - onSubscriptionLoaded(data) - } - } - setContentThemed { - val successMessage = model.uiState.successMessage + val successMessage = editSubscriptionModel.uiState.successMessage // show success message successMessage?.let { Toast.makeText(this, it, Toast.LENGTH_LONG).show() @@ -101,59 +52,23 @@ class EditCalendarActivity: AppCompatActivity() { } EditCalendarScreen( - subscriptionSettingsModel, - credentialsModel, - inputValid, - modelsDirty, - { onDelete() }, - { onSave() }, + EditCalendarModel( + editSubscriptionModel, + subscriptionSettingsModel, + credentialsModel + ), { onShare() }, { finish() } ) } } - /** - * Initialise view models and remember their initial state - */ - private fun onSubscriptionLoaded(subscriptionWithCredential: SubscriptionsDao.SubscriptionWithCredential) { - val subscription = subscriptionWithCredential.subscription - - subscriptionSettingsModel.setUrl(subscription.url.toString()) - subscription.displayName.let { - subscriptionSettingsModel.setTitle(it) - } - subscription.color.let(subscriptionSettingsModel::setColor) - subscription.ignoreEmbeddedAlerts.let(subscriptionSettingsModel::setIgnoreAlerts) - subscription.defaultAlarmMinutes?.toString().let(subscriptionSettingsModel::setDefaultAlarmMinutes) - subscription.defaultAllDayAlarmMinutes?.toString()?.let(subscriptionSettingsModel::setDefaultAllDayAlarmMinutes) - subscription.ignoreDescription.let(subscriptionSettingsModel::setIgnoreDescription) - - val credential = subscriptionWithCredential.credential - val requiresAuth = credential != null - credentialsModel.setRequiresAuth(requiresAuth) - - if (credential != null) { - credential.username.let(credentialsModel::setUsername) - credential.password.let(credentialsModel::setPassword) - } - - // Save state, before user makes changes - initialSubscription = subscription - initialCredential = credential - initialRequiresAuthValue = credentialsModel.uiState.requiresAuth - } - /* user actions */ - private fun onSave() = model.updateSubscription(subscriptionSettingsModel, credentialsModel) - - private fun onDelete() = model.removeSubscription() - private fun onShare() { lifecycleScope.launch { - model.subscriptionWithCredential.value?.let { (subscription, _) -> + editSubscriptionModel.subscriptionWithCredential.value?.let { (subscription, _) -> ShareCompat.IntentBuilder(this@EditCalendarActivity) .setSubject(subscription.displayName) .setText(subscription.url.toString()) From f1289c74696f84c44c993becc41b7148bdc21f7f Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Wed, 8 May 2024 13:45:02 +0200 Subject: [PATCH 05/10] Move toast with success message --- .../at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt | 7 +++++++ .../at/bitfire/icsdroid/ui/views/EditCalendarActivity.kt | 9 --------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt b/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt index c7de362a..e67f7816 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt @@ -1,5 +1,6 @@ package at.bitfire.icsdroid.ui.screen +import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -22,6 +23,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -40,6 +42,11 @@ fun EditCalendarScreen( onShare: () -> Unit, onExit: () -> Unit ) { + // show success message + editCalendarModel.editSubscriptionModel.uiState.successMessage?.let { successMessage -> + Toast.makeText(LocalContext.current, successMessage, Toast.LENGTH_LONG).show() + onExit() + } Scaffold( topBar = { AppBarComposable( editCalendarModel.inputValid, diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/views/EditCalendarActivity.kt b/app/src/main/java/at/bitfire/icsdroid/ui/views/EditCalendarActivity.kt index d42191c8..f2adb0d4 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/views/EditCalendarActivity.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/views/EditCalendarActivity.kt @@ -5,7 +5,6 @@ package at.bitfire.icsdroid.ui.views import android.os.Bundle -import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ShareCompat @@ -42,15 +41,7 @@ class EditCalendarActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentThemed { - val successMessage = editSubscriptionModel.uiState.successMessage - // show success message - successMessage?.let { - Toast.makeText(this, it, Toast.LENGTH_LONG).show() - finish() - } - EditCalendarScreen( EditCalendarModel( editSubscriptionModel, From b62f51370d943cae55237dcaa80aaab4d51180da Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Wed, 8 May 2024 14:09:40 +0200 Subject: [PATCH 06/10] Move model creation --- .../icsdroid/ui/screen/EditCalendarScreen.kt | 50 +++++++++++------ .../icsdroid/ui/views/EditCalendarActivity.kt | 55 ++++--------------- 2 files changed, 46 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt b/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt index e67f7816..723cc6ca 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt @@ -1,5 +1,6 @@ package at.bitfire.icsdroid.ui.screen +import android.app.Application import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column @@ -25,23 +26,35 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import at.bitfire.icsdroid.R +import at.bitfire.icsdroid.db.entity.Subscription +import at.bitfire.icsdroid.model.CredentialsModel import at.bitfire.icsdroid.model.EditCalendarModel +import at.bitfire.icsdroid.model.EditSubscriptionModel +import at.bitfire.icsdroid.model.SubscriptionSettingsModel import at.bitfire.icsdroid.ui.partials.ExtendedTopAppBar import at.bitfire.icsdroid.ui.partials.GenericAlertDialog -import at.bitfire.icsdroid.ui.theme.AppTheme import at.bitfire.icsdroid.ui.views.LoginCredentialsComposable import at.bitfire.icsdroid.ui.views.SubscriptionSettingsComposable @Composable fun EditCalendarScreen( - editCalendarModel: EditCalendarModel = viewModel(), - onShare: () -> Unit, + application: Application, + subscriptionId: Long, + onShare: (subscription: Subscription) -> Unit, onExit: () -> Unit ) { + val credentialsModel: CredentialsModel = viewModel() + val subscriptionSettingsModel: SubscriptionSettingsModel = viewModel() + val editSubscriptionModel: EditSubscriptionModel = viewModel { + EditSubscriptionModel(application, subscriptionId) + } + val editCalendarModel: EditCalendarModel = viewModel { + EditCalendarModel(editSubscriptionModel, subscriptionSettingsModel, credentialsModel) + } + // show success message editCalendarModel.editSubscriptionModel.uiState.successMessage?.let { successMessage -> Toast.makeText(LocalContext.current, successMessage, Toast.LENGTH_LONG).show() @@ -53,7 +66,11 @@ fun EditCalendarScreen( editCalendarModel.modelsDirty, editCalendarModel::onDelete, editCalendarModel::onSave, - onShare, + { + editSubscriptionModel.subscriptionWithCredential.value?.let { + onShare(it.subscription) + } + }, onExit )} ) { paddingValues -> @@ -151,14 +168,15 @@ private fun AppBarComposable( ) } -@Preview -@Composable -fun EditCalendarScreen_Preview() { - AppTheme { - EditCalendarScreen( - editCalendarModel = viewModel(), - onShare = {}, - onExit = {} - ) - } -} \ No newline at end of file +//@Preview +//@Composable +//fun EditCalendarScreen_Preview() { +// AppTheme { +// EditCalendarScreen( +// , +// editCalendarModel = viewModel(), +// onShare = {}, +// onExit = {} +// ) +// } +//} \ No newline at end of file diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/views/EditCalendarActivity.kt b/app/src/main/java/at/bitfire/icsdroid/ui/views/EditCalendarActivity.kt index f2adb0d4..721818c4 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/views/EditCalendarActivity.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/views/EditCalendarActivity.kt @@ -5,69 +5,38 @@ package at.bitfire.icsdroid.ui.views import android.os.Bundle -import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ShareCompat -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope import at.bitfire.icsdroid.R -import at.bitfire.icsdroid.model.CredentialsModel -import at.bitfire.icsdroid.model.EditCalendarModel -import at.bitfire.icsdroid.model.EditSubscriptionModel -import at.bitfire.icsdroid.model.SubscriptionSettingsModel +import at.bitfire.icsdroid.db.entity.Subscription import at.bitfire.icsdroid.ui.screen.EditCalendarScreen import at.bitfire.icsdroid.ui.theme.setContentThemed -import kotlinx.coroutines.launch class EditCalendarActivity: AppCompatActivity() { companion object { + // Used by intents only const val EXTRA_SUBSCRIPTION_ID = "subscriptionId" } - private val subscriptionSettingsModel by viewModels() - private val credentialsModel by viewModels() - - private val editSubscriptionModel by viewModels { - object: ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - val subscriptionId = intent.getLongExtra(EXTRA_SUBSCRIPTION_ID, -1) - return EditSubscriptionModel(application, subscriptionId) as T - } - } - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentThemed { EditCalendarScreen( - EditCalendarModel( - editSubscriptionModel, - subscriptionSettingsModel, - credentialsModel - ), - { onShare() }, + application, + subscriptionId = intent.getLongExtra(EXTRA_SUBSCRIPTION_ID, -1), + { onShare(it) }, { finish() } ) } } - - /* user actions */ - - private fun onShare() { - lifecycleScope.launch { - editSubscriptionModel.subscriptionWithCredential.value?.let { (subscription, _) -> - ShareCompat.IntentBuilder(this@EditCalendarActivity) - .setSubject(subscription.displayName) - .setText(subscription.url.toString()) - .setType("text/plain") - .setChooserTitle(R.string.edit_calendar_send_url) - .startChooser() - } - } - } + private fun onShare(subscription: Subscription) = + ShareCompat.IntentBuilder(this) + .setSubject(subscription.displayName) + .setText(subscription.url.toString()) + .setType("text/plain") + .setChooserTitle(R.string.edit_calendar_send_url) + .startChooser() } \ No newline at end of file From ae3df05495195c8f29e805f11839c5f50a66c9ac Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Wed, 8 May 2024 14:58:54 +0200 Subject: [PATCH 07/10] Minor adjustments - Separate concerns into two composables - move save and delete invocations to composable - fix compose Preview --- .../icsdroid/model/EditCalendarModel.kt | 5 +- .../icsdroid/ui/screen/EditCalendarScreen.kt | 98 +++++++++++++------ 2 files changed, 69 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/model/EditCalendarModel.kt b/app/src/main/java/at/bitfire/icsdroid/model/EditCalendarModel.kt index eb116e0e..71779884 100644 --- a/app/src/main/java/at/bitfire/icsdroid/model/EditCalendarModel.kt +++ b/app/src/main/java/at/bitfire/icsdroid/model/EditCalendarModel.kt @@ -62,7 +62,7 @@ class EditCalendarModel( /** * Initialise view models and remember their initial state */ - fun onSubscriptionLoaded(subscriptionWithCredential: SubscriptionsDao.SubscriptionWithCredential) { + private fun onSubscriptionLoaded(subscriptionWithCredential: SubscriptionsDao.SubscriptionWithCredential) { val subscription = subscriptionWithCredential.subscription subscriptionSettingsModel.setUrl(subscription.url.toString()) @@ -90,7 +90,4 @@ class EditCalendarModel( initialRequiresAuthValue = credentialsModel.uiState.requiresAuth } - fun onSave() = editSubscriptionModel.updateSubscription(subscriptionSettingsModel, credentialsModel) - - fun onDelete() = editSubscriptionModel.removeSubscription() } diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt b/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt index 723cc6ca..a3189c17 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import at.bitfire.icsdroid.R @@ -36,6 +37,7 @@ import at.bitfire.icsdroid.model.EditSubscriptionModel import at.bitfire.icsdroid.model.SubscriptionSettingsModel import at.bitfire.icsdroid.ui.partials.ExtendedTopAppBar import at.bitfire.icsdroid.ui.partials.GenericAlertDialog +import at.bitfire.icsdroid.ui.theme.AppTheme import at.bitfire.icsdroid.ui.views.LoginCredentialsComposable import at.bitfire.icsdroid.ui.views.SubscriptionSettingsComposable @@ -44,7 +46,7 @@ fun EditCalendarScreen( application: Application, subscriptionId: Long, onShare: (subscription: Subscription) -> Unit, - onExit: () -> Unit + onExit: () -> Unit = {} ) { val credentialsModel: CredentialsModel = viewModel() val subscriptionSettingsModel: SubscriptionSettingsModel = viewModel() @@ -54,25 +56,55 @@ fun EditCalendarScreen( val editCalendarModel: EditCalendarModel = viewModel { EditCalendarModel(editSubscriptionModel, subscriptionSettingsModel, credentialsModel) } - + EditCalendarScreen( + inputValid = editCalendarModel.inputValid, + modelsDirty = editCalendarModel.modelsDirty, + successMessage = editCalendarModel.editSubscriptionModel.uiState.successMessage, + onDelete = editSubscriptionModel::removeSubscription, + onSave = { + editSubscriptionModel.updateSubscription(subscriptionSettingsModel, credentialsModel) + }, + { + editSubscriptionModel.subscriptionWithCredential.value?.let { + onShare(it.subscription) + } + }, + onExit, + editCalendarModel.subscriptionSettingsModel.uiState.supportsAuthentication, + editCalendarModel.subscriptionSettingsModel, + editCalendarModel.credentialsModel + ) +} +@Composable +fun EditCalendarScreen( + inputValid: Boolean, + modelsDirty: Boolean, + successMessage: String?, + onDelete: () -> Unit, + onSave: () -> Unit, + onShare: () -> Unit, + onExit: () -> Unit, + supportsAuthentication: Boolean, + subscriptionSettingsModel: SubscriptionSettingsModel, + credentialsModel: CredentialsModel +) { // show success message - editCalendarModel.editSubscriptionModel.uiState.successMessage?.let { successMessage -> + successMessage?.let { Toast.makeText(LocalContext.current, successMessage, Toast.LENGTH_LONG).show() onExit() } + Scaffold( - topBar = { AppBarComposable( - editCalendarModel.inputValid, - editCalendarModel.modelsDirty, - editCalendarModel::onDelete, - editCalendarModel::onSave, - { - editSubscriptionModel.subscriptionWithCredential.value?.let { - onShare(it.subscription) - } - }, - onExit - )} + topBar = { + AppBarComposable( + inputValid, + modelsDirty, + onDelete, + onSave, + onShare, + onExit + ) + } ) { paddingValues -> Column( Modifier @@ -82,14 +114,14 @@ fun EditCalendarScreen( ) { SubscriptionSettingsComposable( modifier = Modifier.fillMaxWidth(), - editCalendarModel.subscriptionSettingsModel, + subscriptionSettingsModel, isCreating = false ) AnimatedVisibility( - visible = editCalendarModel.subscriptionSettingsModel.uiState.supportsAuthentication + visible = supportsAuthentication ) { LoginCredentialsComposable( - editCalendarModel.credentialsModel + credentialsModel ) } } @@ -168,15 +200,21 @@ private fun AppBarComposable( ) } -//@Preview -//@Composable -//fun EditCalendarScreen_Preview() { -// AppTheme { -// EditCalendarScreen( -// , -// editCalendarModel = viewModel(), -// onShare = {}, -// onExit = {} -// ) -// } -//} \ No newline at end of file +@Preview +@Composable +fun EditCalendarScreen_Preview() { + AppTheme { + EditCalendarScreen( + inputValid = true, + modelsDirty = false, + successMessage = "yay!", + onDelete = {}, + onSave = {}, + onShare = {}, + onExit = {}, + supportsAuthentication = true, + subscriptionSettingsModel = viewModel(), + credentialsModel = viewModel() + ) + } +} \ No newline at end of file From 8af50a376c109600f21d9c524889e7a317427e87 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Wed, 8 May 2024 15:12:55 +0200 Subject: [PATCH 08/10] Add documentation --- .../java/at/bitfire/icsdroid/model/EditCalendarModel.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/at/bitfire/icsdroid/model/EditCalendarModel.kt b/app/src/main/java/at/bitfire/icsdroid/model/EditCalendarModel.kt index 71779884..f083b837 100644 --- a/app/src/main/java/at/bitfire/icsdroid/model/EditCalendarModel.kt +++ b/app/src/main/java/at/bitfire/icsdroid/model/EditCalendarModel.kt @@ -29,6 +29,9 @@ class EditCalendarModel( } } + /** + * Whether user input is error free + */ val inputValid: Boolean @Composable get() = remember(subscriptionSettingsModel.uiState, credentialsModel.uiState) { @@ -45,6 +48,9 @@ class EditCalendarModel( titleOK && authOK } + /** + * Whether there are unsaved user changes + */ val modelsDirty: Boolean @Composable get() = remember(subscriptionSettingsModel.uiState, credentialsModel.uiState) { From 8a2a73f905d846cdde2fe11eae0b848bbf954dff Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Wed, 8 May 2024 15:53:49 +0200 Subject: [PATCH 09/10] Name the remaining arguments --- .../icsdroid/ui/screen/EditCalendarScreen.kt | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt b/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt index a3189c17..9d9779f7 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt @@ -64,15 +64,15 @@ fun EditCalendarScreen( onSave = { editSubscriptionModel.updateSubscription(subscriptionSettingsModel, credentialsModel) }, - { + onShare = { editSubscriptionModel.subscriptionWithCredential.value?.let { onShare(it.subscription) } }, - onExit, - editCalendarModel.subscriptionSettingsModel.uiState.supportsAuthentication, - editCalendarModel.subscriptionSettingsModel, - editCalendarModel.credentialsModel + onExit = onExit, + supportsAuthentication = editCalendarModel.subscriptionSettingsModel.uiState.supportsAuthentication, + subscriptionSettingsModel = editCalendarModel.subscriptionSettingsModel, + credentialsModel = editCalendarModel.credentialsModel ) } @Composable @@ -97,12 +97,12 @@ fun EditCalendarScreen( Scaffold( topBar = { AppBarComposable( - inputValid, - modelsDirty, - onDelete, - onSave, - onShare, - onExit + valid = inputValid, + modelsDirty = modelsDirty, + onDelete = onDelete, + onSave = onSave, + onShare = onShare, + onExit = onExit ) } ) { paddingValues -> From 20b05734f0171245ff439587a97e6b2986cf6fc3 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Mon, 13 May 2024 10:52:56 +0200 Subject: [PATCH 10/10] Don't pass view model to child composable screens --- .../icsdroid/ui/screen/EditCalendarScreen.kt | 101 ++++++++++++++++-- .../ui/views/CredentialsComposable.kt | 15 --- .../views/SubscriptionSettingsComposable.kt | 27 ----- 3 files changed, 92 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt b/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt index 9d9779f7..5dc41a42 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt @@ -71,8 +71,30 @@ fun EditCalendarScreen( }, onExit = onExit, supportsAuthentication = editCalendarModel.subscriptionSettingsModel.uiState.supportsAuthentication, - subscriptionSettingsModel = editCalendarModel.subscriptionSettingsModel, - credentialsModel = editCalendarModel.credentialsModel + + // Subscription settings model + url = subscriptionSettingsModel.uiState.url, + title = subscriptionSettingsModel.uiState.title, + titleChanged = subscriptionSettingsModel::setTitle, + color = subscriptionSettingsModel.uiState.color, + colorChanged = subscriptionSettingsModel::setColor, + ignoreAlerts = subscriptionSettingsModel.uiState.ignoreAlerts, + ignoreAlertsChanged = subscriptionSettingsModel::setIgnoreAlerts, + defaultAlarmMinutes = subscriptionSettingsModel.uiState.defaultAlarmMinutes, + defaultAlarmMinutesChanged = subscriptionSettingsModel::setDefaultAlarmMinutes, + defaultAllDayAlarmMinutes = subscriptionSettingsModel.uiState.defaultAllDayAlarmMinutes, + defaultAllDayAlarmMinutesChanged = subscriptionSettingsModel::setDefaultAllDayAlarmMinutes, + ignoreDescription = subscriptionSettingsModel.uiState.ignoreDescription, + onIgnoreDescriptionChanged = subscriptionSettingsModel::setIgnoreDescription, + isCreating = false, + + // Credentials model + requiresAuth = credentialsModel.uiState.requiresAuth, + username = credentialsModel.uiState.username, + password = credentialsModel.uiState.password, + onRequiresAuthChange = credentialsModel::setRequiresAuth, + onUsernameChange = credentialsModel::setUsername, + onPasswordChange = credentialsModel::setPassword, ) } @Composable @@ -85,8 +107,30 @@ fun EditCalendarScreen( onShare: () -> Unit, onExit: () -> Unit, supportsAuthentication: Boolean, - subscriptionSettingsModel: SubscriptionSettingsModel, - credentialsModel: CredentialsModel + + // Subscription settings model + url: String?, + title: String?, + titleChanged: (String) -> Unit, + color: Int?, + colorChanged: (Int) -> Unit, + ignoreAlerts: Boolean, + ignoreAlertsChanged: (Boolean) -> Unit, + defaultAlarmMinutes: Long?, + defaultAlarmMinutesChanged: (String) -> Unit, + defaultAllDayAlarmMinutes: Long?, + defaultAllDayAlarmMinutesChanged: (String) -> Unit, + ignoreDescription: Boolean, + onIgnoreDescriptionChanged: (Boolean) -> Unit, + isCreating: Boolean, + + // Credentials model + requiresAuth: Boolean, + username: String? = null, + password: String? = null, + onRequiresAuthChange: (Boolean) -> Unit, + onUsernameChange: (String) -> Unit, + onPasswordChange: (String) -> Unit ) { // show success message successMessage?.let { @@ -114,14 +158,31 @@ fun EditCalendarScreen( ) { SubscriptionSettingsComposable( modifier = Modifier.fillMaxWidth(), - subscriptionSettingsModel, - isCreating = false + url = url, + title = title, + titleChanged = titleChanged, + color = color, + colorChanged = colorChanged, + ignoreAlerts = ignoreAlerts, + ignoreAlertsChanged = ignoreAlertsChanged, + defaultAlarmMinutes = defaultAlarmMinutes, + defaultAlarmMinutesChanged = defaultAlarmMinutesChanged, + defaultAllDayAlarmMinutes = defaultAllDayAlarmMinutes, + defaultAllDayAlarmMinutesChanged = defaultAllDayAlarmMinutesChanged, + ignoreDescription = ignoreDescription, + onIgnoreDescriptionChanged = onIgnoreDescriptionChanged, + isCreating = isCreating ) AnimatedVisibility( visible = supportsAuthentication ) { LoginCredentialsComposable( - credentialsModel + requiresAuth = requiresAuth, + username = username, + password = password, + onRequiresAuthChange = onRequiresAuthChange, + onUsernameChange = onUsernameChange, + onPasswordChange = onPasswordChange ) } } @@ -213,8 +274,30 @@ fun EditCalendarScreen_Preview() { onShare = {}, onExit = {}, supportsAuthentication = true, - subscriptionSettingsModel = viewModel(), - credentialsModel = viewModel() + + // Subscription settings model + url = "url", + title = "title", + titleChanged = {}, + color = 0, + colorChanged = {}, + ignoreAlerts = true, + ignoreAlertsChanged = {}, + defaultAlarmMinutes = 20L, + defaultAlarmMinutesChanged = {}, + defaultAllDayAlarmMinutes = 10L, + defaultAllDayAlarmMinutesChanged = {}, + ignoreDescription = false, + onIgnoreDescriptionChanged = {}, + isCreating = true, + + // Credentials model + requiresAuth = true, + username = "", + password = "", + onRequiresAuthChange = {}, + onUsernameChange = {}, + onPasswordChange = {} ) } } \ No newline at end of file diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/views/CredentialsComposable.kt b/app/src/main/java/at/bitfire/icsdroid/ui/views/CredentialsComposable.kt index 9c0239e1..3fd1b463 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/views/CredentialsComposable.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/views/CredentialsComposable.kt @@ -33,21 +33,6 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import at.bitfire.icsdroid.R -import at.bitfire.icsdroid.model.CredentialsModel - -@Composable -fun LoginCredentialsComposable( - model: CredentialsModel -) { - LoginCredentialsComposable( - model.uiState.requiresAuth, - model.uiState.username, - model.uiState.password, - onRequiresAuthChange = model::setRequiresAuth, - onUsernameChange = model::setUsername, - onPasswordChange = model::setPassword - ) -} @Composable fun LoginCredentialsComposable( diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/views/SubscriptionSettingsComposable.kt b/app/src/main/java/at/bitfire/icsdroid/ui/views/SubscriptionSettingsComposable.kt index 947905a9..25c50ca2 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/views/SubscriptionSettingsComposable.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/views/SubscriptionSettingsComposable.kt @@ -32,39 +32,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import at.bitfire.icsdroid.R import at.bitfire.icsdroid.calendar.LocalCalendar -import at.bitfire.icsdroid.model.SubscriptionSettingsModel import at.bitfire.icsdroid.ui.partials.ColorPickerDialog import at.bitfire.icsdroid.ui.partials.SwitchSetting import at.bitfire.icsdroid.ui.theme.AppTheme -@Composable -fun SubscriptionSettingsComposable( - modifier: Modifier = Modifier, - model: SubscriptionSettingsModel = viewModel(), - isCreating: Boolean = false -) { - SubscriptionSettingsComposable( - url = model.uiState.url, - title = model.uiState.title, - titleChanged = model::setTitle, - color = model.uiState.color, - colorChanged = model::setColor, - ignoreAlerts = model.uiState.ignoreAlerts, - ignoreAlertsChanged = model::setIgnoreAlerts, - defaultAlarmMinutes = model.uiState.defaultAlarmMinutes, - defaultAlarmMinutesChanged = model::setDefaultAlarmMinutes, - defaultAllDayAlarmMinutes = model.uiState.defaultAllDayAlarmMinutes, - defaultAllDayAlarmMinutesChanged = model::setDefaultAllDayAlarmMinutes, - ignoreDescription = model.uiState.ignoreDescription, - onIgnoreDescriptionChanged = model::setIgnoreDescription, - isCreating = isCreating, - modifier = modifier - ) -} - @Composable fun SubscriptionSettingsComposable( url: String?,