diff --git a/android/Omnivore/app/build.gradle b/android/Omnivore/app/build.gradle index 0fd2aa093..73c63c24b 100644 --- a/android/Omnivore/app/build.gradle +++ b/android/Omnivore/app/build.gradle @@ -17,8 +17,8 @@ android { applicationId "app.omnivore.omnivore" minSdk 26 targetSdk 33 - versionCode 70 - versionName "0.0.70" + versionCode 76 + versionName "0.0.76" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -84,6 +84,7 @@ dependencies { implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.material:material:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" + implementation "androidx.compose.material:material-icons-extended:$compose_version" implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' implementation 'androidx.activity:activity-compose:1.6.1' implementation 'androidx.appcompat:appcompat:1.5.1' @@ -135,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' @@ -150,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/networking/SavedItemMutations.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemMutations.kt index 391124e2e..b61c20d03 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemMutations.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemMutations.kt @@ -1,9 +1,18 @@ package app.omnivore.omnivore.networking +import android.content.ContentValues +import android.net.Uri +import android.util.Log +import androidx.lifecycle.viewModelScope +import app.omnivore.omnivore.Constants +import app.omnivore.omnivore.graphql.generated.SaveUrlMutation import app.omnivore.omnivore.graphql.generated.SetBookmarkArticleMutation import app.omnivore.omnivore.graphql.generated.SetLinkArchivedMutation -import app.omnivore.omnivore.graphql.generated.type.ArchiveLinkInput -import app.omnivore.omnivore.graphql.generated.type.SetBookmarkArticleInput +import app.omnivore.omnivore.graphql.generated.type.* +import com.apollographql.apollo3.ApolloClient +import com.apollographql.apollo3.api.Optional +import kotlinx.coroutines.launch +import java.util.* suspend fun Networker.deleteSavedItem(itemID: String): Boolean { return try { @@ -32,3 +41,14 @@ suspend fun Networker.updateArchiveStatusSavedItem(itemID: String, setAsArchived false } } + +suspend fun Networker.saveUrl(url: Uri): Boolean { + return try { + val clientRequestId = UUID.randomUUID().toString() + val input = SaveUrlInput(url = url.toString(), clientRequestId = clientRequestId, source = "android") + val result = authenticatedApolloClient().mutation(SaveUrlMutation(input)).execute() + result.data?.saveUrl?.onSaveSuccess?.url != null + } catch (e: java.lang.Exception) { + false + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelChip.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelChip.kt index 5f56c3c23..63acda680 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelChip.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelChip.kt @@ -13,10 +13,10 @@ import app.omnivore.omnivore.ui.components.LabelChipColors fun LabelChip( name: String, colors: LabelChipColors, - onSelectionChanged: (String) -> Unit = {}, + modifier: Modifier = Modifier.padding(0.dp), ) { Surface( - modifier = Modifier.padding(4.dp), + modifier = modifier.padding(2.dp), shape = MaterialTheme.shapes.medium, color = colors.containerColor ) { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelCreationDialog.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelCreationDialog.kt index 8c3adb263..7bc4c9ab8 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelCreationDialog.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelCreationDialog.kt @@ -135,6 +135,10 @@ object LabelSwatchHelper { return listOf(shuffledSwatches.last()) + webSwatchHexes + shuffledSwatches.dropLast(1) } + fun random(): String { + return webSwatchHexes.random() + } + private val webSwatchHexes = listOf( "#FF5D99", "#7CFF7B", 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 6adeeb60b..468d08306 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,15 +3,18 @@ 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.interaction.FocusInteraction +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction 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.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ModalBottomSheetLayout import androidx.compose.material.ModalBottomSheetValue @@ -23,120 +26,203 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi 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.focus.onFocusChanged +import androidx.compose.ui.focus.onFocusEvent +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.* +import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.toLowerCase +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog +import app.omnivore.omnivore.models.ServerSyncStatus import app.omnivore.omnivore.persistence.entities.SavedItemLabel import app.omnivore.omnivore.ui.library.LibraryViewModel -import app.omnivore.omnivore.ui.reader.WebReaderParams -import app.omnivore.omnivore.ui.reader.WebReaderViewModel +import com.dokar.chiptextfield.* +import com.google.accompanist.flowlayout.FlowRow +import kotlinx.coroutines.delay +import java.time.LocalDate +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter +import java.util.* + + +//@Composable +//fun LabelsSelectionSheet(viewModel: LibraryViewModel) { +// val isActive: Boolean by viewModel.showLabelsSelectionSheetLiveData.observeAsState(false) +// val labels: List by viewModel.savedItemLabelsLiveData.observeAsState(listOf()) +// val currentSavedItemData = viewModel.currentSavedItemUnderEdit() +// +// val modalBottomSheetState = rememberModalBottomSheetState( +// ModalBottomSheetValue.HalfExpanded, +// confirmStateChange = { it != ModalBottomSheetValue.Hidden } +// ) +// +// if (isActive) { +// ModalBottomSheetLayout( +// sheetBackgroundColor = Color.Transparent, +// sheetState = modalBottomSheetState, +// sheetContent = { +// BottomSheetUI { +// if (currentSavedItemData != null) { +// LabelsSelectionSheetContent( +// labels = labels, +// initialSelectedLabels = currentSavedItemData.labels, +// onCancel = { +// viewModel.showLabelsSelectionSheetLiveData.value = false +// viewModel.labelsSelectionCurrentItemLiveData.value = null +// }, +// isLibraryMode = false, +// onSave = { +// if (it != labels) { +// viewModel.updateSavedItemLabels( +// savedItemID = currentSavedItemData.savedItem.savedItemId, +// labels = it +// ) +// } +// viewModel.labelsSelectionCurrentItemLiveData.value = null +// viewModel.showLabelsSelectionSheetLiveData.value = false +// }, +// onCreateLabel = { newLabelName, labelHexValue -> +// viewModel.createNewSavedItemLabel(newLabelName, labelHexValue) +// } +// ) +// } else { // Is used in library mode +// LabelsSelectionSheetContent( +// labels = labels, +// initialSelectedLabels = viewModel.activeLabelsLiveData.value ?: listOf(), +// onCancel = { viewModel.showLabelsSelectionSheetLiveData.value = false }, +// isLibraryMode = true, +// onSave = { +// viewModel.updateAppliedLabels(it) +// viewModel.labelsSelectionCurrentItemLiveData.value = null +// viewModel.showLabelsSelectionSheetLiveData.value = false +// }, +// onCreateLabel = { newLabelName, labelHexValue -> +// viewModel.createNewSavedItemLabel(newLabelName, labelHexValue) +// } +// ) +// } +// } +// } +// ) {} +// } +//} @Composable -fun WebReaderLabelsSelectionSheet(viewModel: WebReaderViewModel) { - val isActive: Boolean by viewModel.showLabelsSelectionSheetLiveData.observeAsState(false) - val labels: List by viewModel.savedItemLabelsLiveData.observeAsState(listOf()) - val webReaderParams: WebReaderParams? by viewModel.webReaderParamsLiveData.observeAsState(null) +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) + ) + } - val modalBottomSheetState = rememberModalBottomSheetState( - ModalBottomSheetValue.HalfExpanded, - ) - - if (isActive) { - ModalBottomSheetLayout( - sheetBackgroundColor = Color.Transparent, - sheetState = modalBottomSheetState, - sheetContent = { - BottomSheetUI { - LabelsSelectionSheetContent( - labels = labels, - initialSelectedLabels = webReaderParams?.labels ?: listOf(), - onCancel = { - viewModel.showLabelsSelectionSheetLiveData.value = false - }, - isLibraryMode = false, - onSave = { - if (it != labels) { - viewModel.updateSavedItemLabels(savedItemID = webReaderParams?.item?.savedItemId ?: "", labels = it) - } - viewModel.showLabelsSelectionSheetLiveData.value = false - }, - onCreateLabel = { newLabelName, labelHexValue -> - viewModel.createNewSavedItemLabel(newLabelName, labelHexValue) - } - ) - } - } - ) {} + CompositionLocalProvider(LocalViewConfiguration provides viewConfigurationOverride) { + Row( + modifier = Modifier + .padding(start = 10.dp, end = 2.dp) + .padding(vertical = 7.dp) + ) { + Canvas(modifier = Modifier.size(12.dp), onDraw = { + drawCircle(color = chipColors.containerColor) + }) + } } } @Composable -fun LabelsSelectionSheet(viewModel: LibraryViewModel) { - val isActive: Boolean by viewModel.showLabelsSelectionSheetLiveData.observeAsState(false) - val labels: List by viewModel.savedItemLabelsLiveData.observeAsState(listOf()) - val currentSavedItemData = viewModel.currentSavedItemUnderEdit() - - val modalBottomSheetState = rememberModalBottomSheetState( - ModalBottomSheetValue.HalfExpanded, - confirmStateChange = { it != ModalBottomSheetValue.Hidden } - ) - - if (isActive) { - ModalBottomSheetLayout( - sheetBackgroundColor = Color.Transparent, - sheetState = modalBottomSheetState, - sheetContent = { - BottomSheetUI { - if (currentSavedItemData != null) { - LabelsSelectionSheetContent( - labels = labels, - initialSelectedLabels = currentSavedItemData.labels, - onCancel = { - viewModel.showLabelsSelectionSheetLiveData.value = false - viewModel.labelsSelectionCurrentItemLiveData.value = null - }, - isLibraryMode = false, - onSave = { - if (it != labels) { - viewModel.updateSavedItemLabels( - savedItemID = currentSavedItemData.savedItem.savedItemId, - labels = it - ) - } - viewModel.labelsSelectionCurrentItemLiveData.value = null - viewModel.showLabelsSelectionSheetLiveData.value = false - }, - onCreateLabel = { newLabelName, labelHexValue -> - viewModel.createNewSavedItemLabel(newLabelName, labelHexValue) - } - ) - } else { // Is used in library mode - LabelsSelectionSheetContent( - labels = labels, - initialSelectedLabels = viewModel.activeLabelsLiveData.value ?: listOf(), - onCancel = { viewModel.showLabelsSelectionSheetLiveData.value = false }, - isLibraryMode = true, - onSave = { - viewModel.updateAppliedLabels(it) - viewModel.labelsSelectionCurrentItemLiveData.value = null - viewModel.showLabelsSelectionSheetLiveData.value = false - }, - onCreateLabel = { newLabelName, labelHexValue -> - viewModel.createNewSavedItemLabel(newLabelName, labelHexValue) - } - ) - } - } - } - ) {} +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 + ) } } -@OptIn(ExperimentalMaterial3Api::class) +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 LabelChipView(label: SavedItemLabel) : Chip(label.name) { + val label = label +} + +@Composable +@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class, + ExperimentalMaterial3Api::class +) fun LabelsSelectionSheetContent( isLibraryMode: Boolean, labels: List, @@ -145,36 +231,57 @@ fun LabelsSelectionSheetContent( onSave: (List) -> Unit, onCreateLabel: (String, String) -> Unit ) { - val listState = rememberLazyListState() - val selectedLabels = remember { mutableStateOf(initialSelectedLabels) } - var showCreateLabelDialog by remember { mutableStateOf(false ) } + val interactionSource = remember { MutableInteractionSource() } + + val state = rememberChipTextFieldState(initialSelectedLabels.map { + LabelChipView(it) + }) + + val focusRequester = remember { FocusRequester() } + var filterTextValue by remember { mutableStateOf(TextFieldValue()) } + val onFilterTextValueChange: (TextFieldValue) -> Unit = { filterTextValue = it } + + val filteredLabels = labels.filter { label -> + val text = filterTextValue.text.toLowerCase(Locale.current) + val result = (text.isEmpty() || label.name.toLowerCase(Locale.current).startsWith(text)) + val alreadySelected = state.chips.map { it.label.name }.contains(label.name) + result && !alreadySelected + } + + val currentLabel = labels.find { + val text = filterTextValue.text.toLowerCase(Locale.current) + it.name.toLowerCase(Locale.current) == text + } val titleText = if (isLibraryMode) "Filter by Label" else "Set Labels" + val findOrCreateLabel: (name: TextFieldValue) -> SavedItemLabel = { name -> + val found = labels.find { it.name == name.text } + found + ?: SavedItemLabel( + savedItemLabelId = "", + name = name.text, + labelDescription = "", + color = LabelSwatchHelper.random(), + createdAt = LocalDate.now().atStartOfDay().atOffset(ZoneOffset.UTC).format( + DateTimeFormatter.ISO_DATE_TIME), + serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue + ) + } + Surface( modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background), ) { - - if (showCreateLabelDialog) { - LabelCreationDialog( - onDismiss = { showCreateLabelDialog = false }, - onSave = { labelName, hexColor -> - onCreateLabel(labelName, hexColor) - showCreateLabelDialog = false - } - ) - } - Column( verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier - // .verticalScroll(rememberScrollState()) .fillMaxSize() - .padding(horizontal = 0.dp) + .padding(horizontal = 5.dp) ) { + Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, @@ -187,87 +294,97 @@ fun LabelsSelectionSheetContent( Text(titleText, fontWeight = FontWeight.ExtraBold) - TextButton(onClick = { onSave(selectedLabels.value) }) { + TextButton(onClick = { onSave(state.chips.map { it.label }) }) { 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) - ) { + ChipTextField( + state = state, + value = filterTextValue, + onValueChange = onFilterTextValueChange, + onSubmit = { + if (isLibraryMode) { + currentLabel?.let { + LabelChipView(it) + } ?: null + } else { + LabelChipView(findOrCreateLabel(it)) + } + }, + chipLeadingIcon = { chip -> CircleIcon(colorHex = chip.label.color) }, + chipTrailingIcon = { chip -> CloseButton(state, chip) }, + interactionSource = interactionSource, + 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(10.dp), + modifier = Modifier + .defaultMinSize(minHeight = 45.dp) + .fillMaxWidth() + .padding(horizontal = 10.dp) + .focusRequester(focusRequester) +// .onFocusEvent { +// val text = filterTextValue.text +// if (it.hasFocus) { +// val selection = filterTextValue.text.length +// onFilterTextValueChange(filterTextValue.copy(selection = TextRange(selection))) +// } +// } + ) + + if (!isLibraryMode && filterTextValue.text.isNotEmpty() && currentLabel == null) { + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { + val label = findOrCreateLabel(filterTextValue) + state.addChip(LabelChipView(label)) + filterTextValue = TextFieldValue() + } + .padding(horizontal = 10.dp) + .padding(top = 10.dp, bottom = 5.dp) + ) + { + Icon( + imageVector = Icons.Filled.AddCircle, + contentDescription = null, + modifier = Modifier.padding(end = 8.dp) + ) + Text(text = "Create a new label named \"${filterTextValue.text}\"") + } + } + + if (filteredLabels.isNotEmpty()) { + FlowRow(modifier = Modifier.fillMaxWidth().padding(10.dp)) { + filteredLabels.forEach { label -> val chipColors = LabelChipColors.fromHex(label.color) LabelChip( name = label.name, - colors = chipColors - ) - 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, + colors = chipColors, modifier = Modifier - .fillMaxWidth() - .clickable { showCreateLabelDialog = true } - .padding(horizontal = 6.dp) - .padding(vertical = 12.dp) + .clickable { + state.addChip(LabelChipView(label)) + filterTextValue = TextFieldValue() + } ) - { - Icon( - imageVector = Icons.Filled.AddCircle, - contentDescription = null, - modifier = Modifier.padding(end = 8.dp) - ) - Text(text = "Create a new Label") - } } } } } } -} + LaunchedEffect(Unit) { -@Composable -private fun BottomSheetUI(content: @Composable () -> Unit) { - Box( - modifier = Modifier - .wrapContentHeight() - .fillMaxWidth() - .clip(RoundedCornerShape(topEnd = 20.dp, topStart = 20.dp)) - .background(Color.White) - .statusBarsPadding() - ) { - content() } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryFilterBar.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryFilterBar.kt index f7a6c5660..0bb2d40d3 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryFilterBar.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryFilterBar.kt @@ -17,6 +17,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.toLowerCase import androidx.compose.ui.unit.dp import app.omnivore.omnivore.R import app.omnivore.omnivore.persistence.entities.SavedItemLabel @@ -77,7 +79,7 @@ fun LibraryFilterBar(viewModel: LibraryViewModel) { modifier = Modifier.padding(end = 6.dp) ) } - items(activeLabels.sortedBy { it.name }) { label -> + items(activeLabels.sortedWith(compareBy { it.name.toLowerCase(Locale.current) })) { label -> val chipColors = LabelChipColors.fromHex(label.color) AssistChip( 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 1e9b49dd4..c8a268a72 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 @@ -10,17 +10,18 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ModalBottomSheetLayout -import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.* +import androidx.compose.material.DrawerValue import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material3.* +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment @@ -33,38 +34,123 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import app.omnivore.omnivore.R import app.omnivore.omnivore.Routes +import app.omnivore.omnivore.persistence.entities.SavedItemLabel import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights -import app.omnivore.omnivore.ui.components.LabelsSelectionSheet +import app.omnivore.omnivore.ui.components.LabelsSelectionSheetContent import app.omnivore.omnivore.ui.savedItemViews.SavedItemCard import app.omnivore.omnivore.ui.reader.PDFReaderActivity import app.omnivore.omnivore.ui.reader.WebReaderLoadingContainerActivity -import app.omnivore.omnivore.ui.save.SaveSheetActivityBase import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @Composable fun LibraryView( libraryViewModel: LibraryViewModel, navController: NavHostController ) { - Scaffold( - topBar = { - LibraryNavigationBar( - savedItemViewModel = libraryViewModel, - onSearchClicked = { navController.navigate(Routes.Search.route) }, - onSettingsIconClick = { navController.navigate(Routes.Settings.route) } - ) + val scaffoldState: ScaffoldState = rememberScaffoldState() + val showLabelsSelectionSheet: Boolean by libraryViewModel.showLabelsSelectionSheetLiveData.observeAsState(false) - }, - ) { paddingValues -> + val coroutineScope = rememberCoroutineScope() + val modalBottomSheetState = rememberModalBottomSheetState( + ModalBottomSheetValue.Hidden, + confirmStateChange = { it != ModalBottomSheetValue.Hidden } + ) + + if (showLabelsSelectionSheet) { + coroutineScope.launch { + modalBottomSheetState.show() + } + } else { + coroutineScope.launch { + modalBottomSheetState.hide() + } + } + + libraryViewModel.snackbarMessage?.let { + coroutineScope.launch { + scaffoldState.snackbarHostState.showSnackbar(it) + libraryViewModel.clearSnackbarMessage() + } + } + + ModalBottomSheetLayout( + sheetBackgroundColor = Color.Transparent, + sheetState = modalBottomSheetState, + sheetContent = { + BottomSheetContent(libraryViewModel) + Spacer(modifier = Modifier.weight(1.0F)) + } + ) { + Scaffold( + scaffoldState = scaffoldState, + topBar = { + LibraryNavigationBar( + savedItemViewModel = libraryViewModel, + onSearchClicked = { navController.navigate(Routes.Search.route) }, + onSettingsIconClick = { navController.navigate(Routes.Settings.route) } + ) + }, + ) { paddingValues -> LibraryViewContent( libraryViewModel, modifier = Modifier - .padding( - top = paddingValues.calculateTopPadding() - ) + .padding(top = paddingValues.calculateTopPadding()) ) + } + } +} + +@Composable +fun BottomSheetContent(libraryViewModel: LibraryViewModel) { + val showLabelsSelectionSheet: Boolean by libraryViewModel.showLabelsSelectionSheetLiveData.observeAsState(false) + val currentSavedItemData = libraryViewModel.currentSavedItemUnderEdit() + val labels: List by libraryViewModel.savedItemLabelsLiveData.observeAsState(listOf()) + + if (showLabelsSelectionSheet) { + BottomSheetUI { + if (currentSavedItemData != null) { + LabelsSelectionSheetContent( + labels = labels, + initialSelectedLabels = currentSavedItemData.labels, + onCancel = { + libraryViewModel.showLabelsSelectionSheetLiveData.value = false + libraryViewModel.labelsSelectionCurrentItemLiveData.value = null + }, + isLibraryMode = false, + onSave = { + if (it != labels) { + libraryViewModel.updateSavedItemLabels( + savedItemID = currentSavedItemData.savedItem.savedItemId, + labels = it + ) + } + libraryViewModel.labelsSelectionCurrentItemLiveData.value = null + libraryViewModel.showLabelsSelectionSheetLiveData.value = false + }, + onCreateLabel = { newLabelName, labelHexValue -> + libraryViewModel.createNewSavedItemLabel(newLabelName, labelHexValue) + } + ) + } else { // Is used in library mode + LabelsSelectionSheetContent( + labels = labels, + initialSelectedLabels = libraryViewModel.activeLabelsLiveData.value ?: listOf(), + onCancel = { libraryViewModel.showLabelsSelectionSheetLiveData.value = false }, + isLibraryMode = true, + onSave = { + libraryViewModel.updateAppliedLabels(it) + libraryViewModel.labelsSelectionCurrentItemLiveData.value = null + libraryViewModel.showLabelsSelectionSheetLiveData.value = false + }, + onCreateLabel = { newLabelName, labelHexValue -> + libraryViewModel.createNewSavedItemLabel(newLabelName, labelHexValue) + } + ) + } + } } } @@ -88,6 +174,7 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { .fillMaxSize() .pullRefresh(pullRefreshState) ) { + LazyColumn( state = listState, verticalArrangement = Arrangement.Top, @@ -137,10 +224,25 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { modifier = Modifier.align(Alignment.TopCenter) ) - LabelsSelectionSheet(viewModel = libraryViewModel) + // LabelsSelectionSheet(viewModel = libraryViewModel) } } +@Composable +private fun BottomSheetUI(content: @Composable () -> Unit) { + Box( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .clip(RoundedCornerShape(topEnd = 20.dp, topStart = 20.dp)) + .background(Color.White) + .statusBarsPadding() + ) { + content() + } +} + + @Composable fun InfiniteListHandler( listState: LazyListState, diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryViewModel.kt index bffdf8923..def3ea18f 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryViewModel.kt @@ -1,17 +1,26 @@ package app.omnivore.omnivore.ui.library +import android.content.Context +import android.content.Intent +import android.net.Uri import android.util.Log +import android.widget.Toast +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.* import app.omnivore.omnivore.* import app.omnivore.omnivore.dataService.* import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput import app.omnivore.omnivore.graphql.generated.type.SetLabelsInput +import app.omnivore.omnivore.models.ServerSyncStatus import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.persistence.entities.* import com.apollographql.apollo3.api.Optional +import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel @@ -34,6 +43,9 @@ class LibraryViewModel @Inject constructor( private var searchIdx = 0 private var receivedIdx = 0 + var snackbarMessage by mutableStateOf(null) + private set + // Live Data private var itemsLiveDataInternal = dataService.db.savedItemDao().filteredLibraryData( allowedArchiveStates = listOf(0), @@ -75,6 +87,10 @@ class LibraryViewModel @Inject constructor( } } + fun clearSnackbarMessage() { + snackbarMessage = null + } + fun refresh() { cursor = null librarySearchCursor = null @@ -276,6 +292,9 @@ class LibraryViewModel @Inject constructor( labelsSelectionCurrentItemLiveData.value = itemID showLabelsSelectionSheetLiveData.value = true } + else -> { + + } } actionsMenuItemLiveData.postValue(null) } @@ -283,11 +302,39 @@ class LibraryViewModel @Inject constructor( fun updateSavedItemLabels(savedItemID: String, labels: List) { viewModelScope.launch { withContext(Dispatchers.IO) { - val input = SetLabelsInput(labelIds = labels.map { it.savedItemLabelId }, pageId = savedItemID) + val syncedLabels = labels.filter { it.serverSyncStatus == ServerSyncStatus.IS_SYNCED.rawValue } + val unsyncedLabels = labels.filter { it.serverSyncStatus != ServerSyncStatus.IS_SYNCED.rawValue } + + var labelCreationError = false + val createdLabels = unsyncedLabels.mapNotNull { label -> + val result = networker.createNewLabel(CreateLabelInput( + name = label.name, + color = presentIfNotNull(label.color), + description = presentIfNotNull(label.labelDescription), + )) + result?.let { + SavedItemLabel( + savedItemLabelId = result.id, + name = result.name, + color = result.color, + createdAt = result.createdAt.toString(), + labelDescription = result.description, + serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue + ) + } ?: run { + labelCreationError = true + null + } + } + + dataService.db.savedItemLabelDao().insertAll(createdLabels) + + val allLabels = syncedLabels + createdLabels + + val input = SetLabelsInput(labelIds = allLabels.map { it.savedItemLabelId }, pageId = savedItemID) val networkResult = networker.updateLabelsForSavedItem(input) - // TODO: assign a server sync status to these - val crossRefs = labels.map { + val crossRefs = allLabels.map { SavedItemAndSavedItemLabelCrossRef( savedItemLabelId = it.savedItemLabelId, savedItemId = savedItemID @@ -300,6 +347,12 @@ class LibraryViewModel @Inject constructor( // Add back the current labels dataService.db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs) + if (!networkResult || labelCreationError) { + snackbarMessage = "Unable to set labels" + } else { + snackbarMessage = "Labels updated" + } + CoroutineScope(Dispatchers.Main).launch { handleFilterChanges() } @@ -353,5 +406,5 @@ enum class SavedItemAction { Delete, Archive, Unarchive, - EditLabels + EditLabels, } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/notebook/NotebookView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/notebook/NotebookView.kt index c0603ef7a..4bce9ac33 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/notebook/NotebookView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/notebook/NotebookView.kt @@ -120,7 +120,6 @@ fun NotebookView(savedItemId: String, viewModel: NotebookViewModel) { onClick = { val clip = ClipData.newPlainText("notebook", notebookMD(notes, highlights)) clipboard?.let { - it clipboard?.setPrimaryClip(clip) } ?: run { coroutineScope.launch { @@ -289,7 +288,6 @@ fun HighlightsList(item: SavedItemWithLabelsAndHighlights) { onClick = { val clip = ClipData.newPlainText("highlight", highlight.quote) clipboard?.let { - it clipboard?.setPrimaryClip(clip) } ?: run { coroutineScope.launch { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/ReaderPreferencesView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/ReaderPreferencesView.kt index 7d452170b..9f0f8bfbe 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/ReaderPreferencesView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/ReaderPreferencesView.kt @@ -11,6 +11,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.Switch import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -25,6 +27,7 @@ import androidx.compose.ui.unit.sp import app.omnivore.omnivore.R import app.omnivore.omnivore.ui.theme.OmnivoreTheme +@OptIn(ExperimentalMaterial3Api::class) @Composable fun ReaderPreferencesView(webReaderViewModel: WebReaderViewModel) { val isDark = isSystemInDarkTheme() @@ -43,8 +46,6 @@ fun ReaderPreferencesView(webReaderViewModel: WebReaderViewModel) { val themeState = remember { mutableStateOf(currentWebPreferences.storedThemePreference) } - val themeListState = rememberLazyListState() - OmnivoreTheme() { Column( modifier = Modifier @@ -65,16 +66,17 @@ fun ReaderPreferencesView(webReaderViewModel: WebReaderViewModel) { )) Spacer(modifier = Modifier.weight(1.0F)) Box { - OutlinedButton( - shape = RoundedCornerShape(4.dp), + AssistChip( onClick = { isFontListExpanded.value = true }, - colors = ButtonDefaults.buttonColors( - contentColor = Color(red = 137, green = 137, blue = 137), - // containerColor = Color.Transparent, - ), - ) { - Text(selectedWebFontName.value) - } + label = { Text(selectedWebFontName.value, color = Color(red = 137, green = 137, blue = 137)) }, + trailingIcon = { + Icon( + Icons.Default.ArrowDropDown, + contentDescription = "Choose the Reader font", + tint = Color(red = 137, green = 137, blue = 137) + ) + }, + ) if (isFontListExpanded.value) { DropdownMenu( expanded = isFontListExpanded.value, diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt index 6dc195ffc..216942a75 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt @@ -11,6 +11,7 @@ import android.view.* import android.view.View.OnScrollChangeListener import android.view.ViewTreeObserver.OnScrollChangedListener import android.webkit.JavascriptInterface +import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient import androidx.compose.foundation.isSystemInDarkTheme @@ -62,6 +63,21 @@ fun WebReader( viewModel?.showNavBar() view?.animate()?.alpha(1.0f)?.duration = 200 } + + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest? + ): Boolean { + var handled: Boolean? = null + request?.let { + if ((request?.isForMainFrame == true) && (request?.hasGesture() == true) && viewModel != null) { + viewModel?.showOpenLinkSheet(context, request.url) + handled = true + } + } + + return handled ?: super.shouldOverrideUrlLoading(view, request) + } } val javascriptInterface = AndroidWebKitMessenger { actionID, json -> 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 cfbb23703..89d92554c 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 @@ -3,6 +3,7 @@ package app.omnivore.omnivore.ui.reader import android.content.Intent import android.os.Bundle import android.view.View +import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.LocalOnBackPressedDispatcherOwner import androidx.activity.compose.setContent @@ -14,8 +15,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.* +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text @@ -34,7 +38,6 @@ import app.omnivore.omnivore.MainActivity import app.omnivore.omnivore.R import app.omnivore.omnivore.persistence.entities.SavedItemLabel import app.omnivore.omnivore.ui.components.LabelsSelectionSheetContent -import app.omnivore.omnivore.ui.components.WebReaderLabelsSelectionSheet import app.omnivore.omnivore.ui.notebook.NotebookView import app.omnivore.omnivore.ui.notebook.NotebookViewModel import app.omnivore.omnivore.ui.savedItemViews.SavedItemContextMenu @@ -43,6 +46,8 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import kotlin.math.roundToInt +import androidx.compose.material3.Button +import androidx.compose.ui.platform.LocalContext @AndroidEntryPoint @@ -112,6 +117,7 @@ enum class BottomSheetState( NOTEBOOK(), HIGHLIGHTNOTE(), LABELS(), + LINK() } @@ -122,18 +128,11 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, webReaderViewModel: WebReaderViewModel, notebookViewModel: NotebookViewModel) { val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher - - var isMenuExpanded by remember { mutableStateOf(false) } - var bottomSheetState by remember { mutableStateOf(BottomSheetState.NONE) } - - val isDarkMode = isSystemInDarkTheme() - val currentThemeKey = webReaderViewModel.currentThemeKey.observeAsState() - val currentTheme = Themes.values().find { it.themeKey == currentThemeKey.value } + val bottomSheetState: BottomSheetState? by webReaderViewModel.bottomSheetStateLiveData.observeAsState(BottomSheetState.NONE) val webReaderParams: WebReaderParams? by webReaderViewModel.webReaderParamsLiveData.observeAsState(null) - val annotation: String? by webReaderViewModel.annotationLiveData.observeAsState(null) val shouldPopView: Boolean by webReaderViewModel.shouldPopViewLiveData.observeAsState(false) - val toolbarHeightPx: Float by webReaderViewModel.currentToolbarHeightLiveData.observeAsState(0.0f) + val labels: List by webReaderViewModel.savedItemLabelsLiveData.observeAsState(listOf()) @@ -154,42 +153,54 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, val modalBottomSheetState = rememberModalBottomSheetState( initialValue = ModalBottomSheetValue.Hidden, + confirmStateChange = { + if (it == ModalBottomSheetValue.Hidden) { + webReaderViewModel.resetBottomSheet() + } + true + } ) - val themeBackgroundColor = currentTheme?.let { - if (it.themeKey == "System" && isDarkMode) { - Color(0xFF000000) - } else if (it.themeKey == "System" ) { - Color(0xFFFFFFFF) - } else { - Color(it.backgroundColor ?: 0xFFFFFFFF) - } - } ?: Color(0xFFFFFFFF) - val themeTintColor = currentTheme?.let { - if (it.themeKey == "System" && isDarkMode) { - Color(0xFFFFFFFF) - } else if (it.themeKey == "System" ) { - Color(0xFF000000) - } else { - Color(it.foregroundColor ?: 0xFF000000) - } - } ?: Color(0xFF000000) - annotation?.let { - bottomSheetState = BottomSheetState.HIGHLIGHTNOTE - coroutineScope.launch { - modalBottomSheetState.animateTo(ModalBottomSheetValue.Expanded) + when (bottomSheetState) { + BottomSheetState.PREFERENCES -> { + coroutineScope.launch { + if (!modalBottomSheetState.isVisible) { + modalBottomSheetState.show() + } + } + } + BottomSheetState.NOTEBOOK -> { + coroutineScope.launch { + modalBottomSheetState.show() + } + } + BottomSheetState.HIGHLIGHTNOTE -> { + coroutineScope.launch { + modalBottomSheetState.show() + } + } + BottomSheetState.LABELS -> { + coroutineScope.launch { + modalBottomSheetState.show() + } + } + BottomSheetState.LINK -> { + coroutineScope.launch { + modalBottomSheetState.show() + } + } + BottomSheetState.NONE -> { + coroutineScope.launch { + modalBottomSheetState.hide() + } + } + else -> { + coroutineScope.launch { + modalBottomSheetState.hide() + } + } } - } - - val showLabelsSelector: Boolean by webReaderViewModel.showLabelsSelectionSheetLiveData.observeAsState(false) - - if (showLabelsSelector) { - bottomSheetState = BottomSheetState.LABELS - coroutineScope.launch { - modalBottomSheetState.animateTo(ModalBottomSheetValue.HalfExpanded) - } - } ModalBottomSheetLayout( modifier = Modifier @@ -211,37 +222,34 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, } } BottomSheetState.HIGHLIGHTNOTE -> { - annotation?.let { annotation -> + webReaderViewModel.annotation?.let { annotation -> BottomSheetUI(title = "Note") { AnnotationEditView( initialAnnotation = annotation, onSave = { webReaderViewModel.saveAnnotation(it) coroutineScope.launch { - modalBottomSheetState.hide() - bottomSheetState = BottomSheetState.NONE + webReaderViewModel.resetBottomSheet() } }, onCancel = { webReaderViewModel.cancelAnnotationEdit() coroutineScope.launch { - modalBottomSheetState.hide() - bottomSheetState = BottomSheetState.NONE + webReaderViewModel.resetBottomSheet() } } ) } } } - app.omnivore.omnivore.ui.reader.BottomSheetState.LABELS -> { + BottomSheetState.LABELS -> { BottomSheetUI(title = "Notebook") { LabelsSelectionSheetContent( labels = labels, initialSelectedLabels = webReaderParams?.labels ?: listOf(), onCancel = { coroutineScope.launch { - modalBottomSheetState.hide() - bottomSheetState = BottomSheetState.NONE + webReaderViewModel.resetBottomSheet() } }, isLibraryMode = false, @@ -252,8 +260,7 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, ) } coroutineScope.launch { - modalBottomSheetState.hide() - bottomSheetState = BottomSheetState.NONE + webReaderViewModel.resetBottomSheet() } }, onCreateLabel = { newLabelName, labelHexValue -> @@ -262,8 +269,16 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, ) } } + BottomSheetState.LINK -> { + BottomSheetUI(title = "Open Link") { + OpenLinkView(webReaderViewModel) + } + } BottomSheetState.NONE -> { + } + else -> { + } } Spacer(modifier = Modifier.weight(1.0F)) @@ -271,104 +286,135 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, ) { Scaffold( topBar = { - TopAppBar( - modifier = Modifier - .height(height = with(LocalDensity.current) { - toolbarHeightPx.roundToInt().toDp() - }), - backgroundColor = themeBackgroundColor, - elevation = 0.dp, - title = {}, - navigationIcon = { - IconButton(onClick = { - onBackPressedDispatcher?.onBackPressed() - }) { - Icon( - imageVector = Icons.Filled.ArrowBack, - modifier = Modifier, - contentDescription = "Back", - tint = themeTintColor - ) - } - }, - actions = { - if (onLibraryIconTap != null) { - IconButton(onClick = { onLibraryIconTap() }) { - Icon( - imageVector = Icons.Default.Home, - contentDescription = null, - tint = themeTintColor, - ) - } - } - webReaderParams?.let { - IconButton(onClick = { - coroutineScope.launch { - bottomSheetState = BottomSheetState.NOTEBOOK - modalBottomSheetState.animateTo(ModalBottomSheetValue.Expanded) - } - }) { - Icon( - painter = painterResource(id = R.drawable.notebook), - contentDescription = null, - tint = themeTintColor - ) - } - } - IconButton(onClick = { - coroutineScope.launch { - bottomSheetState = BottomSheetState.PREFERENCES - modalBottomSheetState.animateTo(ModalBottomSheetValue.HalfExpanded) - } - }) { - Icon( - painter = painterResource(id = R.drawable.format_letter_case), - contentDescription = null, - tint = themeTintColor - ) - } - IconButton(onClick = { isMenuExpanded = true }) { - Icon( - painter = painterResource(id = R.drawable.dots_horizontal), - contentDescription = null, - tint = themeTintColor - ) - if (isMenuExpanded) { - webReaderParams?.let { params -> - SavedItemContextMenu( - isExpanded = isMenuExpanded, - isArchived = params.item.isArchived, - onDismiss = { isMenuExpanded = false }, - actionHandler = { - webReaderViewModel.handleSavedItemAction( - params.item.savedItemId, - it - ) - } - ) - } - } - } - }, - ) - } - ) { paddingValues -> - if (styledContent != null) { - WebReader( - styledContent = styledContent, - webReaderViewModel = webReaderViewModel - ) - } + ReaderTopAppBar(webReaderViewModel, onLibraryIconTap) + }) { paddingValues -> + if (styledContent != null) { + WebReader( + styledContent = styledContent, + webReaderViewModel = webReaderViewModel + ) } - LaunchedEffect(shouldPopView) { - if (shouldPopView) { - onBackPressedDispatcher?.onBackPressed() + LaunchedEffect(shouldPopView) { + if (shouldPopView) { + onBackPressedDispatcher?.onBackPressed() + } } } } } +@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) +@Composable +fun ReaderTopAppBar(webReaderViewModel: WebReaderViewModel, onLibraryIconTap: (() -> Unit)? = null) { + val context = LocalContext.current + val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher + + val isDarkMode = isSystemInDarkTheme() + val currentThemeKey = webReaderViewModel.currentThemeKey.observeAsState() + val currentTheme = Themes.values().find { it.themeKey == currentThemeKey.value } + val toolbarHeightPx: Float by webReaderViewModel.currentToolbarHeightLiveData.observeAsState(0.0f) + val webReaderParams: WebReaderParams? by webReaderViewModel.webReaderParamsLiveData.observeAsState(null) + var isMenuExpanded by remember { mutableStateOf(false) } + + val themeBackgroundColor = currentTheme?.let { + if (it.themeKey == "System" && isDarkMode) { + Color(0xFF000000) + } else if (it.themeKey == "System" ) { + Color(0xFFFFFFFF) + } else { + Color(it.backgroundColor ?: 0xFFFFFFFF) + } + } ?: Color(0xFFFFFFFF) + + val themeTintColor = currentTheme?.let { + if (it.themeKey == "System" && isDarkMode) { + Color(0xFFFFFFFF) + } else if (it.themeKey == "System" ) { + Color(0xFF000000) + } else { + Color(it.foregroundColor ?: 0xFF000000) + } + } ?: Color(0xFF000000) + + + TopAppBar( + modifier = Modifier + .height(height = with(LocalDensity.current) { + toolbarHeightPx.roundToInt().toDp() + }), + backgroundColor = themeBackgroundColor, + elevation = 0.dp, + title = {}, + navigationIcon = { + IconButton(onClick = { + onBackPressedDispatcher?.onBackPressed() + }) { + Icon( + imageVector = Icons.Filled.ArrowBack, + modifier = Modifier, + contentDescription = "Back", + tint = themeTintColor + ) + } + }, + actions = { + if (onLibraryIconTap != null) { + IconButton(onClick = { onLibraryIconTap() }) { + Icon( + imageVector = Icons.Default.Home, + contentDescription = null, + tint = themeTintColor, + ) + } + } + webReaderParams?.let { + IconButton(onClick = { + webReaderViewModel.setBottomSheet(BottomSheetState.NOTEBOOK) + }) { + Icon( + painter = painterResource(id = R.drawable.notebook), + contentDescription = null, + tint = themeTintColor + ) + } + } + IconButton(onClick = { + webReaderViewModel.setBottomSheet(BottomSheetState.PREFERENCES) + }) { + Icon( + painter = painterResource(id = R.drawable.format_letter_case), + contentDescription = null, + tint = themeTintColor + ) + } + IconButton(onClick = { isMenuExpanded = true }) { + Icon( + painter = painterResource(id = R.drawable.dots_horizontal), + contentDescription = null, + tint = themeTintColor + ) + if (isMenuExpanded) { + webReaderParams?.let { params -> + SavedItemContextMenu( + context = context, + isExpanded = isMenuExpanded, + isArchived = params.item.isArchived, + onDismiss = { isMenuExpanded = false }, + webReaderViewModel = webReaderViewModel, + actionHandler = { + webReaderViewModel.handleSavedItemAction( + params.item.savedItemId, + it + ) + } + ) + } + } + } + }, + ) +} @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @Composable @@ -383,10 +429,44 @@ fun BottomSheetUI(title: String?, content: @Composable () -> Unit) { ) { Scaffold( ) { paddingValues -> - Box(modifier = Modifier - .fillMaxSize()) { + Box(modifier = Modifier.fillMaxSize()) { content() } } } } + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) +@Composable +fun OpenLinkView(webReaderViewModel: WebReaderViewModel) { + val context = LocalContext.current + + Column(modifier = Modifier + .padding(top = 50.dp) + .padding(horizontal = 50.dp), verticalArrangement = Arrangement.spacedBy(20.dp)) { + Row { + Button(onClick = { webReaderViewModel.openCurrentLink(context) }, modifier = Modifier.fillMaxWidth()) { + Text(text = "Open in Browser") + + } + } + Row() { + Button(onClick = { webReaderViewModel.saveCurrentLink(context) }, modifier = Modifier.fillMaxWidth()) { + Text(text = "Save to Omnivore") + + } + } + Row() { + Button(onClick = {webReaderViewModel.copyCurrentLink(context) }, modifier = Modifier.fillMaxWidth()) { + Text(text = "Copy Link") + + } + } + Row { + Button(onClick = {webReaderViewModel.resetBottomSheet() }, modifier = Modifier.fillMaxWidth()) { + Text(text = "Cancel") + + } + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt index 752d42018..8a8729006 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt @@ -1,23 +1,25 @@ package app.omnivore.omnivore.ui.reader +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.net.Uri import android.util.Log -import androidx.compose.foundation.ScrollState -import androidx.compose.runtime.remember -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import android.widget.Toast +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.ContextCompat.startActivity import androidx.lifecycle.* import app.omnivore.omnivore.DatastoreKeys import app.omnivore.omnivore.DatastoreRepository import app.omnivore.omnivore.dataService.* import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput import app.omnivore.omnivore.graphql.generated.type.SetLabelsInput -import app.omnivore.omnivore.persistence.entities.SavedItem import app.omnivore.omnivore.networking.* +import app.omnivore.omnivore.persistence.entities.SavedItem import app.omnivore.omnivore.persistence.entities.SavedItemAndSavedItemLabelCrossRef import app.omnivore.omnivore.persistence.entities.SavedItemLabel import app.omnivore.omnivore.ui.library.SavedItemAction -import com.apollographql.apollo3.api.Optional import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull import com.google.gson.Gson import dagger.hilt.android.lifecycle.HiltViewModel @@ -26,6 +28,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import java.util.* import javax.inject.Inject + data class WebReaderParams( val item: SavedItem, val articleContent: ArticleContent, @@ -56,14 +59,16 @@ class WebReaderViewModel @Inject constructor( var maxToolbarHeightPx = 0.0f val webReaderParamsLiveData = MutableLiveData(null) - val annotationLiveData = MutableLiveData(null) + var annotation: String? = null val javascriptActionLoopUUIDLiveData = MutableLiveData(lastJavascriptActionLoopUUID) val shouldPopViewLiveData = MutableLiveData(false) val hasFetchError = MutableLiveData(false) val currentToolbarHeightLiveData = MutableLiveData(0.0f) - val showLabelsSelectionSheetLiveData = MutableLiveData(false) val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData() + var currentLink: Uri? = null + val bottomSheetStateLiveData = MutableLiveData(BottomSheetState.NONE) + var hasTappedExistingHighlight = false var lastTapCoordinates: TapCoordinates? = null private var isLoading = false @@ -85,6 +90,72 @@ class WebReaderViewModel @Inject constructor( onScrollChange(maxToolbarHeightPx) } + fun setBottomSheet(state: BottomSheetState) { + bottomSheetStateLiveData.postValue(state) + } + + fun resetBottomSheet() { + bottomSheetStateLiveData.postValue(BottomSheetState.NONE) + } + + fun showOpenLinkSheet(context: Context, uri: Uri) { + webReaderParamsLiveData.value?.let { + if (it.item.pageURLString == uri.toString()) { + openLink(context, uri) + } else { + currentLink = uri + bottomSheetStateLiveData.postValue(BottomSheetState.LINK) + } + } + } + + fun showShareLinkSheet(context: Context) { + webReaderParamsLiveData.value?.let { + val browserIntent = Intent(Intent.ACTION_SEND) + + browserIntent.setType("text/plain") + browserIntent.putExtra(Intent.EXTRA_TEXT, it.item.pageURLString) + browserIntent.putExtra(Intent.EXTRA_SUBJECT, it.item.pageURLString) + context.startActivity(browserIntent) + } + } + + fun openCurrentLink(context: Context) { + currentLink?.let { + openLink(context, it) + } + bottomSheetStateLiveData.postValue(BottomSheetState.NONE) + } + + fun openLink(context: Context, uri: Uri) { + val browserIntent = Intent(Intent.ACTION_VIEW, uri) + startActivity(context, browserIntent, null) + } + + fun saveCurrentLink(context: Context) { + currentLink?.let { + viewModelScope.launch { + val success = networker.saveUrl(it) + Toast.makeText(context, if (success) "Link saved" else "Error saving link" , Toast.LENGTH_SHORT).show() + } + } + bottomSheetStateLiveData.postValue(BottomSheetState.NONE) + } + + fun copyCurrentLink(context: Context) { + currentLink?.let { + val clip = ClipData.newPlainText("link", it.toString()) + val clipboard = + context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard.setPrimaryClip(clip) + clipboard?.let { + clipboard?.setPrimaryClip(clip) + Toast.makeText(context, "Link Copied", Toast.LENGTH_SHORT).show() + } + } + bottomSheetStateLiveData.postValue(BottomSheetState.NONE) + } + fun onScrollChange(delta: Float) { val newHeight = (currentToolbarHeightLiveData.value ?: 0.0f) + delta currentToolbarHeightLiveData.value = newHeight.coerceIn(0f, maxToolbarHeightPx) @@ -184,7 +255,7 @@ class WebReaderViewModel @Inject constructor( } } SavedItemAction.EditLabels -> { - showLabelsSelectionSheetLiveData.value = true + bottomSheetStateLiveData.postValue(BottomSheetState.LABELS) } } } @@ -221,10 +292,11 @@ class WebReaderViewModel @Inject constructor( } "annotate" -> { viewModelScope.launch { - val annotation = Gson() + val annotationStr = Gson() .fromJson(jsonString, AnnotationWebViewMessage::class.java) .annotation ?: "" - annotationLiveData.value = annotation + annotation = annotationStr + bottomSheetStateLiveData.postValue(BottomSheetState.HIGHLIGHTNOTE) } } "shareHighlight" -> { @@ -253,7 +325,8 @@ class WebReaderViewModel @Inject constructor( } fun cancelAnnotationEdit() { - annotationLiveData.value = null + annotation = null + resetBottomSheet() } private fun enqueueScript(javascript: String) { @@ -396,7 +469,7 @@ class WebReaderViewModel @Inject constructor( viewModelScope.launch { withContext(Dispatchers.IO) { - val newLabel = networker.createNewLabel(CreateLabelInput(color = Optional.presentIfNotNull(hexColorValue), name = labelName)) + val newLabel = networker.createNewLabel(CreateLabelInput(color = presentIfNotNull(hexColorValue), name = labelName)) newLabel?.let { val savedItemLabel = SavedItemLabel( diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveSheetActivity.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveSheetActivity.kt index 9648fdd25..bb39e5eb2 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveSheetActivity.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveSheetActivity.kt @@ -4,23 +4,47 @@ import android.content.ContentValues import android.content.Intent import android.os.Bundle import android.util.Log +import android.widget.TextView.SavedState +import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.rounded.AddCircle +import androidx.compose.material.icons.rounded.Home +import androidx.compose.material.icons.rounded.Settings +import androidx.compose.material3.BottomAppBarDefaults +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.* +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment.Companion.TopCenter import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import app.omnivore.omnivore.R +import app.omnivore.omnivore.ui.library.SavedItemAction +import app.omnivore.omnivore.ui.reader.WebReaderLoadingContainerActivity import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.seconds // Not sure why we need this class, but directly opening SaveSheetActivity // causes the app to crash. @@ -56,36 +80,49 @@ abstract class SaveSheetActivityBase: AppCompatActivity() { } setContent { - val coroutineScope = rememberCoroutineScope() - val modalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) - val isSheetOpened = remember { mutableStateOf(false) } + val saveState: SaveState by viewModel.saveState.observeAsState(SaveState.NONE) + val scaffoldState: ScaffoldState = rememberScaffoldState() - ModalBottomSheetLayout( - sheetBackgroundColor = Color.Transparent, - sheetState = modalBottomSheetState, - sheetContent = { - BottomSheetUI { - ScreenContent(viewModel, modalBottomSheetState) - } - } - ) {} - BackHandler { - onFinish(coroutineScope, modalBottomSheetState) + val message = when (saveState) { + SaveState.NONE -> "" + SaveState.SAVING -> "Saved to Omnivore" + SaveState.ERROR -> "Error Saving Article" + SaveState.SAVED -> "Saved to Omnivore" } - // Take action based on hidden state - LaunchedEffect(modalBottomSheetState.currentValue) { - when (modalBottomSheetState.currentValue) { - ModalBottomSheetValue.Hidden -> { - handleBottomSheetAtHiddenState( - isSheetOpened, - modalBottomSheetState - ) - } - else -> { - Log.i(TAG, "Bottom sheet ${modalBottomSheetState.currentValue} state") - } + Scaffold( + modifier = Modifier.clickable { + Log.d("debug", "DISMISS SCAFFOLD") + exit() + }, + scaffoldState = scaffoldState, + backgroundColor = Color.Transparent, + + // TODO: In future versions we can present Label, Note, Highlight options here + bottomBar = { + + androidx.compose.material3.BottomAppBar( + + modifier = Modifier + .height(55.dp) + .fillMaxWidth() + .clip(RoundedCornerShape(topEnd = 5.dp, topStart = 5.dp)), + containerColor = MaterialTheme.colors.background, + actions = { + Spacer(modifier = Modifier.width(25.dp)) + Text(message, style = androidx.compose.material3.MaterialTheme.typography.titleMedium) + }, + ) + }, + ) { + + } + + LaunchedEffect(saveState) { + if (saveState == SaveState.SAVED) { + delay(1.5.seconds) + exit() } } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveViewModel.kt index 05ad7b3d6..de4eea0d6 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveViewModel.kt @@ -5,6 +5,7 @@ import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.omnivore.omnivore.Constants @@ -17,13 +18,21 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import java.util.* -import java.util.regex.Pattern import javax.inject.Inject +enum class SaveState { + NONE(), + SAVING(), + ERROR(), + SAVED() +} + @HiltViewModel class SaveViewModel @Inject constructor( private val datastoreRepo: DatastoreRepository ): ViewModel() { + val saveState = MutableLiveData(SaveState.NONE) + var isLoading by mutableStateOf(false) private set @@ -46,6 +55,7 @@ class SaveViewModel @Inject constructor( viewModelScope.launch { isLoading = true message = "Saving to Omnivore..." + saveState.postValue(SaveState.SAVING) val authToken = getAuthToken() @@ -85,6 +95,7 @@ class SaveViewModel @Inject constructor( "There was an error saving your page" } + saveState.postValue(SaveState.SAVED) Log.d(ContentValues.TAG, "Saved URL?: $success") } catch (e: java.lang.Exception) { message = "There was an error saving your page" diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt index ee3fe78bc..6e60e0d77 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt @@ -16,21 +16,22 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.toLowerCase import androidx.compose.ui.unit.* import app.omnivore.omnivore.R import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights import app.omnivore.omnivore.ui.components.LabelChipColors -import app.omnivore.omnivore.ui.library.LibraryViewModel import app.omnivore.omnivore.ui.library.SavedItemAction import app.omnivore.omnivore.ui.library.SavedItemViewModel import coil.compose.rememberAsyncImagePainter -@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class, -) +@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) @Composable fun SavedItemCard(savedItemViewModel: SavedItemViewModel, savedItem: SavedItemWithLabelsAndHighlights, onClickHandler: () -> Unit, actionHandler: (SavedItemAction) -> Unit) { val listState = rememberLazyListState() @@ -64,6 +65,7 @@ fun SavedItemCard(savedItemViewModel: SavedItemViewModel, savedItem: SavedItemWi text = savedItem.savedItem.title, style = TextStyle( fontSize = 18.sp, + color = MaterialTheme.colorScheme.onBackground, fontWeight = FontWeight.SemiBold ), maxLines = 2, @@ -95,23 +97,14 @@ fun SavedItemCard(savedItemViewModel: SavedItemViewModel, savedItem: SavedItemWi ) } - LazyRow( - state = listState, - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(start = 5.dp, bottom = 5.dp, end = 10.dp) - ) { - items(savedItem.labels.sortedBy { it.name }) { label -> + FlowRow(modifier = Modifier.fillMaxWidth().padding(10.dp)) { + savedItem.labels.sortedWith(compareBy { it.name.toLowerCase(Locale.current) }).forEach { label -> val chipColors = LabelChipColors.fromHex(label.color) LabelChip( - // onClick = onClickHandler, name = label.name, colors = chipColors, - // modifier = Modifier.padding(end = 5.dp) ) - } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemContextMenu.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemContextMenu.kt index acbfb0861..70769a7f7 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemContextMenu.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemContextMenu.kt @@ -1,22 +1,28 @@ package app.omnivore.omnivore.ui.savedItemViews +import android.content.Context import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.CheckCircle import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.List +import androidx.compose.material.icons.outlined.Share import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import app.omnivore.omnivore.R import app.omnivore.omnivore.ui.library.SavedItemAction +import app.omnivore.omnivore.ui.reader.WebReaderViewModel @Composable fun SavedItemContextMenu( isExpanded: Boolean, isArchived: Boolean, + context: Context, + webReaderViewModel: WebReaderViewModel, onDismiss: () -> Unit, actionHandler: (SavedItemAction) -> Unit ) { @@ -24,19 +30,19 @@ fun SavedItemContextMenu( expanded = isExpanded, onDismissRequest = onDismiss ) { -// DropdownMenuItem( -// text = { Text("Edit Labels") }, -// onClick = { -// actionHandler(SavedItemAction.EditLabels) -// onDismiss() -// }, -// leadingIcon = { -// Icon( -// painter = painterResource(id = R.drawable.tag), -// contentDescription = null -// ) -// } -// ) + DropdownMenuItem( + text = { Text("Edit Labels") }, + onClick = { + actionHandler(SavedItemAction.EditLabels) + onDismiss() + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.tag), + contentDescription = null + ) + } + ) DropdownMenuItem( text = { Text(if (isArchived) "Unarchive" else "Archive") }, onClick = { @@ -51,6 +57,19 @@ fun SavedItemContextMenu( ) } ) + DropdownMenuItem( + text = { Text("Share Original") }, + onClick = { + webReaderViewModel.showShareLinkSheet(context) + onDismiss() + }, + leadingIcon = { + Icon( + Icons.Outlined.Share, + contentDescription = null + ) + } + ) DropdownMenuItem( text = { Text("Remove Item") }, onClick = { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Theme.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Theme.kt index dd10f6c73..ab9c2e111 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Theme.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Theme.kt @@ -9,62 +9,12 @@ import androidx.compose.ui.platform.LocalContext private val LightColors = lightColorScheme( primary = md_theme_light_primary, onPrimary = md_theme_light_onPrimary, - primaryContainer = md_theme_light_primaryContainer, - onPrimaryContainer = md_theme_light_onPrimaryContainer, - secondary = md_theme_light_secondary, - onSecondary = md_theme_light_onSecondary, - secondaryContainer = md_theme_light_secondaryContainer, - onSecondaryContainer = md_theme_light_onSecondaryContainer, - tertiary = md_theme_light_tertiary, - onTertiary = md_theme_light_onTertiary, - tertiaryContainer = md_theme_light_tertiaryContainer, - onTertiaryContainer = md_theme_light_onTertiaryContainer, - error = md_theme_light_error, - errorContainer = md_theme_light_errorContainer, - onError = md_theme_light_onError, - onErrorContainer = md_theme_light_onErrorContainer, - background = md_theme_light_background, - onBackground = md_theme_light_onBackground, - surface = md_theme_light_surface, - onSurface = md_theme_light_onSurface, - surfaceVariant = md_theme_light_surfaceVariant, - onSurfaceVariant = md_theme_light_onSurfaceVariant, - outline = md_theme_light_outline, - inverseOnSurface = md_theme_light_inverseOnSurface, - inverseSurface = md_theme_light_inverseSurface, - inversePrimary = md_theme_light_inversePrimary, - surfaceTint = md_theme_light_surfaceTint, ) private val DarkColors = darkColorScheme( primary = md_theme_dark_primary, onPrimary = md_theme_dark_onPrimary, - primaryContainer = md_theme_dark_primaryContainer, - onPrimaryContainer = md_theme_dark_onPrimaryContainer, - secondary = md_theme_dark_secondary, - onSecondary = md_theme_dark_onSecondary, - secondaryContainer = md_theme_dark_secondaryContainer, - onSecondaryContainer = md_theme_dark_onSecondaryContainer, - tertiary = md_theme_dark_tertiary, - onTertiary = md_theme_dark_onTertiary, - tertiaryContainer = md_theme_dark_tertiaryContainer, - onTertiaryContainer = md_theme_dark_onTertiaryContainer, - error = md_theme_dark_error, - errorContainer = md_theme_dark_errorContainer, - onError = md_theme_dark_onError, - onErrorContainer = md_theme_dark_onErrorContainer, - background = md_theme_dark_background, - onBackground = md_theme_dark_onBackground, - surface = md_theme_dark_surface, - onSurface = md_theme_dark_onSurface, - surfaceVariant = md_theme_dark_surfaceVariant, - onSurfaceVariant = md_theme_dark_onSurfaceVariant, - outline = md_theme_dark_outline, - inverseOnSurface = md_theme_dark_inverseOnSurface, - inverseSurface = md_theme_dark_inverseSurface, - inversePrimary = md_theme_dark_inversePrimary, - surfaceTint = md_theme_dark_surfaceTint, ) @Composable @@ -73,12 +23,7 @@ fun OmnivoreTheme( useDynamicTheme: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S, content: @Composable () -> Unit ) { - val colorScheme = if (useDynamicTheme) { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } else { - if (darkTheme) darkColorScheme() else lightColorScheme() - } + val colorScheme = if (darkTheme) DarkColors else LightColors MaterialTheme( colorScheme = colorScheme,