Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EditCalendarActvivity: Switch to compose state and kotlin flows #320

99 changes: 99 additions & 0 deletions app/src/main/java/at/bitfire/icsdroid/model/EditCalendarModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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)
}
}
}

/**
* Whether user input is error free
*/
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 there are unsaved user changes
*/
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
*/
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
}

}
220 changes: 220 additions & 0 deletions app/src/main/java/at/bitfire/icsdroid/ui/screen/EditCalendarScreen.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
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
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.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(
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)
}
EditCalendarScreen(
inputValid = editCalendarModel.inputValid,
modelsDirty = editCalendarModel.modelsDirty,
successMessage = editCalendarModel.editSubscriptionModel.uiState.successMessage,
onDelete = editSubscriptionModel::removeSubscription,
onSave = {
editSubscriptionModel.updateSubscription(subscriptionSettingsModel, credentialsModel)
},
{
sunkup marked this conversation as resolved.
Show resolved Hide resolved
editSubscriptionModel.subscriptionWithCredential.value?.let {
onShare(it.subscription)
}
},
onExit,
sunkup marked this conversation as resolved.
Show resolved Hide resolved
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
) {
sunkup marked this conversation as resolved.
Show resolved Hide resolved
// show success message
successMessage?.let {
Toast.makeText(LocalContext.current, successMessage, Toast.LENGTH_LONG).show()
onExit()
}

Scaffold(
topBar = {
AppBarComposable(
inputValid,
modelsDirty,
onDelete,
onSave,
onShare,
onExit
)
}
) { paddingValues ->
Column(
Modifier
.verticalScroll(rememberScrollState())
.padding(paddingValues)
.padding(16.dp)
) {
SubscriptionSettingsComposable(
modifier = Modifier.fillMaxWidth(),
subscriptionSettingsModel,
isCreating = false
)
AnimatedVisibility(
visible = supportsAuthentication
) {
LoginCredentialsComposable(
credentialsModel
)
}
}
}
}

@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(
inputValid = true,
modelsDirty = false,
successMessage = "yay!",
onDelete = {},
onSave = {},
onShare = {},
onExit = {},
supportsAuthentication = true,
subscriptionSettingsModel = viewModel(),
credentialsModel = viewModel()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down