From 8e3c2f65bbff0214b73d07f5b01db2c00abaca00 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 12 May 2023 12:08:40 +0800 Subject: [PATCH] Work on new android label selector --- android/Omnivore/app/build.gradle | 2 + .../ui/components/LabelsSelectionSheet.kt | 271 ++++++++++++++---- .../omnivore/ui/library/LibraryView.kt | 2 +- .../ui/reader/WebReaderLoadingContainer.kt | 15 +- 4 files changed, 227 insertions(+), 63 deletions(-) diff --git a/android/Omnivore/app/build.gradle b/android/Omnivore/app/build.gradle index 735c496ee..d86e223cd 100644 --- a/android/Omnivore/app/build.gradle +++ b/android/Omnivore/app/build.gradle @@ -136,6 +136,7 @@ dependencies { implementation 'com.google.android.gms:play-services-auth:20.4.0' implementation "com.google.accompanist:accompanist-systemuicontroller:0.25.1" + implementation "com.google.accompanist:accompanist-flowlayout:0.25.1" implementation 'io.coil-kt:coil-compose:2.2.0' @@ -151,6 +152,7 @@ dependencies { kapt "androidx.room:room-compiler:$room_version" implementation 'com.github.jeziellago:compose-markdown:0.3.3' + implementation "io.github.dokar3:chiptextfield:0.4.6" } apollo { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelsSelectionSheet.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelsSelectionSheet.kt index 2d6246ba0..e07a32d69 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelsSelectionSheet.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelsSelectionSheet.kt @@ -3,12 +3,14 @@ package app.omnivore.omnivore.ui.components import LabelChip +import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ModalBottomSheetLayout @@ -23,11 +25,22 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import app.omnivore.omnivore.models.ServerSyncStatus import app.omnivore.omnivore.persistence.entities.SavedItemLabel import app.omnivore.omnivore.ui.library.LibraryViewModel +import com.dokar.chiptextfield.* import com.google.accompanist.flowlayout.FlowRow @@ -93,9 +106,109 @@ import com.google.accompanist.flowlayout.FlowRow // } //} -@OptIn(ExperimentalMaterial3Api::class) @Composable +fun CircleIcon(colorHex: String){ + val chipColors = LabelChipColors.fromHex(colorHex) + val viewConfiguration = LocalViewConfiguration.current + val viewConfigurationOverride = remember(viewConfiguration) { + ViewConfigurationOverride( + base = viewConfiguration, + minimumTouchTargetSize = DpSize(24.dp, 24.dp) + ) + } + + CompositionLocalProvider(LocalViewConfiguration provides viewConfigurationOverride) { + Row( + modifier = Modifier + .padding(start = 10.dp, end = 2.dp) + .padding(vertical = 10.dp) + ) { + Canvas(modifier = Modifier.size(12.dp), onDraw = { + drawCircle(color = chipColors.containerColor) + }) + } + } +} + +@Composable +fun CloseButton( + state: ChipTextFieldState, + chip: T, + modifier: Modifier = Modifier, + backgroundColor: Color = Color.Transparent, + strokeColor: Color = Color.White, + startPadding: Dp = 0.dp, + endPadding: Dp = 4.dp +) { + Row( + modifier = modifier + .padding(start = startPadding, end = endPadding) + ) { + CloseButtonImpl( + onClick = { state.removeChip(chip) }, + backgroundColor = backgroundColor, + strokeColor = strokeColor + ) + } +} + +internal class ViewConfigurationOverride( + base: ViewConfiguration, + override val doubleTapMinTimeMillis: Long = base.doubleTapMinTimeMillis, + override val doubleTapTimeoutMillis: Long = base.doubleTapTimeoutMillis, + override val longPressTimeoutMillis: Long = base.longPressTimeoutMillis, + override val touchSlop: Float = base.touchSlop, + override val minimumTouchTargetSize: DpSize = base.minimumTouchTargetSize +) : ViewConfiguration + +@Composable +private fun CloseButtonImpl( + onClick: () -> Unit, + backgroundColor: Color, + strokeColor: Color, + modifier: Modifier = Modifier, +) { + val padding = with(LocalDensity.current) { 6.dp.toPx() } + val strokeWidth = with(LocalDensity.current) { 1.2.dp.toPx() } + val viewConfiguration = LocalViewConfiguration.current + val viewConfigurationOverride = remember(viewConfiguration) { + ViewConfigurationOverride( + base = viewConfiguration, + minimumTouchTargetSize = DpSize(24.dp, 24.dp) + ) + } + CompositionLocalProvider(LocalViewConfiguration provides viewConfigurationOverride) { + Canvas( + modifier = modifier + .size(18.dp) + .clip(CircleShape) + .background(backgroundColor) + .clickable(onClick = onClick) + ) { + drawLine( + color = strokeColor, + start = Offset(padding, padding), + end = Offset(size.width - padding, size.height - padding), + strokeWidth = strokeWidth + ) + drawLine( + color = strokeColor, + start = Offset(padding, size.height - padding), + end = Offset(size.width - padding, padding), + strokeWidth = strokeWidth + ) + } + } +} + +class LabelChip(label: SavedItemLabel) : Chip(label.name) { + val label = label +} + +@Composable +@OptIn(ExperimentalMaterialApi::class) fun LabelsSelectionSheetContent( +// viewModel: LibraryViewModel, isLibraryMode: Boolean, labels: List, initialSelectedLabels: List, @@ -107,13 +220,32 @@ fun LabelsSelectionSheetContent( val selectedLabels = remember { mutableStateOf(initialSelectedLabels) } var showCreateLabelDialog by remember { mutableStateOf(false ) } + val focusRequester = remember { FocusRequester() } + + val titleText = if (isLibraryMode) "Filter by Label" else "Set Labels" + val findOrCreateLabel: (name: String) -> SavedItemLabel = { name -> + val found = labels.find { it.name == name } + found + ?: SavedItemLabel( + savedItemLabelId = "", + name = name, + color = "#FFFFFF", + createdAt = "", + labelDescription = "", + serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue + ) + } + Surface( modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background), ) { + var value by remember { mutableStateOf("Initial text") } + val state = rememberChipTextFieldState() + if (showCreateLabelDialog) { LabelCreationDialog( @@ -129,9 +261,9 @@ fun LabelsSelectionSheetContent( verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier - // .verticalScroll(rememberScrollState()) + // .verticalScroll(rememberScrollState()) .fillMaxSize() - .padding(horizontal = 0.dp) + .padding(horizontal = 5.dp) ) { Row( @@ -139,6 +271,7 @@ fun LabelsSelectionSheetContent( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() + .padding(vertical = 5.dp) ) { TextButton(onClick = onCancel) { Text(text = "Cancel") @@ -150,69 +283,97 @@ fun LabelsSelectionSheetContent( Text(text = if (isLibraryMode) "Search" else "Save") } } - LazyColumn( - state = listState, - verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxSize() - ) { - items(labels) { label -> - val isLabelSelected = selectedLabels.value.contains(label) - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .clickable { - if (isLabelSelected) { - selectedLabels.value = - selectedLabels.value.filter { it.savedItemLabelId != label.savedItemLabelId } - } else { - selectedLabels.value = selectedLabels.value + listOf(label) - } - } - .padding(horizontal = 10.dp, vertical = 6.dp) - ) { - val chipColors = LabelChipColors.fromHex(label.color) + ChipTextField( + state = state, + onSubmit = { LabelChip(findOrCreateLabel(it)) }, + chipLeadingIcon = { chip -> CircleIcon(colorHex = chip.label.color) }, + chipTrailingIcon = { chip -> CloseButton(state, chip) }, + chipStyle = ChipTextFieldDefaults.chipStyle( + shape = androidx.compose.material.MaterialTheme.shapes.medium, + unfocusedBorderWidth = 0.dp, + focusedTextColor = Color(0xFFAEAEAF), + focusedBorderColor = Color(0xFF2A2A2A), + focusedBackgroundColor = Color(0xFF2A2A2A) + ), + colors = androidx.compose.material.TextFieldDefaults.textFieldColors( + textColor = Color(0xFFAEAEAF), + backgroundColor = Color(0xFF3D3D3D) + ), + contentPadding = PaddingValues(15.dp), + modifier = Modifier + .defaultMinSize(minHeight = 45.dp) + .fillMaxWidth() + .padding(horizontal = 10.dp) + .focusRequester(focusRequester) + ) - LabelChip( - name = label.name, - colors = chipColors - ) - if (isLabelSelected) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null - ) - } - } - Divider(color = MaterialTheme.colorScheme.outlineVariant, thickness = 1.dp) - } + LazyColumn( + state = listState, + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize() + ) { + items(labels) { label -> + val isLabelSelected = selectedLabels.value.contains(label) - if (!isLibraryMode) { - item { Row( - horizontalArrangement = Arrangement.Start, + horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .clickable { showCreateLabelDialog = true } - .padding(horizontal = 6.dp) - .padding(vertical = 12.dp) - ) - { - Icon( - imageVector = Icons.Filled.AddCircle, - contentDescription = null, - modifier = Modifier.padding(end = 8.dp) + .clickable { + if (isLabelSelected) { + selectedLabels.value = + selectedLabels.value.filter { it.savedItemLabelId != label.savedItemLabelId } + } else { + selectedLabels.value = selectedLabels.value + listOf(label) + state.addChip(app.omnivore.omnivore.ui.components.LabelChip(label)) + } + } + .padding(horizontal = 10.dp, vertical = 6.dp) + ) { + val chipColors = LabelChipColors.fromHex(label.color) + + LabelChip( + name = label.name, + colors = chipColors ) - Text(text = "Create a new Label") + if (isLabelSelected) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null + ) + } + } + Divider(color = MaterialTheme.colorScheme.outlineVariant, thickness = 1.dp) + } + + if (!isLibraryMode) { + item { + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { showCreateLabelDialog = true } + .padding(horizontal = 6.dp) + .padding(vertical = 12.dp) + ) + { + Icon( + imageVector = Icons.Filled.AddCircle, + contentDescription = null, + modifier = Modifier.padding(end = 8.dp) + ) + Text(text = "Create a new Label") + } } } } } - } + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryView.kt index 20dbac19e..631ec9418 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryView.kt @@ -59,7 +59,7 @@ fun LibraryView( if (showLabelsSelectionSheet) { coroutineScope.launch { - modalBottomSheetState.animateTo(ModalBottomSheetValue.HalfExpanded) + modalBottomSheetState.show() } } else { coroutineScope.launch { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderLoadingContainer.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderLoadingContainer.kt index 44581b1bd..e549fd629 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderLoadingContainer.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderLoadingContainer.kt @@ -176,6 +176,7 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, Color(it.backgroundColor ?: 0xFFFFFFFF) } } ?: Color(0xFFFFFFFF) + val themeTintColor = currentTheme?.let { if (it.themeKey == "System" && isDarkMode) { Color(0xFFFFFFFF) @@ -189,27 +190,27 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, when (bottomSheetState) { BottomSheetState.PREFERENCES -> { coroutineScope.launch { - modalBottomSheetState.animateTo(ModalBottomSheetValue.HalfExpanded) + modalBottomSheetState.show() } } BottomSheetState.NOTEBOOK -> { coroutineScope.launch { - modalBottomSheetState.animateTo(ModalBottomSheetValue.Expanded) + modalBottomSheetState.show() } } BottomSheetState.HIGHLIGHTNOTE -> { coroutineScope.launch { - modalBottomSheetState.animateTo(ModalBottomSheetValue.Expanded) + modalBottomSheetState.show() } } BottomSheetState.LABELS -> { coroutineScope.launch { - modalBottomSheetState.animateTo(ModalBottomSheetValue.HalfExpanded) + modalBottomSheetState.show() } } BottomSheetState.LINK -> { coroutineScope.launch { - modalBottomSheetState.animateTo(ModalBottomSheetValue.HalfExpanded) + modalBottomSheetState.show() } } BottomSheetState.NONE -> { @@ -346,7 +347,7 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, IconButton(onClick = { coroutineScope.launch { webReaderViewModel.setBottomSheet(BottomSheetState.NOTEBOOK) - modalBottomSheetState.animateTo(ModalBottomSheetValue.Expanded) + modalBottomSheetState.show() } }) { Icon( @@ -359,7 +360,7 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, IconButton(onClick = { coroutineScope.launch { webReaderViewModel.setBottomSheet(BottomSheetState.PREFERENCES) - modalBottomSheetState.animateTo(ModalBottomSheetValue.HalfExpanded) + modalBottomSheetState.show() } }) { Icon(