From c7f80f3f9747e1e5b1086c87f1db85fa87dd8dea Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Wed, 3 Apr 2024 12:56:46 +0200 Subject: [PATCH] Create tabs --- .../icsdroid/ui/views/EnterUrlComposable.kt | 260 +++++++++++++----- app/src/main/res/values/strings.xml | 8 +- 2 files changed, 198 insertions(+), 70 deletions(-) diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/views/EnterUrlComposable.kt b/app/src/main/java/at/bitfire/icsdroid/ui/views/EnterUrlComposable.kt index 1c7a06f7..361188ff 100644 --- a/app/src/main/java/at/bitfire/icsdroid/ui/views/EnterUrlComposable.kt +++ b/app/src/main/java/at/bitfire/icsdroid/ui/views/EnterUrlComposable.kt @@ -5,13 +5,19 @@ package at.bitfire.icsdroid.ui.views import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -24,12 +30,15 @@ import androidx.compose.material.icons.rounded.Warning import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -41,7 +50,9 @@ import androidx.compose.ui.unit.dp import at.bitfire.icsdroid.R import at.bitfire.icsdroid.ui.ResourceInfo import at.bitfire.icsdroid.ui.partials.AlertDialog +import kotlinx.coroutines.launch +@OptIn(ExperimentalFoundationApi::class) @Composable fun EnterUrlComposable( requiresAuth: Boolean, @@ -93,89 +104,202 @@ fun EnterUrlComposable( .padding(horizontal = 16.dp) .verticalScroll(rememberScrollState()) ) { + val scope = rememberCoroutineScope() + val state = rememberPagerState(pageCount = { 2 }) + + TabRow(state.currentPage) { + Tab(state.currentPage == 0, onClick = { + scope.launch { state.scrollToPage(0) } + }) { + Text( + stringResource(R.string.add_calendar_subscribe_url), + modifier = Modifier.padding(8.dp) + ) + } + Tab(state.currentPage == 1, onClick = { + scope.launch { state.scrollToPage(1) } + }) { + Text( + stringResource(R.string.add_calendar_subscribe_file), + modifier = Modifier.padding(8.dp) + ) + } + } + // Instead of adding vertical padding to column, use spacer so that if content is // scrolled, it is not spaced Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(R.string.add_calendar_url_text), - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.fillMaxWidth() - ) - - TextField( - value = url ?: "", - onValueChange = onUrlChange, + HorizontalPager( + state, modifier = Modifier + .fillMaxWidth() + .weight(1f), + verticalAlignment = Alignment.Top + ) { index -> + Column( + modifier = Modifier + .padding(8.dp) .fillMaxWidth() - .padding(end = 16.dp), - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.None, - keyboardType = KeyboardType.Uri, - imeAction = ImeAction.Go - ), - keyboardActions = KeyboardActions { onSubmit() }, - maxLines = 1, - singleLine = true, - placeholder = { Text(stringResource(R.string.add_calendar_url_sample)) }, - isError = urlError != null, - enabled = !isVerifyingUrl - ) - AnimatedVisibility(visible = urlError != null) { - Text( - text = urlError ?: "", - color = MaterialTheme.colorScheme.error, - modifier = Modifier.fillMaxWidth(), - style = MaterialTheme.typography.bodySmall - ) + .verticalScroll(rememberScrollState())) { + when (index) { + 0 -> SubscribeToUrl( + url, + onUrlChange, + onSubmit, + urlError, + isVerifyingUrl, + isInsecure, + supportsAuthentication, + requiresAuth, + username, + password, + onRequiresAuthChange, + onUsernameChange, + onPasswordChange + ) + + 1 -> SubscribeToFile( + url, + onUrlChange, + onSubmit, + urlError, + isVerifyingUrl, + onPickFileRequested + ) + } + } } + Spacer(modifier = Modifier.height(16.dp)) + } + } +} + +@Composable +private fun ColumnScope.SubscribeToUrl( + url: String?, + onUrlChange: (String) -> Unit, + onSubmit: () -> Unit, + error: String?, + verifying: Boolean, + isInsecure: Boolean, + supportsAuthentication: Boolean, + requiresAuth: Boolean, + username: String?, + password: String?, + onRequiresAuthChange: (Boolean) -> Unit, + onUsernameChange: (String) -> Unit, + onPasswordChange: (String) -> Unit +) { + ResourceInput( + url, + onUrlChange, + verifying, + onSubmit, + error, + labelText = stringResource(R.string.add_calendar_pick_url_label), + description = stringResource(R.string.add_calendar_pick_url_text) + ) + AnimatedVisibility( + visible = isInsecure, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Row(Modifier.fillMaxWidth()) { + Icon(imageVector = Icons.Rounded.Warning, contentDescription = null) + Text( - text = stringResource(R.string.add_calendar_pick_file_text), + text = stringResource(R.string.add_calendar_authentication_without_https_warning), style = MaterialTheme.typography.bodyLarge, - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp) + modifier = Modifier.fillMaxWidth() ) + } + } + AnimatedVisibility(visible = supportsAuthentication) { + LoginCredentialsComposable( + requiresAuth, + username, + password, + onRequiresAuthChange, + onUsernameChange, + onPasswordChange + ) + } +} - TextButton( - onClick = onPickFileRequested, - modifier = Modifier.padding(vertical = 15.dp), - enabled = !isVerifyingUrl - ) { - Text(stringResource(R.string.add_calendar_pick_file)) - } - - AnimatedVisibility( - visible = isInsecure, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Row(Modifier.fillMaxWidth()) { - Icon(imageVector = Icons.Rounded.Warning, contentDescription = null) +@Composable +private fun ColumnScope.SubscribeToFile( + uri: String?, + onUriChange: (String) -> Unit, + onSubmit: () -> Unit, + error: String?, + verifying: Boolean, + onPickFileRequested: () -> Unit +) { + ResourceInput( + uri, + onUriChange, + verifying, + onSubmit, + error, + stringResource(R.string.add_calendar_pick_file), + stringResource(R.string.add_calendar_pick_file_text), + onPickFileRequested + ) +} - Text( - text = stringResource(R.string.add_calendar_authentication_without_https_warning), - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.fillMaxWidth() - ) +@Composable +private fun ColumnScope.ResourceInput( + uri: String?, + onUriChange: (String) -> Unit, + verifying: Boolean, + onSubmit: () -> Unit, + error: String?, + labelText: String, + description: String, + onClick: () -> Unit = {} +) { + Text( + text = description, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.fillMaxWidth() + ) + TextField( + value = uri ?: "", + onValueChange = onUriChange, + modifier = Modifier + .fillMaxWidth() + .padding(end = 16.dp), + enabled = !verifying, + readOnly = true, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + keyboardType = KeyboardType.Uri, + imeAction = ImeAction.Go + ), + keyboardActions = KeyboardActions { onSubmit() }, + maxLines = 1, + singleLine = true, + placeholder = { Text(labelText) }, + isError = error != null, + interactionSource = remember { MutableInteractionSource() }.also { interactionSource -> + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { + if (it is PressInteraction.Release) + onClick() } } - - AnimatedVisibility(visible = supportsAuthentication) { - LoginCredentialsComposable( - requiresAuth, - username, - password, - onRequiresAuthChange, - onUsernameChange, - onPasswordChange - ) - } - - Spacer(modifier = Modifier.height(16.dp)) } + ) + AnimatedVisibility(visible = error != null) { + Text( + text = error ?: "", + color = MaterialTheme.colorScheme.error, + modifier = Modifier.fillMaxWidth(), + style = MaterialTheme.typography.bodySmall + ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 42659d99..4bac42c6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -55,6 +55,8 @@ Subscribe to calendar + Subscribe to URL + Subscribe to file Valid URI required Authentication Third parties can easily intercept your credentials. Use HTTPS for secure authentication. @@ -66,9 +68,11 @@ Title & Color Calendar name Pick color - Enter a Webcal address: + Webcal address + Enter a Webcal address. + File path + Select a file from local storage. https://example.com/webcal.ics - Alternatively, select a file from local storage. Pick file User name Validating calendar resource…