From 431866da8bbabe644b8b22dc3adef1556c69461e Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 10 May 2023 13:32:50 +0800 Subject: [PATCH 01/20] Start to move bottomsheet handling into viewModel so it can control the current bottom state This is needed so we can implement the "Open Link" bottom sheet and re-enable the Labels bottom sheet. --- .../ui/components/LabelsSelectionSheet.kt | 43 ------------------- .../omnivore/omnivore/ui/reader/WebReader.kt | 12 ++++++ .../ui/reader/WebReaderLoadingContainer.kt | 34 +++++---------- .../omnivore/ui/reader/WebReaderViewModel.kt | 13 ++++++ 4 files changed, 35 insertions(+), 67 deletions(-) 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..28dff7936 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 @@ -9,9 +9,7 @@ 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.RoundedCornerShape -import androidx.compose.foundation.verticalScroll import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ModalBottomSheetLayout import androidx.compose.material.ModalBottomSheetValue @@ -28,50 +26,9 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog 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 -@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) - - 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) - } - ) - } - } - ) {} - } -} @Composable fun LabelsSelectionSheet(viewModel: LibraryViewModel) { 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..fdfb893ac 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,17 @@ fun WebReader( viewModel?.showNavBar() view?.animate()?.alpha(1.0f)?.duration = 200 } + + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest? + ): Boolean { + Log.d("reader","SHOULD OVERRIDE REQUEST: ${request?.url} hasGesture: ${request?.hasGesture()} isForMainFrame: ${request?.isForMainFrame()}") + if ((request?.isForMainFrame == true) && (request?.hasGesture() == true)) { + return false + } + return 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..8c4f5e196 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 @@ -34,7 +34,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 @@ -124,7 +123,7 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher var isMenuExpanded by remember { mutableStateOf(false) } - var bottomSheetState by remember { mutableStateOf(BottomSheetState.NONE) } + val bottomSheetState: BottomSheetState? by webReaderViewModel.bottomSheetStateLiveData.observeAsState(BottomSheetState.NONE) val isDarkMode = isSystemInDarkTheme() val currentThemeKey = webReaderViewModel.currentThemeKey.observeAsState() @@ -175,22 +174,6 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, } } ?: Color(0xFF000000) - annotation?.let { - bottomSheetState = BottomSheetState.HIGHLIGHTNOTE - coroutineScope.launch { - modalBottomSheetState.animateTo(ModalBottomSheetValue.Expanded) - } - } - - val showLabelsSelector: Boolean by webReaderViewModel.showLabelsSelectionSheetLiveData.observeAsState(false) - - if (showLabelsSelector) { - bottomSheetState = BottomSheetState.LABELS - coroutineScope.launch { - modalBottomSheetState.animateTo(ModalBottomSheetValue.HalfExpanded) - } - } - ModalBottomSheetLayout( modifier = Modifier .statusBarsPadding(), @@ -219,14 +202,14 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, webReaderViewModel.saveAnnotation(it) coroutineScope.launch { modalBottomSheetState.hide() - bottomSheetState = BottomSheetState.NONE + webReaderViewModel.resetBottomSheet() } }, onCancel = { webReaderViewModel.cancelAnnotationEdit() coroutineScope.launch { modalBottomSheetState.hide() - bottomSheetState = BottomSheetState.NONE + webReaderViewModel.resetBottomSheet() } } ) @@ -241,7 +224,7 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, onCancel = { coroutineScope.launch { modalBottomSheetState.hide() - bottomSheetState = BottomSheetState.NONE + webReaderViewModel.resetBottomSheet() } }, isLibraryMode = false, @@ -253,7 +236,7 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, } coroutineScope.launch { modalBottomSheetState.hide() - bottomSheetState = BottomSheetState.NONE + webReaderViewModel.resetBottomSheet() } }, onCreateLabel = { newLabelName, labelHexValue -> @@ -264,6 +247,9 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, } BottomSheetState.NONE -> { + } + else -> { + } } Spacer(modifier = Modifier.weight(1.0F)) @@ -304,7 +290,7 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, webReaderParams?.let { IconButton(onClick = { coroutineScope.launch { - bottomSheetState = BottomSheetState.NOTEBOOK + webReaderViewModel.setBottomSheet(BottomSheetState.NOTEBOOK) modalBottomSheetState.animateTo(ModalBottomSheetValue.Expanded) } }) { @@ -317,7 +303,7 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, } IconButton(onClick = { coroutineScope.launch { - bottomSheetState = BottomSheetState.PREFERENCES + webReaderViewModel.setBottomSheet(BottomSheetState.PREFERENCES) modalBottomSheetState.animateTo(ModalBottomSheetValue.HalfExpanded) } }) { 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..5210ccfd6 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 @@ -2,6 +2,7 @@ package app.omnivore.omnivore.ui.reader import android.util.Log import androidx.compose.foundation.ScrollState +import androidx.compose.material.ModalBottomSheetValue import androidx.compose.runtime.remember import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection @@ -64,6 +65,8 @@ class WebReaderViewModel @Inject constructor( val showLabelsSelectionSheetLiveData = MutableLiveData(false) val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData() + val bottomSheetStateLiveData = MutableLiveData(BottomSheetState.NONE) + var hasTappedExistingHighlight = false var lastTapCoordinates: TapCoordinates? = null private var isLoading = false @@ -85,6 +88,14 @@ class WebReaderViewModel @Inject constructor( onScrollChange(maxToolbarHeightPx) } + fun setBottomSheet(state: BottomSheetState) { + bottomSheetStateLiveData.postValue(state) + } + + fun resetBottomSheet() { + bottomSheetStateLiveData.postValue(BottomSheetState.NONE) + } + fun onScrollChange(delta: Float) { val newHeight = (currentToolbarHeightLiveData.value ?: 0.0f) + delta currentToolbarHeightLiveData.value = newHeight.coerceIn(0f, maxToolbarHeightPx) @@ -185,6 +196,7 @@ class WebReaderViewModel @Inject constructor( } SavedItemAction.EditLabels -> { showLabelsSelectionSheetLiveData.value = true + bottomSheetStateLiveData.postValue(BottomSheetState.LABELS) } } } @@ -225,6 +237,7 @@ class WebReaderViewModel @Inject constructor( .fromJson(jsonString, AnnotationWebViewMessage::class.java) .annotation ?: "" annotationLiveData.value = annotation + bottomSheetStateLiveData.postValue(BottomSheetState.HIGHLIGHTNOTE) } } "shareHighlight" -> { From de3db625f7b6017f7bd1c259246991c3b5d37a09 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 10 May 2023 14:44:36 +0800 Subject: [PATCH 02/20] ADd the set labels sheet back in --- .../omnivore/omnivore/ui/reader/WebReader.kt | 12 ++++--- .../ui/reader/WebReaderLoadingContainer.kt | 33 +++++++++++++++++++ .../omnivore/ui/reader/WebReaderViewModel.kt | 8 +++-- .../ui/savedItemViews/SavedItemContextMenu.kt | 26 +++++++-------- 4 files changed, 60 insertions(+), 19 deletions(-) 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 fdfb893ac..7e2eb5cac 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 @@ -68,11 +68,15 @@ fun WebReader( view: WebView?, request: WebResourceRequest? ): Boolean { - Log.d("reader","SHOULD OVERRIDE REQUEST: ${request?.url} hasGesture: ${request?.hasGesture()} isForMainFrame: ${request?.isForMainFrame()}") - if ((request?.isForMainFrame == true) && (request?.hasGesture() == true)) { - return false + var handled: Boolean? = null + request?.let { + if ((request?.isForMainFrame == true) && (request?.hasGesture() == true) && viewModel != null) { + viewModel?.showOpenLinkSheet(request.url) + handled = true + } } - return super.shouldOverrideUrlLoading(view, request) + + return handled ?: super.shouldOverrideUrlLoading(view, request) } } 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 8c4f5e196..e14807717 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 @@ -174,6 +174,39 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, } } ?: Color(0xFF000000) + when (bottomSheetState) { + BottomSheetState.PREFERENCES -> { + coroutineScope.launch { + modalBottomSheetState.animateTo(ModalBottomSheetValue.HalfExpanded) + } + } + BottomSheetState.NOTEBOOK -> { + coroutineScope.launch { + modalBottomSheetState.animateTo(ModalBottomSheetValue.Expanded) + } + } + BottomSheetState.HIGHLIGHTNOTE -> { + coroutineScope.launch { + modalBottomSheetState.animateTo(ModalBottomSheetValue.Expanded) + } + } + BottomSheetState.LABELS -> { + coroutineScope.launch { + modalBottomSheetState.animateTo(ModalBottomSheetValue.HalfExpanded) + } + } + BottomSheetState.NONE -> { + coroutineScope.launch { + modalBottomSheetState.hide() + } + } + else -> { + coroutineScope.launch { + modalBottomSheetState.hide() + } + } + } + ModalBottomSheetLayout( modifier = Modifier .statusBarsPadding(), 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 5210ccfd6..fb96c4dc1 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,5 +1,6 @@ package app.omnivore.omnivore.ui.reader +import android.net.Uri import android.util.Log import androidx.compose.foundation.ScrollState import androidx.compose.material.ModalBottomSheetValue @@ -24,6 +25,7 @@ import com.google.gson.Gson import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* import kotlinx.coroutines.flow.distinctUntilChanged +import java.net.URI import java.util.* import javax.inject.Inject @@ -62,7 +64,6 @@ class WebReaderViewModel @Inject constructor( val shouldPopViewLiveData = MutableLiveData(false) val hasFetchError = MutableLiveData(false) val currentToolbarHeightLiveData = MutableLiveData(0.0f) - val showLabelsSelectionSheetLiveData = MutableLiveData(false) val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData() val bottomSheetStateLiveData = MutableLiveData(BottomSheetState.NONE) @@ -96,6 +97,10 @@ class WebReaderViewModel @Inject constructor( bottomSheetStateLiveData.postValue(BottomSheetState.NONE) } + fun showOpenLinkSheet(uri: Uri) { + bottomSheetStateLiveData.postValue(BottomSheetState.LABELS) + } + fun onScrollChange(delta: Float) { val newHeight = (currentToolbarHeightLiveData.value ?: 0.0f) + delta currentToolbarHeightLiveData.value = newHeight.coerceIn(0f, maxToolbarHeightPx) @@ -195,7 +200,6 @@ class WebReaderViewModel @Inject constructor( } } SavedItemAction.EditLabels -> { - showLabelsSelectionSheetLiveData.value = true bottomSheetStateLiveData.postValue(BottomSheetState.LABELS) } } 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..34a50a11c 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 @@ -24,19 +24,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 = { From 711a1cd32c7d4be14e6b1539c7ba4eb6c0bc26c5 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 10 May 2023 14:48:59 +0800 Subject: [PATCH 03/20] Remove unneeded live data --- .../omnivore/ui/reader/WebReaderLoadingContainer.kt | 3 +-- .../omnivore/omnivore/ui/reader/WebReaderViewModel.kt | 9 +++++---- 2 files changed, 6 insertions(+), 6 deletions(-) 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 e14807717..ca85a41fd 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 @@ -130,7 +130,6 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, val currentTheme = Themes.values().find { it.themeKey == currentThemeKey.value } 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) @@ -227,7 +226,7 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, } } BottomSheetState.HIGHLIGHTNOTE -> { - annotation?.let { annotation -> + webReaderViewModel.annotation?.let { annotation -> BottomSheetUI(title = "Note") { AnnotationEditView( initialAnnotation = annotation, 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 fb96c4dc1..c4d124b77 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 @@ -59,7 +59,7 @@ 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) @@ -237,10 +237,10 @@ 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) } } @@ -270,7 +270,8 @@ class WebReaderViewModel @Inject constructor( } fun cancelAnnotationEdit() { - annotationLiveData.value = null + annotation = null + resetBottomSheet() } private fun enqueueScript(javascript: String) { From d03d8b931a14afac916946c355e3308d934f6c5f Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 10 May 2023 21:03:28 +0800 Subject: [PATCH 04/20] New bottomsheet for handling link opening --- android/Omnivore/app/build.gradle | 1 + .../omnivore/networking/SavedItemMutations.kt | 24 +++++++- .../omnivore/ui/notebook/NotebookView.kt | 2 - .../ui/reader/WebReaderLoadingContainer.kt | 61 ++++++++++++++++++- .../omnivore/ui/reader/WebReaderViewModel.kt | 57 +++++++++++++---- .../omnivore/ui/save/SaveViewModel.kt | 1 - 6 files changed, 127 insertions(+), 19 deletions(-) diff --git a/android/Omnivore/app/build.gradle b/android/Omnivore/app/build.gradle index 0fd2aa093..04dde458a 100644 --- a/android/Omnivore/app/build.gradle +++ b/android/Omnivore/app/build.gradle @@ -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' 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/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/WebReaderLoadingContainer.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderLoadingContainer.kt index ca85a41fd..e1bcccf68 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 @@ -42,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 @@ -111,6 +117,7 @@ enum class BottomSheetState( NOTEBOOK(), HIGHLIGHTNOTE(), LABELS(), + LINK() } @@ -152,6 +159,12 @@ 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 { @@ -194,6 +207,11 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, modalBottomSheetState.animateTo(ModalBottomSheetValue.HalfExpanded) } } + BottomSheetState.LINK -> { + coroutineScope.launch { + modalBottomSheetState.animateTo(ModalBottomSheetValue.HalfExpanded) + } + } BottomSheetState.NONE -> { coroutineScope.launch { modalBottomSheetState.hide() @@ -248,7 +266,7 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, } } } - app.omnivore.omnivore.ui.reader.BottomSheetState.LABELS -> { + BottomSheetState.LABELS -> { BottomSheetUI(title = "Notebook") { LabelsSelectionSheetContent( labels = labels, @@ -277,6 +295,11 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, ) } } + BottomSheetState.LINK -> { + BottomSheetUI(title = "Open Link") { + OpenLinkView(webReaderViewModel) + } + } BottomSheetState.NONE -> { } @@ -401,10 +424,42 @@ 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 c4d124b77..5ca0c49d2 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,34 +1,35 @@ 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.material.ModalBottomSheetValue -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.runtime.Composable +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 import kotlinx.coroutines.* import kotlinx.coroutines.flow.distinctUntilChanged -import java.net.URI import java.util.* import javax.inject.Inject + data class WebReaderParams( val item: SavedItem, val articleContent: ArticleContent, @@ -66,6 +67,7 @@ class WebReaderViewModel @Inject constructor( val currentToolbarHeightLiveData = MutableLiveData(0.0f) val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData() + var currentLink: Uri? = null val bottomSheetStateLiveData = MutableLiveData(BottomSheetState.NONE) var hasTappedExistingHighlight = false @@ -98,7 +100,40 @@ class WebReaderViewModel @Inject constructor( } fun showOpenLinkSheet(uri: Uri) { - bottomSheetStateLiveData.postValue(BottomSheetState.LABELS) + currentLink = uri + bottomSheetStateLiveData.postValue(BottomSheetState.LINK) + } + + fun openCurrentLink(context: Context) { + currentLink?.let { + val browserIntent = Intent(Intent.ACTION_VIEW, it) + startActivity(context, browserIntent, null) + } + bottomSheetStateLiveData.postValue(BottomSheetState.NONE) + } + + 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) { @@ -414,7 +449,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/SaveViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveViewModel.kt index 05ad7b3d6..3f68ccdaa 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 @@ -17,7 +17,6 @@ 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 @HiltViewModel From c16c5e4ca291f58b17defb70b8b13a48930612f9 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 10 May 2023 21:39:59 +0800 Subject: [PATCH 05/20] Special handling of the Open Original button --- .../omnivore/omnivore/ui/reader/WebReader.kt | 2 +- .../ui/reader/WebReaderLoadingContainer.kt | 6 ++--- .../omnivore/ui/reader/WebReaderViewModel.kt | 22 +++++++++++++------ 3 files changed, 19 insertions(+), 11 deletions(-) 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 7e2eb5cac..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 @@ -71,7 +71,7 @@ fun WebReader( var handled: Boolean? = null request?.let { if ((request?.isForMainFrame == true) && (request?.hasGesture() == true) && viewModel != null) { - viewModel?.showOpenLinkSheet(request.url) + viewModel?.showOpenLinkSheet(context, request.url) handled = true } } 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 e1bcccf68..44581b1bd 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 @@ -160,9 +160,9 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, val modalBottomSheetState = rememberModalBottomSheetState( initialValue = ModalBottomSheetValue.Hidden, confirmStateChange = { -// if (it == ModalBottomSheetValue.Hidden) { -// webReaderViewModel.resetBottomSheet() -// } + if (it == ModalBottomSheetValue.Hidden) { + webReaderViewModel.resetBottomSheet() + } true } ) 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 5ca0c49d2..ce81b62ff 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 @@ -7,8 +7,6 @@ import android.content.Intent import android.net.Uri import android.util.Log import android.widget.Toast -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext import androidx.core.content.ContextCompat.startActivity import androidx.lifecycle.* import app.omnivore.omnivore.DatastoreKeys @@ -99,19 +97,29 @@ class WebReaderViewModel @Inject constructor( bottomSheetStateLiveData.postValue(BottomSheetState.NONE) } - fun showOpenLinkSheet(uri: Uri) { - currentLink = uri - bottomSheetStateLiveData.postValue(BottomSheetState.LINK) + 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 openCurrentLink(context: Context) { currentLink?.let { - val browserIntent = Intent(Intent.ACTION_VIEW, it) - startActivity(context, browserIntent, null) + 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 { From 1df725be6b288f6015d206e03e44f942cf1e2939 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 10 May 2023 23:04:30 +0800 Subject: [PATCH 06/20] Bump version --- android/Omnivore/app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/Omnivore/app/build.gradle b/android/Omnivore/app/build.gradle index 04dde458a..735c496ee 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 72 + versionName "0.0.72" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From f4722645e829da8a76d3ce7b2e28256c17c1d4a7 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 11 May 2023 12:47:01 +0800 Subject: [PATCH 07/20] Sort labels based on lowercase locale --- .../omnivore/omnivore/ui/library/LibraryFilterBar.kt | 4 +++- .../omnivore/ui/savedItemViews/SavedItemCard.kt | 11 +++++------ 2 files changed, 8 insertions(+), 7 deletions(-) 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/savedItemViews/SavedItemCard.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt index ee3fe78bc..1de03a3b1 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) @Composable fun SavedItemCard(savedItemViewModel: SavedItemViewModel, savedItem: SavedItemWithLabelsAndHighlights, onClickHandler: () -> Unit, actionHandler: (SavedItemAction) -> Unit) { val listState = rememberLazyListState() @@ -102,14 +103,12 @@ fun SavedItemCard(savedItemViewModel: SavedItemViewModel, savedItem: SavedItemWi modifier = Modifier .padding(start = 5.dp, bottom = 5.dp, end = 10.dp) ) { - items(savedItem.labels.sortedBy { it.name }) { label -> + items(savedItem.labels.sortedWith(compareBy { it.name.toLowerCase(Locale.current) })) { label -> val chipColors = LabelChipColors.fromHex(label.color) LabelChip( - // onClick = onClickHandler, name = label.name, colors = chipColors, - // modifier = Modifier.padding(end = 5.dp) ) } From 99f0d945a6e96262af49fcf010032dc44a725e6f Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 11 May 2023 13:22:31 +0800 Subject: [PATCH 08/20] Simplify label selection sheet bottom modal interaction --- .../ui/components/LabelsSelectionSheet.kt | 138 ++++++++---------- .../omnivore/ui/library/LibraryView.kt | 123 ++++++++++++++-- 2 files changed, 170 insertions(+), 91 deletions(-) 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 28dff7936..2d6246ba0 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 @@ -28,69 +28,70 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import app.omnivore.omnivore.persistence.entities.SavedItemLabel import app.omnivore.omnivore.ui.library.LibraryViewModel +import com.google.accompanist.flowlayout.FlowRow -@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 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) +// } +// ) +// } +// } +// } +// ) {} +// } +//} @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -132,6 +133,7 @@ fun LabelsSelectionSheetContent( .fillMaxSize() .padding(horizontal = 0.dp) ) { + Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, @@ -214,17 +216,3 @@ fun LabelsSelectionSheetContent( } } } - -@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/LibraryView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryView.kt index 1e9b49dd4..20dbac19e 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 @@ -33,38 +33,114 @@ 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 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.animateTo(ModalBottomSheetValue.HalfExpanded) + } + } else { + coroutineScope.launch { + modalBottomSheetState.hide() + } + } + + ModalBottomSheetLayout( + sheetBackgroundColor = Color.Transparent, + sheetState = modalBottomSheetState, + sheetContent = { + BottomSheetContent(libraryViewModel) + Spacer(modifier = Modifier.weight(1.0F)) + } + ) { + Scaffold( + 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) + } + ) + } + } } } @@ -137,10 +213,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, From 8e3c2f65bbff0214b73d07f5b01db2c00abaca00 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 12 May 2023 12:08:40 +0800 Subject: [PATCH 09/20] 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( From 6c2059807f797d4dc963e5c684c2c927288ba38b Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 12 May 2023 16:32:59 +0800 Subject: [PATCH 10/20] More work on Android label selector --- .../omnivore/ui/components/LabelChip.kt | 4 +- .../ui/components/LabelsSelectionSheet.kt | 219 +++++++++--------- 2 files changed, 111 insertions(+), 112 deletions(-) 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..f6ece7532 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(4.dp), shape = MaterialTheme.shapes.medium, color = colors.containerColor ) { 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 e07a32d69..a64be9322 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 @@ -6,6 +6,9 @@ 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 @@ -23,17 +26,23 @@ 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.platform.LocalDensity -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.LocalViewConfiguration -import androidx.compose.ui.platform.ViewConfiguration +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 @@ -42,6 +51,7 @@ import app.omnivore.omnivore.persistence.entities.SavedItemLabel import app.omnivore.omnivore.ui.library.LibraryViewModel import com.dokar.chiptextfield.* import com.google.accompanist.flowlayout.FlowRow +import kotlinx.coroutines.delay //@Composable @@ -121,7 +131,7 @@ fun CircleIcon(colorHex: String){ Row( modifier = Modifier .padding(start = 10.dp, end = 2.dp) - .padding(vertical = 10.dp) + .padding(vertical = 7.dp) ) { Canvas(modifier = Modifier.size(12.dp), onDraw = { drawCircle(color = chipColors.containerColor) @@ -201,14 +211,15 @@ private fun CloseButtonImpl( } } -class LabelChip(label: SavedItemLabel) : Chip(label.name) { +class LabelChipView(label: SavedItemLabel) : Chip(label.name) { val label = label } @Composable -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class, + ExperimentalMaterial3Api::class +) fun LabelsSelectionSheetContent( -// viewModel: LibraryViewModel, isLibraryMode: Boolean, labels: List, initialSelectedLabels: List, @@ -216,21 +227,37 @@ fun LabelsSelectionSheetContent( onSave: (List) -> Unit, onCreateLabel: (String, String) -> Unit ) { - val listState = rememberLazyListState() - val selectedLabels = remember { mutableStateOf(initialSelectedLabels) } - var showCreateLabelDialog by remember { mutableStateOf(false ) } + val keyboardController = LocalSoftwareKeyboardController.current + 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: String) -> SavedItemLabel = { name -> - val found = labels.find { it.name == name } + val findOrCreateLabel: (name: TextFieldValue) -> SavedItemLabel = { name -> + val found = labels.find { it.name == name.text } found ?: SavedItemLabel( savedItemLabelId = "", - name = name, + name = name.text, color = "#FFFFFF", createdAt = "", labelDescription = "", @@ -243,25 +270,10 @@ fun LabelsSelectionSheetContent( .fillMaxSize() .background(MaterialTheme.colorScheme.background), ) { - var value by remember { mutableStateOf("Initial text") } - val state = rememberChipTextFieldState() - - - 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 = 5.dp) ) { @@ -271,7 +283,6 @@ fun LabelsSelectionSheetContent( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .padding(vertical = 5.dp) ) { TextButton(onClick = onCancel) { Text(text = "Cancel") @@ -279,101 +290,89 @@ 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") } } - 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), + ChipTextField( + state = state, + value = filterTextValue, + onValueChange = onFilterTextValueChange, + onSubmit = { 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 - .defaultMinSize(minHeight = 45.dp) .fillMaxWidth() + .clickable { + val label = findOrCreateLabel(filterTextValue) + state.addChip(LabelChipView(label)) + filterTextValue = TextFieldValue() + } .padding(horizontal = 10.dp) - .focusRequester(focusRequester) + .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}\"") + } + } - LazyColumn( - state = listState, - verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize() - ) { - items(labels) { label -> - val isLabelSelected = selectedLabels.value.contains(label) + if (filteredLabels.isNotEmpty()) { + FlowRow(modifier = Modifier.fillMaxWidth().padding(10.dp)) { + filteredLabels.forEach { label -> + val chipColors = LabelChipColors.fromHex(label.color) - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, + LabelChip( + name = label.name, + colors = chipColors, modifier = Modifier - .fillMaxWidth() .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)) - } + state.addChip(LabelChipView(label)) + filterTextValue = TextFieldValue() } - .padding(horizontal = 10.dp, vertical = 6.dp) - ) { - 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, - 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() - } + } + } + LaunchedEffect(Unit) { + } } From 42d9c5fea895dbf1394524b21769d847b9b4aa97 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 12 May 2023 17:00:16 +0800 Subject: [PATCH 11/20] Dont create labels in the library mode --- .../omnivore/ui/components/LabelsSelectionSheet.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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 a64be9322..e8f0abef0 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 @@ -227,7 +227,6 @@ fun LabelsSelectionSheetContent( onSave: (List) -> Unit, onCreateLabel: (String, String) -> Unit ) { - val keyboardController = LocalSoftwareKeyboardController.current val interactionSource = remember { MutableInteractionSource() } val state = rememberChipTextFieldState(initialSelectedLabels.map { @@ -299,7 +298,15 @@ fun LabelsSelectionSheetContent( state = state, value = filterTextValue, onValueChange = onFilterTextValueChange, - onSubmit = { LabelChipView(findOrCreateLabel(it)) }, + 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, From 9fba4aa1afe6b2c66c713997d3905cda1b97cbc1 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 12 May 2023 17:07:52 +0800 Subject: [PATCH 12/20] Use FlowRow for labels on cards --- .../omnivore/omnivore/ui/components/LabelChip.kt | 2 +- .../omnivore/ui/savedItemViews/SavedItemCard.kt | 13 +++---------- 2 files changed, 4 insertions(+), 11 deletions(-) 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 f6ece7532..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 @@ -16,7 +16,7 @@ fun LabelChip( 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/savedItemViews/SavedItemCard.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt index 1de03a3b1..3abedc409 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 @@ -31,7 +31,7 @@ import app.omnivore.omnivore.ui.library.SavedItemAction import app.omnivore.omnivore.ui.library.SavedItemViewModel import coil.compose.rememberAsyncImagePainter -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) @Composable fun SavedItemCard(savedItemViewModel: SavedItemViewModel, savedItem: SavedItemWithLabelsAndHighlights, onClickHandler: () -> Unit, actionHandler: (SavedItemAction) -> Unit) { val listState = rememberLazyListState() @@ -96,21 +96,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.sortedWith(compareBy { it.name.toLowerCase(Locale.current) })) { 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( name = label.name, colors = chipColors, ) - } } From b9c4a6a47682343b112d146abc98e4b4fdc4275e Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 15 May 2023 12:05:58 +0800 Subject: [PATCH 13/20] Create labels inline --- .../ui/components/LabelCreationDialog.kt | 4 ++++ .../ui/components/LabelsSelectionSheet.kt | 9 +++++++-- .../omnivore/ui/library/LibraryViewModel.kt | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) 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 e8f0abef0..6d64c6501 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 @@ -52,6 +52,10 @@ import app.omnivore.omnivore.ui.library.LibraryViewModel 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 @@ -257,9 +261,10 @@ fun LabelsSelectionSheetContent( ?: SavedItemLabel( savedItemLabelId = "", name = name.text, - color = "#FFFFFF", - createdAt = "", labelDescription = "", + color = "#FFFFFF", // LabelSwatchHelper.random(), + createdAt = LocalDate.now().atStartOfDay().atOffset(ZoneOffset.UTC).format( + DateTimeFormatter.ISO_DATE_TIME), serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue ) } 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..d08496186 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 @@ -9,9 +9,11 @@ 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 @@ -283,6 +285,22 @@ class LibraryViewModel @Inject constructor( fun updateSavedItemLabels(savedItemID: String, labels: List) { viewModelScope.launch { withContext(Dispatchers.IO) { + val synced = labels.filter { it.serverSyncStatus == ServerSyncStatus.IS_SYNCED.rawValue } + val unsynced = labels.filter { it.serverSyncStatus != ServerSyncStatus.IS_SYNCED.rawValue } + + var labelIds = mutableListOf() + labelIds.addAll(synced.map { it.savedItemLabelId }) + + unsynced.forEach { label -> + val result = networker.createNewLabel(CreateLabelInput( + name = label.name, + color = presentIfNotNull(label.color), + description = presentIfNotNull(label.labelDescription), + )) + result?.let { + labelIds.add(it.id) + } + } val input = SetLabelsInput(labelIds = labels.map { it.savedItemLabelId }, pageId = savedItemID) val networkResult = networker.updateLabelsForSavedItem(input) From 49af48a6950f832436a2bdda6eb29afc8024fc70 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 15 May 2023 14:59:22 +0800 Subject: [PATCH 14/20] Set labels on creation --- .../ui/components/LabelsSelectionSheet.kt | 2 +- .../omnivore/ui/library/LibraryView.kt | 19 +++++-- .../omnivore/ui/library/LibraryViewModel.kt | 50 +++++++++++++++---- .../ui/savedItemViews/SavedItemCard.kt | 1 + 4 files changed, 57 insertions(+), 15 deletions(-) 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 6d64c6501..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 @@ -262,7 +262,7 @@ fun LabelsSelectionSheetContent( savedItemLabelId = "", name = name.text, labelDescription = "", - color = "#FFFFFF", // LabelSwatchHelper.random(), + color = LabelSwatchHelper.random(), createdAt = LocalDate.now().atStartOfDay().atOffset(ZoneOffset.UTC).format( DateTimeFormatter.ISO_DATE_TIME), serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue 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 631ec9418..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 @@ -49,6 +50,7 @@ fun LibraryView( libraryViewModel: LibraryViewModel, navController: NavHostController ) { + val scaffoldState: ScaffoldState = rememberScaffoldState() val showLabelsSelectionSheet: Boolean by libraryViewModel.showLabelsSelectionSheetLiveData.observeAsState(false) val coroutineScope = rememberCoroutineScope() @@ -67,6 +69,13 @@ fun LibraryView( } } + libraryViewModel.snackbarMessage?.let { + coroutineScope.launch { + scaffoldState.snackbarHostState.showSnackbar(it) + libraryViewModel.clearSnackbarMessage() + } + } + ModalBottomSheetLayout( sheetBackgroundColor = Color.Transparent, sheetState = modalBottomSheetState, @@ -76,6 +85,7 @@ fun LibraryView( } ) { Scaffold( + scaffoldState = scaffoldState, topBar = { LibraryNavigationBar( savedItemViewModel = libraryViewModel, @@ -164,6 +174,7 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { .fillMaxSize() .pullRefresh(pullRefreshState) ) { + LazyColumn( state = listState, verticalArrangement = Arrangement.Top, 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 d08496186..ce6334c5d 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,9 +1,14 @@ package app.omnivore.omnivore.ui.library +import android.content.Context 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.* @@ -36,6 +41,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), @@ -77,6 +85,10 @@ class LibraryViewModel @Inject constructor( } } + fun clearSnackbarMessage() { + snackbarMessage = null + } + fun refresh() { cursor = null librarySearchCursor = null @@ -285,27 +297,39 @@ class LibraryViewModel @Inject constructor( fun updateSavedItemLabels(savedItemID: String, labels: List) { viewModelScope.launch { withContext(Dispatchers.IO) { - val synced = labels.filter { it.serverSyncStatus == ServerSyncStatus.IS_SYNCED.rawValue } - val unsynced = labels.filter { it.serverSyncStatus != ServerSyncStatus.IS_SYNCED.rawValue } + val syncedLabels = labels.filter { it.serverSyncStatus == ServerSyncStatus.IS_SYNCED.rawValue } + val unsyncedLabels = labels.filter { it.serverSyncStatus != ServerSyncStatus.IS_SYNCED.rawValue } - var labelIds = mutableListOf() - labelIds.addAll(synced.map { it.savedItemLabelId }) - - unsynced.forEach { label -> + 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 { - labelIds.add(it.id) + 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 } } - val input = SetLabelsInput(labelIds = labels.map { it.savedItemLabelId }, pageId = savedItemID) + + 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 @@ -318,6 +342,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() } 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 3abedc409..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 @@ -65,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, From 549481020b02e5656f9ecc297b562b8b47769acb Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 15 May 2023 15:36:06 +0800 Subject: [PATCH 15/20] Better state handling of the reader bottom sheet --- .../omnivore/ui/reader/WebReaderLoadingContainer.kt | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) 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 e549fd629..9a00ce12f 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 @@ -190,7 +190,9 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, when (bottomSheetState) { BottomSheetState.PREFERENCES -> { coroutineScope.launch { - modalBottomSheetState.show() + if (!modalBottomSheetState.isVisible) { + modalBottomSheetState.show() + } } } BottomSheetState.NOTEBOOK -> { @@ -252,14 +254,12 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, onSave = { webReaderViewModel.saveAnnotation(it) coroutineScope.launch { - modalBottomSheetState.hide() webReaderViewModel.resetBottomSheet() } }, onCancel = { webReaderViewModel.cancelAnnotationEdit() coroutineScope.launch { - modalBottomSheetState.hide() webReaderViewModel.resetBottomSheet() } } @@ -274,7 +274,6 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, initialSelectedLabels = webReaderParams?.labels ?: listOf(), onCancel = { coroutineScope.launch { - modalBottomSheetState.hide() webReaderViewModel.resetBottomSheet() } }, @@ -286,7 +285,6 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, ) } coroutineScope.launch { - modalBottomSheetState.hide() webReaderViewModel.resetBottomSheet() } }, @@ -347,7 +345,6 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, IconButton(onClick = { coroutineScope.launch { webReaderViewModel.setBottomSheet(BottomSheetState.NOTEBOOK) - modalBottomSheetState.show() } }) { Icon( @@ -360,7 +357,6 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, IconButton(onClick = { coroutineScope.launch { webReaderViewModel.setBottomSheet(BottomSheetState.PREFERENCES) - modalBottomSheetState.show() } }) { Icon( From f6b248400759bce6f824f843b6af048c30b09173 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 15 May 2023 18:21:24 +0800 Subject: [PATCH 16/20] Resolve issue where changing theme would hide the bottom modal --- .../ui/reader/WebReaderLoadingContainer.kt | 237 +++++++++--------- 1 file changed, 121 insertions(+), 116 deletions(-) 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 9a00ce12f..0e7a9d8d9 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 @@ -128,17 +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) } val bottomSheetState: BottomSheetState? by webReaderViewModel.bottomSheetStateLiveData.observeAsState(BottomSheetState.NONE) - val isDarkMode = isSystemInDarkTheme() - val currentThemeKey = webReaderViewModel.currentThemeKey.observeAsState() - val currentTheme = Themes.values().find { it.themeKey == currentThemeKey.value } - val webReaderParams: WebReaderParams? by webReaderViewModel.webReaderParamsLiveData.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()) @@ -167,25 +161,6 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, } ) - 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) when (bottomSheetState) { BottomSheetState.PREFERENCES -> { @@ -311,102 +286,130 @@ 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 { - webReaderViewModel.setBottomSheet(BottomSheetState.NOTEBOOK) - } - }) { - Icon( - painter = painterResource(id = R.drawable.notebook), - contentDescription = null, - tint = themeTintColor - ) - } - } - IconButton(onClick = { - coroutineScope.launch { - 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( - 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 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( + isExpanded = isMenuExpanded, + isArchived = params.item.isArchived, + onDismiss = { isMenuExpanded = false }, + actionHandler = { + webReaderViewModel.handleSavedItemAction( + params.item.savedItemId, + it + ) + } + ) + } + } + } + }, + ) +} @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @Composable @@ -433,7 +436,9 @@ fun BottomSheetUI(title: String?, content: @Composable () -> Unit) { fun OpenLinkView(webReaderViewModel: WebReaderViewModel) { val context = LocalContext.current - Column(modifier = Modifier.padding(top = 50.dp).padding(horizontal = 50.dp), verticalArrangement = Arrangement.spacedBy(20.dp)) { + 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") From 3cf60df78368550f2c5b7fce9a4d3f4e0740ce8b Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 15 May 2023 19:20:17 +0800 Subject: [PATCH 17/20] Add share menu item in the reader --- .../omnivore/ui/library/LibraryViewModel.kt | 7 ++- .../ui/reader/ReaderPreferencesView.kt | 24 ++++---- .../ui/reader/WebReaderLoadingContainer.kt | 7 ++- .../omnivore/ui/reader/WebReaderViewModel.kt | 12 ++++ .../ui/savedItemViews/SavedItemContextMenu.kt | 19 +++++++ .../app/omnivore/omnivore/ui/theme/Theme.kt | 57 +------------------ 6 files changed, 57 insertions(+), 69 deletions(-) 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 ce6334c5d..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,6 +1,8 @@ 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 @@ -290,6 +292,9 @@ class LibraryViewModel @Inject constructor( labelsSelectionCurrentItemLiveData.value = itemID showLabelsSelectionSheetLiveData.value = true } + else -> { + + } } actionsMenuItemLiveData.postValue(null) } @@ -401,5 +406,5 @@ enum class SavedItemAction { Delete, Archive, Unarchive, - EditLabels + EditLabels, } 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/WebReaderLoadingContainer.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderLoadingContainer.kt index 0e7a9d8d9..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 @@ -307,6 +307,9 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, @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 } @@ -345,7 +348,7 @@ fun ReaderTopAppBar(webReaderViewModel: WebReaderViewModel, onLibraryIconTap: (( title = {}, navigationIcon = { IconButton(onClick = { - // onBackPressedDispatcher?.onBackPressed() + onBackPressedDispatcher?.onBackPressed() }) { Icon( imageVector = Icons.Filled.ArrowBack, @@ -394,9 +397,11 @@ fun ReaderTopAppBar(webReaderViewModel: WebReaderViewModel, onLibraryIconTap: (( 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, 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 ce81b62ff..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 @@ -7,6 +7,7 @@ import android.content.Intent import android.net.Uri import android.util.Log import android.widget.Toast +import androidx.compose.ui.platform.LocalContext import androidx.core.content.ContextCompat.startActivity import androidx.lifecycle.* import app.omnivore.omnivore.DatastoreKeys @@ -108,6 +109,17 @@ class WebReaderViewModel @Inject constructor( } } + 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) 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 34a50a11c..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 ) { @@ -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, From 03d7d80b4f4630149fd2795fdbd021967c1434cd Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 16 May 2023 11:23:58 +0800 Subject: [PATCH 18/20] Use bottombar when saving --- android/Omnivore/app/build.gradle | 4 +- .../omnivore/ui/save/SaveSheetActivity.kt | 89 +++++++++++++------ .../omnivore/ui/save/SaveViewModel.kt | 12 +++ .../import-handler/test/csv/data/simple.csv | 2 +- 4 files changed, 78 insertions(+), 29 deletions(-) diff --git a/android/Omnivore/app/build.gradle b/android/Omnivore/app/build.gradle index d86e223cd..d9f11b263 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 72 - versionName "0.0.72" + versionCode 75 + versionName "0.0.75" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { 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 3f68ccdaa..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 @@ -19,10 +20,19 @@ import kotlinx.coroutines.runBlocking import java.util.* 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 @@ -45,6 +55,7 @@ class SaveViewModel @Inject constructor( viewModelScope.launch { isLoading = true message = "Saving to Omnivore..." + saveState.postValue(SaveState.SAVING) val authToken = getAuthToken() @@ -84,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/packages/import-handler/test/csv/data/simple.csv b/packages/import-handler/test/csv/data/simple.csv index 8202bc11b..930fb7b7d 100644 --- a/packages/import-handler/test/csv/data/simple.csv +++ b/packages/import-handler/test/csv/data/simple.csv @@ -1,2 +1,2 @@ -"https://omnivore.app" +"https://omnivore.app",ARCHIVED,"[Reading,Newsletter]" "https://google.com" From 7dca0a5d5a3e4b878e7ab5067fddc0d0cabb60c0 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 16 May 2023 11:36:29 +0800 Subject: [PATCH 19/20] Bump Android version --- android/Omnivore/app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/Omnivore/app/build.gradle b/android/Omnivore/app/build.gradle index d9f11b263..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 75 - versionName "0.0.75" + versionCode 76 + versionName "0.0.76" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 0b599c3bcc3c25a08a8c1e11d6d441e5a22be45e Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 16 May 2023 12:13:20 +0800 Subject: [PATCH 20/20] Revert unchanged file --- packages/import-handler/test/csv/data/simple.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/import-handler/test/csv/data/simple.csv b/packages/import-handler/test/csv/data/simple.csv index 930fb7b7d..8202bc11b 100644 --- a/packages/import-handler/test/csv/data/simple.csv +++ b/packages/import-handler/test/csv/data/simple.csv @@ -1,2 +1,2 @@ -"https://omnivore.app",ARCHIVED,"[Reading,Newsletter]" +"https://omnivore.app" "https://google.com"