From c036d26a3fb7c5bd7c74cb2cc4854e5bfce37fc1 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 22 Feb 2023 13:04:25 -0800 Subject: [PATCH 01/48] display labels on saved item card cells --- .../omnivore/ui/components/LabelChipColors.kt | 20 ++++++++++ .../omnivore/ui/library/LibraryView.kt | 2 +- .../ui/savedItemViews/SavedItemCard.kt | 39 +++++++++++++++++-- 3 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelChipColors.kt diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelChipColors.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelChipColors.kt new file mode 100644 index 000000000..2ee6b8434 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelChipColors.kt @@ -0,0 +1,20 @@ +package app.omnivore.omnivore.ui.components + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance + +data class LabelChipColors( + val textColor: Color, + val containerColor: Color +) { + companion object { + fun fromHex(hex: String): LabelChipColors { + val labelColor = Color(android.graphics.Color.parseColor(hex)) + + return LabelChipColors( + textColor = if (labelColor.luminance() > 0.5) Color.Black else Color.White, + containerColor = labelColor + ) + } + } +} 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 05d02e9fa..00b8a6315 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 @@ -65,7 +65,6 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { val cardsData: List by libraryViewModel.itemsLiveData.observeAsState(listOf()) val searchedCardsData: List by libraryViewModel.searchItemsLiveData.observeAsState(listOf()) - val searchText: String by libraryViewModel.searchTextLiveData.observeAsState("") Box( modifier = Modifier @@ -89,6 +88,7 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { items(if (libraryViewModel.showSearchField) searchedCardsData else cardsData) { cardDataWithLabels -> SavedItemCard( cardData = cardDataWithLabels.cardData, + labels = cardDataWithLabels.labels, onClickHandler = { val activityClass = if (cardDataWithLabels.cardData.isPDF()) PDFReaderActivity::class.java else WebReaderLoadingContainerActivity::class.java val intent = Intent(context, activityClass) 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 d179667e6..c97ba8609 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 @@ -1,27 +1,35 @@ package app.omnivore.omnivore.ui.savedItemViews +import android.content.res.Resources.Theme import androidx.compose.foundation.* import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* +import androidx.compose.material3.SuggestionChipDefaults.elevatedSuggestionChipColors import androidx.compose.runtime.* 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.graphics.luminance import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import app.omnivore.omnivore.persistence.entities.SavedItem import app.omnivore.omnivore.persistence.entities.SavedItemCardData +import app.omnivore.omnivore.persistence.entities.SavedItemLabel +import app.omnivore.omnivore.ui.components.LabelChipColors import app.omnivore.omnivore.ui.library.SavedItemAction import coil.compose.rememberAsyncImagePainter -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable -fun SavedItemCard(cardData: SavedItemCardData, onClickHandler: () -> Unit, actionHandler: (SavedItemAction) -> Unit) { +fun SavedItemCard(cardData: SavedItemCardData, labels: List, onClickHandler: () -> Unit, actionHandler: (SavedItemAction) -> Unit) { var isMenuExpanded by remember { mutableStateOf(false) } val publisherDisplayName = cardData.publisherDisplayName() + val listState = rememberLazyListState() Column { Row( @@ -79,6 +87,31 @@ fun SavedItemCard(cardData: SavedItemCardData, onClickHandler: () -> Unit, actio } } + LazyRow( + state = listState, + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(start = 6.dp) + ) { + items(labels.sortedBy { it.name }) { label -> + val chipColors = LabelChipColors.fromHex(label.color) + + SuggestionChip( + onClick = {}, + label = { Text(label.name) }, + border = null, + colors = elevatedSuggestionChipColors( + containerColor = chipColors.containerColor, + labelColor = chipColors.textColor, + iconContentColor = chipColors.textColor + ), + modifier = Modifier + .padding(horizontal = 4.dp) + ) + } + } + Divider(color = MaterialTheme.colorScheme.outlineVariant, thickness = 1.dp) SavedItemContextMenu( From 415af59550a58859d11d2b47dccfc7b0f54a86a3 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 22 Feb 2023 16:23:44 -0800 Subject: [PATCH 02/48] stub in labels selection sheet --- .../omnivore/ui/library/LibraryFilterBar.kt | 11 +++++++ .../omnivore/ui/library/LibraryView.kt | 30 +++++++++++++++++++ .../omnivore/ui/library/LibraryViewModel.kt | 3 ++ .../ui/savedItemViews/SavedItemCard.kt | 2 -- 4 files changed, 44 insertions(+), 2 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 60f4df207..0829aa8f3 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 @@ -48,6 +48,17 @@ fun LibraryFilterBar(viewModel: LibraryViewModel) { }, modifier = Modifier.padding(end = 6.dp) ) + AssistChip( + onClick = { viewModel.showLabelsSelectionSheetLiveData.value = true }, + label = { Text("Labels") }, + trailingIcon = { + Icon( + Icons.Default.ArrowDropDown, + contentDescription = "drop down button to open label selection sheet" + ) + }, + modifier = Modifier.padding(end = 6.dp) + ) } SavedItemFilterContextMenu( 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 00b8a6315..32c265eb2 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 @@ -7,6 +7,7 @@ import androidx.compose.foundation.lazy.LazyColumn 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.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh @@ -16,8 +17,10 @@ import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog import androidx.navigation.NavHostController import app.omnivore.omnivore.Routes import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels @@ -109,6 +112,8 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { state = pullRefreshState, modifier = Modifier.align(Alignment.TopCenter) ) + + LabelsSelectionSheet(viewModel = libraryViewModel) } } @@ -136,3 +141,28 @@ fun InfiniteListHandler( } } } + +@Composable +fun LabelsSelectionSheet(viewModel: LibraryViewModel) { + val isActive: Boolean by viewModel.showLabelsSelectionSheetLiveData.observeAsState(false) + + if (isActive) { + Dialog(onDismissRequest = { viewModel.showLabelsSelectionSheetLiveData.value = false } ) { + Surface( + modifier = Modifier.fillMaxSize(), + shape = RoundedCornerShape(16.dp) + ) { + Box( + contentAlignment = Alignment.Center + ) { + Text(modifier = Modifier.align(Alignment.TopCenter), + text = "top") + Text("center") + Text( + modifier = Modifier.align(Alignment.BottomCenter), + text = "bottom") + } + } + } + } +} 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 4ae32dd0e..40f984fd4 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.util.Log +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -41,6 +43,7 @@ class LibraryViewModel @Inject constructor( val itemsLiveData = MediatorLiveData>() val appliedFilterLiveData = MutableLiveData(SavedItemFilter.INBOX) val appliedSortFilterLiveData = MutableLiveData(SavedItemSortFilter.NEWEST) + val showLabelsSelectionSheetLiveData = MutableLiveData(false) var isRefreshing by mutableStateOf(false) var showSearchField by mutableStateOf(false) 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 c97ba8609..bd81e3ef9 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 @@ -1,6 +1,5 @@ package app.omnivore.omnivore.ui.savedItemViews -import android.content.res.Resources.Theme import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow @@ -14,7 +13,6 @@ 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.graphics.luminance import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp From 9e1c64e125f59810559192566135557af7d24dc7 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Sun, 26 Feb 2023 16:32:30 -0800 Subject: [PATCH 03/48] display list of labels in library view when labels chip is tapped --- .../persistence/entities/SavedItemLabel.kt | 5 + .../omnivore/ui/library/LibraryView.kt | 93 +++++++++++++++++-- .../omnivore/ui/library/LibraryViewModel.kt | 10 +- 3 files changed, 92 insertions(+), 16 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItemLabel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItemLabel.kt index ea3b3aa40..6c5fbd870 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItemLabel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItemLabel.kt @@ -1,5 +1,6 @@ package app.omnivore.omnivore.persistence.entities +import androidx.lifecycle.LiveData import androidx.room.* @Entity @@ -16,6 +17,10 @@ data class SavedItemLabel( interface SavedItemLabelDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAll(items: List) + + @Transaction + @Query("SELECT * FROM SavedItemLabel WHERE serverSyncStatus != 2 ORDER BY name ASC") + fun getSavedItemLabelsLiveData(): LiveData> } @Entity( 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 32c265eb2..823d2b5ff 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 @@ -2,6 +2,7 @@ package app.omnivore.omnivore.ui.library import android.content.Intent 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.LazyListState @@ -9,6 +10,8 @@ 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.icons.Icons +import androidx.compose.material.icons.filled.Check import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState @@ -17,13 +20,15 @@ import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.navigation.NavHostController import app.omnivore.omnivore.Routes import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels +import app.omnivore.omnivore.persistence.entities.SavedItemLabel +import app.omnivore.omnivore.ui.components.LabelChipColors import app.omnivore.omnivore.ui.savedItemViews.SavedItemCard import app.omnivore.omnivore.ui.reader.PDFReaderActivity import app.omnivore.omnivore.ui.reader.WebReaderLoadingContainerActivity @@ -142,25 +147,93 @@ fun InfiniteListHandler( } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun LabelsSelectionSheet(viewModel: LibraryViewModel) { + val listState = rememberLazyListState() val isActive: Boolean by viewModel.showLabelsSelectionSheetLiveData.observeAsState(false) + val labels: List by viewModel.savedItemLabelsLiveData.observeAsState(listOf()) + val selectedLabels = remember { mutableStateOf(viewModel.activeLabelsLiveData.value) } if (isActive) { Dialog(onDismissRequest = { viewModel.showLabelsSelectionSheetLiveData.value = false } ) { Surface( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), shape = RoundedCornerShape(16.dp) ) { - Box( - contentAlignment = Alignment.Center + LazyColumn( + state = listState, + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 6.dp) ) { - Text(modifier = Modifier.align(Alignment.TopCenter), - text = "top") - Text("center") - Text( - modifier = Modifier.align(Alignment.BottomCenter), - text = "bottom") + item { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + ) { + TextButton(onClick = { viewModel.showLabelsSelectionSheetLiveData.value = false }) { + Text(text = "Cancel") + } + + Text("Filter by Label", fontWeight = FontWeight.ExtraBold) + + TextButton( + onClick = { + selectedLabels.value?.let { + viewModel.activeLabelsLiveData.value = it + } + viewModel.showLabelsSelectionSheetLiveData.value = false + } + ) { + Text(text = "Done") + } + } + } + items(labels) { label -> + val isLabelSelected = (selectedLabels.value ?: listOf()).contains(label) + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { + if (isLabelSelected) { + selectedLabels.value = (selectedLabels.value ?: listOf()).filter { it.savedItemLabelId != label.savedItemLabelId } + } else { + selectedLabels.value = (selectedLabels.value ?: listOf()) + listOf(label) + } + } + .padding(horizontal = 6.dp) + ) { + val chipColors = LabelChipColors.fromHex(label.color) + + SuggestionChip( + onClick = {}, + label = { Text(label.name) }, + border = null, + colors = SuggestionChipDefaults.elevatedSuggestionChipColors( + containerColor = chipColors.containerColor, + labelColor = chipColors.textColor, + iconContentColor = chipColors.textColor + ) + ) + if (isLabelSelected) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null + ) + } + } + Divider(color = MaterialTheme.colorScheme.outlineVariant, thickness = 1.dp) + } } } } 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 40f984fd4..8591eb275 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,13 +1,9 @@ package app.omnivore.omnivore.ui.library import android.util.Log -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -15,9 +11,8 @@ import androidx.lifecycle.viewModelScope import app.omnivore.omnivore.* import app.omnivore.omnivore.dataService.* import app.omnivore.omnivore.networking.* -import app.omnivore.omnivore.persistence.entities.SavedItem import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels -import app.omnivore.omnivore.ui.reader.WebFont +import app.omnivore.omnivore.persistence.entities.SavedItemLabel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* import java.time.Instant @@ -44,11 +39,14 @@ class LibraryViewModel @Inject constructor( val appliedFilterLiveData = MutableLiveData(SavedItemFilter.INBOX) val appliedSortFilterLiveData = MutableLiveData(SavedItemSortFilter.NEWEST) val showLabelsSelectionSheetLiveData = MutableLiveData(false) + val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData() var isRefreshing by mutableStateOf(false) var showSearchField by mutableStateOf(false) var hasLoadedInitialFilters = false + var activeLabelsLiveData = MutableLiveData>(listOf()) + fun loadInitialFilterValues() { if (hasLoadedInitialFilters) { return } hasLoadedInitialFilters = false From 0282dcb4ad273a902f5f1d00a5977d56301ec817 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Sun, 26 Feb 2023 17:55:56 -0800 Subject: [PATCH 04/48] apply label filters in library live data --- .../omnivore/dataService/LibraryLiveData.kt | 6 + .../omnivore/ui/library/LibraryFilterBar.kt | 106 ++++++++---- .../omnivore/ui/library/LibraryView.kt | 153 +++++++++--------- .../omnivore/ui/library/LibraryViewModel.kt | 12 +- 4 files changed, 167 insertions(+), 110 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibraryLiveData.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibraryLiveData.kt index 8a198b9fd..b9cd3967f 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibraryLiveData.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibraryLiveData.kt @@ -63,6 +63,12 @@ fun DataService.libraryLiveData( } } } + + if (labels.isNotEmpty()) { + mediatorLiveData.value = (mediatorLiveData.value ?: listOf()).filter { + it.labels.intersect(labels.toSet()).any() + } + } } return mediatorLiveData 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 0829aa8f3..1e3dad14a 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 @@ -1,64 +1,102 @@ package app.omnivore.omnivore.ui.library import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.Close import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import app.omnivore.omnivore.persistence.entities.SavedItemLabel +import app.omnivore.omnivore.ui.components.LabelChipColors @OptIn(ExperimentalMaterial3Api::class) @Composable fun LibraryFilterBar(viewModel: LibraryViewModel) { var isSavedItemFilterMenuExpanded by remember { mutableStateOf(false) } val activeSavedItemFilter: SavedItemFilter by viewModel.appliedFilterLiveData.observeAsState(SavedItemFilter.INBOX) + val activeLabels: List by viewModel.activeLabelsLiveData.observeAsState(listOf()) var isSavedItemSortFilterMenuExpanded by remember { mutableStateOf(false) } val activeSavedItemSortFilter: SavedItemSortFilter by viewModel.appliedSortFilterLiveData.observeAsState(SavedItemSortFilter.NEWEST) + val listState = rememberLazyListState() Column { - Row( + LazyRow( + state = listState, horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically, modifier = Modifier + .padding(start = 6.dp) .fillMaxWidth() ) { - AssistChip( - onClick = { isSavedItemFilterMenuExpanded = true }, - label = { Text(activeSavedItemFilter.displayText) }, - trailingIcon = { - Icon( - Icons.Default.ArrowDropDown, - contentDescription = "drop down button to change primary library filter" - ) - }, - modifier = Modifier.padding(end = 6.dp) - ) - AssistChip( - onClick = { isSavedItemSortFilterMenuExpanded = true }, - label = { Text(activeSavedItemSortFilter.displayText) }, - trailingIcon = { - Icon( - Icons.Default.ArrowDropDown, - contentDescription = "drop down button to change library sort order" - ) - }, - modifier = Modifier.padding(end = 6.dp) - ) - AssistChip( - onClick = { viewModel.showLabelsSelectionSheetLiveData.value = true }, - label = { Text("Labels") }, - trailingIcon = { - Icon( - Icons.Default.ArrowDropDown, - contentDescription = "drop down button to open label selection sheet" - ) - }, - modifier = Modifier.padding(end = 6.dp) - ) + item { + AssistChip( + onClick = { isSavedItemFilterMenuExpanded = true }, + label = { Text(activeSavedItemFilter.displayText) }, + trailingIcon = { + Icon( + Icons.Default.ArrowDropDown, + contentDescription = "drop down button to change primary library filter" + ) + }, + modifier = Modifier.padding(end = 6.dp) + ) + AssistChip( + onClick = { isSavedItemSortFilterMenuExpanded = true }, + label = { Text(activeSavedItemSortFilter.displayText) }, + trailingIcon = { + Icon( + Icons.Default.ArrowDropDown, + contentDescription = "drop down button to change library sort order" + ) + }, + modifier = Modifier.padding(end = 6.dp) + ) + AssistChip( + onClick = { viewModel.showLabelsSelectionSheetLiveData.value = true }, + label = { Text("Labels") }, + trailingIcon = { + Icon( + Icons.Default.ArrowDropDown, + contentDescription = "drop down button to open label selection sheet" + ) + }, + modifier = Modifier.padding(end = 6.dp) + ) + } + items(activeLabels.sortedBy { it.name }) { label -> + val chipColors = LabelChipColors.fromHex(label.color) + + AssistChip( + onClick = { + viewModel.updateAppliedLabels( + (viewModel.activeLabelsLiveData.value ?: listOf()).filter { it.savedItemLabelId != label.savedItemLabelId } + ) + }, + label = { Text(label.name) }, + border = null, + colors = SuggestionChipDefaults.elevatedSuggestionChipColors( + containerColor = chipColors.containerColor, + labelColor = chipColors.textColor, + iconContentColor = chipColors.textColor + ), + trailingIcon = { + Icon( + Icons.Default.Close, + contentDescription = "close icon to remove label" + ) + }, + modifier = Modifier + .padding(horizontal = 4.dp) + ) + } } SavedItemFilterContextMenu( 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 823d2b5ff..a43fd26f4 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 @@ -147,95 +147,102 @@ fun InfiniteListHandler( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun LabelsSelectionSheet(viewModel: LibraryViewModel) { - val listState = rememberLazyListState() val isActive: Boolean by viewModel.showLabelsSelectionSheetLiveData.observeAsState(false) - val labels: List by viewModel.savedItemLabelsLiveData.observeAsState(listOf()) - val selectedLabels = remember { mutableStateOf(viewModel.activeLabelsLiveData.value) } if (isActive) { Dialog(onDismissRequest = { viewModel.showLabelsSelectionSheetLiveData.value = false } ) { - Surface( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background), - shape = RoundedCornerShape(16.dp) - ) { - LazyColumn( - state = listState, - verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.CenterHorizontally, + LabelsSelectionSheetContent(viewModel = viewModel, initialSelectedLabels = viewModel.activeLabelsLiveData.value ?: listOf()) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LabelsSelectionSheetContent(viewModel: LibraryViewModel, initialSelectedLabels: List) { + val listState = rememberLazyListState() + val labels: List by viewModel.savedItemLabelsLiveData.observeAsState(listOf()) + val selectedLabels = remember { mutableStateOf(initialSelectedLabels) } + + Surface( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + shape = RoundedCornerShape(16.dp) + ) { + LazyColumn( + state = listState, + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 6.dp) + ) { + item { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .fillMaxSize() - .padding(horizontal = 6.dp) + .fillMaxWidth() ) { - item { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - ) { - TextButton(onClick = { viewModel.showLabelsSelectionSheetLiveData.value = false }) { - Text(text = "Cancel") - } - - Text("Filter by Label", fontWeight = FontWeight.ExtraBold) - - TextButton( - onClick = { - selectedLabels.value?.let { - viewModel.activeLabelsLiveData.value = it - } - viewModel.showLabelsSelectionSheetLiveData.value = false - } - ) { - Text(text = "Done") - } - } + TextButton(onClick = { viewModel.showLabelsSelectionSheetLiveData.value = false }) { + Text(text = "Cancel") } - items(labels) { label -> - val isLabelSelected = (selectedLabels.value ?: listOf()).contains(label) - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .clickable { - if (isLabelSelected) { - selectedLabels.value = (selectedLabels.value ?: listOf()).filter { it.savedItemLabelId != label.savedItemLabelId } - } else { - selectedLabels.value = (selectedLabels.value ?: listOf()) + listOf(label) - } - } - .padding(horizontal = 6.dp) - ) { - val chipColors = LabelChipColors.fromHex(label.color) + Text("Filter by Label", fontWeight = FontWeight.ExtraBold) - SuggestionChip( - onClick = {}, - label = { Text(label.name) }, - border = null, - colors = SuggestionChipDefaults.elevatedSuggestionChipColors( - containerColor = chipColors.containerColor, - labelColor = chipColors.textColor, - iconContentColor = chipColors.textColor - ) - ) - if (isLabelSelected) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null - ) + TextButton( + onClick = { + selectedLabels.value?.let { + viewModel.updateAppliedLabels(it) } + viewModel.showLabelsSelectionSheetLiveData.value = false } - Divider(color = MaterialTheme.colorScheme.outlineVariant, thickness = 1.dp) + ) { + Text(text = "Done") } } } + items(labels) { label -> + val isLabelSelected = (selectedLabels.value ?: listOf()).contains(label) + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { + if (isLabelSelected) { + selectedLabels.value = (selectedLabels.value + ?: listOf()).filter { it.savedItemLabelId != label.savedItemLabelId } + } else { + selectedLabels.value = (selectedLabels.value ?: listOf()) + listOf(label) + } + } + .padding(horizontal = 6.dp) + ) { + val chipColors = LabelChipColors.fromHex(label.color) + + SuggestionChip( + onClick = {}, + label = { Text(label.name) }, + border = null, + colors = SuggestionChipDefaults.elevatedSuggestionChipColors( + containerColor = chipColors.containerColor, + labelColor = chipColors.textColor, + iconContentColor = chipColors.textColor + ) + ) + if (isLabelSelected) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null + ) + } + } + Divider(color = MaterialTheme.colorScheme.outlineVariant, thickness = 1.dp) + } } } } 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 8591eb275..cc8730d76 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 @@ -40,13 +40,12 @@ class LibraryViewModel @Inject constructor( val appliedSortFilterLiveData = MutableLiveData(SavedItemSortFilter.NEWEST) val showLabelsSelectionSheetLiveData = MutableLiveData(false) val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData() + val activeLabelsLiveData = MutableLiveData>(listOf()) var isRefreshing by mutableStateOf(false) var showSearchField by mutableStateOf(false) var hasLoadedInitialFilters = false - var activeLabelsLiveData = MutableLiveData>(listOf()) - fun loadInitialFilterValues() { if (hasLoadedInitialFilters) { return } hasLoadedInitialFilters = false @@ -129,11 +128,18 @@ class LibraryViewModel @Inject constructor( } } + fun updateAppliedLabels(labels: List) { + viewModelScope.launch { + activeLabelsLiveData.value = labels + handleFilterChanges() + } + } + suspend fun handleFilterChanges() { if (searchTextLiveData.value != "") { performSearch(true) } else if (appliedSortFilterLiveData.value != null && appliedFilterLiveData.value != null) { - itemsLiveDataInternal = dataService.libraryLiveData(appliedFilterLiveData.value!!, appliedSortFilterLiveData.value!!, listOf()) + itemsLiveDataInternal = dataService.libraryLiveData(appliedFilterLiveData.value!!, appliedSortFilterLiveData.value!!, activeLabelsLiveData.value ?: listOf()) itemsLiveData.removeSource(itemsLiveDataInternal) itemsLiveData.addSource(itemsLiveDataInternal, itemsLiveData::setValue) } From e66531cd773c5a13301e86bbbc1dbfe01bfa09c0 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 28 Feb 2023 12:37:16 -0800 Subject: [PATCH 05/48] use search api to fill gaps sync might miss --- .../omnivore/dataService/LibrarySync.kt | 33 ++++++++++ .../omnivore/networking/SearchQuery.kt | 64 +++++++++++++++---- .../omnivore/ui/library/LibraryView.kt | 6 +- .../omnivore/ui/library/LibraryViewModel.kt | 37 +++++++---- 4 files changed, 114 insertions(+), 26 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt index fb73fe3ed..c312f0c64 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt @@ -4,6 +4,39 @@ import android.util.Log import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.persistence.entities.* +suspend fun DataService.librarySearch(cursor: String?, query: String): SavedItemSyncResult { + val searchResult = networker.search(cursor = cursor, limit = 10, query = query) + + val savedItems = searchResult.items.map { it.item } + + db.savedItemDao().insertAll(savedItems) + + val labels: MutableList = mutableListOf() + val crossRefs: MutableList = mutableListOf() + + // save labels + for (searchItem in searchResult.items) { + labels.addAll(searchItem.labels) + + val newCrossRefs = searchItem.labels.map { + SavedItemAndSavedItemLabelCrossRef(savedItemLabelId = it.savedItemLabelId, savedItemId = searchItem.item.savedItemId) + } + + crossRefs.addAll(newCrossRefs) + } + + db.savedItemLabelDao().insertAll(labels) + db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs) + + return SavedItemSyncResult( + hasError = false, + hasMoreItems = false, + cursor = searchResult.cursor, + count = searchResult.items.size, + savedItemSlugs = savedItems.map { it.slug } + ) +} + suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): SavedItemSyncResult { val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since) ?: return SavedItemSyncResult.errorResult diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt index f1718846e..a1706493e 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt @@ -2,7 +2,9 @@ package app.omnivore.omnivore.networking import app.omnivore.omnivore.graphql.generated.SearchQuery import app.omnivore.omnivore.graphql.generated.TypeaheadSearchQuery +import app.omnivore.omnivore.persistence.entities.SavedItem import app.omnivore.omnivore.persistence.entities.SavedItemCardData +import app.omnivore.omnivore.persistence.entities.SavedItemLabel import com.apollographql.apollo3.api.Optional data class SearchQueryResponse( @@ -10,6 +12,16 @@ data class SearchQueryResponse( val cardsData: List ) +data class LibrarySearchQueryResponse( + val cursor: String?, + val items: List +) + +data class LibrarySearchItem( + val item: SavedItem, + val labels: List +) + suspend fun Networker.typeaheadSearch( query: String ): SearchQueryResponse { @@ -44,7 +56,7 @@ suspend fun Networker.search( cursor: String? = null, limit: Int = 15, query: String -): SearchQueryResponse { +): LibrarySearchQueryResponse { try { val result = authenticatedApolloClient().query( SearchQuery( @@ -57,22 +69,46 @@ suspend fun Networker.search( val newCursor = result.data?.search?.onSearchSuccess?.pageInfo?.endCursor val itemList = result.data?.search?.onSearchSuccess?.edges ?: listOf() - val cardsData = itemList.map { - SavedItemCardData( - savedItemId = it.node.id, - slug = it.node.slug, - publisherURLString = it.node.originalArticleUrl, - title = it.node.title, - author = it.node.author, - imageURLString = it.node.image, - isArchived = it.node.isArchived, - pageURLString = it.node.url, - contentReader = it.node.contentReader.rawValue, + val searchItems = itemList.map { + LibrarySearchItem( + item = SavedItem( + savedItemId = it.node.id, + title = it.node.title, + createdAt = it.node.createdAt as String, + savedAt = it.node.savedAt as String, + readAt = it.node.readAt as String?, + updatedAt = it.node.updatedAt as String?, + readingProgress = it.node.readingProgressPercent, + readingProgressAnchor = it.node.readingProgressAnchorIndex, + imageURLString = it.node.image, + pageURLString = it.node.url, + descriptionText = it.node.description, + publisherURLString = it.node.originalArticleUrl, + siteName = it.node.siteName, + author = it.node.author, + publishDate = it.node.publishedAt as String?, + slug = it.node.slug, + isArchived = it.node.isArchived, + contentReader = it.node.contentReader.rawValue, + content = null + ), + labels = (it.node.labels ?: listOf()).map { label -> + SavedItemLabel( + savedItemLabelId = label.id, + name = label.name, + color = label.color, + createdAt = null, + labelDescription = null + ) + } ) } - return SearchQueryResponse(newCursor, cardsData) + return LibrarySearchQueryResponse( + cursor = newCursor, + items = searchItems + ) } catch (e: java.lang.Exception) { - return SearchQueryResponse(null, listOf()) + return LibrarySearchQueryResponse(null, listOf()) } } 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 a43fd26f4..6dec98082 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 @@ -109,7 +109,11 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { } InfiniteListHandler(listState = listState) { - libraryViewModel.load() + if (cardsData.isEmpty()) { + libraryViewModel.load() + } else { + libraryViewModel.loadUsingSearchAPI() + } } PullRefreshIndicator( 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 cc8730d76..d7af687cb 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 @@ -25,6 +25,7 @@ class LibraryViewModel @Inject constructor( private val datastoreRepo: DatastoreRepository ): ViewModel() { private var cursor: String? = null + private var librarySearchCursor: String? = null // These are used to make sure we handle search result // responses in the right order @@ -108,6 +109,21 @@ class LibraryViewModel @Inject constructor( performSearch(clearPreviousSearch) } else { syncItems() + loadUsingSearchAPI() + } + } + } + + fun loadUsingSearchAPI() { + viewModelScope.launch { + withContext(Dispatchers.IO) { + val result = dataService.librarySearch(cursor = librarySearchCursor, query = searchQueryString()) + result.cursor?.let { + librarySearchCursor = it + } + CoroutineScope(Dispatchers.Main).launch { + isRefreshing = false + } } } } @@ -136,6 +152,7 @@ class LibraryViewModel @Inject constructor( } suspend fun handleFilterChanges() { + librarySearchCursor = null if (searchTextLiveData.value != "") { performSearch(true) } else if (appliedSortFilterLiveData.value != null && appliedFilterLiveData.value != null) { @@ -194,7 +211,7 @@ class LibraryViewModel @Inject constructor( searchIdx += 1 // Execute the search - val searchResult = networker.typeaheadSearch(searchQueryString()) + val searchResult = networker.typeaheadSearch(searchTextLiveData.value ?: "") // Search results aren't guaranteed to return in order so this // will discard old results that are returned while a user is typing. @@ -237,16 +254,14 @@ class LibraryViewModel @Inject constructor( } private fun searchQueryString(): String { - return searchTextLiveData.value ?: "" - // Unused code for typeahead search -// var query = "${appliedFilterLiveData.value?.queryString} ${appliedSortFilterLiveData.value?.queryString}" -// val searchText = searchTextLiveData.value ?: "" -// -// if (searchText.isNotEmpty()) { -// query += " $searchText" -// } -// -// return query + var query = "${appliedFilterLiveData.value?.queryString} ${appliedSortFilterLiveData.value?.queryString}" + val searchText = searchTextLiveData.value ?: "" + + if (searchText.isNotEmpty()) { + query += " $searchText" + } + + return query } } From a38eae380ed17bc85437d0be94febce9e23502f6 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 28 Feb 2023 12:49:39 -0800 Subject: [PATCH 06/48] add labels to library search calls --- .../app/omnivore/omnivore/ui/library/LibraryViewModel.kt | 5 +++++ 1 file changed, 5 insertions(+) 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 d7af687cb..e4fc5c6df 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 @@ -261,6 +261,11 @@ class LibraryViewModel @Inject constructor( query += " $searchText" } + activeLabelsLiveData.value?.let { + query += " label:" + query += it.joinToString { label -> label.name } + } + return query } } From 8c7a2b67684173fa386354d101c6f8dc4498c728 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 28 Feb 2023 12:50:25 -0800 Subject: [PATCH 07/48] bump android to build 27 --- 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 b53fa8a6c..203cebd49 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 26 - versionName "0.0.26" + versionCode 27 + versionName "0.0.27" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From c7860a8d7423c36448cfb7399c096b2499de042c Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 1 Mar 2023 11:28:03 -0800 Subject: [PATCH 08/48] bump android build number to 28 --- android/Omnivore/app/build.gradle | 4 ++-- .../java/app/omnivore/omnivore/ui/library/LibraryViewModel.kt | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/android/Omnivore/app/build.gradle b/android/Omnivore/app/build.gradle index 203cebd49..ecfebee7f 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 27 - versionName "0.0.27" + versionCode 28 + versionName "0.0.28" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { 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 e4fc5c6df..18ce116ac 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 @@ -51,6 +51,9 @@ class LibraryViewModel @Inject constructor( if (hasLoadedInitialFilters) { return } hasLoadedInitialFilters = false + // TODO: Fetch all labels + + runBlocking { datastoreRepo.getString(DatastoreKeys.lastUsedSavedItemFilter)?.let { str -> try { @@ -152,7 +155,6 @@ class LibraryViewModel @Inject constructor( } suspend fun handleFilterChanges() { - librarySearchCursor = null if (searchTextLiveData.value != "") { performSearch(true) } else if (appliedSortFilterLiveData.value != null && appliedFilterLiveData.value != null) { From 7f3c484ed7f48bb51dca5b2ac9a6bf845772b797 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 1 Mar 2023 12:04:38 -0800 Subject: [PATCH 09/48] fetch all labels at initialization --- .../app/src/main/graphql/Labels.graphql | 12 ++++++++++ .../dataService/SavedItemLabelSync.kt | 8 +++++++ .../networking/SavedItemLabelQuery.kt | 24 +++++++++++++++++++ .../omnivore/ui/library/LibraryViewModel.kt | 7 ++++-- 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 android/Omnivore/app/src/main/graphql/Labels.graphql create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SavedItemLabelSync.kt create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemLabelQuery.kt diff --git a/android/Omnivore/app/src/main/graphql/Labels.graphql b/android/Omnivore/app/src/main/graphql/Labels.graphql new file mode 100644 index 000000000..d61a6c868 --- /dev/null +++ b/android/Omnivore/app/src/main/graphql/Labels.graphql @@ -0,0 +1,12 @@ +query GetLabels { + labels { + ... on LabelsSuccess { + labels { + ...LabelFields + } + } + ... on LabelsError { + errorCodes + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SavedItemLabelSync.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SavedItemLabelSync.kt new file mode 100644 index 000000000..95d59e0bb --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SavedItemLabelSync.kt @@ -0,0 +1,8 @@ +package app.omnivore.omnivore.dataService + +import app.omnivore.omnivore.networking.savedItemLabels + +suspend fun DataService.syncLabels() { + val fetchedLabels = networker.savedItemLabels() + db.savedItemLabelDao().insertAll(fetchedLabels) +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemLabelQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemLabelQuery.kt new file mode 100644 index 000000000..521249da6 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemLabelQuery.kt @@ -0,0 +1,24 @@ +package app.omnivore.omnivore.networking + +import app.omnivore.omnivore.graphql.generated.GetLabelsQuery +import app.omnivore.omnivore.persistence.entities.SavedItemLabel + + +suspend fun Networker.savedItemLabels(): List { + try { + val result = authenticatedApolloClient().query(GetLabelsQuery()).execute() + val labels = result.data?.labels?.onLabelsSuccess?.labels ?: listOf() + + return labels.map { + SavedItemLabel( + savedItemLabelId = it.labelFields.id, + name = it.labelFields.name, + color = it.labelFields.color, + createdAt = it.labelFields.createdAt as String?, + labelDescription = it.labelFields.description + ) + } + } catch (e: java.lang.Exception) { + return listOf() + } +} 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 18ce116ac..9e1076011 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 @@ -51,8 +51,11 @@ class LibraryViewModel @Inject constructor( if (hasLoadedInitialFilters) { return } hasLoadedInitialFilters = false - // TODO: Fetch all labels - + viewModelScope.launch { + withContext(Dispatchers.IO) { + dataService.syncLabels() + } + } runBlocking { datastoreRepo.getString(DatastoreKeys.lastUsedSavedItemFilter)?.let { str -> From e9c57a6702b75c2d7296f5346608491a5c811e31 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 1 Mar 2023 12:49:42 -0800 Subject: [PATCH 10/48] add load initial func --- .../java/app/omnivore/omnivore/dataService/DataService.kt | 4 +++- .../java/app/omnivore/omnivore/persistence/AppDatabase.kt | 2 +- .../omnivore/omnivore/persistence/entities/SavedItemLabel.kt | 3 +-- .../java/app/omnivore/omnivore/ui/library/LibraryView.kt | 5 ++++- .../app/omnivore/omnivore/ui/library/LibraryViewModel.kt | 5 +++++ 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/DataService.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/DataService.kt index c25eb280c..dde348784 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/DataService.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/DataService.kt @@ -14,7 +14,9 @@ class DataService @Inject constructor( val db = Room.databaseBuilder( context, AppDatabase::class.java, "omnivore-database" - ).build() + ) + .fallbackToDestructiveMigration() + .build() fun clearDatabase() { CoroutineScope(Dispatchers.IO).launch { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/AppDatabase.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/AppDatabase.kt index 56faf0998..2557e4e7e 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/AppDatabase.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/AppDatabase.kt @@ -13,7 +13,7 @@ import app.omnivore.omnivore.persistence.entities.* SavedItemAndSavedItemLabelCrossRef::class, SavedItemAndHighlightCrossRef::class ], - version = 2 + version = 3 ) abstract class AppDatabase : RoomDatabase() { abstract fun viewerDao(): ViewerDao diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItemLabel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItemLabel.kt index 6c5fbd870..a35b67fba 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItemLabel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItemLabel.kt @@ -35,8 +35,7 @@ interface SavedItemLabelDao { ForeignKey( entity = SavedItemLabel::class, parentColumns = arrayOf("savedItemLabelId"), - childColumns = arrayOf("savedItemLabelId"), - onDelete = ForeignKey.CASCADE + childColumns = arrayOf("savedItemLabelId") ) ] ) 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 6dec98082..c1e89f1e9 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 @@ -1,6 +1,7 @@ package app.omnivore.omnivore.ui.library import android.content.Intent +import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -110,8 +111,10 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { InfiniteListHandler(listState = listState) { if (cardsData.isEmpty()) { - libraryViewModel.load() + Log.d("sync", "loading with load func") + libraryViewModel.initialLoad() } else { + Log.d("sync", "loading with search api") libraryViewModel.loadUsingSearchAPI() } } 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 9e1076011..ca67daabd 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 @@ -107,6 +107,11 @@ class LibraryViewModel @Inject constructor( } } + fun initialLoad() { + if (hasLoadedInitialFilters) { return } + load() + } + fun load(clearPreviousSearch: Boolean = false) { loadInitialFilterValues() From 7c6743a4e83eb5d071dc1f2fbec0f4015b20b08c Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 1 Mar 2023 14:19:48 -0800 Subject: [PATCH 11/48] move labels selection sheet to new file --- .../ui/components/LabelsSelectionSheet.kt | 124 ++++++++++++++++++ .../omnivore/ui/library/LibraryView.kt | 101 +------------- .../omnivore/ui/library/LibraryViewModel.kt | 6 +- .../omnivore/ui/reader/WebReaderViewModel.kt | 3 + .../ui/savedItemViews/SavedItemContextMenu.kt | 14 ++ 5 files changed, 147 insertions(+), 101 deletions(-) create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelsSelectionSheet.kt 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 new file mode 100644 index 000000000..28af4a225 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelsSelectionSheet.kt @@ -0,0 +1,124 @@ +package app.omnivore.omnivore.ui.components + +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.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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 + +@Composable +fun LabelsSelectionSheet(viewModel: LibraryViewModel) { + val isActive: Boolean by viewModel.showLabelsSelectionSheetLiveData.observeAsState(false) + + if (isActive) { + Dialog(onDismissRequest = { viewModel.showLabelsSelectionSheetLiveData.value = false } ) { + LabelsSelectionSheetContent(viewModel = viewModel, initialSelectedLabels = viewModel.activeLabelsLiveData.value ?: listOf()) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LabelsSelectionSheetContent(viewModel: LibraryViewModel, initialSelectedLabels: List) { + val listState = rememberLazyListState() + val labels: List by viewModel.savedItemLabelsLiveData.observeAsState(listOf()) + val selectedLabels = remember { mutableStateOf(initialSelectedLabels) } + + Surface( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + shape = RoundedCornerShape(16.dp) + ) { + LazyColumn( + state = listState, + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 6.dp) + ) { + item { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + ) { + TextButton(onClick = { viewModel.showLabelsSelectionSheetLiveData.value = false }) { + Text(text = "Cancel") + } + + Text("Filter by Label", fontWeight = FontWeight.ExtraBold) + + TextButton( + onClick = { + selectedLabels.value?.let { + viewModel.updateAppliedLabels(it) + } + viewModel.showLabelsSelectionSheetLiveData.value = false + } + ) { + Text(text = "Done") + } + } + } + items(labels) { label -> + val isLabelSelected = (selectedLabels.value ?: listOf()).contains(label) + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { + if (isLabelSelected) { + selectedLabels.value = (selectedLabels.value + ?: listOf()).filter { it.savedItemLabelId != label.savedItemLabelId } + } else { + selectedLabels.value = (selectedLabels.value ?: listOf()) + listOf(label) + } + } + .padding(horizontal = 6.dp) + ) { + val chipColors = LabelChipColors.fromHex(label.color) + + SuggestionChip( + onClick = {}, + label = { Text(label.name) }, + border = null, + colors = SuggestionChipDefaults.elevatedSuggestionChipColors( + containerColor = chipColors.containerColor, + labelColor = chipColors.textColor, + iconContentColor = chipColors.textColor + ) + ) + if (isLabelSelected) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null + ) + } + } + Divider(color = MaterialTheme.colorScheme.outlineVariant, thickness = 1.dp) + } + } + } +} 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 c1e89f1e9..a14260f6d 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 @@ -30,6 +30,7 @@ import app.omnivore.omnivore.Routes import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels import app.omnivore.omnivore.persistence.entities.SavedItemLabel import app.omnivore.omnivore.ui.components.LabelChipColors +import app.omnivore.omnivore.ui.components.LabelsSelectionSheet import app.omnivore.omnivore.ui.savedItemViews.SavedItemCard import app.omnivore.omnivore.ui.reader.PDFReaderActivity import app.omnivore.omnivore.ui.reader.WebReaderLoadingContainerActivity @@ -153,103 +154,3 @@ fun InfiniteListHandler( } } } - -@Composable -fun LabelsSelectionSheet(viewModel: LibraryViewModel) { - val isActive: Boolean by viewModel.showLabelsSelectionSheetLiveData.observeAsState(false) - - if (isActive) { - Dialog(onDismissRequest = { viewModel.showLabelsSelectionSheetLiveData.value = false } ) { - LabelsSelectionSheetContent(viewModel = viewModel, initialSelectedLabels = viewModel.activeLabelsLiveData.value ?: listOf()) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun LabelsSelectionSheetContent(viewModel: LibraryViewModel, initialSelectedLabels: List) { - val listState = rememberLazyListState() - val labels: List by viewModel.savedItemLabelsLiveData.observeAsState(listOf()) - val selectedLabels = remember { mutableStateOf(initialSelectedLabels) } - - Surface( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background), - shape = RoundedCornerShape(16.dp) - ) { - LazyColumn( - state = listState, - verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 6.dp) - ) { - item { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - ) { - TextButton(onClick = { viewModel.showLabelsSelectionSheetLiveData.value = false }) { - Text(text = "Cancel") - } - - Text("Filter by Label", fontWeight = FontWeight.ExtraBold) - - TextButton( - onClick = { - selectedLabels.value?.let { - viewModel.updateAppliedLabels(it) - } - viewModel.showLabelsSelectionSheetLiveData.value = false - } - ) { - Text(text = "Done") - } - } - } - items(labels) { label -> - val isLabelSelected = (selectedLabels.value ?: listOf()).contains(label) - - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .clickable { - if (isLabelSelected) { - selectedLabels.value = (selectedLabels.value - ?: listOf()).filter { it.savedItemLabelId != label.savedItemLabelId } - } else { - selectedLabels.value = (selectedLabels.value ?: listOf()) + listOf(label) - } - } - .padding(horizontal = 6.dp) - ) { - val chipColors = LabelChipColors.fromHex(label.color) - - SuggestionChip( - onClick = {}, - label = { Text(label.name) }, - border = null, - colors = SuggestionChipDefaults.elevatedSuggestionChipColors( - containerColor = chipColors.containerColor, - labelColor = chipColors.textColor, - iconContentColor = chipColors.textColor - ) - ) - if (isLabelSelected) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null - ) - } - } - Divider(color = MaterialTheme.colorScheme.outlineVariant, thickness = 1.dp) - } - } - } -} 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 ca67daabd..5808462fb 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 @@ -260,6 +260,9 @@ class LibraryViewModel @Inject constructor( dataService.unarchiveSavedItem(itemID) } } + SavedItemAction.EditLabels -> { + Log.d("label", "itemID") + } } } @@ -283,5 +286,6 @@ class LibraryViewModel @Inject constructor( enum class SavedItemAction { Delete, Archive, - Unarchive + Unarchive, + EditLabels } 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 612d45af4..d3de2c1aa 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 @@ -157,6 +157,9 @@ class WebReaderViewModel @Inject constructor( popToLibraryView() } } + SavedItemAction.EditLabels -> { + Log.d("label", "itemID") + } } } 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 7725d62d7..450b3e767 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,6 +1,7 @@ package app.omnivore.omnivore.ui.savedItemViews 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.material3.DropdownMenu @@ -21,6 +22,19 @@ fun SavedItemContextMenu( expanded = isExpanded, onDismissRequest = onDismiss ) { + DropdownMenuItem( + text = { Text("Edit Labels") }, + onClick = { + actionHandler(SavedItemAction.EditLabels) + onDismiss() + }, + leadingIcon = { + Icon( + Icons.Outlined.CheckCircle, // TODO: use more appropriate icon + contentDescription = null + ) + } + ) DropdownMenuItem( text = { Text(if (isArchived) "Unarchive" else "Archive") }, onClick = { From d95496040dee82c2d8e59a7ee6b7bf9b0434137f Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 2 Mar 2023 16:38:09 -0800 Subject: [PATCH 12/48] make network request to update labels for an item --- .../app/src/main/graphql/ApplyLabels.graphql | 12 ++++ .../networking/SavedItemLabelMutations.kt | 13 +++++ .../ui/components/LabelsSelectionSheet.kt | 57 ++++++++++++++----- .../omnivore/ui/library/LibraryViewModel.kt | 35 +++++++++++- 4 files changed, 100 insertions(+), 17 deletions(-) create mode 100644 android/Omnivore/app/src/main/graphql/ApplyLabels.graphql create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemLabelMutations.kt diff --git a/android/Omnivore/app/src/main/graphql/ApplyLabels.graphql b/android/Omnivore/app/src/main/graphql/ApplyLabels.graphql new file mode 100644 index 000000000..0e9fe8cf0 --- /dev/null +++ b/android/Omnivore/app/src/main/graphql/ApplyLabels.graphql @@ -0,0 +1,12 @@ +mutation SetLabels($input: SetLabelsInput!) { + setLabels(input: $input) { + ... on SetLabelsSuccess { + labels { + ...LabelFields + } + } + ... on SetLabelsError { + errorCodes + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemLabelMutations.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemLabelMutations.kt new file mode 100644 index 000000000..39755b22f --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemLabelMutations.kt @@ -0,0 +1,13 @@ +package app.omnivore.omnivore.networking + +import app.omnivore.omnivore.graphql.generated.SetLabelsMutation +import app.omnivore.omnivore.graphql.generated.type.SetLabelsInput + +suspend fun Networker.updateLabelsForSavedItem(input: SetLabelsInput): Boolean { + return try { + val result = authenticatedApolloClient().mutation(SetLabelsMutation(input)).execute() + return result.data?.setLabels?.onSetLabelsSuccess?.labels != null + } catch (e: java.lang.Exception) { + false + } +} 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 28af4a225..788317d10 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 @@ -26,19 +26,55 @@ import app.omnivore.omnivore.ui.library.LibraryViewModel @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() if (isActive) { - Dialog(onDismissRequest = { viewModel.showLabelsSelectionSheetLiveData.value = false } ) { - LabelsSelectionSheetContent(viewModel = viewModel, initialSelectedLabels = viewModel.activeLabelsLiveData.value ?: listOf()) + Dialog(onDismissRequest = { + viewModel.labelsSelectionCurrentItemLiveData.value = null + viewModel.showLabelsSelectionSheetLiveData.value = false + } ) { + if (currentSavedItemData != null) { + LabelsSelectionSheetContent( + labels = labels, + initialSelectedLabels = currentSavedItemData.labels, + onCancel = { + viewModel.showLabelsSelectionSheetLiveData.value = false + viewModel.labelsSelectionCurrentItemLiveData.value = null + }, + onSave = { + if (it != labels) { + viewModel.updateSavedItemLabels(savedItemID = currentSavedItemData.cardData.savedItemId, labels = it) + } + viewModel.labelsSelectionCurrentItemLiveData.value = null + viewModel.showLabelsSelectionSheetLiveData.value = false + } + ) + } else { + LabelsSelectionSheetContent( + labels = labels, + initialSelectedLabels = viewModel.activeLabelsLiveData.value ?: listOf(), + onCancel = { viewModel.showLabelsSelectionSheetLiveData.value = false }, + onSave = { + viewModel.updateAppliedLabels(it) + viewModel.labelsSelectionCurrentItemLiveData.value = null + viewModel.showLabelsSelectionSheetLiveData.value = false + } + ) + } } } } @OptIn(ExperimentalMaterial3Api::class) @Composable -fun LabelsSelectionSheetContent(viewModel: LibraryViewModel, initialSelectedLabels: List) { +fun LabelsSelectionSheetContent( + labels: List, + initialSelectedLabels: List, + onCancel: () -> Unit, + onSave: (List) -> Unit +) { val listState = rememberLazyListState() - val labels: List by viewModel.savedItemLabelsLiveData.observeAsState(listOf()) val selectedLabels = remember { mutableStateOf(initialSelectedLabels) } Surface( @@ -62,26 +98,19 @@ fun LabelsSelectionSheetContent(viewModel: LibraryViewModel, initialSelectedLabe modifier = Modifier .fillMaxWidth() ) { - TextButton(onClick = { viewModel.showLabelsSelectionSheetLiveData.value = false }) { + TextButton(onClick = onCancel) { Text(text = "Cancel") } Text("Filter by Label", fontWeight = FontWeight.ExtraBold) - TextButton( - onClick = { - selectedLabels.value?.let { - viewModel.updateAppliedLabels(it) - } - viewModel.showLabelsSelectionSheetLiveData.value = false - } - ) { + TextButton(onClick = { onSave(selectedLabels.value) }) { Text(text = "Done") } } } items(labels) { label -> - val isLabelSelected = (selectedLabels.value ?: listOf()).contains(label) + val isLabelSelected = selectedLabels.value.contains(label) Row( horizontalArrangement = Arrangement.SpaceBetween, 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 5808462fb..d2b273654 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 @@ -10,9 +10,9 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.omnivore.omnivore.* import app.omnivore.omnivore.dataService.* +import app.omnivore.omnivore.graphql.generated.type.SetLabelsInput import app.omnivore.omnivore.networking.* -import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels -import app.omnivore.omnivore.persistence.entities.SavedItemLabel +import app.omnivore.omnivore.persistence.entities.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* import java.time.Instant @@ -40,6 +40,7 @@ class LibraryViewModel @Inject constructor( val appliedFilterLiveData = MutableLiveData(SavedItemFilter.INBOX) val appliedSortFilterLiveData = MutableLiveData(SavedItemSortFilter.NEWEST) val showLabelsSelectionSheetLiveData = MutableLiveData(false) + val labelsSelectionCurrentItemLiveData = MutableLiveData(null) val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData() val activeLabelsLiveData = MutableLiveData>(listOf()) @@ -261,11 +262,39 @@ class LibraryViewModel @Inject constructor( } } SavedItemAction.EditLabels -> { - Log.d("label", "itemID") + labelsSelectionCurrentItemLiveData.value = itemID + showLabelsSelectionSheetLiveData.value = true } } } + fun updateSavedItemLabels(savedItemID: String, labels: List) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + val input = SetLabelsInput(labelIds = labels.map { it.savedItemLabelId }, pageId = savedItemID) + val networkResult = networker.updateLabelsForSavedItem(input) + + // TODO: assign a server sync status to these + val crossRefs = labels.map { + SavedItemAndSavedItemLabelCrossRef( + savedItemLabelId = it.savedItemLabelId, + savedItemId = savedItemID + ) + } + + dataService.db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs) + } + } + } + + fun currentSavedItemUnderEdit(): SavedItemCardDataWithLabels? { + labelsSelectionCurrentItemLiveData.value?.let { itemID -> + return itemsLiveData.value?.first { it.cardData.savedItemId == itemID } + } + + return null + } + private fun searchQueryString(): String { var query = "${appliedFilterLiveData.value?.queryString} ${appliedSortFilterLiveData.value?.queryString}" val searchText = searchTextLiveData.value ?: "" From db1fc75de2dcb49475f4e6c58e30d1f07b3975e3 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 3 Mar 2023 11:35:44 -0800 Subject: [PATCH 13/48] remove all labels in room db before applying current labels to an item --- .../omnivore/persistence/entities/SavedItemLabel.kt | 3 +++ .../app/omnivore/omnivore/ui/library/LibraryViewModel.kt | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItemLabel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItemLabel.kt index a35b67fba..1809ddf91 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItemLabel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItemLabel.kt @@ -68,6 +68,9 @@ data class SavedItemCardDataWithLabels( interface SavedItemAndSavedItemLabelCrossRefDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAll(items: List) + + @Query("DELETE FROM savedItemAndSavedItemLabelCrossRef WHERE savedItemId = :savedItemId") + fun deleteRefsBySavedItemId(savedItemId: String) } // has many highlights 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 d2b273654..f1e1ca855 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 @@ -282,7 +282,15 @@ class LibraryViewModel @Inject constructor( ) } + // Remove all labels first + dataService.db.savedItemAndSavedItemLabelCrossRefDao().deleteRefsBySavedItemId(savedItemID) + + // Add back the current labels dataService.db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs) + + CoroutineScope(Dispatchers.Main).launch { + handleFilterChanges() + } } } } From f4b1c99e5f988a3a39db83923a14cc2ddb3d05e8 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 3 Mar 2023 13:47:44 -0800 Subject: [PATCH 14/48] add create label button --- .../ui/components/LabelsSelectionSheet.kt | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) 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 788317d10..38fae7b93 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 @@ -1,5 +1,6 @@ package app.omnivore.omnivore.ui.components +import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -8,7 +9,9 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AddCircle import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -42,6 +45,7 @@ fun LabelsSelectionSheet(viewModel: LibraryViewModel) { viewModel.showLabelsSelectionSheetLiveData.value = false viewModel.labelsSelectionCurrentItemLiveData.value = null }, + isLibraryMode = false, onSave = { if (it != labels) { viewModel.updateSavedItemLabels(savedItemID = currentSavedItemData.cardData.savedItemId, labels = it) @@ -55,6 +59,7 @@ fun LabelsSelectionSheet(viewModel: LibraryViewModel) { labels = labels, initialSelectedLabels = viewModel.activeLabelsLiveData.value ?: listOf(), onCancel = { viewModel.showLabelsSelectionSheetLiveData.value = false }, + isLibraryMode = true, onSave = { viewModel.updateAppliedLabels(it) viewModel.labelsSelectionCurrentItemLiveData.value = null @@ -69,6 +74,7 @@ fun LabelsSelectionSheet(viewModel: LibraryViewModel) { @OptIn(ExperimentalMaterial3Api::class) @Composable fun LabelsSelectionSheetContent( + isLibraryMode: Boolean, labels: List, initialSelectedLabels: List, onCancel: () -> Unit, @@ -76,6 +82,7 @@ fun LabelsSelectionSheetContent( ) { val listState = rememberLazyListState() val selectedLabels = remember { mutableStateOf(initialSelectedLabels) } + val titleText = if (isLibraryMode) "Filter by Label" else "Set Labels" Surface( modifier = Modifier @@ -102,7 +109,7 @@ fun LabelsSelectionSheetContent( Text(text = "Cancel") } - Text("Filter by Label", fontWeight = FontWeight.ExtraBold) + Text(titleText, fontWeight = FontWeight.ExtraBold) TextButton(onClick = { onSave(selectedLabels.value) }) { Text(text = "Done") @@ -148,6 +155,30 @@ fun LabelsSelectionSheetContent( } Divider(color = MaterialTheme.colorScheme.outlineVariant, thickness = 1.dp) } + + if (!isLibraryMode) { + item { + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { + Log.d("label", "add new label button tapped") + } + .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") + } + } + } } } } From e1a27a3c151349c1c9ad693411cf6a9a20a8b53d Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 3 Mar 2023 13:52:28 -0800 Subject: [PATCH 15/48] make labels sheet header sticky --- .../ui/components/LabelsSelectionSheet.kt | 139 +++++++++--------- 1 file changed, 72 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 38fae7b93..d55833ada 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 @@ -90,92 +90,97 @@ fun LabelsSelectionSheetContent( .background(MaterialTheme.colorScheme.background), shape = RoundedCornerShape(16.dp) ) { - LazyColumn( - state = listState, + Column( verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .fillMaxSize() .padding(horizontal = 6.dp) ) { - item { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - ) { - TextButton(onClick = onCancel) { - Text(text = "Cancel") - } + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + ) { + TextButton(onClick = onCancel) { + Text(text = "Cancel") + } - Text(titleText, fontWeight = FontWeight.ExtraBold) + Text(titleText, fontWeight = FontWeight.ExtraBold) - TextButton(onClick = { onSave(selectedLabels.value) }) { - Text(text = "Done") - } + TextButton(onClick = { onSave(selectedLabels.value) }) { + Text(text = "Done") } } - items(labels) { label -> - val isLabelSelected = selectedLabels.value.contains(label) + 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 - ?: listOf()).filter { it.savedItemLabelId != label.savedItemLabelId } - } else { - selectedLabels.value = (selectedLabels.value ?: listOf()) + listOf(label) - } - } - .padding(horizontal = 6.dp) - ) { - val chipColors = LabelChipColors.fromHex(label.color) - - SuggestionChip( - onClick = {}, - label = { Text(label.name) }, - border = null, - colors = SuggestionChipDefaults.elevatedSuggestionChipColors( - containerColor = chipColors.containerColor, - labelColor = chipColors.textColor, - iconContentColor = chipColors.textColor - ) - ) - if (isLabelSelected) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null - ) - } - } - Divider(color = MaterialTheme.colorScheme.outlineVariant, thickness = 1.dp) - } - - if (!isLibraryMode) { - item { Row( - horizontalArrangement = Arrangement.Start, + horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() .clickable { - Log.d("label", "add new label button tapped") + if (isLabelSelected) { + selectedLabels.value = (selectedLabels.value + ?: listOf()).filter { it.savedItemLabelId != label.savedItemLabelId } + } else { + selectedLabels.value = (selectedLabels.value ?: listOf()) + listOf(label) + } } .padding(horizontal = 6.dp) - .padding(vertical = 12.dp) - ) - { - Icon( - imageVector = Icons.Filled.AddCircle, - contentDescription = null, - modifier = Modifier.padding(end = 8.dp) + ) { + val chipColors = LabelChipColors.fromHex(label.color) + + SuggestionChip( + onClick = {}, + label = { Text(label.name) }, + border = null, + colors = SuggestionChipDefaults.elevatedSuggestionChipColors( + containerColor = chipColors.containerColor, + labelColor = chipColors.textColor, + iconContentColor = chipColors.textColor + ) ) - 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 { + Log.d("label", "add new label button tapped") + } + .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") + } } } } From e455ab0325b7818962fe00bc618321385c3feabd Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 6 Mar 2023 12:00:57 -0800 Subject: [PATCH 16/48] stub in LabelCreationDialog --- .../ui/components/LabelCreationDialog.kt | 20 +++++++++++++++++++ .../ui/components/LabelsSelectionSheet.kt | 19 ++++++++++++++---- .../ui/reader/WebPreferencesDialog.kt | 8 ++++---- 3 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelCreationDialog.kt 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 new file mode 100644 index 000000000..242b31445 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelCreationDialog.kt @@ -0,0 +1,20 @@ +package app.omnivore.omnivore.ui.components + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.window.Dialog + +@Composable +fun LabelCreationDialog(onDismiss: () -> Unit, onSave: () -> Unit) { + Dialog(onDismissRequest = { onDismiss() }) { + Surface( + modifier = Modifier + .fillMaxSize() + ) { + Text("Add Label Dialog") + } + } +} 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 d55833ada..59709b8d1 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 @@ -13,18 +13,18 @@ import androidx.compose.material.icons.filled.AddCircle import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +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.WebPreferencesView +import app.omnivore.omnivore.ui.reader.WebReaderViewModel @Composable fun LabelsSelectionSheet(viewModel: LibraryViewModel) { @@ -82,6 +82,8 @@ fun LabelsSelectionSheetContent( ) { val listState = rememberLazyListState() val selectedLabels = remember { mutableStateOf(initialSelectedLabels) } + var showCreateLabelDialog by remember { mutableStateOf(false ) } + val titleText = if (isLibraryMode) "Filter by Label" else "Set Labels" Surface( @@ -90,6 +92,14 @@ fun LabelsSelectionSheetContent( .background(MaterialTheme.colorScheme.background), shape = RoundedCornerShape(16.dp) ) { + + if (showCreateLabelDialog) { + LabelCreationDialog( + onDismiss = { showCreateLabelDialog = false }, + onSave = { showCreateLabelDialog = false } + ) + } + Column( verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally, @@ -169,6 +179,7 @@ fun LabelsSelectionSheetContent( .fillMaxWidth() .clickable { Log.d("label", "add new label button tapped") + showCreateLabelDialog = true } .padding(horizontal = 6.dp) .padding(vertical = 12.dp) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt index f7ab439c5..1b6600a33 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt @@ -1,10 +1,8 @@ package app.omnivore.omnivore.ui.reader -import android.util.Log import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll @@ -14,8 +12,10 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowRight -import androidx.compose.material.icons.filled.KeyboardArrowUp -import androidx.compose.material3.* +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember From f6cf5682819a0d0d87947e76a369fcf9bf26bf45 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 6 Mar 2023 17:04:55 -0800 Subject: [PATCH 17/48] layout label creation dialog --- .../ui/components/LabelCreationDialog.kt | 95 ++++++++++++++++++- 1 file changed, 91 insertions(+), 4 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 242b31445..fccf2ddda 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 @@ -1,20 +1,107 @@ package app.omnivore.omnivore.ui.components -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Surface -import androidx.compose.material3.Text +import android.util.Log +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.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AddCircle +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog +@OptIn(ExperimentalMaterial3Api::class) @Composable fun LabelCreationDialog(onDismiss: () -> Unit, onSave: () -> Unit) { + var labelName by rememberSaveable { mutableStateOf("") } + val focusManager = LocalFocusManager.current + Dialog(onDismissRequest = { onDismiss() }) { Surface( modifier = Modifier .fillMaxSize() ) { - Text("Add Label Dialog") + Surface( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + shape = RoundedCornerShape(16.dp) + ) { + + Column( + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 6.dp) + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + ) { + TextButton(onClick = onDismiss) { + Text(text = "Cancel") + } + + Text("Create New Label", fontWeight = FontWeight.ExtraBold) + + TextButton(onClick = { onSave() }) { + Text(text = "Create") + } + } + + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + ) { + Text("Assign a name and color.") + } + + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + ) { + OutlinedTextField( + value = labelName, + placeholder = { Text(text = "Label Name") }, + onValueChange = { labelName = it }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }) + ) + } + + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + ) { + Text("Add a color grid here.") + } + } + } } } } From 05a0ff341978bf1f8937b8e2bd92d1110095ed06 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 7 Mar 2023 20:08:13 -0800 Subject: [PATCH 18/48] create basic create label UI --- .../ui/components/LabelCreationDialog.kt | 99 ++++++++++++++++--- .../ui/components/LabelsSelectionSheet.kt | 5 +- 2 files changed, 91 insertions(+), 13 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 fccf2ddda..8c3adb263 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 @@ -1,17 +1,15 @@ package app.omnivore.omnivore.ui.components -import android.util.Log +import androidx.compose.foundation.BorderStroke 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.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.AddCircle -import androidx.compose.material.icons.filled.Check import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -20,6 +18,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction @@ -28,9 +27,11 @@ import androidx.compose.ui.window.Dialog @OptIn(ExperimentalMaterial3Api::class) @Composable -fun LabelCreationDialog(onDismiss: () -> Unit, onSave: () -> Unit) { +fun LabelCreationDialog(onDismiss: () -> Unit, onSave: (String, String) -> Unit) { var labelName by rememberSaveable { mutableStateOf("") } val focusManager = LocalFocusManager.current + val swatchHexes = LabelSwatchHelper.allHexes() + var selectedHex by rememberSaveable { mutableStateOf(swatchHexes.first()) } Dialog(onDismissRequest = { onDismiss() }) { Surface( @@ -63,7 +64,7 @@ fun LabelCreationDialog(onDismiss: () -> Unit, onSave: () -> Unit) { Text("Create New Label", fontWeight = FontWeight.ExtraBold) - TextButton(onClick = { onSave() }) { + TextButton(onClick = { onSave(labelName, selectedHex) }) { Text(text = "Create") } } @@ -73,6 +74,7 @@ fun LabelCreationDialog(onDismiss: () -> Unit, onSave: () -> Unit) { verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() + .padding(vertical = 10.dp) ) { Text("Assign a name and color.") } @@ -82,6 +84,7 @@ fun LabelCreationDialog(onDismiss: () -> Unit, onSave: () -> Unit) { verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() + .padding(bottom = 10.dp) ) { OutlinedTextField( value = labelName, @@ -94,14 +97,86 @@ fun LabelCreationDialog(onDismiss: () -> Unit, onSave: () -> Unit) { Row( horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.Top, modifier = Modifier - .fillMaxWidth() + .height(130.dp) + .padding(10.dp) ) { - Text("Add a color grid here.") + LazyHorizontalGrid( + rows = GridCells.Fixed(2), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + items(swatchHexes) { hex -> + val labelChipColors = LabelChipColors.fromHex(hex) + val borderThickness = if (selectedHex == hex) 2.dp else 0.dp + OutlinedButton( + onClick = { selectedHex = hex }, + modifier= Modifier + .size(50.dp), + shape = CircleShape, + border= BorderStroke(borderThickness, Color.Black), + contentPadding = PaddingValues(0.dp), + colors = ButtonDefaults.outlinedButtonColors(containerColor = labelChipColors.containerColor), + content = {} + ) + } + } } } } } } } + +object LabelSwatchHelper { + fun allHexes(): List { + val shuffledSwatches = swatchHexes.shuffled() + return listOf(shuffledSwatches.last()) + webSwatchHexes + shuffledSwatches.dropLast(1) + } + + private val webSwatchHexes = listOf( + "#FF5D99", + "#7CFF7B", + "#FFD234", + "#7BE4FF", + "#CE88EF", + "#EF8C43" + ) + + private val swatchHexes = listOf( + "#fff034", + "#efff34", + "#d1ff34", + "#b2ff34", + "#94ff34", + "#75ff34", + "#57ff34", + "#38ff34", + "#34ff4e", + "#34ff6d", + "#34ff8b", + "#34ffa9", + "#34ffc8", + "#34ffe6", + "#34f9ff", + "#34dbff", + "#34bcff", + "#349eff", + "#347fff", + "#3461ff", + "#3443ff", + "#4434ff", + "#6234ff", + "#8134ff", + "#9f34ff", + "#be34ff", + "#dc34ff", + "#fb34ff", + "#ff34e5", + "#ff34c7", + "#ff34a8", + "#ff348a", + "#ff346b" + ) +} 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 59709b8d1..1d2b49ebd 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 @@ -96,7 +96,10 @@ fun LabelsSelectionSheetContent( if (showCreateLabelDialog) { LabelCreationDialog( onDismiss = { showCreateLabelDialog = false }, - onSave = { showCreateLabelDialog = false } + onSave = { labelName, hexColor -> + Log.d("label", "creating label with name: $labelName and hex: $hexColor") + showCreateLabelDialog = false + } ) } From fa97d01d0cc2a674f8bffff05ba607bcb7d7f5a4 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 7 Mar 2023 20:30:55 -0800 Subject: [PATCH 19/48] add callback to save newly created item in vm --- .../ui/components/LabelsSelectionSheet.kt | 23 +++++++++++-------- .../omnivore/ui/library/LibraryViewModel.kt | 4 ++++ 2 files changed, 17 insertions(+), 10 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 1d2b49ebd..41a235693 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,9 +52,12 @@ fun LabelsSelectionSheet(viewModel: LibraryViewModel) { } viewModel.labelsSelectionCurrentItemLiveData.value = null viewModel.showLabelsSelectionSheetLiveData.value = false + }, + onCreateLabel = { newLabelName, labelHexValue -> + viewModel.createNewSavedItemLabel(newLabelName, labelHexValue) } ) - } else { + } else { // Is used in library mode LabelsSelectionSheetContent( labels = labels, initialSelectedLabels = viewModel.activeLabelsLiveData.value ?: listOf(), @@ -64,6 +67,9 @@ fun LabelsSelectionSheet(viewModel: LibraryViewModel) { viewModel.updateAppliedLabels(it) viewModel.labelsSelectionCurrentItemLiveData.value = null viewModel.showLabelsSelectionSheetLiveData.value = false + }, + onCreateLabel = { newLabelName, labelHexValue -> + viewModel.createNewSavedItemLabel(newLabelName, labelHexValue) } ) } @@ -78,7 +84,8 @@ fun LabelsSelectionSheetContent( labels: List, initialSelectedLabels: List, onCancel: () -> Unit, - onSave: (List) -> Unit + onSave: (List) -> Unit, + onCreateLabel: (String, String) -> Unit ) { val listState = rememberLazyListState() val selectedLabels = remember { mutableStateOf(initialSelectedLabels) } @@ -97,7 +104,7 @@ fun LabelsSelectionSheetContent( LabelCreationDialog( onDismiss = { showCreateLabelDialog = false }, onSave = { labelName, hexColor -> - Log.d("label", "creating label with name: $labelName and hex: $hexColor") + onCreateLabel(labelName, hexColor) showCreateLabelDialog = false } ) @@ -143,10 +150,9 @@ fun LabelsSelectionSheetContent( .fillMaxWidth() .clickable { if (isLabelSelected) { - selectedLabels.value = (selectedLabels.value - ?: listOf()).filter { it.savedItemLabelId != label.savedItemLabelId } + selectedLabels.value = selectedLabels.value.filter { it.savedItemLabelId != label.savedItemLabelId } } else { - selectedLabels.value = (selectedLabels.value ?: listOf()) + listOf(label) + selectedLabels.value = selectedLabels.value + listOf(label) } } .padding(horizontal = 6.dp) @@ -180,10 +186,7 @@ fun LabelsSelectionSheetContent( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .clickable { - Log.d("label", "add new label button tapped") - showCreateLabelDialog = true - } + .clickable { showCreateLabelDialog = true } .padding(horizontal = 6.dp) .padding(vertical = 12.dp) ) 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 f1e1ca855..5073880aa 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 @@ -295,6 +295,10 @@ class LibraryViewModel @Inject constructor( } } + fun createNewSavedItemLabel(labelName: String, hexColorValue: String) { + Log.d("labels", "creating label with name: $labelName and hex: $hexColorValue") + } + fun currentSavedItemUnderEdit(): SavedItemCardDataWithLabels? { labelsSelectionCurrentItemLiveData.value?.let { itemID -> return itemsLiveData.value?.first { it.cardData.savedItemId == itemID } From b5a65cbc911baf2e8daa715164865b875f35c418 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 7 Mar 2023 20:41:15 -0800 Subject: [PATCH 20/48] add create label gql mutation --- .../app/src/main/graphql/CreateLabel.graphql | 16 ++++++++++++++++ .../networking/SavedItemLabelMutations.kt | 11 +++++++++++ 2 files changed, 27 insertions(+) create mode 100644 android/Omnivore/app/src/main/graphql/CreateLabel.graphql diff --git a/android/Omnivore/app/src/main/graphql/CreateLabel.graphql b/android/Omnivore/app/src/main/graphql/CreateLabel.graphql new file mode 100644 index 000000000..d3b438b2e --- /dev/null +++ b/android/Omnivore/app/src/main/graphql/CreateLabel.graphql @@ -0,0 +1,16 @@ +mutation CreateLabel($input: CreateLabelInput!) { + createLabel(input: $input) { + ... on CreateLabelSuccess { + label { + id + name + color + description + createdAt + } + } + ... on CreateLabelError { + errorCodes + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemLabelMutations.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemLabelMutations.kt index 39755b22f..e7120536f 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemLabelMutations.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemLabelMutations.kt @@ -1,6 +1,8 @@ package app.omnivore.omnivore.networking +import app.omnivore.omnivore.graphql.generated.CreateLabelMutation import app.omnivore.omnivore.graphql.generated.SetLabelsMutation +import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput import app.omnivore.omnivore.graphql.generated.type.SetLabelsInput suspend fun Networker.updateLabelsForSavedItem(input: SetLabelsInput): Boolean { @@ -11,3 +13,12 @@ suspend fun Networker.updateLabelsForSavedItem(input: SetLabelsInput): Boolean { false } } + +suspend fun Networker.createNewLabel(input: CreateLabelInput): CreateLabelMutation.Label? { + return try { + val result = authenticatedApolloClient().mutation(CreateLabelMutation(input)).execute() + return result.data?.createLabel?.onCreateLabelSuccess?.label + } catch (e: java.lang.Exception) { + null + } +} From e27a0dd3f05c1b029460fdc7a7cce5a8a0b6f402 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 7 Mar 2023 20:47:09 -0800 Subject: [PATCH 21/48] persist new label to db --- .../omnivore/ui/library/LibraryViewModel.kt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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 5073880aa..6e078be31 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 @@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope 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.networking.* import app.omnivore.omnivore.persistence.entities.* @@ -296,7 +297,23 @@ class LibraryViewModel @Inject constructor( } fun createNewSavedItemLabel(labelName: String, hexColorValue: String) { - Log.d("labels", "creating label with name: $labelName and hex: $hexColorValue") + viewModelScope.launch { + withContext(Dispatchers.IO) { + val newLabel = networker.createNewLabel(CreateLabelInput(color = hexColorValue, name = labelName)) + + newLabel?.let { + val savedItemLabel = SavedItemLabel( + savedItemLabelId = it.id, + name = it.name, + color = it.color, + createdAt = it.createdAt as String?, + labelDescription = it.description + ) + + dataService.db.savedItemLabelDao().insertAll(listOf(savedItemLabel)) + } + } + } } fun currentSavedItemUnderEdit(): SavedItemCardDataWithLabels? { From 21ab6dd9cd241d4112c60dffaf439139f1cc546b Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 14 Mar 2023 09:38:26 -0700 Subject: [PATCH 22/48] handle label chages from within web reader activity --- .../ui/components/LabelsSelectionSheet.kt | 39 ++++++++-- .../ui/reader/WebReaderLoadingContainer.kt | 4 ++ .../omnivore/ui/reader/WebReaderViewModel.kt | 71 +++++++++++++++++-- 3 files changed, 105 insertions(+), 9 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 41a235693..cf0ab9623 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 @@ -1,6 +1,5 @@ package app.omnivore.omnivore.ui.components -import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -11,21 +10,50 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AddCircle import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -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.WebPreferencesView +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) + + if (isActive) { + Dialog(onDismissRequest = { + viewModel.showLabelsSelectionSheetLiveData.value = false + } ) { + 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) { val isActive: Boolean by viewModel.showLabelsSelectionSheetLiveData.observeAsState(false) @@ -150,7 +178,8 @@ fun LabelsSelectionSheetContent( .fillMaxWidth() .clickable { if (isLabelSelected) { - selectedLabels.value = selectedLabels.value.filter { it.savedItemLabelId != label.savedItemLabelId } + selectedLabels.value = + selectedLabels.value.filter { it.savedItemLabelId != label.savedItemLabelId } } else { selectedLabels.value = selectedLabels.value + listOf(label) } 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 d87b1cf70..6d6d85228 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 @@ -30,7 +30,9 @@ import androidx.compose.ui.unit.dp import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.viewmodel.compose.viewModel import app.omnivore.omnivore.MainActivity +import app.omnivore.omnivore.ui.components.WebReaderLabelsSelectionSheet import app.omnivore.omnivore.ui.savedItemViews.SavedItemContextMenu import app.omnivore.omnivore.ui.theme.OmnivoreTheme import com.google.accompanist.systemuicontroller.rememberSystemUiController @@ -188,6 +190,8 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, o } ) } + + WebReaderLabelsSelectionSheet(webReaderViewModel) } LaunchedEffect(shouldPopView) { 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 d3de2c1aa..bec21906c 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 @@ -12,8 +12,12 @@ import androidx.lifecycle.viewModelScope 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.SavedItemAndSavedItemLabelCrossRef +import app.omnivore.omnivore.persistence.entities.SavedItemLabel import app.omnivore.omnivore.ui.library.SavedItemAction import com.google.gson.Gson import dagger.hilt.android.lifecycle.HiltViewModel @@ -23,7 +27,8 @@ import javax.inject.Inject data class WebReaderParams( val item: SavedItem, - val articleContent: ArticleContent + val articleContent: ArticleContent, + val labels: List ) data class AnnotationWebViewMessage( @@ -46,12 +51,16 @@ 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() var hasTappedExistingHighlight = false var lastTapCoordinates: TapCoordinates? = null private var isLoading = false + private var slug: String? = null fun loadItem(slug: String?, requestID: String?) { + this.slug = slug if (isLoading || webReaderParamsLiveData.value != null) { return } isLoading = true Log.d("reader", "load item called") @@ -88,6 +97,7 @@ class WebReaderViewModel @Inject constructor( val isSuccessful = webReaderParams?.articleContent?.contentStatus == "SUCCEEDED" if (webReaderParams != null && isSuccessful) { + this.slug = webReaderParams.item.slug webReaderParamsLiveData.postValue(webReaderParams) isLoading = false } else if (requestCount < 7) { @@ -114,7 +124,13 @@ class WebReaderViewModel @Inject constructor( ) Log.d("sync", "data loaded from db") - webReaderParamsLiveData.postValue(WebReaderParams(persistedItem.savedItem, articleContent)) + webReaderParamsLiveData.postValue( + WebReaderParams( + persistedItem.savedItem, + articleContent, + persistedItem.labels + ) + ) } isLoading = false } @@ -134,7 +150,7 @@ class WebReaderViewModel @Inject constructor( labelsJSONString = Gson().toJson(articleQueryResult.labels) ) - return WebReaderParams(article, articleContent) + return WebReaderParams(article, articleContent, articleQueryResult.labels) } fun handleSavedItemAction(itemID: String, action: SavedItemAction) { @@ -158,7 +174,7 @@ class WebReaderViewModel @Inject constructor( } } SavedItemAction.EditLabels -> { - Log.d("label", "itemID") + showLabelsSelectionSheetLiveData.value = true } } } @@ -317,4 +333,51 @@ class WebReaderViewModel @Inject constructor( val script = "var event = new Event('updateFontFamily');event.fontFamily = '${font.rawValue}';document.dispatchEvent(event);" enqueueScript(script) } + + fun updateSavedItemLabels(savedItemID: String, labels: List) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + val input = SetLabelsInput(labelIds = labels.map { it.savedItemLabelId }, pageId = savedItemID) + val networkResult = networker.updateLabelsForSavedItem(input) + + // TODO: assign a server sync status to these + val crossRefs = labels.map { + SavedItemAndSavedItemLabelCrossRef( + savedItemLabelId = it.savedItemLabelId, + savedItemId = savedItemID + ) + } + + // Remove all labels first + dataService.db.savedItemAndSavedItemLabelCrossRefDao().deleteRefsBySavedItemId(savedItemID) + + // Add back the current labels + dataService.db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs) + + slug?.let { + loadItemFromDB(it) + } + } + } + } + + fun createNewSavedItemLabel(labelName: String, hexColorValue: String) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + val newLabel = networker.createNewLabel(CreateLabelInput(color = hexColorValue, name = labelName)) + + newLabel?.let { + val savedItemLabel = SavedItemLabel( + savedItemLabelId = it.id, + name = it.name, + color = it.color, + createdAt = it.createdAt as String?, + labelDescription = it.description + ) + + dataService.db.savedItemLabelDao().insertAll(listOf(savedItemLabel)) + } + } + } + } } From 953622e5043f0c55a25785faccda3480157d8f47 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 14 Mar 2023 10:20:12 -0700 Subject: [PATCH 23/48] update labels on webview when labels change --- .../app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt | 6 ++++++ 1 file changed, 6 insertions(+) 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 bec21906c..9962f1b77 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 @@ -357,6 +357,12 @@ class WebReaderViewModel @Inject constructor( slug?.let { loadItemFromDB(it) } + + // Send labels to webview + val script = "var event = new Event('updateLabels');event.labels = ${Gson().toJson(labels)};document.dispatchEvent(event);" + CoroutineScope(Dispatchers.Main).launch { + enqueueScript(script) + } } } } From dbbbee7fab9b2bddd260210fef8e2db343c1d717 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 14 Mar 2023 10:44:25 -0700 Subject: [PATCH 24/48] use more appropriate icons for labels and archive actions --- .../omnivore/ui/savedItemViews/SavedItemContextMenu.kt | 6 ++++-- .../app/src/main/res/drawable-v24/archive_outline.xml | 1 + android/Omnivore/app/src/main/res/drawable-v24/tag.xml | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 android/Omnivore/app/src/main/res/drawable-v24/archive_outline.xml create mode 100644 android/Omnivore/app/src/main/res/drawable-v24/tag.xml 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 450b3e767..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 @@ -9,6 +9,8 @@ import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource +import app.omnivore.omnivore.R import app.omnivore.omnivore.ui.library.SavedItemAction @Composable @@ -30,7 +32,7 @@ fun SavedItemContextMenu( }, leadingIcon = { Icon( - Icons.Outlined.CheckCircle, // TODO: use more appropriate icon + painter = painterResource(id = R.drawable.tag), contentDescription = null ) } @@ -44,7 +46,7 @@ fun SavedItemContextMenu( }, leadingIcon = { Icon( - Icons.Outlined.List, // TODO: use more appropriate icon + painter = painterResource(id = R.drawable.archive_outline), contentDescription = null ) } diff --git a/android/Omnivore/app/src/main/res/drawable-v24/archive_outline.xml b/android/Omnivore/app/src/main/res/drawable-v24/archive_outline.xml new file mode 100644 index 000000000..c8af82753 --- /dev/null +++ b/android/Omnivore/app/src/main/res/drawable-v24/archive_outline.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/android/Omnivore/app/src/main/res/drawable-v24/tag.xml b/android/Omnivore/app/src/main/res/drawable-v24/tag.xml new file mode 100644 index 000000000..1c9e83a1c --- /dev/null +++ b/android/Omnivore/app/src/main/res/drawable-v24/tag.xml @@ -0,0 +1 @@ + From 9a40f7c08d8d6a9964c083770a4e7aa4e24d63b3 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 14 Mar 2023 10:57:15 -0700 Subject: [PATCH 25/48] update icons --- .../java/app/omnivore/omnivore/ui/library/SearchBar.kt | 4 +++- .../omnivore/ui/reader/WebReaderLoadingContainer.kt | 10 ++++++---- .../app/src/main/res/drawable-v24/account_outline.xml | 1 + .../app/src/main/res/drawable-v24/dots_horizontal.xml | 1 + .../src/main/res/drawable-v24/format_letter_case.xml | 1 + 5 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 android/Omnivore/app/src/main/res/drawable-v24/account_outline.xml create mode 100644 android/Omnivore/app/src/main/res/drawable-v24/dots_horizontal.xml create mode 100644 android/Omnivore/app/src/main/res/drawable-v24/format_letter_case.xml diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt index 40bf1a5f6..f23d1b6c3 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt @@ -22,8 +22,10 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp +import app.omnivore.omnivore.R @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -65,7 +67,7 @@ fun SearchBar( IconButton(onClick = onSettingsIconClick) { Icon( - imageVector = Icons.Filled.Settings, + painter = painterResource(id = R.drawable.account_outline), contentDescription = null ) } 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 6d6d85228..2fd99ba6e 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 @@ -26,12 +26,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.lifecycle.viewmodel.compose.viewModel import app.omnivore.omnivore.MainActivity +import app.omnivore.omnivore.R import app.omnivore.omnivore.ui.components.WebReaderLabelsSelectionSheet import app.omnivore.omnivore.ui.savedItemViews.SavedItemContextMenu import app.omnivore.omnivore.ui.theme.OmnivoreTheme @@ -144,15 +146,15 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, o ) } } - IconButton(onClick = { isMenuExpanded = true }) { + IconButton(onClick = { showWebPreferencesDialog = true }) { Icon( - imageVector = Icons.Filled.Menu, + painter = painterResource(id = R.drawable.format_letter_case), contentDescription = null ) } - IconButton(onClick = { showWebPreferencesDialog = true }) { + IconButton(onClick = { isMenuExpanded = true }) { Icon( - imageVector = Icons.Filled.Settings, // TODO: set a better icon + painter = painterResource(id = R.drawable.dots_horizontal), contentDescription = null ) } diff --git a/android/Omnivore/app/src/main/res/drawable-v24/account_outline.xml b/android/Omnivore/app/src/main/res/drawable-v24/account_outline.xml new file mode 100644 index 000000000..b510dc29c --- /dev/null +++ b/android/Omnivore/app/src/main/res/drawable-v24/account_outline.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/android/Omnivore/app/src/main/res/drawable-v24/dots_horizontal.xml b/android/Omnivore/app/src/main/res/drawable-v24/dots_horizontal.xml new file mode 100644 index 000000000..1c04b685b --- /dev/null +++ b/android/Omnivore/app/src/main/res/drawable-v24/dots_horizontal.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/android/Omnivore/app/src/main/res/drawable-v24/format_letter_case.xml b/android/Omnivore/app/src/main/res/drawable-v24/format_letter_case.xml new file mode 100644 index 000000000..dc963d128 --- /dev/null +++ b/android/Omnivore/app/src/main/res/drawable-v24/format_letter_case.xml @@ -0,0 +1 @@ + \ No newline at end of file From 730cc0283e823aa263f045da9e94275fc5796bef Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 14 Mar 2023 12:18:06 -0700 Subject: [PATCH 26/48] use lazyColumn to layout settings screen --- .../omnivore/omnivore/ui/library/SearchBar.kt | 4 +- .../omnivore/ui/settings/SettingsContent.kt | 60 ++++++++++++++----- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt index f23d1b6c3..570d55deb 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt @@ -35,7 +35,7 @@ fun SearchBar( ) { val searchText: String by libraryViewModel.searchTextLiveData.observeAsState("") - SmallTopAppBar( + TopAppBar( title = { if (libraryViewModel.showSearchField) { SearchField(searchText) { libraryViewModel.updateSearchText(it) } @@ -43,7 +43,7 @@ fun SearchBar( Text("Library") } }, - colors = TopAppBarDefaults.smallTopAppBarColors( + colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surfaceVariant ), actions = { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt index fbedadde0..2124ab889 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt @@ -1,7 +1,12 @@ import android.annotation.SuppressLint +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Home +import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -11,13 +16,15 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import app.omnivore.omnivore.Routes import app.omnivore.omnivore.ui.auth.LoginViewModel +import app.omnivore.omnivore.ui.library.LibraryViewContent +import app.omnivore.omnivore.ui.library.LibraryViewModel +import app.omnivore.omnivore.ui.library.SearchBar import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInOptions import io.intercom.android.sdk.Intercom import io.intercom.android.sdk.IntercomSpace @OptIn(ExperimentalMaterial3Api::class) -@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable fun SettingsView( loginViewModel: LoginViewModel, @@ -25,11 +32,8 @@ fun SettingsView( ) { Scaffold( topBar = { - SmallTopAppBar( + TopAppBar( title = { Text("Settings") }, - colors = TopAppBarDefaults.smallTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ), actions = { IconButton(onClick = { navController.navigate(Routes.Library.route) }) { Icon( @@ -37,23 +41,49 @@ fun SettingsView( contentDescription = null ) } - } + }, colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) ) } + ) { paddingValues -> + SettingsViewContent( + loginViewModel = loginViewModel, + modifier = Modifier + .padding( + top = paddingValues.calculateTopPadding(), + bottom = paddingValues.calculateBottomPadding() + ) + ) + } +} + +@Composable +fun SettingsViewContent(loginViewModel: LoginViewModel, modifier: Modifier) { + val listState = rememberLazyListState() + + Box( + modifier = modifier.fillMaxSize() ) { - Column( - verticalArrangement = Arrangement.Center, + LazyColumn( + state = listState, + verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier + .background(MaterialTheme.colorScheme.background) .fillMaxSize() - .navigationBarsPadding() - .padding(horizontal = 16.dp) + .padding(horizontal = 6.dp) ) { - LogoutButton { loginViewModel.logout() } - Button(onClick = { - Intercom.client().present(space = IntercomSpace.Messages) - }) { - Text(text = "Open Help Center") + item { + LogoutButton { loginViewModel.logout() } + } + + item { + Button(onClick = { + Intercom.client().present(space = IntercomSpace.Messages) + }) { + Text(text = "Open Help Center") + } } } } From 5a9ca1e5f9fbd0705e3a0461dae0bba57358d077 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 14 Mar 2023 17:48:49 -0700 Subject: [PATCH 27/48] create settings row composable --- .../omnivore/ui/settings/SettingsContent.kt | 74 +++++++++++++++++-- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt index 2124ab889..5a4892c1b 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt @@ -1,10 +1,14 @@ import android.annotation.SuppressLint +import android.util.Log 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.rememberLazyListState import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowForward +import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Home import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material3.* @@ -75,16 +79,22 @@ fun SettingsViewContent(loginViewModel: LoginViewModel, modifier: Modifier) { .padding(horizontal = 6.dp) ) { item { - LogoutButton { loginViewModel.logout() } + SettingRow(text = "Logout") { Log.d("settings", "logout button tapped") } } + item { SectionDivider() } + item { Spacer(Modifier.height(60.dp)) } + item { SectionDivider() } + item { - Button(onClick = { - Intercom.client().present(space = IntercomSpace.Messages) - }) { - Text(text = "Open Help Center") - } + SettingRow(text = "Feedback") { Intercom.client().present(space = IntercomSpace.Messages) } } + + item { SectionDivider() } + +// item { +// LogoutButton { loginViewModel.logout() } +// } } } } @@ -106,3 +116,55 @@ fun LogoutButton(actionHandler: () -> Unit) { Text(text = "Logout") } } + +@Composable +private fun SectionDivider() { + Divider( + modifier = Modifier.padding(horizontal = 12.dp), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f) + ) +} + +@Composable +private fun SettingRow(text: String, includeIcon: Boolean = true, tapAction: () -> Unit) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { tapAction() } + ) { + Text( + text = text, + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(16.dp), + style = MaterialTheme.typography.titleMedium + ) + + if (includeIcon) { + Icon( + imageVector = Icons.Filled.ArrowForward, + contentDescription = null + ) + } + } +} + +// profile pic and name + +// labels +// emails +// subscriptions +// clubs + +// push notifications +// text to speech + +// documentation +// feedback +// privacy policy +// terms and conditions + +// manage account +// logout From 3ef3bb63b20af4000d9a65b79a9ceef10f8fcee1 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 15 Mar 2023 10:07:16 -0700 Subject: [PATCH 28/48] stub in settings sections and rows --- .../omnivore/ui/settings/SettingsContent.kt | 82 ++++++++++--------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt index 5a4892c1b..11a179501 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt @@ -1,16 +1,13 @@ -import android.annotation.SuppressLint import android.util.Log 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.rememberLazyListState -import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowForward -import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Home -import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -20,9 +17,6 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import app.omnivore.omnivore.Routes import app.omnivore.omnivore.ui.auth.LoginViewModel -import app.omnivore.omnivore.ui.library.LibraryViewContent -import app.omnivore.omnivore.ui.library.LibraryViewModel -import app.omnivore.omnivore.ui.library.SearchBar import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInOptions import io.intercom.android.sdk.Intercom @@ -69,28 +63,49 @@ fun SettingsViewContent(loginViewModel: LoginViewModel, modifier: Modifier) { Box( modifier = modifier.fillMaxSize() ) { - LazyColumn( - state = listState, + Column( verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .background(MaterialTheme.colorScheme.background) .fillMaxSize() .padding(horizontal = 6.dp) + .verticalScroll(rememberScrollState()) ) { - item { - SettingRow(text = "Logout") { Log.d("settings", "logout button tapped") } - } - item { SectionDivider() } - item { Spacer(Modifier.height(60.dp)) } - item { SectionDivider() } + // profile pic and name - item { - SettingRow(text = "Feedback") { Intercom.client().present(space = IntercomSpace.Messages) } - } + SettingRow(text = "Labels") { Log.d("settings", "labels button tapped") } + RowDivider() + SettingRow(text = "Emails") { Log.d("settings", "emails button tapped") } + RowDivider() + SettingRow(text = "Subscriptions") { Log.d("settings", "subscriptions button tapped") } + RowDivider() + SettingRow(text = "Clubs") { Log.d("settings", "clubs button tapped") } + + SectionSpacer() + + SettingRow(text = "Push Notifications") { Log.d("settings", "pn button tapped") } + RowDivider() + SettingRow(text = "Text to Speech") { Log.d("settings", "tts button tapped") } + + SectionSpacer() + + SettingRow(text = "Documentation") { Log.d("settings", "docs button tapped") } + RowDivider() + SettingRow(text = "Feedback") { Intercom.client().present(space = IntercomSpace.Messages) } + RowDivider() + SettingRow(text = "Privacy Policy") { Log.d("settings", "privacy button tapped") } + RowDivider() + SettingRow(text = "Terms and Conditions") { Log.d("settings", "t&c button tapped") } + + SectionSpacer() + + SettingRow(text = "Manage Account") { Log.d("settings", "logout button tapped") } + RowDivider() + SettingRow(text = "Logout", includeIcon = false) { Log.d("settings", "logout button tapped") } + RowDivider() - item { SectionDivider() } // item { // LogoutButton { loginViewModel.logout() } @@ -118,13 +133,20 @@ fun LogoutButton(actionHandler: () -> Unit) { } @Composable -private fun SectionDivider() { +private fun RowDivider() { Divider( modifier = Modifier.padding(horizontal = 12.dp), color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f) ) } +@Composable +private fun SectionSpacer() { + RowDivider() + Spacer(Modifier.height(60.dp)) + RowDivider() +} + @Composable private fun SettingRow(text: String, includeIcon: Boolean = true, tapAction: () -> Unit) { Row( @@ -150,21 +172,3 @@ private fun SettingRow(text: String, includeIcon: Boolean = true, tapAction: () } } } - -// profile pic and name - -// labels -// emails -// subscriptions -// clubs - -// push notifications -// text to speech - -// documentation -// feedback -// privacy policy -// terms and conditions - -// manage account -// logout From 872b8cf7226694f21fd989f3875ba819c4d5bfe7 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 15 Mar 2023 11:05:52 -0700 Subject: [PATCH 29/48] add alert dialog for logout action --- .../omnivore/ui/settings/SettingsContent.kt | 56 +++++++++++++------ 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt index 11a179501..ab2f05b02 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt @@ -10,6 +10,8 @@ import androidx.compose.material.icons.filled.ArrowForward import androidx.compose.material.icons.filled.Home import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -58,7 +60,7 @@ fun SettingsView( @Composable fun SettingsViewContent(loginViewModel: LoginViewModel, modifier: Modifier) { - val listState = rememberLazyListState() + val showLogoutDialog = remember { mutableStateOf(false) } Box( modifier = modifier.fillMaxSize() @@ -103,33 +105,51 @@ fun SettingsViewContent(loginViewModel: LoginViewModel, modifier: Modifier) { SettingRow(text = "Manage Account") { Log.d("settings", "logout button tapped") } RowDivider() - SettingRow(text = "Logout", includeIcon = false) { Log.d("settings", "logout button tapped") } + SettingRow(text = "Logout", includeIcon = false) { showLogoutDialog.value = true } RowDivider() + } - -// item { -// LogoutButton { loginViewModel.logout() } -// } + if (showLogoutDialog.value) { + LogoutDialog { performLogout -> + if (performLogout) { + loginViewModel.logout() + } + showLogoutDialog.value = false + } } } } + @Composable -fun LogoutButton(actionHandler: () -> Unit) { +fun LogoutDialog(onClose: (Boolean) -> Unit) { val context = LocalContext.current - Button(onClick = { - // Sign out google users - val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .build() + AlertDialog( + onDismissRequest = { onClose(false) }, + title = { Text(text = "Logout") }, + text = { + Text("Are you sure you want to logout?") + }, + confirmButton = { + Button(onClick = { + // Sign out google users + val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .build() - val googleSignIn = GoogleSignIn.getClient(context, signInOptions) - googleSignIn.signOut() - - actionHandler() - }) { - Text(text = "Logout") - } + val googleSignIn = GoogleSignIn.getClient(context, signInOptions) + googleSignIn.signOut() + onClose(true) + }) { + Text("Confirm") + } + }, + dismissButton = { + Button(onClick = { onClose(false) }) { + Text("Cancel") + } + } + ) } @Composable From 1f15392b0502eb3a4b270d0b4cfd1d575ed7fbe0 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Sun, 19 Mar 2023 15:35:51 -0700 Subject: [PATCH 30/48] add logout dialog and static terms/privacy page embeds --- .../main/java/app/omnivore/omnivore/Routes.kt | 3 + .../app/omnivore/omnivore/ui/root/RootView.kt | 13 ++++ .../omnivore/ui/settings/LogoutDialog.kt | 40 ++++++++++ .../omnivore/ui/settings/PolicyWebView.kt | 73 +++++++++++++++++++ .../omnivore/ui/settings/SettingsContent.kt | 56 ++++---------- 5 files changed, 142 insertions(+), 43 deletions(-) create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/LogoutDialog.kt create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/PolicyWebView.kt diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Routes.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Routes.kt index a1e5ece80..0ab373cc9 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Routes.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Routes.kt @@ -3,4 +3,7 @@ package app.omnivore.omnivore sealed class Routes(val route: String) { object Library : Routes("Library") object Settings: Routes("Settings") + object Documentation: Routes("Documentation") + object PrivacyPolicy: Routes("PrivacyPolicy") + object TermsAndConditions: Routes("TermsAndConditions") } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt index c1aaecf48..93dfe469e 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt @@ -18,6 +18,7 @@ import app.omnivore.omnivore.ui.auth.LoginViewModel import app.omnivore.omnivore.ui.auth.WelcomeScreen import app.omnivore.omnivore.ui.library.LibraryView import app.omnivore.omnivore.ui.library.LibraryViewModel +import app.omnivore.omnivore.ui.settings.PolicyWebView import com.google.accompanist.systemuicontroller.rememberSystemUiController @Composable @@ -78,5 +79,17 @@ fun PrimaryNavigator( composable(Routes.Settings.route) { SettingsView(loginViewModel = loginViewModel, navController = navController) } + + composable(Routes.Documentation.route) { + PolicyWebView(navController = navController, url = "https://docs.omnivore.app") + } + + composable(Routes.PrivacyPolicy.route) { + PolicyWebView(navController = navController, url = "https://omnivore.app/app/privacy") + } + + composable(Routes.TermsAndConditions.route) { + PolicyWebView(navController = navController, url = "https://omnivore.app/app/terms") + } } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/LogoutDialog.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/LogoutDialog.kt new file mode 100644 index 000000000..7cb634467 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/LogoutDialog.kt @@ -0,0 +1,40 @@ +package app.omnivore.omnivore.ui.settings + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInOptions + +@Composable +fun LogoutDialog(onClose: (Boolean) -> Unit) { + val context = LocalContext.current + + AlertDialog( + onDismissRequest = { onClose(false) }, + title = { Text(text = "Logout") }, + text = { + Text("Are you sure you want to logout?") + }, + confirmButton = { + Button(onClick = { + // Sign out google users + val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .build() + + val googleSignIn = GoogleSignIn.getClient(context, signInOptions) + googleSignIn.signOut() + onClose(true) + }) { + Text("Confirm") + } + }, + dismissButton = { + Button(onClick = { onClose(false) }) { + Text("Cancel") + } + } + ) +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/PolicyWebView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/PolicyWebView.kt new file mode 100644 index 000000000..f51e01b40 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/PolicyWebView.kt @@ -0,0 +1,73 @@ +package app.omnivore.omnivore.ui.settings + +import android.annotation.SuppressLint +import android.view.ViewGroup +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Home +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.viewinterop.AndroidView +import androidx.navigation.NavHostController +import app.omnivore.omnivore.Routes + +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "SetJavaScriptEnabled") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PolicyWebView(navController: NavHostController, url: String) { + Scaffold( + topBar = { + TopAppBar( + title = { androidx.compose.material3.Text("Settings") }, + actions = { + IconButton(onClick = { navController.navigate(Routes.Settings.route) }) { + Icon( + imageVector = Icons.Default.Home, + contentDescription = null + ) + } + }, colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) + } + ) { + val isDarkMode = isSystemInDarkTheme() + + Box { + AndroidView(factory = { + WebView(it).apply { + if (isDarkMode) { + setBackgroundColor(Color.Transparent.hashCode()) + } + + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + + settings.javaScriptEnabled = true + settings.allowContentAccess = true + settings.allowFileAccess = true + settings.domStorageEnabled = true + + alpha = 0.0f + + webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + view?.animate()?.alpha(1.0f)?.duration = 200 + } + } + + loadUrl(url) + } + }, update = {} + ) + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt index ab2f05b02..1ad965582 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import app.omnivore.omnivore.Routes import app.omnivore.omnivore.ui.auth.LoginViewModel +import app.omnivore.omnivore.ui.settings.LogoutDialog import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInOptions import io.intercom.android.sdk.Intercom @@ -49,6 +50,7 @@ fun SettingsView( ) { paddingValues -> SettingsViewContent( loginViewModel = loginViewModel, + navController = navController, modifier = Modifier .padding( top = paddingValues.calculateTopPadding(), @@ -59,7 +61,7 @@ fun SettingsView( } @Composable -fun SettingsViewContent(loginViewModel: LoginViewModel, modifier: Modifier) { +fun SettingsViewContent(loginViewModel: LoginViewModel, navController: NavHostController, modifier: Modifier) { val showLogoutDialog = remember { mutableStateOf(false) } Box( @@ -82,24 +84,24 @@ fun SettingsViewContent(loginViewModel: LoginViewModel, modifier: Modifier) { SettingRow(text = "Emails") { Log.d("settings", "emails button tapped") } RowDivider() SettingRow(text = "Subscriptions") { Log.d("settings", "subscriptions button tapped") } - RowDivider() - SettingRow(text = "Clubs") { Log.d("settings", "clubs button tapped") } +// RowDivider() +// SettingRow(text = "Clubs") { Log.d("settings", "clubs button tapped") } SectionSpacer() - SettingRow(text = "Push Notifications") { Log.d("settings", "pn button tapped") } - RowDivider() - SettingRow(text = "Text to Speech") { Log.d("settings", "tts button tapped") } +// SettingRow(text = "Push Notifications") { Log.d("settings", "pn button tapped") } +// RowDivider() +// SettingRow(text = "Text to Speech") { Log.d("settings", "tts button tapped") } +// +// SectionSpacer() - SectionSpacer() - - SettingRow(text = "Documentation") { Log.d("settings", "docs button tapped") } + SettingRow(text = "Documentation") { navController.navigate(Routes.Documentation.route) } RowDivider() SettingRow(text = "Feedback") { Intercom.client().present(space = IntercomSpace.Messages) } RowDivider() - SettingRow(text = "Privacy Policy") { Log.d("settings", "privacy button tapped") } + SettingRow(text = "Privacy Policy") { navController.navigate(Routes.PrivacyPolicy.route) } RowDivider() - SettingRow(text = "Terms and Conditions") { Log.d("settings", "t&c button tapped") } + SettingRow(text = "Terms and Conditions") { navController.navigate(Routes.TermsAndConditions.route) } SectionSpacer() @@ -120,38 +122,6 @@ fun SettingsViewContent(loginViewModel: LoginViewModel, modifier: Modifier) { } } - -@Composable -fun LogoutDialog(onClose: (Boolean) -> Unit) { - val context = LocalContext.current - - AlertDialog( - onDismissRequest = { onClose(false) }, - title = { Text(text = "Logout") }, - text = { - Text("Are you sure you want to logout?") - }, - confirmButton = { - Button(onClick = { - // Sign out google users - val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .build() - - val googleSignIn = GoogleSignIn.getClient(context, signInOptions) - googleSignIn.signOut() - onClose(true) - }) { - Text("Confirm") - } - }, - dismissButton = { - Button(onClick = { onClose(false) }) { - Text("Cancel") - } - } - ) -} - @Composable private fun RowDivider() { Divider( From e1c2d041fdeaa7882ba40c0b6930330c404573df Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Sun, 19 Mar 2023 16:04:25 -0700 Subject: [PATCH 31/48] attempt to set cookie for theme in android webview --- .../java/app/omnivore/omnivore/ui/settings/PolicyWebView.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/PolicyWebView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/PolicyWebView.kt index f51e01b40..9c990af8e 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/PolicyWebView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/PolicyWebView.kt @@ -2,6 +2,7 @@ package app.omnivore.omnivore.ui.settings import android.annotation.SuppressLint import android.view.ViewGroup +import android.webkit.CookieManager import android.webkit.WebView import android.webkit.WebViewClient import androidx.compose.foundation.isSystemInDarkTheme @@ -64,7 +65,9 @@ fun PolicyWebView(navController: NavHostController, url: String) { } } - loadUrl(url) +// val themeID = if (isDarkMode) "Gray" else "LightGray" +// loadUrl(url, mutableMapOf("Set-Cookie" to "theme=$themeID; Max-Age=31536000;")) + loadUrl(url, mutableMapOf()) } }, update = {} ) From f98b97fd2997d16942f2581e6e305c5e25f9ea61 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 20 Mar 2023 15:01:38 -0700 Subject: [PATCH 32/48] update settings cell icon --- .../main/java/app/omnivore/omnivore/ui/library/SearchBar.kt | 2 +- .../java/app/omnivore/omnivore/ui/settings/PolicyWebView.kt | 5 ++--- .../app/omnivore/omnivore/ui/settings/SettingsContent.kt | 4 +++- .../Omnivore/app/src/main/res/drawable-v24/chevron_right.xml | 1 + 4 files changed, 7 insertions(+), 5 deletions(-) create mode 100644 android/Omnivore/app/src/main/res/drawable-v24/chevron_right.xml diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt index 570d55deb..ca7ff4ff2 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt @@ -67,7 +67,7 @@ fun SearchBar( IconButton(onClick = onSettingsIconClick) { Icon( - painter = painterResource(id = R.drawable.account_outline), + imageVector = Icons.Default.Settings, contentDescription = null ) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/PolicyWebView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/PolicyWebView.kt index 9c990af8e..fa93fad06 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/PolicyWebView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/PolicyWebView.kt @@ -2,13 +2,12 @@ package app.omnivore.omnivore.ui.settings import android.annotation.SuppressLint import android.view.ViewGroup -import android.webkit.CookieManager import android.webkit.WebView import android.webkit.WebViewClient import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color @@ -27,7 +26,7 @@ fun PolicyWebView(navController: NavHostController, url: String) { actions = { IconButton(onClick = { navController.navigate(Routes.Settings.route) }) { Icon( - imageVector = Icons.Default.Home, + imageVector = Icons.Default.Settings, contentDescription = null ) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt index 1ad965582..9fd299760 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt @@ -15,8 +15,10 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController +import app.omnivore.omnivore.R import app.omnivore.omnivore.Routes import app.omnivore.omnivore.ui.auth.LoginViewModel import app.omnivore.omnivore.ui.settings.LogoutDialog @@ -156,7 +158,7 @@ private fun SettingRow(text: String, includeIcon: Boolean = true, tapAction: () if (includeIcon) { Icon( - imageVector = Icons.Filled.ArrowForward, + painter = painterResource(id = R.drawable.chevron_right), contentDescription = null ) } diff --git a/android/Omnivore/app/src/main/res/drawable-v24/chevron_right.xml b/android/Omnivore/app/src/main/res/drawable-v24/chevron_right.xml new file mode 100644 index 000000000..1272abd0f --- /dev/null +++ b/android/Omnivore/app/src/main/res/drawable-v24/chevron_right.xml @@ -0,0 +1 @@ + \ No newline at end of file From 03adc8c3d6ae66743036e21e8bf664a36a76aff9 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 20 Mar 2023 15:17:20 -0700 Subject: [PATCH 33/48] disable settings cells that are in progress --- .../omnivore/ui/settings/SettingsContent.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt index 9fd299760..d53ee724b 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt @@ -81,15 +81,15 @@ fun SettingsViewContent(loginViewModel: LoginViewModel, navController: NavHostCo // profile pic and name - SettingRow(text = "Labels") { Log.d("settings", "labels button tapped") } - RowDivider() - SettingRow(text = "Emails") { Log.d("settings", "emails button tapped") } - RowDivider() - SettingRow(text = "Subscriptions") { Log.d("settings", "subscriptions button tapped") } +// SettingRow(text = "Labels") { Log.d("settings", "labels button tapped") } +// RowDivider() +// SettingRow(text = "Emails") { Log.d("settings", "emails button tapped") } +// RowDivider() +// SettingRow(text = "Subscriptions") { Log.d("settings", "subscriptions button tapped") } // RowDivider() // SettingRow(text = "Clubs") { Log.d("settings", "clubs button tapped") } - SectionSpacer() +// SectionSpacer() // SettingRow(text = "Push Notifications") { Log.d("settings", "pn button tapped") } // RowDivider() @@ -107,8 +107,8 @@ fun SettingsViewContent(loginViewModel: LoginViewModel, navController: NavHostCo SectionSpacer() - SettingRow(text = "Manage Account") { Log.d("settings", "logout button tapped") } - RowDivider() +// SettingRow(text = "Manage Account") { Log.d("settings", "logout button tapped") } +// RowDivider() SettingRow(text = "Logout", includeIcon = false) { showLogoutDialog.value = true } RowDivider() } From 65843d3048823bb6933ee0d7e8490b514f135b7b Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 20 Mar 2023 15:27:39 -0700 Subject: [PATCH 34/48] stub in a settings viewmodel --- .../app/omnivore/omnivore/MainActivity.kt | 4 ++- .../app/omnivore/omnivore/ui/root/RootView.kt | 12 ++++++--- .../omnivore/ui/settings/ManageAccount.kt | 26 +++++++++++++++++++ .../omnivore/ui/settings/SettingsContent.kt | 5 +++- .../omnivore/ui/settings/SettingsViewModel.kt | 17 ++++++++++++ 5 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/ManageAccount.kt create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsViewModel.kt diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt index 609ec802c..b37795e62 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt @@ -16,6 +16,7 @@ import androidx.core.view.WindowInsetsCompat import app.omnivore.omnivore.ui.auth.LoginViewModel import app.omnivore.omnivore.ui.library.LibraryViewModel import app.omnivore.omnivore.ui.root.RootView +import app.omnivore.omnivore.ui.settings.SettingsViewModel import app.omnivore.omnivore.ui.theme.OmnivoreTheme import com.pspdfkit.PSPDFKit import dagger.hilt.android.AndroidEntryPoint @@ -31,6 +32,7 @@ class MainActivity : ComponentActivity() { val loginViewModel: LoginViewModel by viewModels() val libraryViewModel: LibraryViewModel by viewModels() + val settingsViewModel: SettingsViewModel by viewModels() val context = this @@ -51,7 +53,7 @@ class MainActivity : ComponentActivity() { .fillMaxSize() .background(color = Color.Black) ) { - RootView(loginViewModel, libraryViewModel) + RootView(loginViewModel, libraryViewModel, settingsViewModel) } } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt index 93dfe469e..01e5ffb26 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt @@ -19,12 +19,14 @@ import app.omnivore.omnivore.ui.auth.WelcomeScreen import app.omnivore.omnivore.ui.library.LibraryView import app.omnivore.omnivore.ui.library.LibraryViewModel import app.omnivore.omnivore.ui.settings.PolicyWebView +import app.omnivore.omnivore.ui.settings.SettingsViewModel import com.google.accompanist.systemuicontroller.rememberSystemUiController @Composable fun RootView( loginViewModel: LoginViewModel, - libraryViewModel: LibraryViewModel + libraryViewModel: LibraryViewModel, + settingsViewModel: SettingsViewModel ) { val hasAuthToken: Boolean by loginViewModel.hasAuthTokenLiveData.observeAsState(false) val systemUiController = rememberSystemUiController() @@ -46,7 +48,8 @@ fun RootView( if (hasAuthToken) { PrimaryNavigator( loginViewModel = loginViewModel, - libraryViewModel = libraryViewModel + libraryViewModel = libraryViewModel, + settingsViewModel = settingsViewModel ) } else { WelcomeScreen(viewModel = loginViewModel) @@ -64,7 +67,8 @@ fun RootView( @Composable fun PrimaryNavigator( loginViewModel: LoginViewModel, - libraryViewModel: LibraryViewModel + libraryViewModel: LibraryViewModel, + settingsViewModel: SettingsViewModel ) { val navController = rememberNavController() @@ -77,7 +81,7 @@ fun PrimaryNavigator( } composable(Routes.Settings.route) { - SettingsView(loginViewModel = loginViewModel, navController = navController) + SettingsView(loginViewModel = loginViewModel, settingsViewModel = settingsViewModel, navController = navController) } composable(Routes.Documentation.route) { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/ManageAccount.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/ManageAccount.kt new file mode 100644 index 000000000..5a221ebc9 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/ManageAccount.kt @@ -0,0 +1,26 @@ +package app.omnivore.omnivore.ui.settings + +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import app.omnivore.omnivore.ui.reader.WebPreferencesView +import app.omnivore.omnivore.ui.reader.WebReaderViewModel + +//@Composable +//fun ManageAccountDialog(onDismiss: () -> Unit, webReaderViewModel: WebReaderViewModel) { +// Dialog(onDismissRequest = { onDismiss() }) { +// Surface( +// shape = RoundedCornerShape(16.dp), +// color = Color.White, +// modifier = Modifier +// .height(300.dp) +// ) { +// WebPreferencesView(webReaderViewModel) +// } +// } +//} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt index d53ee724b..e033d9e6f 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt @@ -22,6 +22,7 @@ import app.omnivore.omnivore.R import app.omnivore.omnivore.Routes import app.omnivore.omnivore.ui.auth.LoginViewModel import app.omnivore.omnivore.ui.settings.LogoutDialog +import app.omnivore.omnivore.ui.settings.SettingsViewModel import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInOptions import io.intercom.android.sdk.Intercom @@ -31,6 +32,7 @@ import io.intercom.android.sdk.IntercomSpace @Composable fun SettingsView( loginViewModel: LoginViewModel, + settingsViewModel: SettingsViewModel, navController: NavHostController, ) { Scaffold( @@ -52,6 +54,7 @@ fun SettingsView( ) { paddingValues -> SettingsViewContent( loginViewModel = loginViewModel, + settingsViewModel = settingsViewModel, navController = navController, modifier = Modifier .padding( @@ -63,7 +66,7 @@ fun SettingsView( } @Composable -fun SettingsViewContent(loginViewModel: LoginViewModel, navController: NavHostController, modifier: Modifier) { +fun SettingsViewContent(loginViewModel: LoginViewModel, settingsViewModel: SettingsViewModel, navController: NavHostController, modifier: Modifier) { val showLogoutDialog = remember { mutableStateOf(false) } Box( diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsViewModel.kt new file mode 100644 index 000000000..f0b0b349b --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsViewModel.kt @@ -0,0 +1,17 @@ +package app.omnivore.omnivore.ui.settings + +import androidx.lifecycle.ViewModel +import app.omnivore.omnivore.DatastoreRepository +import app.omnivore.omnivore.dataService.DataService +import app.omnivore.omnivore.networking.Networker +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class SettingsViewModel @Inject constructor( + private val networker: Networker, + private val dataService: DataService, + private val datastoreRepo: DatastoreRepository +): ViewModel() { + fun stubby() {} +} From 971c4da5a856a5a2ed5597533118ada9199088bb Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 20 Mar 2023 15:46:35 -0700 Subject: [PATCH 35/48] stub in manage account dialog --- .../omnivore/ui/settings/ManageAccount.kt | 83 +++++++++++++++---- .../omnivore/ui/settings/SettingsContent.kt | 17 ++-- .../omnivore/ui/settings/SettingsViewModel.kt | 4 +- 3 files changed, 84 insertions(+), 20 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/ManageAccount.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/ManageAccount.kt index 5a221ebc9..3fe9a6752 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/ManageAccount.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/ManageAccount.kt @@ -1,26 +1,81 @@ package app.omnivore.omnivore.ui.settings -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState 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.Check +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowRight +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.Icon import androidx.compose.material3.Surface import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog +import app.omnivore.omnivore.ui.reader.Stepper +import app.omnivore.omnivore.ui.reader.WebFont import app.omnivore.omnivore.ui.reader.WebPreferencesView import app.omnivore.omnivore.ui.reader.WebReaderViewModel -//@Composable -//fun ManageAccountDialog(onDismiss: () -> Unit, webReaderViewModel: WebReaderViewModel) { -// Dialog(onDismissRequest = { onDismiss() }) { -// Surface( -// shape = RoundedCornerShape(16.dp), -// color = Color.White, -// modifier = Modifier -// .height(300.dp) -// ) { -// WebPreferencesView(webReaderViewModel) -// } -// } -//} +@Composable +fun ManageAccountDialog(onDismiss: () -> Unit, settingsViewModel: SettingsViewModel) { + Dialog(onDismissRequest = { onDismiss() }) { + Surface( + shape = RoundedCornerShape(16.dp), + color = Color.White, + modifier = Modifier + .height(300.dp) + ) { + ManageAccountView(settingsViewModel = settingsViewModel) + } + } +} + +@Composable +fun ManageAccountView(settingsViewModel: SettingsViewModel) { +// val currentWebPreferences = webReaderViewModel.storedWebPreferences(isSystemInDarkTheme()) +// val isFontListExpanded = remember { mutableStateOf(false) } +// val highContrastTextSwitchState = remember { mutableStateOf(currentWebPreferences.prefersHighContrastText) } +// val selectedWebFontRawValue = remember { mutableStateOf(currentWebPreferences.fontFamily.rawValue) } + + Column( + modifier = Modifier + .padding(top = 6.dp, start = 6.dp, end = 6.dp, bottom = 6.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp, bottom = 12.dp), + horizontalArrangement = Arrangement.Center + ) { + Text("Manage Account") + } + + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable(onClick = { settingsViewModel.resetDataCache() }) + ) { + Text("Reset Data Cache") + Spacer(modifier = Modifier.weight(1.0F)) + Icon(imageVector = Icons.Filled.Refresh, contentDescription = null) + } + } + } +} + diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt index e033d9e6f..21a0baaf8 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsContent.kt @@ -9,9 +9,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowForward import androidx.compose.material.icons.filled.Home import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -22,6 +20,7 @@ import app.omnivore.omnivore.R import app.omnivore.omnivore.Routes import app.omnivore.omnivore.ui.auth.LoginViewModel import app.omnivore.omnivore.ui.settings.LogoutDialog +import app.omnivore.omnivore.ui.settings.ManageAccountDialog import app.omnivore.omnivore.ui.settings.SettingsViewModel import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInOptions @@ -68,6 +67,7 @@ fun SettingsView( @Composable fun SettingsViewContent(loginViewModel: LoginViewModel, settingsViewModel: SettingsViewModel, navController: NavHostController, modifier: Modifier) { val showLogoutDialog = remember { mutableStateOf(false) } + val showManageAccountDialog = remember { mutableStateOf(false ) } Box( modifier = modifier.fillMaxSize() @@ -110,8 +110,8 @@ fun SettingsViewContent(loginViewModel: LoginViewModel, settingsViewModel: Setti SectionSpacer() -// SettingRow(text = "Manage Account") { Log.d("settings", "logout button tapped") } -// RowDivider() + SettingRow(text = "Manage Account") { showManageAccountDialog.value = true } + RowDivider() SettingRow(text = "Logout", includeIcon = false) { showLogoutDialog.value = true } RowDivider() } @@ -124,6 +124,13 @@ fun SettingsViewContent(loginViewModel: LoginViewModel, settingsViewModel: Setti showLogoutDialog.value = false } } + + if (showManageAccountDialog.value) { + ManageAccountDialog( + onDismiss = { showManageAccountDialog.value = false }, + settingsViewModel = settingsViewModel + ) + } } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsViewModel.kt index f0b0b349b..3204b0ea0 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsViewModel.kt @@ -13,5 +13,7 @@ class SettingsViewModel @Inject constructor( private val dataService: DataService, private val datastoreRepo: DatastoreRepository ): ViewModel() { - fun stubby() {} + fun resetDataCache() { + + } } From 1b30662b71e3410309fa8b0dd318721f647c307a Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 20 Mar 2023 16:36:41 -0700 Subject: [PATCH 36/48] add a data cache rest utton to android --- .../omnivore/ui/settings/ManageAccount.kt | 16 ---------------- .../omnivore/ui/settings/SettingsViewModel.kt | 8 +++++++- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/ManageAccount.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/ManageAccount.kt index 3fe9a6752..2d85e440e 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/ManageAccount.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/ManageAccount.kt @@ -1,32 +1,21 @@ package app.omnivore.omnivore.ui.settings import androidx.compose.foundation.clickable -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState 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.Check -import androidx.compose.material.icons.filled.KeyboardArrowDown -import androidx.compose.material.icons.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.Icon import androidx.compose.material3.Surface import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog -import app.omnivore.omnivore.ui.reader.Stepper -import app.omnivore.omnivore.ui.reader.WebFont -import app.omnivore.omnivore.ui.reader.WebPreferencesView -import app.omnivore.omnivore.ui.reader.WebReaderViewModel @Composable fun ManageAccountDialog(onDismiss: () -> Unit, settingsViewModel: SettingsViewModel) { @@ -44,11 +33,6 @@ fun ManageAccountDialog(onDismiss: () -> Unit, settingsViewModel: SettingsViewMo @Composable fun ManageAccountView(settingsViewModel: SettingsViewModel) { -// val currentWebPreferences = webReaderViewModel.storedWebPreferences(isSystemInDarkTheme()) -// val isFontListExpanded = remember { mutableStateOf(false) } -// val highContrastTextSwitchState = remember { mutableStateOf(currentWebPreferences.prefersHighContrastText) } -// val selectedWebFontRawValue = remember { mutableStateOf(currentWebPreferences.fontFamily.rawValue) } - Column( modifier = Modifier .padding(top = 6.dp, start = 6.dp, end = 6.dp, bottom = 6.dp) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsViewModel.kt index 3204b0ea0..6d071c80c 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/settings/SettingsViewModel.kt @@ -1,10 +1,13 @@ package app.omnivore.omnivore.ui.settings import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.omnivore.omnivore.DatastoreKeys import app.omnivore.omnivore.DatastoreRepository import app.omnivore.omnivore.dataService.DataService import app.omnivore.omnivore.networking.Networker import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -14,6 +17,9 @@ class SettingsViewModel @Inject constructor( private val datastoreRepo: DatastoreRepository ): ViewModel() { fun resetDataCache() { - + viewModelScope.launch { + datastoreRepo.clearValue(DatastoreKeys.libraryLastSyncTimestamp) + dataService.clearDatabase() + } } } From b5f8a07a7fdff3a6d27f14cdfc075b5e0723861c Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 20 Mar 2023 16:55:46 -0700 Subject: [PATCH 37/48] check if label query list is not empty efore appending label to query string --- android/Omnivore/app/build.gradle | 4 ++-- .../java/app/omnivore/omnivore/dataService/LibrarySync.kt | 4 ++++ .../app/omnivore/omnivore/ui/library/LibraryViewModel.kt | 6 ++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/android/Omnivore/app/build.gradle b/android/Omnivore/app/build.gradle index ecfebee7f..d6eb30ece 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 28 - versionName "0.0.28" + versionCode 29 + versionName "0.0.29" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt index c312f0c64..b677c484f 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt @@ -28,6 +28,8 @@ suspend fun DataService.librarySearch(cursor: String?, query: String): SavedItem db.savedItemLabelDao().insertAll(labels) db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs) + Log.d("sync", "found ${searchResult.items.size} items with search api. Query: $query") + return SavedItemSyncResult( hasError = false, hasMoreItems = false, @@ -93,6 +95,8 @@ suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): S db.savedItemLabelDao().insertAll(labels) db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs) + Log.d("sync", "found ${syncResult.items.size} items with sync api. Since: $since") + return SavedItemSyncResult( hasError = false, hasMoreItems = syncResult.hasMoreItems, 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 6e078be31..9dd990619 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 @@ -333,8 +333,10 @@ class LibraryViewModel @Inject constructor( } activeLabelsLiveData.value?.let { - query += " label:" - query += it.joinToString { label -> label.name } + if (it.isNotEmpty()) { + query += " label:" + query += it.joinToString { label -> label.name } + } } return query From eaf46057f4cbf55d43aaa6dab90d506b84615248 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 23 Mar 2023 13:10:06 -0700 Subject: [PATCH 38/48] open reader view when label chips are tapped --- android/Omnivore/app/build.gradle | 4 ++-- .../omnivore/omnivore/ui/library/LibraryView.kt | 8 -------- .../omnivore/ui/savedItemViews/SavedItemCard.kt | 14 ++++++++------ 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/android/Omnivore/app/build.gradle b/android/Omnivore/app/build.gradle index d6eb30ece..562721d48 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 29 - versionName "0.0.29" + versionCode 30 + versionName "0.0.30" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { 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 a14260f6d..70804fc19 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 @@ -3,16 +3,12 @@ package app.omnivore.omnivore.ui.library import android.content.Intent import android.util.Log 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.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.icons.Icons -import androidx.compose.material.icons.filled.Check import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState @@ -22,14 +18,10 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog import androidx.navigation.NavHostController import app.omnivore.omnivore.Routes import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels -import app.omnivore.omnivore.persistence.entities.SavedItemLabel -import app.omnivore.omnivore.ui.components.LabelChipColors import app.omnivore.omnivore.ui.components.LabelsSelectionSheet import app.omnivore.omnivore.ui.savedItemViews.SavedItemCard import app.omnivore.omnivore.ui.reader.PDFReaderActivity 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 bd81e3ef9..ee98ec1f2 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 @@ -29,17 +29,19 @@ fun SavedItemCard(cardData: SavedItemCardData, labels: List, onC val publisherDisplayName = cardData.publisherDisplayName() val listState = rememberLazyListState() - Column { + Column( + modifier = Modifier + .combinedClickable( + onClick = onClickHandler, + onLongClick = { isMenuExpanded = true } + ) + ) { Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.Top, modifier = Modifier .fillMaxWidth() .padding(12.dp) - .combinedClickable( - onClick = onClickHandler, - onLongClick = { isMenuExpanded = true } - ) .background(if (isMenuExpanded) Color.LightGray else Color.Transparent) ) { Column( @@ -96,7 +98,7 @@ fun SavedItemCard(cardData: SavedItemCardData, labels: List, onC val chipColors = LabelChipColors.fromHex(label.color) SuggestionChip( - onClick = {}, + onClick = onClickHandler, label = { Text(label.name) }, border = null, colors = elevatedSuggestionChipColors( From af142b190bcf2caa0da2e4253a97f90b4e0921f8 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 23 Mar 2023 13:34:33 -0700 Subject: [PATCH 39/48] reset cursor when initial load is called (happens after rest data cache action) --- .../app/omnivore/omnivore/ui/library/LibraryViewModel.kt | 8 ++++++++ 1 file changed, 8 insertions(+) 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 9dd990619..4f73811c8 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 @@ -110,6 +110,14 @@ class LibraryViewModel @Inject constructor( } fun initialLoad() { + if (getLastSyncTime() == null) { + hasLoadedInitialFilters = false + cursor = null + librarySearchCursor = null + searchIdx = 0 + receivedIdx = 0 + } + if (hasLoadedInitialFilters) { return } load() } From 1ee83041fbbb35d30e425ba4f226bd5b9a23f64d Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 23 Mar 2023 16:54:04 -0700 Subject: [PATCH 40/48] load from db before fetching from server --- .../app/omnivore/omnivore/dataService/LibrarySync.kt | 11 ++++++++--- .../omnivore/omnivore/ui/library/LibraryViewModel.kt | 8 +++++--- .../omnivore/omnivore/ui/reader/WebReaderViewModel.kt | 4 ++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt index b677c484f..a6afb3c24 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt @@ -106,7 +106,14 @@ suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): S ) } -suspend fun DataService.syncSavedItemContent(slug: String) { +suspend fun DataService.syncSavedItemContent(slug: String, skipIfStoredInDB: Boolean) { + if (skipIfStoredInDB) { + val existingItem = db.savedItemDao().getSavedItemWithLabelsAndHighlights(slug) + if (existingItem?.savedItem?.content != null) { + return + } + } + val syncResult = networker.savedItem(slug) val savedItem = syncResult.item ?: return @@ -129,8 +136,6 @@ suspend fun DataService.syncSavedItemContent(slug: String) { } db.savedItemAndHighlightCrossRefDao().insertAll(highlightCrossRefs) - - Log.d("sync", "saved content for item with id: ${savedItem.savedItemId}") } data class SavedItemSyncResult( 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 4f73811c8..063dfcfaf 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 @@ -145,6 +145,10 @@ class LibraryViewModel @Inject constructor( CoroutineScope(Dispatchers.Main).launch { isRefreshing = false } + + result.savedItemSlugs.map { + dataService.syncSavedItemContent(it, skipIfStoredInDB = true) + } } } } @@ -201,14 +205,12 @@ class LibraryViewModel @Inject constructor( // Fetch content for the initial batch only if (isInitialBatch) { for (slug in result.savedItemSlugs) { - dataService.syncSavedItemContent(slug) + dataService.syncSavedItemContent(slug, skipIfStoredInDB = false) } } val totalCount = count + result.count - Log.d("sync", "fetched ${result.count} items") - if (!result.hasError && result.hasMoreItems && result.cursor != null) { performItemSync( cursor = result.cursor, 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 9962f1b77..3e9b3af76 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 @@ -81,14 +81,14 @@ class WebReaderViewModel @Inject constructor( } private suspend fun loadItemUsingSlug(slug: String) { + loadItemFromDB(slug) + val webReaderParams = loadItemFromServer(slug) if (webReaderParams != null) { Log.d("reader", "data loaded from server") webReaderParamsLiveData.postValue(webReaderParams) isLoading = false - } else { - loadItemFromDB(slug) } } From e7143a5081d1e8c860a9396aca8082607237c49a Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 28 Mar 2023 13:17:19 -0700 Subject: [PATCH 41/48] throttle content fetches to avoid 429 errors --- .../omnivore/dataService/LibrarySync.kt | 15 ++++++++------- .../omnivore/ui/library/LibraryViewModel.kt | 18 ++++++++++++++++-- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt index a6afb3c24..43fffe011 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt @@ -106,15 +106,15 @@ suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): S ) } -suspend fun DataService.syncSavedItemContent(slug: String, skipIfStoredInDB: Boolean) { - if (skipIfStoredInDB) { - val existingItem = db.savedItemDao().getSavedItemWithLabelsAndHighlights(slug) - if (existingItem?.savedItem?.content != null) { - return - } - } +fun DataService.isSavedItemContentStoredInDB(slug: String): Boolean { + val existingItem = db.savedItemDao().getSavedItemWithLabelsAndHighlights(slug) + val content = existingItem?.savedItem?.content ?: "" + return content.length > 10 +} +suspend fun DataService.fetchSavedItemContent(slug: String) { val syncResult = networker.savedItem(slug) + val isSuccess = syncResult.item != null val savedItem = syncResult.item ?: return db.savedItemDao().insert(savedItem) @@ -138,6 +138,7 @@ suspend fun DataService.syncSavedItemContent(slug: String, skipIfStoredInDB: Boo db.savedItemAndHighlightCrossRefDao().insertAll(highlightCrossRefs) } + data class SavedItemSyncResult( val hasError: Boolean, val hasMoreItems: Boolean, 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 063dfcfaf..012615283 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 @@ -16,6 +16,7 @@ import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.persistence.entities.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel import java.time.Instant import javax.inject.Inject @@ -25,6 +26,8 @@ class LibraryViewModel @Inject constructor( private val dataService: DataService, private val datastoreRepo: DatastoreRepository ): ViewModel() { + private val contentRequestChannel = Channel(capacity = Channel.UNLIMITED) + private var cursor: String? = null private var librarySearchCursor: String? = null @@ -81,6 +84,11 @@ class LibraryViewModel @Inject constructor( viewModelScope.launch { handleFilterChanges() + for (slug in contentRequestChannel) { + CoroutineScope(Dispatchers.IO).launch { + dataService.fetchSavedItemContent(slug) + } + } } } @@ -147,7 +155,12 @@ class LibraryViewModel @Inject constructor( } result.savedItemSlugs.map { - dataService.syncSavedItemContent(it, skipIfStoredInDB = true) + val isSavedInDB = dataService.isSavedItemContentStoredInDB(it) + + if (!isSavedInDB) { + delay(2000) + contentRequestChannel.send(it) + } } } } @@ -205,7 +218,8 @@ class LibraryViewModel @Inject constructor( // Fetch content for the initial batch only if (isInitialBatch) { for (slug in result.savedItemSlugs) { - dataService.syncSavedItemContent(slug, skipIfStoredInDB = false) + delay(250) + contentRequestChannel.send(slug) } } From 889511e28280a33630e356d7db44dd5c9a9c04c2 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 28 Mar 2023 13:21:49 -0700 Subject: [PATCH 42/48] bump build to 31 --- 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 562721d48..16b61c7b8 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 30 - versionName "0.0.30" + versionCode 31 + versionName "0.0.31" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 222dbd945e33a400835b2140a699a7487eb94e3f Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 4 Apr 2023 11:40:47 -0700 Subject: [PATCH 43/48] bump android build number to 32 --- 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 16b61c7b8..e77030675 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 31 - versionName "0.0.31" + versionCode 32 + versionName "0.0.32" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From cbb3221ea31a52b736501ca9420d5f9e4f91c891 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 4 Apr 2023 12:44:25 -0700 Subject: [PATCH 44/48] add segmented control for theme preference --- .../ui/components/SegmentedControl.kt | 146 ++++++++++++++++++ .../ui/reader/WebPreferencesDialog.kt | 15 +- 2 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/SegmentedControl.kt diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/SegmentedControl.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/SegmentedControl.kt new file mode 100644 index 000000000..dc5b46716 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/SegmentedControl.kt @@ -0,0 +1,146 @@ +package app.omnivore.omnivore.ui.components + +import androidx.annotation.ColorRes +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.OutlinedButton +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex + +// https://gist.githubusercontent.com/manojbhadane/afe14d552a520bca83f80eef22dacade/raw/1427fc4d0e3ccbc30484976fc1c1424c2fa613f3/jc-sc-1.kt + +/** + * items : list of items to be render + * defaultSelectedItemIndex : to highlight item by default (Optional) + * useFixedWidth : set true if you want to set fix width to item (Optional) + * itemWidth : Provide item width if useFixedWidth is set to true (Optional) + * cornerRadius : To make control as rounded (Optional) + * color : Set color to control (Optional) + * onItemSelection : Get selected item index + */ + +@Composable +fun SegmentedControl( + items: List, + defaultSelectedItemIndex: Int = 0, + useFixedWidth: Boolean = false, + itemWidth: Dp = 120.dp, + cornerRadius : Int = 10, + @ColorRes color : Int = com.pspdfkit.R.color.material_deep_teal_200, + onItemSelection: (selectedItemIndex: Int) -> Unit +) { + val selectedIndex = remember { mutableStateOf(defaultSelectedItemIndex) } + + Row( + modifier = Modifier + ) { + items.forEachIndexed { index, item -> + OutlinedButton( + modifier = when (index) { + 0 -> { + if (useFixedWidth) { + Modifier + .width(itemWidth) + .offset(0.dp, 0.dp) + .zIndex(if (selectedIndex.value == index) 1f else 0f) + } else { + Modifier + .wrapContentSize() + .offset(0.dp, 0.dp) + .zIndex(if (selectedIndex.value == index) 1f else 0f) + } + } else -> { + if (useFixedWidth) + Modifier + .width(itemWidth) + .offset((-1 * index).dp, 0.dp) + .zIndex(if (selectedIndex.value == index) 1f else 0f) + else Modifier + .wrapContentSize() + .offset((-1 * index).dp, 0.dp) + .zIndex(if (selectedIndex.value == index) 1f else 0f) + } + }, + onClick = { + selectedIndex.value = index + onItemSelection(selectedIndex.value) + }, + shape = when (index) { + /** + * left outer button + */ + 0 -> RoundedCornerShape( + topStartPercent = cornerRadius, + topEndPercent = 0, + bottomStartPercent = cornerRadius, + bottomEndPercent = 0 + ) + /** + * right outer button + */ + items.size - 1 -> RoundedCornerShape( + topStartPercent = 0, + topEndPercent = cornerRadius, + bottomStartPercent = 0, + bottomEndPercent = cornerRadius + ) + /** + * middle button + */ + else -> RoundedCornerShape( + topStartPercent = 0, + topEndPercent = 0, + bottomStartPercent = 0, + bottomEndPercent = 0 + ) + }, + border = BorderStroke( + 1.dp, if (selectedIndex.value == index) { + colorResource(id = color) + } else { + colorResource(id = color).copy(alpha = 0.75f) + } + ), + colors = if (selectedIndex.value == index) { + /** + * selected colors + */ + ButtonDefaults.outlinedButtonColors( + backgroundColor = colorResource( + id = color + ) + ) + } else { + /** + * not selected colors + */ + ButtonDefaults.outlinedButtonColors(backgroundColor = Color.Transparent) + }, + ) { + Text( + text = item, + fontWeight = FontWeight.Normal, + color = if (selectedIndex.value == index) { + Color.White + } else { + colorResource(id = color).copy(alpha = 0.9f) + }, + ) + } + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt index 1b6600a33..ed0527f6f 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt @@ -1,5 +1,6 @@ package app.omnivore.omnivore.ui.reader +import android.util.Log import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* @@ -26,6 +27,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import app.omnivore.omnivore.R +import app.omnivore.omnivore.ui.components.SegmentedControl @Composable fun WebPreferencesDialog(onDismiss: () -> Unit, webReaderViewModel: WebReaderViewModel) { @@ -34,7 +36,7 @@ fun WebPreferencesDialog(onDismiss: () -> Unit, webReaderViewModel: WebReaderVie shape = RoundedCornerShape(16.dp), color = Color.White, modifier = Modifier - .height(300.dp) + .height(350.dp) ) { WebPreferencesView(webReaderViewModel) } @@ -95,6 +97,17 @@ fun WebPreferencesView(webReaderViewModel: WebReaderViewModel) { ) } + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Theme:") + Spacer(modifier = Modifier.weight(1.0F)) + SegmentedControl( + items = listOf("Light", "Dark", "System"), + defaultSelectedItemIndex = 0 + ) { + Log.e("CustomToggle", "Selected item: $it") + } + } + Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier From f647665a04c1899989e10a9598a55a873f6332a8 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 4 Apr 2023 13:17:37 -0700 Subject: [PATCH 45/48] adjust segmented control colors --- .../ui/components/SegmentedControl.kt | 65 ++++--------------- .../ui/reader/WebPreferencesDialog.kt | 6 +- 2 files changed, 16 insertions(+), 55 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/SegmentedControl.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/SegmentedControl.kt index dc5b46716..1d52780dd 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/SegmentedControl.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/SegmentedControl.kt @@ -4,10 +4,10 @@ import androidx.annotation.ColorRes import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedButton import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -17,30 +17,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.colorResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex // https://gist.githubusercontent.com/manojbhadane/afe14d552a520bca83f80eef22dacade/raw/1427fc4d0e3ccbc30484976fc1c1424c2fa613f3/jc-sc-1.kt -/** - * items : list of items to be render - * defaultSelectedItemIndex : to highlight item by default (Optional) - * useFixedWidth : set true if you want to set fix width to item (Optional) - * itemWidth : Provide item width if useFixedWidth is set to true (Optional) - * cornerRadius : To make control as rounded (Optional) - * color : Set color to control (Optional) - * onItemSelection : Get selected item index - */ - @Composable fun SegmentedControl( items: List, defaultSelectedItemIndex: Int = 0, - useFixedWidth: Boolean = false, - itemWidth: Dp = 120.dp, cornerRadius : Int = 10, - @ColorRes color : Int = com.pspdfkit.R.color.material_deep_teal_200, onItemSelection: (selectedItemIndex: Int) -> Unit ) { val selectedIndex = remember { mutableStateOf(defaultSelectedItemIndex) } @@ -50,31 +36,10 @@ fun SegmentedControl( ) { items.forEachIndexed { index, item -> OutlinedButton( - modifier = when (index) { - 0 -> { - if (useFixedWidth) { - Modifier - .width(itemWidth) - .offset(0.dp, 0.dp) - .zIndex(if (selectedIndex.value == index) 1f else 0f) - } else { - Modifier - .wrapContentSize() - .offset(0.dp, 0.dp) - .zIndex(if (selectedIndex.value == index) 1f else 0f) - } - } else -> { - if (useFixedWidth) - Modifier - .width(itemWidth) - .offset((-1 * index).dp, 0.dp) - .zIndex(if (selectedIndex.value == index) 1f else 0f) - else Modifier - .wrapContentSize() - .offset((-1 * index).dp, 0.dp) - .zIndex(if (selectedIndex.value == index) 1f else 0f) - } - }, + modifier = Modifier + .wrapContentSize() + .offset((-1 * index).dp, 0.dp) + .zIndex(if (selectedIndex.value == index) 1f else 0f), onClick = { selectedIndex.value = index onItemSelection(selectedIndex.value) @@ -110,34 +75,26 @@ fun SegmentedControl( }, border = BorderStroke( 1.dp, if (selectedIndex.value == index) { - colorResource(id = color) + MaterialTheme.colors.secondary.copy(alpha = 0.75f) } else { - colorResource(id = color).copy(alpha = 0.75f) + MaterialTheme.colors.secondary } ), colors = if (selectedIndex.value == index) { - /** - * selected colors - */ ButtonDefaults.outlinedButtonColors( - backgroundColor = colorResource( - id = color - ) + backgroundColor = MaterialTheme.colors.secondary ) } else { - /** - * not selected colors - */ - ButtonDefaults.outlinedButtonColors(backgroundColor = Color.Transparent) + ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.background) }, ) { Text( text = item, fontWeight = FontWeight.Normal, color = if (selectedIndex.value == index) { - Color.White + MaterialTheme.colors.onSecondary } else { - colorResource(id = color).copy(alpha = 0.9f) + MaterialTheme.colors.onBackground }, ) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt index ed0527f6f..d68b80018 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebPreferencesDialog.kt @@ -97,7 +97,11 @@ fun WebPreferencesView(webReaderViewModel: WebReaderViewModel) { ) } - Row(verticalAlignment = Alignment.CenterVertically) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(vertical = 4.dp) + ) { Text("Theme:") Spacer(modifier = Modifier.weight(1.0F)) SegmentedControl( From 3399741b823827e9f140cc45a8b6e60fc152169c Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 6 Apr 2023 14:44:48 -0700 Subject: [PATCH 46/48] regenerate android web bundle --- android/Omnivore/app/src/main/assets/bundle.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/android/Omnivore/app/src/main/assets/bundle.js b/android/Omnivore/app/src/main/assets/bundle.js index a00fb68c5..4c1ef1b7e 100644 --- a/android/Omnivore/app/src/main/assets/bundle.js +++ b/android/Omnivore/app/src/main/assets/bundle.js @@ -1,6 +1,2 @@ /*! For license information please see bundle.js.LICENSE.txt */ -<<<<<<< HEAD -(()=>{var e,t,n={7162:(e,t,n)=>{e.exports=n(5047)},6279:function(e,t){var n="undefined"!=typeof self?self:this,r=function(){function e(){this.fetch=!1,this.DOMException=n.DOMException}return e.prototype=n,new e}();!function(e){!function(t){var n="URLSearchParams"in e,r="Symbol"in e&&"iterator"in Symbol,o="FileReader"in e&&"Blob"in e&&function(){try{return new Blob,!0}catch(e){return!1}}(),i="FormData"in e,a="ArrayBuffer"in e;if(a)var l=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],s=ArrayBuffer.isView||function(e){return e&&l.indexOf(Object.prototype.toString.call(e))>-1};function u(e){if("string"!=typeof e&&(e=String(e)),/[^a-z0-9\-#$%&'*+.^_`|~]/i.test(e))throw new TypeError("Invalid character in header field name");return e.toLowerCase()}function c(e){return"string"!=typeof e&&(e=String(e)),e}function f(e){var t={next:function(){var t=e.shift();return{done:void 0===t,value:t}}};return r&&(t[Symbol.iterator]=function(){return t}),t}function d(e){this.map={},e instanceof d?e.forEach((function(e,t){this.append(t,e)}),this):Array.isArray(e)?e.forEach((function(e){this.append(e[0],e[1])}),this):e&&Object.getOwnPropertyNames(e).forEach((function(t){this.append(t,e[t])}),this)}function p(e){if(e.bodyUsed)return Promise.reject(new TypeError("Already read"));e.bodyUsed=!0}function h(e){return new Promise((function(t,n){e.onload=function(){t(e.result)},e.onerror=function(){n(e.error)}}))}function g(e){var t=new FileReader,n=h(t);return t.readAsArrayBuffer(e),n}function m(e){if(e.slice)return e.slice(0);var t=new Uint8Array(e.byteLength);return t.set(new Uint8Array(e)),t.buffer}function v(){return this.bodyUsed=!1,this._initBody=function(e){var t;this._bodyInit=e,e?"string"==typeof e?this._bodyText=e:o&&Blob.prototype.isPrototypeOf(e)?this._bodyBlob=e:i&&FormData.prototype.isPrototypeOf(e)?this._bodyFormData=e:n&&URLSearchParams.prototype.isPrototypeOf(e)?this._bodyText=e.toString():a&&o&&(t=e)&&DataView.prototype.isPrototypeOf(t)?(this._bodyArrayBuffer=m(e.buffer),this._bodyInit=new Blob([this._bodyArrayBuffer])):a&&(ArrayBuffer.prototype.isPrototypeOf(e)||s(e))?this._bodyArrayBuffer=m(e):this._bodyText=e=Object.prototype.toString.call(e):this._bodyText="",this.headers.get("content-type")||("string"==typeof e?this.headers.set("content-type","text/plain;charset=UTF-8"):this._bodyBlob&&this._bodyBlob.type?this.headers.set("content-type",this._bodyBlob.type):n&&URLSearchParams.prototype.isPrototypeOf(e)&&this.headers.set("content-type","application/x-www-form-urlencoded;charset=UTF-8"))},o&&(this.blob=function(){var e=p(this);if(e)return e;if(this._bodyBlob)return Promise.resolve(this._bodyBlob);if(this._bodyArrayBuffer)return Promise.resolve(new Blob([this._bodyArrayBuffer]));if(this._bodyFormData)throw new Error("could not read FormData body as blob");return Promise.resolve(new Blob([this._bodyText]))},this.arrayBuffer=function(){return this._bodyArrayBuffer?p(this)||Promise.resolve(this._bodyArrayBuffer):this.blob().then(g)}),this.text=function(){var e,t,n,r=p(this);if(r)return r;if(this._bodyBlob)return e=this._bodyBlob,n=h(t=new FileReader),t.readAsText(e),n;if(this._bodyArrayBuffer)return Promise.resolve(function(e){for(var t=new Uint8Array(e),n=new Array(t.length),r=0;r-1?r:n),this.mode=t.mode||this.mode||null,this.signal=t.signal||this.signal,this.referrer=null,("GET"===this.method||"HEAD"===this.method)&&o)throw new TypeError("Body not allowed for GET or HEAD requests");this._initBody(o)}function w(e){var t=new FormData;return e.trim().split("&").forEach((function(e){if(e){var n=e.split("="),r=n.shift().replace(/\+/g," "),o=n.join("=").replace(/\+/g," ");t.append(decodeURIComponent(r),decodeURIComponent(o))}})),t}function x(e,t){t||(t={}),this.type="default",this.status=void 0===t.status?200:t.status,this.ok=this.status>=200&&this.status<300,this.statusText="statusText"in t?t.statusText:"OK",this.headers=new d(t.headers),this.url=t.url||"",this._initBody(e)}b.prototype.clone=function(){return new b(this,{body:this._bodyInit})},v.call(b.prototype),v.call(x.prototype),x.prototype.clone=function(){return new x(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new d(this.headers),url:this.url})},x.error=function(){var e=new x(null,{status:0,statusText:""});return e.type="error",e};var E=[301,302,303,307,308];x.redirect=function(e,t){if(-1===E.indexOf(t))throw new RangeError("Invalid status code");return new x(null,{status:t,headers:{location:e}})},t.DOMException=e.DOMException;try{new t.DOMException}catch(e){t.DOMException=function(e,t){this.message=e,this.name=t;var n=Error(e);this.stack=n.stack},t.DOMException.prototype=Object.create(Error.prototype),t.DOMException.prototype.constructor=t.DOMException}function k(e,n){return new Promise((function(r,i){var a=new b(e,n);if(a.signal&&a.signal.aborted)return i(new t.DOMException("Aborted","AbortError"));var l=new XMLHttpRequest;function s(){l.abort()}l.onload=function(){var e,t,n={status:l.status,statusText:l.statusText,headers:(e=l.getAllResponseHeaders()||"",t=new d,e.replace(/\r?\n[\t ]+/g," ").split(/\r?\n/).forEach((function(e){var n=e.split(":"),r=n.shift().trim();if(r){var o=n.join(":").trim();t.append(r,o)}})),t)};n.url="responseURL"in l?l.responseURL:n.headers.get("X-Request-URL");var o="response"in l?l.response:l.responseText;r(new x(o,n))},l.onerror=function(){i(new TypeError("Network request failed"))},l.ontimeout=function(){i(new TypeError("Network request failed"))},l.onabort=function(){i(new t.DOMException("Aborted","AbortError"))},l.open(a.method,a.url,!0),"include"===a.credentials?l.withCredentials=!0:"omit"===a.credentials&&(l.withCredentials=!1),"responseType"in l&&o&&(l.responseType="blob"),a.headers.forEach((function(e,t){l.setRequestHeader(t,e)})),a.signal&&(a.signal.addEventListener("abort",s),l.onreadystatechange=function(){4===l.readyState&&a.signal.removeEventListener("abort",s)}),l.send(void 0===a._bodyInit?null:a._bodyInit)}))}k.polyfill=!0,e.fetch||(e.fetch=k,e.Headers=d,e.Request=b,e.Response=x),t.Headers=d,t.Request=b,t.Response=x,t.fetch=k,Object.defineProperty(t,"__esModule",{value:!0})}({})}(r),r.fetch.ponyfill=!0,delete r.fetch.polyfill;var o=r;(t=o.fetch).default=o.fetch,t.fetch=o.fetch,t.Headers=o.Headers,t.Request=o.Request,t.Response=o.Response,e.exports=t},1427:e=>{var t=function(){this.Diff_Timeout=1,this.Diff_EditCost=4,this.Match_Threshold=.5,this.Match_Distance=1e3,this.Patch_DeleteThreshold=.5,this.Patch_Margin=4,this.Match_MaxBits=32},n=-1;t.Diff=function(e,t){return[e,t]},t.prototype.diff_main=function(e,n,r,o){void 0===o&&(o=this.Diff_Timeout<=0?Number.MAX_VALUE:(new Date).getTime()+1e3*this.Diff_Timeout);var i=o;if(null==e||null==n)throw new Error("Null input. (diff_main)");if(e==n)return e?[new t.Diff(0,e)]:[];void 0===r&&(r=!0);var a=r,l=this.diff_commonPrefix(e,n),s=e.substring(0,l);e=e.substring(l),n=n.substring(l),l=this.diff_commonSuffix(e,n);var u=e.substring(e.length-l);e=e.substring(0,e.length-l),n=n.substring(0,n.length-l);var c=this.diff_compute_(e,n,a,i);return s&&c.unshift(new t.Diff(0,s)),u&&c.push(new t.Diff(0,u)),this.diff_cleanupMerge(c),c},t.prototype.diff_compute_=function(e,r,o,i){var a;if(!e)return[new t.Diff(1,r)];if(!r)return[new t.Diff(n,e)];var l=e.length>r.length?e:r,s=e.length>r.length?r:e,u=l.indexOf(s);if(-1!=u)return a=[new t.Diff(1,l.substring(0,u)),new t.Diff(0,s),new t.Diff(1,l.substring(u+s.length))],e.length>r.length&&(a[0][0]=a[2][0]=n),a;if(1==s.length)return[new t.Diff(n,e),new t.Diff(1,r)];var c=this.diff_halfMatch_(e,r);if(c){var f=c[0],d=c[1],p=c[2],h=c[3],g=c[4],m=this.diff_main(f,p,o,i),v=this.diff_main(d,h,o,i);return m.concat([new t.Diff(0,g)],v)}return o&&e.length>100&&r.length>100?this.diff_lineMode_(e,r,i):this.diff_bisect_(e,r,i)},t.prototype.diff_lineMode_=function(e,r,o){var i=this.diff_linesToChars_(e,r);e=i.chars1,r=i.chars2;var a=i.lineArray,l=this.diff_main(e,r,!1,o);this.diff_charsToLines_(l,a),this.diff_cleanupSemantic(l),l.push(new t.Diff(0,""));for(var s=0,u=0,c=0,f="",d="";s=1&&c>=1){l.splice(s-u-c,u+c),s=s-u-c;for(var p=this.diff_main(f,d,!1,o),h=p.length-1;h>=0;h--)l.splice(s,0,p[h]);s+=p.length}c=0,u=0,f="",d=""}s++}return l.pop(),l},t.prototype.diff_bisect_=function(e,r,o){for(var i=e.length,a=r.length,l=Math.ceil((i+a)/2),s=l,u=2*l,c=new Array(u),f=new Array(u),d=0;do);b++){for(var w=-b+g;w<=b-m;w+=2){for(var x=s+w,E=(O=w==-b||w!=b&&c[x-1]i)m+=2;else if(E>a)g+=2;else if(h&&(_=s+p-w)>=0&&_=(S=i-f[_]))return this.diff_bisectSplit_(e,r,O,E,o)}for(var k=-b+v;k<=b-y;k+=2){for(var S,_=s+k,C=(S=k==-b||k!=b&&f[_-1]i)y+=2;else if(C>a)v+=2;else if(!h){var O;if((x=s+p-k)>=0&&x=(S=i-S))return this.diff_bisectSplit_(e,r,O,E,o)}}}return[new t.Diff(n,e),new t.Diff(1,r)]},t.prototype.diff_bisectSplit_=function(e,t,n,r,o){var i=e.substring(0,n),a=t.substring(0,r),l=e.substring(n),s=t.substring(r),u=this.diff_main(i,a,!1,o),c=this.diff_main(l,s,!1,o);return u.concat(c)},t.prototype.diff_linesToChars_=function(e,t){var n=[],r={};function o(e){for(var t="",o=0,a=-1,l=n.length;ar?e=e.substring(n-r):nt.length?e:t,r=e.length>t.length?t:e;if(n.length<4||2*r.length=e.length?[r,i,a,l,c]:null}var a,l,s,u,c,f=i(n,r,Math.ceil(n.length/4)),d=i(n,r,Math.ceil(n.length/2));return f||d?(a=d?f&&f[4].length>d[4].length?f:d:f,e.length>t.length?(l=a[0],s=a[1],u=a[2],c=a[3]):(u=a[0],c=a[1],l=a[2],s=a[3]),[l,s,u,c,a[4]]):null},t.prototype.diff_cleanupSemantic=function(e){for(var r=!1,o=[],i=0,a=null,l=0,s=0,u=0,c=0,f=0;l0?o[i-1]:-1,s=0,u=0,c=0,f=0,a=null,r=!0)),l++;for(r&&this.diff_cleanupMerge(e),this.diff_cleanupSemanticLossless(e),l=1;l=g?(h>=d.length/2||h>=p.length/2)&&(e.splice(l,0,new t.Diff(0,p.substring(0,h))),e[l-1][1]=d.substring(0,d.length-h),e[l+1][1]=p.substring(h),l++):(g>=d.length/2||g>=p.length/2)&&(e.splice(l,0,new t.Diff(0,d.substring(0,g))),e[l-1][0]=1,e[l-1][1]=p.substring(0,p.length-g),e[l+1][0]=n,e[l+1][1]=d.substring(g),l++),l++}l++}},t.prototype.diff_cleanupSemanticLossless=function(e){function n(e,n){if(!e||!n)return 6;var r=e.charAt(e.length-1),o=n.charAt(0),i=r.match(t.nonAlphaNumericRegex_),a=o.match(t.nonAlphaNumericRegex_),l=i&&r.match(t.whitespaceRegex_),s=a&&o.match(t.whitespaceRegex_),u=l&&r.match(t.linebreakRegex_),c=s&&o.match(t.linebreakRegex_),f=u&&e.match(t.blanklineEndRegex_),d=c&&n.match(t.blanklineStartRegex_);return f||d?5:u||c?4:i&&!l&&s?3:l||s?2:i||a?1:0}for(var r=1;r=d&&(d=p,u=o,c=i,f=a)}e[r-1][1]!=u&&(u?e[r-1][1]=u:(e.splice(r-1,1),r--),e[r][1]=c,f?e[r+1][1]=f:(e.splice(r+1,1),r--))}r++}},t.nonAlphaNumericRegex_=/[^a-zA-Z0-9]/,t.whitespaceRegex_=/\s/,t.linebreakRegex_=/[\r\n]/,t.blanklineEndRegex_=/\n\r?\n$/,t.blanklineStartRegex_=/^\r?\n\r?\n/,t.prototype.diff_cleanupEfficiency=function(e){for(var r=!1,o=[],i=0,a=null,l=0,s=!1,u=!1,c=!1,f=!1;l0?o[i-1]:-1,c=f=!1),r=!0)),l++;r&&this.diff_cleanupMerge(e)},t.prototype.diff_cleanupMerge=function(e){e.push(new t.Diff(0,""));for(var r,o=0,i=0,a=0,l="",s="";o1?(0!==i&&0!==a&&(0!==(r=this.diff_commonPrefix(s,l))&&(o-i-a>0&&0==e[o-i-a-1][0]?e[o-i-a-1][1]+=s.substring(0,r):(e.splice(0,0,new t.Diff(0,s.substring(0,r))),o++),s=s.substring(r),l=l.substring(r)),0!==(r=this.diff_commonSuffix(s,l))&&(e[o][1]=s.substring(s.length-r)+e[o][1],s=s.substring(0,s.length-r),l=l.substring(0,l.length-r))),o-=i+a,e.splice(o,i+a),l.length&&(e.splice(o,0,new t.Diff(n,l)),o++),s.length&&(e.splice(o,0,new t.Diff(1,s)),o++),o++):0!==o&&0==e[o-1][0]?(e[o-1][1]+=e[o][1],e.splice(o,1)):o++,a=0,i=0,l="",s=""}""===e[e.length-1][1]&&e.pop();var u=!1;for(o=1;ot));r++)a=o,l=i;return e.length!=r&&e[r][0]===n?l:l+(t-a)},t.prototype.diff_prettyHtml=function(e){for(var t=[],r=/&/g,o=//g,a=/\n/g,l=0;l");switch(s){case 1:t[l]=''+u+"";break;case n:t[l]=''+u+"";break;case 0:t[l]=""+u+""}}return t.join("")},t.prototype.diff_text1=function(e){for(var t=[],n=0;nthis.Match_MaxBits)throw new Error("Pattern too long for this browser.");var r=this.match_alphabet_(t),o=this;function i(e,r){var i=e/t.length,a=Math.abs(n-r);return o.Match_Distance?i+a/o.Match_Distance:a?1:i}var a=this.Match_Threshold,l=e.indexOf(t,n);-1!=l&&(a=Math.min(i(0,l),a),-1!=(l=e.lastIndexOf(t,n+t.length))&&(a=Math.min(i(0,l),a)));var s,u,c=1<=h;v--){var y=r[e.charAt(v-1)];if(m[v]=0===p?(m[v+1]<<1|1)&y:(m[v+1]<<1|1)&y|(f[v+1]|f[v])<<1|1|f[v+1],m[v]&c){var b=i(p,v-1);if(b<=a){if(a=b,!((l=v-1)>n))break;h=Math.max(1,2*n-l)}}}if(i(p+1,n)>a)break;f=m}return l},t.prototype.match_alphabet_=function(e){for(var t={},n=0;n2&&(this.diff_cleanupSemantic(a),this.diff_cleanupEfficiency(a));else if(e&&"object"==typeof e&&void 0===r&&void 0===o)a=e,i=this.diff_text1(a);else if("string"==typeof e&&r&&"object"==typeof r&&void 0===o)i=e,a=r;else{if("string"!=typeof e||"string"!=typeof r||!o||"object"!=typeof o)throw new Error("Unknown call format to patch_make.");i=e,a=o}if(0===a.length)return[];for(var l=[],s=new t.patch_obj,u=0,c=0,f=0,d=i,p=i,h=0;h=2*this.Patch_Margin&&u&&(this.patch_addContext_(s,d),l.push(s),s=new t.patch_obj,u=0,d=p,c=f)}1!==g&&(c+=m.length),g!==n&&(f+=m.length)}return u&&(this.patch_addContext_(s,d),l.push(s)),l},t.prototype.patch_deepCopy=function(e){for(var n=[],r=0;rthis.Match_MaxBits?-1!=(l=this.match_main(t,c.substring(0,this.Match_MaxBits),u))&&(-1==(f=this.match_main(t,c.substring(c.length-this.Match_MaxBits),u+c.length-this.Match_MaxBits))||l>=f)&&(l=-1):l=this.match_main(t,c,u),-1==l)i[a]=!1,o-=e[a].length2-e[a].length1;else if(i[a]=!0,o=l-u,c==(s=-1==f?t.substring(l,l+c.length):t.substring(l,f+this.Match_MaxBits)))t=t.substring(0,l)+this.diff_text2(e[a].diffs)+t.substring(l+c.length);else{var d=this.diff_main(c,s,!1);if(c.length>this.Match_MaxBits&&this.diff_levenshtein(d)/c.length>this.Patch_DeleteThreshold)i[a]=!1;else{this.diff_cleanupSemanticLossless(d);for(var p,h=0,g=0;ga[0][1].length){var l=n-a[0][1].length;a[0][1]=r.substring(a[0][1].length)+a[0][1],i.start1-=l,i.start2-=l,i.length1+=l,i.length2+=l}return 0==(a=(i=e[e.length-1]).diffs).length||0!=a[a.length-1][0]?(a.push(new t.Diff(0,r)),i.length1+=n,i.length2+=n):n>a[a.length-1][1].length&&(l=n-a[a.length-1][1].length,a[a.length-1][1]+=r.substring(0,l),i.length1+=l,i.length2+=l),r},t.prototype.patch_splitMax=function(e){for(var r=this.Match_MaxBits,o=0;o2*r?(u.length1+=d.length,a+=d.length,c=!1,u.diffs.push(new t.Diff(f,d)),i.diffs.shift()):(d=d.substring(0,r-u.length1-this.Patch_Margin),u.length1+=d.length,a+=d.length,0===f?(u.length2+=d.length,l+=d.length):c=!1,u.diffs.push(new t.Diff(f,d)),d==i.diffs[0][1]?i.diffs.shift():i.diffs[0][1]=i.diffs[0][1].substring(d.length))}s=(s=this.diff_text2(u.diffs)).substring(s.length-this.Patch_Margin);var p=this.diff_text1(i.diffs).substring(0,this.Patch_Margin);""!==p&&(u.length1+=p.length,u.length2+=p.length,0!==u.diffs.length&&0===u.diffs[u.diffs.length-1][0]?u.diffs[u.diffs.length-1][1]+=p:u.diffs.push(new t.Diff(0,p))),c||e.splice(++o,0,u)}}},t.prototype.patch_toText=function(e){for(var t=[],n=0;n{"use strict";e.exports=function(e){var t=e.uri,n=e.name,r=e.type;this.uri=t,this.name=n,this.type=r}},2929:(e,t,n)=>{"use strict";var r=n(1278);e.exports=function e(t,n,o){var i;void 0===n&&(n=""),void 0===o&&(o=r);var a=new Map;function l(e,t){var n=a.get(t);n?n.push.apply(n,e):a.set(t,e)}if(o(t))i=null,l([n],t);else{var s=n?n+".":"";if("undefined"!=typeof FileList&&t instanceof FileList)i=Array.prototype.map.call(t,(function(e,t){return l([""+s+t],e),null}));else if(Array.isArray(t))i=t.map((function(t,n){var r=e(t,""+s+n,o);return r.files.forEach(l),r.clone}));else if(t&&t.constructor===Object)for(var u in i={},t){var c=e(t[u],""+s+u,o);c.files.forEach(l),i[u]=c.clone}else i=t}return{clone:i,files:a}}},9384:(e,t,n)=>{"use strict";t.ReactNativeFile=n(7570),t.extractFiles=n(2929),t.isExtractableFile=n(1278)},1278:(e,t,n)=>{"use strict";var r=n(7570);e.exports=function(e){return"undefined"!=typeof File&&e instanceof File||"undefined"!=typeof Blob&&e instanceof Blob||e instanceof r}},1688:e=>{e.exports="object"==typeof self?self.FormData:window.FormData},8749:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var o=n(9384),i=r(n(1688)),a=function(e){return o.isExtractableFile(e)||null!==e&&"object"==typeof e&&"function"==typeof e.pipe};t.default=function(e,t,n){var r=o.extractFiles({query:e,variables:t,operationName:n},"",a),l=r.clone,s=r.files;if(0===s.size){if(!Array.isArray(e))return JSON.stringify(l);if(void 0!==t&&!Array.isArray(t))throw new Error("Cannot create request body with given variable type, array expected");var u=e.reduce((function(e,n,r){return e.push({query:n,variables:t?t[r]:void 0}),e}),[]);return JSON.stringify(u)}var c=new("undefined"==typeof FormData?i.default:FormData);c.append("operations",JSON.stringify(l));var f={},d=0;return s.forEach((function(e){f[++d]=e})),c.append("map",JSON.stringify(f)),d=0,s.forEach((function(e,t){c.append(""+ ++d,t)})),c}},6647:function(e,t,n){"use strict";var r=this&&this.__assign||function(){return r=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&o[o.length-1])||6!==i[0]&&2!==i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){var t=e.prototype.toJSON;"function"==typeof t||(0,r.default)(0),e.prototype.inspect=t,o.default&&(e.prototype[o.default]=t)};var r=i(n(5006)),o=i(n(8019));function i(e){return e&&e.__esModule?e:{default:e}}},8048:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){return a(e,[])};var r,o=(r=n(8019))&&r.__esModule?r:{default:r};function i(e){return i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},i(e)}function a(e,t){switch(i(e)){case"string":return JSON.stringify(e);case"function":return e.name?"[function ".concat(e.name,"]"):"[function]";case"object":return null===e?"null":function(e,t){if(-1!==t.indexOf(e))return"[Circular]";var n=[].concat(t,[e]),r=function(e){var t=e[String(o.default)];return"function"==typeof t?t:"function"==typeof e.inspect?e.inspect:void 0}(e);if(void 0!==r){var i=r.call(e);if(i!==e)return"string"==typeof i?i:a(i,n)}else if(Array.isArray(e))return function(e,t){if(0===e.length)return"[]";if(t.length>2)return"[Array]";for(var n=Math.min(10,e.length),r=e.length-n,o=[],i=0;i1&&o.push("... ".concat(r," more items")),"["+o.join(", ")+"]"}(e,n);return function(e,t){var n=Object.keys(e);return 0===n.length?"{}":t.length>2?"["+function(e){var t=Object.prototype.toString.call(e).replace(/^\[object /,"").replace(/]$/,"");if("Object"===t&&"function"==typeof e.constructor){var n=e.constructor.name;if("string"==typeof n&&""!==n)return n}return t}(e)+"]":"{ "+n.map((function(n){return n+": "+a(e[n],t)})).join(", ")+" }"}(e,n)}(e,t);default:return String(e)}}},5006:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t){if(!Boolean(e))throw new Error(null!=t?t:"Unexpected invariant triggered.")}},8019:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var n="function"==typeof Symbol&&"function"==typeof Symbol.for?Symbol.for("nodejs.util.inspect.custom"):void 0;t.default=n},4560:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.isNode=function(e){return null!=e&&"string"==typeof e.kind},t.Token=t.Location=void 0;var r,o=(r=n(2678))&&r.__esModule?r:{default:r},i=function(){function e(e,t,n){this.start=e.start,this.end=t.end,this.startToken=e,this.endToken=t,this.source=n}return e.prototype.toJSON=function(){return{start:this.start,end:this.end}},e}();t.Location=i,(0,o.default)(i);var a=function(){function e(e,t,n,r,o,i,a){this.kind=e,this.start=t,this.end=n,this.line=r,this.column=o,this.value=a,this.prev=i,this.next=null}return e.prototype.toJSON=function(){return{kind:this.kind,value:this.value,line:this.line,column:this.column}},e}();t.Token=a,(0,o.default)(a)},9501:(e,t)=>{"use strict";function n(e){for(var t=0;ta&&n(t[l-1]);)--l;return t.slice(a,l).join("\n")},t.getBlockStringIndentation=r,t.printBlockString=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n=arguments.length>2&&void 0!==arguments[2]&&arguments[2],r=-1===e.indexOf("\n"),o=" "===e[0]||"\t"===e[0],i='"'===e[e.length-1],a="\\"===e[e.length-1],l=!r||i||a||n,s="";return!l||r&&o||(s+="\n"+t),s+=t?e.replace(/\n/g,"\n"+t):e,l&&(s+="\n"),'"""'+s.replace(/"""/g,'\\"""')+'"""'}},3083:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.print=function(e){return(0,r.visit)(e,{leave:i})};var r=n(2624),o=n(9501),i={Name:function(e){return e.value},Variable:function(e){return"$"+e.name},Document:function(e){return l(e.definitions,"\n\n")+"\n"},OperationDefinition:function(e){var t=e.operation,n=e.name,r=u("(",l(e.variableDefinitions,", "),")"),o=l(e.directives," "),i=e.selectionSet;return n||o||r||"query"!==t?l([t,l([n,r]),o,i]," "):i},VariableDefinition:function(e){var t=e.variable,n=e.type,r=e.defaultValue,o=e.directives;return t+": "+n+u(" = ",r)+u(" ",l(o," "))},SelectionSet:function(e){return s(e.selections)},Field:function(e){var t=e.alias,n=e.name,r=e.arguments,o=e.directives,i=e.selectionSet,a=u("",t,": ")+n,s=a+u("(",l(r,", "),")");return s.length>80&&(s=a+u("(\n",c(l(r,"\n")),"\n)")),l([s,l(o," "),i]," ")},Argument:function(e){return e.name+": "+e.value},FragmentSpread:function(e){return"..."+e.name+u(" ",l(e.directives," "))},InlineFragment:function(e){var t=e.typeCondition,n=e.directives,r=e.selectionSet;return l(["...",u("on ",t),l(n," "),r]," ")},FragmentDefinition:function(e){var t=e.name,n=e.typeCondition,r=e.variableDefinitions,o=e.directives,i=e.selectionSet;return"fragment ".concat(t).concat(u("(",l(r,", "),")")," ")+"on ".concat(n," ").concat(u("",l(o," ")," "))+i},IntValue:function(e){return e.value},FloatValue:function(e){return e.value},StringValue:function(e,t){var n=e.value;return e.block?(0,o.printBlockString)(n,"description"===t?"":" "):JSON.stringify(n)},BooleanValue:function(e){return e.value?"true":"false"},NullValue:function(){return"null"},EnumValue:function(e){return e.value},ListValue:function(e){return"["+l(e.values,", ")+"]"},ObjectValue:function(e){return"{"+l(e.fields,", ")+"}"},ObjectField:function(e){return e.name+": "+e.value},Directive:function(e){return"@"+e.name+u("(",l(e.arguments,", "),")")},NamedType:function(e){return e.name},ListType:function(e){return"["+e.type+"]"},NonNullType:function(e){return e.type+"!"},SchemaDefinition:a((function(e){var t=e.directives,n=e.operationTypes;return l(["schema",l(t," "),s(n)]," ")})),OperationTypeDefinition:function(e){return e.operation+": "+e.type},ScalarTypeDefinition:a((function(e){return l(["scalar",e.name,l(e.directives," ")]," ")})),ObjectTypeDefinition:a((function(e){var t=e.name,n=e.interfaces,r=e.directives,o=e.fields;return l(["type",t,u("implements ",l(n," & ")),l(r," "),s(o)]," ")})),FieldDefinition:a((function(e){var t=e.name,n=e.arguments,r=e.type,o=e.directives;return t+(d(n)?u("(\n",c(l(n,"\n")),"\n)"):u("(",l(n,", "),")"))+": "+r+u(" ",l(o," "))})),InputValueDefinition:a((function(e){var t=e.name,n=e.type,r=e.defaultValue,o=e.directives;return l([t+": "+n,u("= ",r),l(o," ")]," ")})),InterfaceTypeDefinition:a((function(e){var t=e.name,n=e.interfaces,r=e.directives,o=e.fields;return l(["interface",t,u("implements ",l(n," & ")),l(r," "),s(o)]," ")})),UnionTypeDefinition:a((function(e){var t=e.name,n=e.directives,r=e.types;return l(["union",t,l(n," "),r&&0!==r.length?"= "+l(r," | "):""]," ")})),EnumTypeDefinition:a((function(e){var t=e.name,n=e.directives,r=e.values;return l(["enum",t,l(n," "),s(r)]," ")})),EnumValueDefinition:a((function(e){return l([e.name,l(e.directives," ")]," ")})),InputObjectTypeDefinition:a((function(e){var t=e.name,n=e.directives,r=e.fields;return l(["input",t,l(n," "),s(r)]," ")})),DirectiveDefinition:a((function(e){var t=e.name,n=e.arguments,r=e.repeatable,o=e.locations;return"directive @"+t+(d(n)?u("(\n",c(l(n,"\n")),"\n)"):u("(",l(n,", "),")"))+(r?" repeatable":"")+" on "+l(o," | ")})),SchemaExtension:function(e){var t=e.directives,n=e.operationTypes;return l(["extend schema",l(t," "),s(n)]," ")},ScalarTypeExtension:function(e){return l(["extend scalar",e.name,l(e.directives," ")]," ")},ObjectTypeExtension:function(e){var t=e.name,n=e.interfaces,r=e.directives,o=e.fields;return l(["extend type",t,u("implements ",l(n," & ")),l(r," "),s(o)]," ")},InterfaceTypeExtension:function(e){var t=e.name,n=e.interfaces,r=e.directives,o=e.fields;return l(["extend interface",t,u("implements ",l(n," & ")),l(r," "),s(o)]," ")},UnionTypeExtension:function(e){var t=e.name,n=e.directives,r=e.types;return l(["extend union",t,l(n," "),r&&0!==r.length?"= "+l(r," | "):""]," ")},EnumTypeExtension:function(e){var t=e.name,n=e.directives,r=e.values;return l(["extend enum",t,l(n," "),s(r)]," ")},InputObjectTypeExtension:function(e){var t=e.name,n=e.directives,r=e.fields;return l(["extend input",t,l(n," "),s(r)]," ")}};function a(e){return function(t){return l([t.description,e(t)],"\n")}}function l(e){var t,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";return null!==(t=null==e?void 0:e.filter((function(e){return e})).join(n))&&void 0!==t?t:""}function s(e){return u("{\n",c(l(e,"\n")),"\n}")}function u(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"";return null!=t&&""!==t?e+t+n:""}function c(e){return u(" ",e.replace(/\n/g,"\n "))}function f(e){return-1!==e.indexOf("\n")}function d(e){return null!=e&&e.some(f)}},2624:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.visit=function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:a,r=void 0,u=Array.isArray(e),c=[e],f=-1,d=[],p=void 0,h=void 0,g=void 0,m=[],v=[],y=e;do{var b=++f===c.length,w=b&&0!==d.length;if(b){if(h=0===v.length?void 0:m[m.length-1],p=g,g=v.pop(),w){if(u)p=p.slice();else{for(var x={},E=0,k=Object.keys(p);E{var r=n(7772).Symbol;e.exports=r},3366:(e,t,n)=>{var r=n(857),o=n(2107),i=n(7157),a=r?r.toStringTag:void 0;e.exports=function(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":a&&a in Object(e)?o(e):i(e)}},1704:(e,t,n)=>{var r=n(2153),o=/^\s+/;e.exports=function(e){return e?e.slice(0,r(e)+1).replace(o,""):e}},1242:(e,t,n)=>{var r="object"==typeof n.g&&n.g&&n.g.Object===Object&&n.g;e.exports=r},2107:(e,t,n)=>{var r=n(857),o=Object.prototype,i=o.hasOwnProperty,a=o.toString,l=r?r.toStringTag:void 0;e.exports=function(e){var t=i.call(e,l),n=e[l];try{e[l]=void 0;var r=!0}catch(e){}var o=a.call(e);return r&&(t?e[l]=n:delete e[l]),o}},7157:e=>{var t=Object.prototype.toString;e.exports=function(e){return t.call(e)}},7772:(e,t,n)=>{var r=n(1242),o="object"==typeof self&&self&&self.Object===Object&&self,i=r||o||Function("return this")();e.exports=i},2153:e=>{var t=/\s/;e.exports=function(e){for(var n=e.length;n--&&t.test(e.charAt(n)););return n}},4073:(e,t,n)=>{var r=n(9259),o=n(1100),i=n(7642),a=Math.max,l=Math.min;e.exports=function(e,t,n){var s,u,c,f,d,p,h=0,g=!1,m=!1,v=!0;if("function"!=typeof e)throw new TypeError("Expected a function");function y(t){var n=s,r=u;return s=u=void 0,h=t,f=e.apply(r,n)}function b(e){return h=e,d=setTimeout(x,t),g?y(e):f}function w(e){var n=e-p;return void 0===p||n>=t||n<0||m&&e-h>=c}function x(){var e=o();if(w(e))return E(e);d=setTimeout(x,function(e){var n=t-(e-p);return m?l(n,c-(e-h)):n}(e))}function E(e){return d=void 0,v&&s?y(e):(s=u=void 0,f)}function k(){var e=o(),n=w(e);if(s=arguments,u=this,p=e,n){if(void 0===d)return b(p);if(m)return clearTimeout(d),d=setTimeout(x,t),y(p)}return void 0===d&&(d=setTimeout(x,t)),f}return t=i(t)||0,r(n)&&(g=!!n.leading,c=(m="maxWait"in n)?a(i(n.maxWait)||0,t):c,v="trailing"in n?!!n.trailing:v),k.cancel=function(){void 0!==d&&clearTimeout(d),h=0,s=p=u=d=void 0},k.flush=function(){return void 0===d?f:E(o())},k}},9259:e=>{e.exports=function(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}},5125:e=>{e.exports=function(e){return null!=e&&"object"==typeof e}},4795:(e,t,n)=>{var r=n(3366),o=n(5125);e.exports=function(e){return"symbol"==typeof e||o(e)&&"[object Symbol]"==r(e)}},1100:(e,t,n)=>{var r=n(7772);e.exports=function(){return r.Date.now()}},7642:(e,t,n)=>{var r=n(1704),o=n(9259),i=n(4795),a=/^[-+]0x[0-9a-f]+$/i,l=/^0b[01]+$/i,s=/^0o[0-7]+$/i,u=parseInt;e.exports=function(e){if("number"==typeof e)return e;if(i(e))return NaN;if(o(e)){var t="function"==typeof e.valueOf?e.valueOf():e;e=o(t)?t+"":t}if("string"!=typeof e)return 0===e?e:+e;e=r(e);var n=l.test(e);return n||s.test(e)?u(e.slice(2),n?2:8):a.test(e)?NaN:+e}},9410:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(){let e=null;return{mountedInstances:new Set,updateHead:t=>{const n=e=Promise.resolve().then((()=>{if(n!==e)return;e=null;const i={};t.forEach((e=>{if("link"===e.type&&e.props["data-optimized-fonts"]){if(document.querySelector(`style[data-href="${e.props["data-href"]}"]`))return;e.props.href=e.props["data-href"],e.props["data-href"]=void 0}const t=i[e.type]||[];t.push(e),i[e.type]=t}));const a=i.title?i.title[0]:null;let l="";if(a){const{children:e}=a.props;l="string"==typeof e?e:Array.isArray(e)?e.join(""):""}l!==document.title&&(document.title=l),["meta","base","link","style","script"].forEach((e=>{!function(e,t){const n=document.getElementsByTagName("head")[0],i=n.querySelector("meta[name=next-head-count]"),a=Number(i.content),l=[];for(let t=0,n=i.previousElementSibling;t{for(let t=0,n=l.length;t{var t;return null===(t=e.parentNode)||void 0===t?void 0:t.removeChild(e)})),u.forEach((e=>n.insertBefore(e,i))),i.content=(a-l.length+u.length).toString()}(e,i[e]||[])}))}))}}},t.isEqualNode=o,t.DOMAttributeNames=void 0;const n={acceptCharset:"accept-charset",className:"class",htmlFor:"for",httpEquiv:"http-equiv",noModule:"noModule"};function r({type:e,props:t}){const r=document.createElement(e);for(const o in t){if(!t.hasOwnProperty(o))continue;if("children"===o||"dangerouslySetInnerHTML"===o)continue;if(void 0===t[o])continue;const i=n[o]||o.toLowerCase();"script"!==e||"async"!==i&&"defer"!==i&&"noModule"!==i?r.setAttribute(i,t[o]):r[i]=!!t[o]}const{children:o,dangerouslySetInnerHTML:i}=t;return i?r.innerHTML=i.__html||"":o&&(r.textContent="string"==typeof o?o:Array.isArray(o)?o.join(""):""),r}function o(e,t){if(e instanceof HTMLElement&&t instanceof HTMLElement){const n=t.getAttribute("nonce");if(n&&!e.getAttribute("nonce")){const r=t.cloneNode(!0);return r.setAttribute("nonce",""),r.nonce=n,n===e.nonce&&e.isEqualNode(r)}}return e.isEqualNode(t)}t.DOMAttributeNames=n,("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&(Object.assign(t.default,t),e.exports=t.default)},4529:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,o=(r=n(2784))&&r.__esModule?r:{default:r},i=n(6640),a=n(9518),l=n(3321);const s={};function u(e,t,n,r){if("undefined"==typeof window||!e)return;if(!i.isLocalURL(t))return;e.prefetch(t,n,r).catch((e=>{}));const o=r&&void 0!==r.locale?r.locale:e&&e.locale;s[t+"%"+n+(o?"%"+o:"")]=!0}var c=o.default.forwardRef(((e,t)=>{const{legacyBehavior:n=!0!==Boolean(window.omnivoreEnv.__NEXT_NEW_LINK_BEHAVIOR)}=e;let r;const{href:c,as:f,children:d,prefetch:p,passHref:h,replace:g,shallow:m,scroll:v,locale:y,onClick:b,onMouseEnter:w}=e,x=function(e,t){if(null==e)return{};var n,r,o=function(e,t){if(null==e)return{};var n,r,o={},i=Object.keys(e);for(r=0;r=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}(e,["href","as","children","prefetch","passHref","replace","shallow","scroll","locale","onClick","onMouseEnter"]);r=d,n&&"string"==typeof r&&(r=o.default.createElement("a",null,r));const E=!1!==p,k=a.useRouter(),{href:S,as:_}=o.default.useMemo((()=>{const[e,t]=i.resolveHref(k,c,!0);return{href:e,as:f?i.resolveHref(k,f):t||e}}),[k,c,f]),C=o.default.useRef(S),O=o.default.useRef(_);let L;n&&(L=o.default.Children.only(r));const P=n?L&&"object"==typeof L&&L.ref:t,[R,j,T]=l.useIntersection({rootMargin:"200px"}),A=o.default.useCallback((e=>{O.current===_&&C.current===S||(T(),O.current=_,C.current=S),R(e),P&&("function"==typeof P?P(e):"object"==typeof P&&(P.current=e))}),[_,P,S,T,R]);o.default.useEffect((()=>{const e=j&&E&&i.isLocalURL(S),t=void 0!==y?y:k&&k.locale,n=s[S+"%"+_+(t?"%"+t:"")];e&&!n&&u(k,S,_,{locale:t})}),[_,S,j,y,E,k]);const M={ref:A,onClick:e=>{n||"function"!=typeof b||b(e),n&&L.props&&"function"==typeof L.props.onClick&&L.props.onClick(e),e.defaultPrevented||function(e,t,n,r,o,a,l,s){const{nodeName:u}=e.currentTarget;("A"!==u.toUpperCase()||!function(e){const{target:t}=e.currentTarget;return t&&"_self"!==t||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.nativeEvent&&2===e.nativeEvent.which}(e)&&i.isLocalURL(n))&&(e.preventDefault(),t[o?"replace":"push"](n,r,{shallow:a,locale:s,scroll:l}))}(e,k,S,_,g,m,v,y)},onMouseEnter:e=>{n||"function"!=typeof w||w(e),n&&L.props&&"function"==typeof L.props.onMouseEnter&&L.props.onMouseEnter(e),i.isLocalURL(S)&&u(k,S,_,{priority:!0})}};if(!n||h||"a"===L.type&&!("href"in L.props)){const e=void 0!==y?y:k&&k.locale,t=k&&k.isLocaleDomain&&i.getDomainLocale(_,e,k&&k.locales,k&&k.domainLocales);M.href=t||i.addBasePath(i.addLocale(_,e,k&&k.defaultLocale))}return n?o.default.cloneElement(L,M):o.default.createElement("a",Object.assign({},x,M),r)}));t.default=c,("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&(Object.assign(t.default,t),e.exports=t.default)},1119:(e,t)=>{"use strict";function n(e){return e.endsWith("/")&&"/"!==e?e.slice(0,-1):e}Object.defineProperty(t,"__esModule",{value:!0}),t.removePathTrailingSlash=n,t.normalizePathTrailingSlash=void 0;const r=window.omnivoreEnv.__NEXT_TRAILING_SLASH?e=>/\.[^/]+\/?$/.test(e)?n(e):e.endsWith("/")?e:e+"/":n;t.normalizePathTrailingSlash=r,("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&(Object.assign(t.default,t),e.exports=t.default)},1976:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.cancelIdleCallback=t.requestIdleCallback=void 0;const n="undefined"!=typeof self&&self.requestIdleCallback&&self.requestIdleCallback.bind(window)||function(e){let t=Date.now();return setTimeout((function(){e({didTimeout:!1,timeRemaining:function(){return Math.max(0,50-(Date.now()-t))}})}),1)};t.requestIdleCallback=n;const r="undefined"!=typeof self&&self.cancelIdleCallback&&self.cancelIdleCallback.bind(window)||function(e){return clearTimeout(e)};t.cancelIdleCallback=r,("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&(Object.assign(t.default,t),e.exports=t.default)},7928:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.markAssetError=u,t.isAssetError=function(e){return e&&s in e},t.getClientBuildManifest=f,t.getMiddlewareManifest=function(){return self.__MIDDLEWARE_MANIFEST?Promise.resolve(self.__MIDDLEWARE_MANIFEST):c(new Promise((e=>{const t=self.__MIDDLEWARE_MANIFEST_CB;self.__MIDDLEWARE_MANIFEST_CB=()=>{e(self.__MIDDLEWARE_MANIFEST),t&&t()}})),i,u(new Error("Failed to load client middleware manifest")))},t.createRouteLoader=function(e){const t=new Map,n=new Map,r=new Map,s=new Map;function f(e){{let t=n.get(e);return t||(document.querySelector(`script[src^="${e}"]`)?Promise.resolve():(n.set(e,t=function(e,t){return new Promise(((n,r)=>{(t=document.createElement("script")).onload=n,t.onerror=()=>r(u(new Error(`Failed to load script: ${e}`))),t.crossOrigin=window.omnivoreEnv.__NEXT_CROSS_ORIGIN,t.src=e,document.body.appendChild(t)}))}(e)),t))}}function p(e){let t=r.get(e);return t||(r.set(e,t=fetch(e).then((t=>{if(!t.ok)throw new Error(`Failed to load stylesheet: ${e}`);return t.text().then((t=>({href:e,content:t})))})).catch((e=>{throw u(e)}))),t)}return{whenEntrypoint:e=>a(e,t),onEntrypoint(e,n){(n?Promise.resolve().then((()=>n())).then((e=>({component:e&&e.default||e,exports:e})),(e=>({error:e}))):Promise.resolve(void 0)).then((n=>{const r=t.get(e);r&&"resolve"in r?n&&(t.set(e,n),r.resolve(n)):(n?t.set(e,n):t.delete(e),s.delete(e))}))},loadRoute(n,r){return a(n,s,(()=>c(d(e,n).then((({scripts:e,css:r})=>Promise.all([t.has(n)?[]:Promise.all(e.map(f)),Promise.all(r.map(p))]))).then((e=>this.whenEntrypoint(n).then((t=>({entrypoint:t,styles:e[1]}))))),i,u(new Error(`Route did not complete loading: ${n}`))).then((({entrypoint:e,styles:t})=>{const n=Object.assign({styles:t},e);return"error"in e?e:n})).catch((e=>{if(r)throw e;return{error:e}})).finally((()=>{}))))},prefetch(t){let n;return(n=navigator.connection)&&(n.saveData||/2g/.test(n.effectiveType))?Promise.resolve():d(e,t).then((e=>Promise.all(l?e.scripts.map((e=>{return t=e,n="script",new Promise(((e,o)=>{const i=`\n link[rel="prefetch"][href^="${t}"],\n link[rel="preload"][href^="${t}"],\n script[src^="${t}"]`;if(document.querySelector(i))return e();(r=document.createElement("link")).as=n,r.rel="prefetch",r.crossOrigin=window.omnivoreEnv.__NEXT_CROSS_ORIGIN,r.onload=e,r.onerror=o,r.href=t,document.head.appendChild(r)}));var t,n,r})):[]))).then((()=>{o.requestIdleCallback((()=>this.loadRoute(t,!0).catch((()=>{}))))})).catch((()=>{}))}}},(r=n(9983))&&r.__esModule;var r,o=n(1976);const i=3800;function a(e,t,n){let r,o=t.get(e);if(o)return"future"in o?o.future:Promise.resolve(o);const i=new Promise((e=>{r=e}));return t.set(e,o={resolve:r,future:i}),n?n().then((e=>(r(e),e))).catch((n=>{throw t.delete(e),n})):i}const l=function(e){try{return e=document.createElement("link"),!!window.MSInputMethodContext&&!!document.documentMode||e.relList.supports("prefetch")}catch(e){return!1}}(),s=Symbol("ASSET_LOAD_ERROR");function u(e){return Object.defineProperty(e,s,{})}function c(e,t,n){return new Promise(((r,i)=>{let a=!1;e.then((e=>{a=!0,r(e)})).catch(i),o.requestIdleCallback((()=>setTimeout((()=>{a||i(n)}),t)))}))}function f(){return self.__BUILD_MANIFEST?Promise.resolve(self.__BUILD_MANIFEST):c(new Promise((e=>{const t=self.__BUILD_MANIFEST_CB;self.__BUILD_MANIFEST_CB=()=>{e(self.__BUILD_MANIFEST),t&&t()}})),i,u(new Error("Failed to load client build manifest")))}function d(e,t){return f().then((n=>{if(!(t in n))throw u(new Error(`Failed to lookup route: ${t}`));const r=n[t].map((t=>e+"/_next/"+encodeURI(t)));return{scripts:r.filter((e=>e.endsWith(".js"))),css:r.filter((e=>e.endsWith(".css")))}}))}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&(Object.assign(t.default,t),e.exports=t.default)},9518:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"Router",{enumerable:!0,get:function(){return o.default}}),Object.defineProperty(t,"withRouter",{enumerable:!0,get:function(){return l.default}}),t.useRouter=function(){return r.default.useContext(i.RouterContext)},t.createRouter=function(...e){return u.router=new o.default(...e),u.readyCallbacks.forEach((e=>e())),u.readyCallbacks=[],u.router},t.makePublicRouterInstance=function(e){const t=e,n={};for(const e of c)"object"!=typeof t[e]?n[e]=t[e]:n[e]=Object.assign(Array.isArray(t[e])?[]:{},t[e]);return n.events=o.default.events,f.forEach((e=>{n[e]=(...n)=>t[e](...n)})),n},t.default=void 0;var r=s(n(2784)),o=s(n(6640)),i=n(6510),a=s(n(274)),l=s(n(9564));function s(e){return e&&e.__esModule?e:{default:e}}const u={router:null,readyCallbacks:[],ready(e){if(this.router)return e();"undefined"!=typeof window&&this.readyCallbacks.push(e)}},c=["pathname","route","query","asPath","components","isFallback","basePath","locale","locales","defaultLocale","isReady","isPreview","isLocaleDomain","domainLocales"],f=["push","replace","reload","back","prefetch","beforePopState"];function d(){if(!u.router)throw new Error('No router instance found.\nYou should only use "next/router" on the client side of your app.\n');return u.router}Object.defineProperty(u,"events",{get:()=>o.default.events}),c.forEach((e=>{Object.defineProperty(u,e,{get:()=>d()[e]})})),f.forEach((e=>{u[e]=(...t)=>d()[e](...t)})),["routeChangeStart","beforeHistoryChange","routeChangeComplete","routeChangeError","hashChangeStart","hashChangeComplete"].forEach((e=>{u.ready((()=>{o.default.events.on(e,((...t)=>{const n=`on${e.charAt(0).toUpperCase()}${e.substring(1)}`,r=u;if(r[n])try{r[n](...t)}catch(e){console.error(`Error when running the Router event: ${n}`),console.error(a.default(e)?`${e.message}\n${e.stack}`:e+"")}}))}))}));var p=u;t.default=p,("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&(Object.assign(t.default,t),e.exports=t.default)},9515:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.handleClientScriptLoad=p,t.initScriptLoader=function(e){e.forEach(p),[...document.querySelectorAll('[data-nscript="beforeInteractive"]'),...document.querySelectorAll('[data-nscript="beforePageRender"]')].forEach((e=>{const t=e.id||e.getAttribute("src");c.add(t)}))},t.default=void 0;var r=function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){var r=Object.defineProperty&&Object.getOwnPropertyDescriptor?Object.getOwnPropertyDescriptor(e,n):{};r.get||r.set?Object.defineProperty(t,n,r):t[n]=e[n]}return t.default=e,t}(n(2784)),o=n(7177),i=n(9410),a=n(1976);function l(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function s(e){for(var t=1;t{const{src:t,id:n,onLoad:r=(()=>{}),dangerouslySetInnerHTML:o,children:a="",strategy:l="afterInteractive",onError:s}=e,d=n||t;if(d&&c.has(d))return;if(u.has(t))return c.add(d),void u.get(t).then(r,s);const p=document.createElement("script"),h=new Promise(((e,t)=>{p.addEventListener("load",(function(t){e(),r&&r.call(this,t)})),p.addEventListener("error",(function(e){t(e)}))})).catch((function(e){s&&s(e)}));t&&u.set(t,h),c.add(d),o?p.innerHTML=o.__html||"":a?p.textContent="string"==typeof a?a:Array.isArray(a)?a.join(""):"":t&&(p.src=t);for(const[t,n]of Object.entries(e)){if(void 0===n||f.includes(t))continue;const e=i.DOMAttributeNames[t]||t.toLowerCase();p.setAttribute(e,n)}"worker"===l&&p.setAttribute("type","text/partytown"),p.setAttribute("data-nscript",l),document.body.appendChild(p)};function p(e){const{strategy:t="afterInteractive"}=e;"lazyOnload"===t?window.addEventListener("load",(()=>{a.requestIdleCallback((()=>d(e)))})):d(e)}t.default=function(e){const{src:t="",onLoad:n=(()=>{}),strategy:i="afterInteractive",onError:l}=e,u=function(e,t){if(null==e)return{};var n,r,o=function(e,t){if(null==e)return{};var n,r,o={},i=Object.keys(e);for(r=0;r=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}(e,["src","onLoad","strategy","onError"]),{updateScripts:f,scripts:p,getIsSsr:h}=r.useContext(o.HeadManagerContext);return r.useEffect((()=>{"afterInteractive"===i?d(e):"lazyOnload"===i&&function(e){"complete"===document.readyState?a.requestIdleCallback((()=>d(e))):window.addEventListener("load",(()=>{a.requestIdleCallback((()=>d(e)))}))}(e)}),[e,i]),"beforeInteractive"!==i&&"worker"!==i||(f?(p[i]=(p[i]||[]).concat([s({src:t,onLoad:n,onError:l},u)]),f(p)):h&&h()?c.add(u.id||t):h&&!h()&&d(e)),null},("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&(Object.assign(t.default,t),e.exports=t.default)},3321:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.useIntersection=function({rootRef:e,rootMargin:t,disabled:n}){const s=n||!i,u=r.useRef(),[c,f]=r.useState(!1),[d,p]=r.useState(e?e.current:null),h=r.useCallback((e=>{u.current&&(u.current(),u.current=void 0),s||c||e&&e.tagName&&(u.current=function(e,t,n){const{id:r,observer:o,elements:i}=function(e){const t={root:e.root||null,margin:e.rootMargin||""};let n,r=l.find((e=>e.root===t.root&&e.margin===t.margin));if(r?n=a.get(r):(n=a.get(t),l.push(t)),n)return n;const o=new Map,i=new IntersectionObserver((e=>{e.forEach((e=>{const t=o.get(e.target),n=e.isIntersecting||e.intersectionRatio>0;t&&n&&t(n)}))}),e);return a.set(t,n={id:t,observer:i,elements:o}),n}(n);return i.set(e,(e=>e&&f(e))),o.observe(e),function(){if(i.delete(e),o.unobserve(e),0===i.size){o.disconnect(),a.delete(r);let e=l.findIndex((e=>e.root===r.root&&e.margin===r.margin));e>-1&&l.splice(e,1)}}}(e,0,{root:d,rootMargin:t}))}),[s,d,t,c]),g=r.useCallback((()=>{f(!1)}),[]);return r.useEffect((()=>{if(!i&&!c){const e=o.requestIdleCallback((()=>f(!0)));return()=>o.cancelIdleCallback(e)}}),[c]),r.useEffect((()=>{e&&p(e.current)}),[e]),[h,c,g]};var r=n(2784),o=n(1976);const i="undefined"!=typeof IntersectionObserver,a=new Map,l=[];("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&(Object.assign(t.default,t),e.exports=t.default)},9564:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){function t(t){return o.default.createElement(e,Object.assign({router:i.useRouter()},t))}return t.getInitialProps=e.getInitialProps,t.origGetInitialProps=e.origGetInitialProps,t};var r,o=(r=n(2784))&&r.__esModule?r:{default:r},i=n(9518);("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&(Object.assign(t.default,t),e.exports=t.default)},9264:(e,t)=>{"use strict";function n(e,t){void 0===t&&(t={});for(var n=function(e){for(var t=[],n=0;n=48&&s<=57||s>=65&&s<=90||s>=97&&s<=122||95===s))break;a+=e[l++]}if(!a)throw new TypeError("Missing parameter name at "+n);t.push({type:"NAME",index:n,value:a}),n=l}else t.push({type:"CLOSE",index:n,value:e[n++]});else t.push({type:"OPEN",index:n,value:e[n++]});else t.push({type:"ESCAPED_CHAR",index:n++,value:e[n++]});else t.push({type:"MODIFIER",index:n,value:e[n++]})}return t.push({type:"END",index:n,value:""}),t}(e),r=t.prefixes,o=void 0===r?"./":r,a="[^"+i(t.delimiter||"/#?")+"]+?",l=[],s=0,u=0,c="",f=function(e){if(u-1:void 0===E;o||(g+="(?:"+h+"(?="+p+"))?"),k||(g+="(?="+h+"|"+p+")")}return new RegExp(g,a(n))}function s(e,t,r){return e instanceof RegExp?function(e,t){if(!t)return e;var n=e.source.match(/\((?!\?)/g);if(n)for(var r=0;r{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=o,t.getProperError=function(e){return o(e)?e:new Error(r.isPlainObject(e)?JSON.stringify(e):e+"")};var r=n(9910);function o(e){return"object"==typeof e&&null!==e&&"name"in e&&"message"in e}},8058:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.escapeStringRegexp=function(e){return n.test(e)?e.replace(r,"\\$&"):e};const n=/[|\\{}()[\]^$+*?.-]/,r=/[|\\{}()[\]^$+*?.-]/g},7177:(e,t,n)=>{"use strict";var r;Object.defineProperty(t,"__esModule",{value:!0}),t.HeadManagerContext=void 0;const o=((r=n(2784))&&r.__esModule?r:{default:r}).default.createContext({});t.HeadManagerContext=o},927:(e,t)=>{"use strict";t.D=function(e,t,n){let r;if(e){n&&(n=n.toLowerCase());for(const a of e){var o,i;if(t===(null===(o=a.domain)||void 0===o?void 0:o.split(":")[0].toLowerCase())||n===a.defaultLocale.toLowerCase()||(null===(i=a.locales)||void 0===i?void 0:i.some((e=>e.toLowerCase()===n)))){r=a;break}}}return r}},816:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.normalizeLocalePath=function(e,t){let n;const r=e.split("/");return(t||[]).some((t=>!(!r[1]||r[1].toLowerCase()!==t.toLowerCase()||(n=t,r.splice(1,1),e=r.join("/")||"/",0)))),{pathname:e,detectedLocale:n}}},9910:(e,t)=>{"use strict";function n(e){return Object.prototype.toString.call(e)}Object.defineProperty(t,"__esModule",{value:!0}),t.getObjectClassLabel=n,t.isPlainObject=function(e){if("[object Object]"!==n(e))return!1;const t=Object.getPrototypeOf(e);return null===t||t===Object.prototype}},7471:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(){const e=Object.create(null);return{on(t,n){(e[t]||(e[t]=[])).push(n)},off(t,n){e[t]&&e[t].splice(e[t].indexOf(n)>>>0,1)},emit(t,...n){(e[t]||[]).slice().map((e=>{e(...n)}))}}}},997:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.denormalizePagePath=function(e){let t=o.normalizePathSep(e);return t.startsWith("/index/")&&!r.isDynamicRoute(t)?t.slice(6):"/index"!==t?t:"/"};var r=n(9150),o=n(9356)},9356:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.normalizePathSep=function(e){return e.replace(/\\/g,"/")}},6510:(e,t,n)=>{"use strict";var r;Object.defineProperty(t,"__esModule",{value:!0}),t.RouterContext=void 0;const o=((r=n(2784))&&r.__esModule?r:{default:r}).default.createContext(null);t.RouterContext=o},6640:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getDomainLocale=function(e,t,n,r){if(window.omnivoreEnv.__NEXT_I18N_SUPPORT){t=t||s.normalizeLocalePath(e,n).detectedLocale;const o=w(r,void 0,t);return!!o&&`http${o.http?"":"s"}://${o.domain}${x||""}${t===o.defaultLocale?"":`/${t}`}${e}`}return!1},t.addLocale=_,t.delLocale=C,t.hasBasePath=L,t.addBasePath=P,t.delBasePath=R,t.isLocalURL=j,t.interpolateAs=T,t.resolveHref=M,t.default=void 0;var r=n(1119),o=n(7928),i=n(9515),a=function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){var r=Object.defineProperty&&Object.getOwnPropertyDescriptor?Object.getOwnPropertyDescriptor(e,n):{};r.get||r.set?Object.defineProperty(t,n,r):t[n]=e[n]}return t.default=e,t}(n(274)),l=n(997),s=n(816),u=b(n(7471)),c=n(1624),f=n(7482),d=n(1577),p=n(646),h=b(n(5317)),g=n(3107),m=n(4794),v=n(2763),y=n(6555);function b(e){return e&&e.__esModule?e:{default:e}}let w;window.omnivoreEnv.__NEXT_I18N_SUPPORT&&(w=n(927).D);const x=window.omnivoreEnv.__NEXT_ROUTER_BASEPATH||"";function E(){return Object.assign(new Error("Route Cancelled"),{cancelled:!0})}function k(e,t){if(!e.startsWith("/")||!t)return e;const n=O(e);return r.normalizePathTrailingSlash(`${t}${n}`)+e.slice(n.length)}function S(e,t){return(e=O(e))===t||e.startsWith(t+"/")}function _(e,t,n){if(window.omnivoreEnv.__NEXT_I18N_SUPPORT&&t&&t!==n){const n=O(e).toLowerCase();if(!S(n,"/"+t.toLowerCase())&&!S(n,"/api"))return k(e,"/"+t)}return e}function C(e,t){if(window.omnivoreEnv.__NEXT_I18N_SUPPORT){const n=O(e),r=n.toLowerCase(),o=t&&t.toLowerCase();return t&&(r.startsWith("/"+o+"/")||r==="/"+o)?(n.length===t.length+1?"/":"")+e.slice(t.length+1):e}return e}function O(e){const t=e.indexOf("?"),n=e.indexOf("#");return(t>-1||n>-1)&&(e=e.substring(0,t>-1?t:n)),e}function L(e){return S(e,x)}function P(e){return k(e,x)}function R(e){return(e=e.slice(x.length)).startsWith("/")||(e=`/${e}`),e}function j(e){if(e.startsWith("/")||e.startsWith("#")||e.startsWith("?"))return!0;try{const t=c.getLocationOrigin(),n=new URL(e,t);return n.origin===t&&L(n.pathname)}catch(e){return!1}}function T(e,t,n){let r="";const o=m.getRouteRegex(e),i=o.groups,a=(t!==e?g.getRouteMatcher(o)(t):"")||n;r=e;const l=Object.keys(i);return l.every((e=>{let t=a[e]||"";const{repeat:n,optional:o}=i[e];let l=`[${n?"...":""}${e}]`;return o&&(l=`${t?"":"/"}[${l}]`),n&&!Array.isArray(t)&&(t=[t]),(o||e in a)&&(r=r.replace(l,n?t.map((e=>encodeURIComponent(e))).join("/"):encodeURIComponent(t))||"/")}))||(r=""),{params:l,result:r}}function A(e,t){const n={};return Object.keys(e).forEach((r=>{t.includes(r)||(n[r]=e[r])})),n}function M(e,t,n){let o,i="string"==typeof t?t:y.formatWithValidation(t);const a=i.match(/^[a-zA-Z]{1,}:\/\//),l=a?i.slice(a[0].length):i;if((l.split("?")[0]||"").match(/(\/\/|\\)/)){console.error(`Invalid href passed to next/router: ${i}, repeated forward-slashes (//) or backslashes \\ are not valid in the href`);const e=c.normalizeRepeatedSlashes(l);i=(a?a[0]:"")+e}if(!j(i))return n?[i]:i;try{o=new URL(i.startsWith("#")?e.asPath:e.pathname,"http://n")}catch(e){o=new URL("/","http://n")}try{const e=new URL(i,o);e.pathname=r.normalizePathTrailingSlash(e.pathname);let t="";if(f.isDynamicRoute(e.pathname)&&e.searchParams&&n){const n=p.searchParamsToUrlQuery(e.searchParams),{result:r,params:o}=T(e.pathname,e.pathname,n);r&&(t=y.formatWithValidation({pathname:r,hash:e.hash,query:A(n,o)}))}const a=e.origin===o.origin?e.href.slice(e.origin.length):e.href;return n?[a,t||a]:a}catch(e){return n?[i]:i}}function I(e){const t=c.getLocationOrigin();return e.startsWith(t)?e.substring(t.length):e}function D(e,t,n){let[r,o]=M(e,t,!0);const i=c.getLocationOrigin(),a=r.startsWith(i),l=o&&o.startsWith(i);r=I(r),o=o?I(o):o;const s=a?r:P(r),u=n?I(M(e,n)):o||r;return{url:s,as:l?u:P(u)}}function N(e,t){const n=r.removePathTrailingSlash(l.denormalizePagePath(e));return"/404"===n||"/_error"===n?e:(t.includes(n)||t.some((t=>{if(f.isDynamicRoute(t)&&m.getRouteRegex(t).re.test(n))return e=t,!0})),r.removePathTrailingSlash(e))}const F=window.omnivoreEnv.__NEXT_SCROLL_RESTORATION&&"undefined"!=typeof window&&"scrollRestoration"in window.history&&!!function(){try{let e="__next";return sessionStorage.setItem(e,e),sessionStorage.removeItem(e),!0}catch(e){}}(),z=Symbol("SSG_DATA_NOT_FOUND");function B(e,t,n){return fetch(e,{credentials:"same-origin"}).then((r=>{if(!r.ok){if(t>1&&r.status>=500)return B(e,t-1,n);if(404===r.status)return r.json().then((e=>{if(e.notFound)return{notFound:z};throw new Error("Failed to load static props")}));throw new Error("Failed to load static props")}return n.text?r.text():r.json()}))}function $(e,t,n,r,i){const{href:a}=new URL(e,window.location.href);return void 0!==r[a]?r[a]:r[a]=B(e,t?3:1,{text:n}).catch((e=>{throw t||o.markAssetError(e),e})).then((e=>(i||delete r[a],e))).catch((e=>{throw delete r[a],e}))}class W{constructor(e,t,n,{initialProps:o,pageLoader:i,App:a,wrapApp:l,Component:s,err:u,subscription:p,isFallback:h,locale:g,locales:m,defaultLocale:v,domainLocales:b,isPreview:E,isRsc:k}){this.sdc={},this.sdr={},this.sde={},this._idx=0,this.onPopState=e=>{const t=e.state;if(!t){const{pathname:e,query:t}=this;return void this.changeState("replaceState",y.formatWithValidation({pathname:P(e),query:t}),c.getURL())}if(!t.__N)return;let n;const{url:r,as:o,options:i,idx:a}=t;if(window.omnivoreEnv.__NEXT_SCROLL_RESTORATION&&F&&this._idx!==a){try{sessionStorage.setItem("__next_scroll_"+this._idx,JSON.stringify({x:self.pageXOffset,y:self.pageYOffset}))}catch{}try{const e=sessionStorage.getItem("__next_scroll_"+a);n=JSON.parse(e)}catch{n={x:0,y:0}}}this._idx=a;const{pathname:l}=d.parseRelativeUrl(r);this.isSsr&&o===P(this.asPath)&&l===P(this.pathname)||this._bps&&!this._bps(t)||this.change("replaceState",r,o,Object.assign({},i,{shallow:i.shallow&&this._shallow,locale:i.locale||this.defaultLocale}),n)};const S=r.removePathTrailingSlash(e);this.components={},"/_error"!==e&&(this.components[S]={Component:s,initial:!0,props:o,err:u,__N_SSG:o&&o.__N_SSG,__N_SSP:o&&o.__N_SSP,__N_RSC:!!k}),this.components["/_app"]={Component:a,styleSheets:[]},this.events=W.events,this.pageLoader=i;const _=f.isDynamicRoute(e)&&self.__NEXT_DATA__.autoExport;if(this.basePath=x,this.sub=p,this.clc=null,this._wrapApp=l,this.isSsr=!0,this.isLocaleDomain=!1,this.isReady=!(!(self.__NEXT_DATA__.gssp||self.__NEXT_DATA__.gip||self.__NEXT_DATA__.appGip&&!self.__NEXT_DATA__.gsp)&&(_||self.location.search||window.omnivoreEnv.__NEXT_HAS_REWRITES)),window.omnivoreEnv.__NEXT_I18N_SUPPORT&&(this.locales=m,this.defaultLocale=v,this.domainLocales=b,this.isLocaleDomain=!!w(b,self.location.hostname)),this.state={route:S,pathname:e,query:t,asPath:_?e:n,isPreview:!!E,locale:window.omnivoreEnv.__NEXT_I18N_SUPPORT?g:void 0,isFallback:h},"undefined"!=typeof window){if(!n.startsWith("//")){const r={locale:g};r._shouldResolveHref=n!==e,this.changeState("replaceState",y.formatWithValidation({pathname:P(e),query:t}),c.getURL(),r)}window.addEventListener("popstate",this.onPopState),window.omnivoreEnv.__NEXT_SCROLL_RESTORATION&&F&&(window.history.scrollRestoration="manual")}}reload(){window.location.reload()}back(){window.history.back()}push(e,t,n={}){if(window.omnivoreEnv.__NEXT_SCROLL_RESTORATION&&F)try{sessionStorage.setItem("__next_scroll_"+this._idx,JSON.stringify({x:self.pageXOffset,y:self.pageYOffset}))}catch{}return({url:e,as:t}=D(this,e,t)),this.change("pushState",e,t,n)}replace(e,t,n={}){return({url:e,as:t}=D(this,e,t)),this.change("replaceState",e,t,n)}async change(e,t,n,l,u){if(!j(t))return window.location.href=t,!1;const p=l._h||l._shouldResolveHref||O(t)===O(n),v={...this.state};l._h&&(this.isReady=!0);const b=v.locale;if(window.omnivoreEnv.__NEXT_I18N_SUPPORT){v.locale=!1===l.locale?this.defaultLocale:l.locale||v.locale,void 0===l.locale&&(l.locale=v.locale);const e=d.parseRelativeUrl(L(n)?R(n):n),r=s.normalizeLocalePath(e.pathname,this.locales);r.detectedLocale&&(v.locale=r.detectedLocale,e.pathname=P(e.pathname),n=y.formatWithValidation(e),t=P(s.normalizeLocalePath(L(t)?R(t):t,this.locales).pathname));let o=!1;var x;window.omnivoreEnv.__NEXT_I18N_SUPPORT&&((null===(x=this.locales)||void 0===x?void 0:x.includes(v.locale))||(e.pathname=_(e.pathname,v.locale),window.location.href=y.formatWithValidation(e),o=!0));const i=w(this.domainLocales,void 0,v.locale);if(window.omnivoreEnv.__NEXT_I18N_SUPPORT&&!o&&i&&this.isLocaleDomain&&self.location.hostname!==i.domain){const e=R(n);window.location.href=`http${i.http?"":"s"}://${i.domain}${P(`${v.locale===i.defaultLocale?"":`/${v.locale}`}${"/"===e?"":e}`||"/")}`,o=!0}if(o)return new Promise((()=>{}))}l._h||(this.isSsr=!1),c.ST&&performance.mark("routeChange");const{shallow:E=!1,scroll:k=!0}=l,S={shallow:E};this._inFlightRoute&&this.abortComponentLoad(this._inFlightRoute,S),n=P(_(L(n)?R(n):n,l.locale,this.defaultLocale));const M=C(L(n)?R(n):n,v.locale);this._inFlightRoute=n;let I=b!==v.locale;if(!l._h&&this.onlyAHashChange(M)&&!I)return v.asPath=M,W.events.emit("hashChangeStart",n,S),this.changeState(e,t,n,{...l,scroll:!1}),k&&this.scrollToHash(M),this.set(v,this.components[v.route],null),W.events.emit("hashChangeComplete",n,S),!0;let F,B,$=d.parseRelativeUrl(t),{pathname:H,query:U}=$;try{[F,{__rewrites:B}]=await Promise.all([this.pageLoader.getPageList(),o.getClientBuildManifest(),this.pageLoader.getMiddlewareList()])}catch(e){return window.location.href=n,!1}this.urlIsNew(M)||I||(e="replaceState");let V=n;if(H=H?r.removePathTrailingSlash(R(H)):H,p&&"/_error"!==H)if(l._shouldResolveHref=!0,window.omnivoreEnv.__NEXT_HAS_REWRITES&&n.startsWith("/")){const e=h.default(P(_(M,v.locale)),F,B,U,(e=>N(e,F)),this.locales);if(e.externalDest)return location.href=n,!0;V=e.asPath,e.matchedPage&&e.resolvedHref&&(H=e.resolvedHref,$.pathname=P(H),t=y.formatWithValidation($))}else $.pathname=N(H,F),$.pathname!==H&&(H=$.pathname,$.pathname=P(H),t=y.formatWithValidation($));if(!j(n))return window.location.href=n,!1;if(V=C(R(V),v.locale),(!l.shallow||1===l._h)&&(1!==l._h||f.isDynamicRoute(r.removePathTrailingSlash(H)))){const r=await this._preflightRequest({as:n,cache:!0,pages:F,pathname:H,query:U,locale:v.locale,isPreview:v.isPreview});if("rewrite"===r.type)U={...U,...r.parsedAs.query},V=r.asPath,H=r.resolvedHref,$.pathname=r.resolvedHref,t=y.formatWithValidation($);else{if("redirect"===r.type&&r.newAs)return this.change(e,r.newUrl,r.newAs,l);if("redirect"===r.type&&r.destination)return window.location.href=r.destination,new Promise((()=>{}));if("refresh"===r.type&&n!==window.location.pathname)return window.location.href=n,new Promise((()=>{}))}}const q=r.removePathTrailingSlash(H);if(f.isDynamicRoute(q)){const e=d.parseRelativeUrl(V),r=e.pathname,o=m.getRouteRegex(q),i=g.getRouteMatcher(o)(r),a=q===r,l=a?T(q,r,U):{};if(!i||a&&!l.result){const e=Object.keys(o.groups).filter((e=>!U[e]));if(e.length>0)throw new Error((a?`The provided \`href\` (${t}) value is missing query values (${e.join(", ")}) to be interpolated properly. `:`The provided \`as\` value (${r}) is incompatible with the \`href\` value (${q}). `)+"Read more: https://nextjs.org/docs/messages/"+(a?"href-interpolation-failed":"incompatible-href-as"))}else a?n=y.formatWithValidation(Object.assign({},e,{pathname:l.result,query:A(U,l.params)})):Object.assign(U,i)}W.events.emit("routeChangeStart",n,S);try{var K,G;let r=await this.getRouteInfo(q,H,U,n,V,S,v.locale,v.isPreview),{error:o,props:a,__N_SSG:s,__N_SSP:c}=r;const f=r.Component;if(f&&f.unstable_scriptLoader&&[].concat(f.unstable_scriptLoader()).forEach((e=>{i.handleClientScriptLoad(e.props)})),(s||c)&&a){if(a.pageProps&&a.pageProps.__N_REDIRECT){const t=a.pageProps.__N_REDIRECT;if(t.startsWith("/")&&!1!==a.pageProps.__N_REDIRECT_BASE_PATH){const n=d.parseRelativeUrl(t);n.pathname=N(n.pathname,F);const{url:r,as:o}=D(this,t,t);return this.change(e,r,o,l)}return window.location.href=t,new Promise((()=>{}))}if(v.isPreview=!!a.__N_PREVIEW,a.notFound===z){let e;try{await this.fetchComponent("/404"),e="/404"}catch(t){e="/_error"}r=await this.getRouteInfo(e,e,U,n,V,{shallow:!1},v.locale,v.isPreview)}}W.events.emit("beforeHistoryChange",n,S),this.changeState(e,t,n,l),l._h&&"/_error"===H&&500===(null===(K=self.__NEXT_DATA__.props)||void 0===K||null===(G=K.pageProps)||void 0===G?void 0:G.statusCode)&&(null==a?void 0:a.pageProps)&&(a.pageProps.statusCode=500);const p=l.shallow&&v.route===q;var X;const h=(null!==(X=l.scroll)&&void 0!==X?X:!p)?{x:0,y:0}:null;if(await this.set({...v,route:q,pathname:H,query:U,asPath:M,isFallback:!1},r,null!=u?u:h).catch((e=>{if(!e.cancelled)throw e;o=o||e})),o)throw W.events.emit("routeChangeError",o,M,S),o;return window.omnivoreEnv.__NEXT_I18N_SUPPORT&&v.locale&&(document.documentElement.lang=v.locale),W.events.emit("routeChangeComplete",n,S),!0}catch(e){if(a.default(e)&&e.cancelled)return!1;throw e}}changeState(e,t,n,r={}){"pushState"===e&&c.getURL()===n||(this._shallow=r.shallow,window.history[e]({url:t,as:n,options:r,__N:!0,idx:this._idx="pushState"!==e?this._idx:this._idx+1},"",n))}async handleRouteInfoError(e,t,n,r,i,l){if(e.cancelled)throw e;if(o.isAssetError(e)||l)throw W.events.emit("routeChangeError",e,r,i),window.location.href=r,E();try{let r,o,i;void 0!==r&&void 0!==o||({page:r,styleSheets:o}=await this.fetchComponent("/_error"));const a={props:i,Component:r,styleSheets:o,err:e,error:e};if(!a.props)try{a.props=await this.getInitialProps(r,{err:e,pathname:t,query:n})}catch(e){console.error("Error in error page `getInitialProps`: ",e),a.props={}}return a}catch(e){return this.handleRouteInfoError(a.default(e)?e:new Error(e+""),t,n,r,i,!0)}}async getRouteInfo(e,t,n,r,o,i,l,s){try{const a=this.components[e];if(i.shallow&&a&&this.route===e)return a;let u;a&&!("initial"in a)&&(u=a);const c=u||await this.fetchComponent(e).then((e=>({Component:e.page,styleSheets:e.styleSheets,__N_SSG:e.mod.__N_SSG,__N_SSP:e.mod.__N_SSP,__N_RSC:!!e.mod.__next_rsc__}))),{Component:f,__N_SSG:d,__N_SSP:p,__N_RSC:h}=c;let g;const m=p&&h;(d||p||h)&&(g=this.pageLoader.getDataHref({href:y.formatWithValidation({pathname:t,query:n}),asPath:o,ssg:d,flight:m,locale:l}));const v=await this._getData((()=>(d||p||h)&&!m?$(g,this.isSsr,!1,d?this.sdc:this.sdr,!!d&&!s):this.getInitialProps(f,{pathname:t,query:n,asPath:r,locale:l,locales:this.locales,defaultLocale:this.defaultLocale})));if(h)if(m){const{data:e}=await this._getData((()=>this._getFlightData(g)));v.pageProps=Object.assign(v.pageProps,{__flight__:e})}else{const{__flight__:e}=v;v.pageProps=Object.assign({},v.pageProps,{__flight__:e})}return c.props=v,this.components[e]=c,c}catch(e){return this.handleRouteInfoError(a.getProperError(e),t,n,r,i)}}set(e,t,n){return this.state=e,this.sub(t,this.components["/_app"].Component,n)}beforePopState(e){this._bps=e}onlyAHashChange(e){if(!this.asPath)return!1;const[t,n]=this.asPath.split("#"),[r,o]=e.split("#");return!(!o||t!==r||n!==o)||t===r&&n!==o}scrollToHash(e){const[,t=""]=e.split("#");if(""===t||"top"===t)return void window.scrollTo(0,0);const n=document.getElementById(t);if(n)return void n.scrollIntoView();const r=document.getElementsByName(t)[0];r&&r.scrollIntoView()}urlIsNew(e){return this.asPath!==e}async prefetch(e,t=e,n={}){let i=d.parseRelativeUrl(e),{pathname:a,query:l}=i;if(window.omnivoreEnv.__NEXT_I18N_SUPPORT&&!1===n.locale){a=s.normalizeLocalePath(a,this.locales).pathname,i.pathname=a,e=y.formatWithValidation(i);let r=d.parseRelativeUrl(t);const o=s.normalizeLocalePath(r.pathname,this.locales);r.pathname=o.pathname,n.locale=o.detectedLocale||this.defaultLocale,t=y.formatWithValidation(r)}const u=await this.pageLoader.getPageList();let c=t;if(window.omnivoreEnv.__NEXT_HAS_REWRITES&&t.startsWith("/")){let n;({__rewrites:n}=await o.getClientBuildManifest());const r=h.default(P(_(t,this.locale)),u,n,i.query,(e=>N(e,u)),this.locales);if(r.externalDest)return;c=C(R(r.asPath),this.locale),r.matchedPage&&r.resolvedHref&&(a=r.resolvedHref,i.pathname=a,e=y.formatWithValidation(i))}else i.pathname=N(i.pathname,u),i.pathname!==a&&(a=i.pathname,i.pathname=a,e=y.formatWithValidation(i));const f=await this._preflightRequest({as:P(t),cache:!0,pages:u,pathname:a,query:l,locale:this.locale,isPreview:this.isPreview});"rewrite"===f.type&&(i.pathname=f.resolvedHref,a=f.resolvedHref,l={...l,...f.parsedAs.query},c=f.asPath,e=y.formatWithValidation(i));const p=r.removePathTrailingSlash(a);await Promise.all([this.pageLoader._isSsg(p).then((t=>!!t&&$(this.pageLoader.getDataHref({href:e,asPath:c,ssg:!0,locale:void 0!==n.locale?n.locale:this.locale}),!1,!1,this.sdc,!0))),this.pageLoader[n.priority?"loadPage":"prefetch"](p)])}async fetchComponent(e){let t=!1;const n=this.clc=()=>{t=!0},r=()=>{if(t){const t=new Error(`Abort fetching component for route: "${e}"`);throw t.cancelled=!0,t}n===this.clc&&(this.clc=null)};try{const t=await this.pageLoader.loadPage(e);return r(),t}catch(e){throw r(),e}}_getData(e){let t=!1;const n=()=>{t=!0};return this.clc=n,e().then((e=>{if(n===this.clc&&(this.clc=null),t){const e=new Error("Loading initial props cancelled");throw e.cancelled=!0,e}return e}))}_getFlightData(e){return $(e,!0,!0,this.sdc,!1).then((e=>({data:e})))}async _preflightRequest(e){const t=O(e.as),n=C(L(t)?R(t):t,e.locale);if(!(await this.pageLoader.getMiddlewareList()).some((([e,t])=>g.getRouteMatcher(v.getMiddlewareRegex(e,!t))(n))))return{type:"next"};const o=_(e.as,e.locale);let i;try{i=await this._getPreflightData({preflightHref:o,shouldCache:e.cache,isPreview:e.isPreview})}catch(t){return{type:"redirect",destination:e.as}}if(i.rewrite){if(!i.rewrite.startsWith("/"))return{type:"redirect",destination:e.as};const t=d.parseRelativeUrl(s.normalizeLocalePath(L(i.rewrite)?R(i.rewrite):i.rewrite,this.locales).pathname),n=r.removePathTrailingSlash(t.pathname);let o,a;return e.pages.includes(n)?(o=!0,a=n):(a=N(n,e.pages),a!==t.pathname&&e.pages.includes(a)&&(o=!0)),{type:"rewrite",asPath:t.pathname,parsedAs:t,matchedPage:o,resolvedHref:a}}if(i.redirect){if(i.redirect.startsWith("/")){const e=r.removePathTrailingSlash(s.normalizeLocalePath(L(i.redirect)?R(i.redirect):i.redirect,this.locales).pathname),{url:t,as:n}=D(this,e,e);return{type:"redirect",newUrl:t,newAs:n}}return{type:"redirect",destination:i.redirect}}return i.refresh&&!i.ssr?{type:"refresh"}:{type:"next"}}_getPreflightData(e){const{preflightHref:t,shouldCache:n=!1,isPreview:r}=e,{href:o}=new URL(t,window.location.href);return!r&&n&&this.sde[o]?Promise.resolve(this.sde[o]):fetch(t,{method:"HEAD",credentials:"same-origin",headers:{"x-middleware-preflight":"1"}}).then((e=>{if(!e.ok)throw new Error("Failed to preflight request");return{cache:e.headers.get("x-middleware-cache"),redirect:e.headers.get("Location"),refresh:e.headers.has("x-middleware-refresh"),rewrite:e.headers.get("x-middleware-rewrite"),ssr:!!e.headers.get("x-middleware-ssr")}})).then((e=>(n&&"no-cache"!==e.cache&&(this.sde[o]=e),e))).catch((e=>{throw delete this.sde[o],e}))}getInitialProps(e,t){const{Component:n}=this.components["/_app"],r=this._wrapApp(n);return t.AppTree=r,c.loadGetInitialProps(n,{AppTree:r,Component:e,router:this,ctx:t})}abortComponentLoad(e,t){this.clc&&(W.events.emit("routeChangeError",E(),e,t),this.clc(),this.clc=null)}get route(){return this.state.route}get pathname(){return this.state.pathname}get query(){return this.state.query}get asPath(){return this.state.asPath}get locale(){return this.state.locale}get isFallback(){return this.state.isFallback}get isPreview(){return this.state.isPreview}}t.default=W,W.events=u.default()},6555:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.formatUrl=i,t.formatWithValidation=function(e){return i(e)},t.urlObjectKeys=void 0;var r=function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){var r=Object.defineProperty&&Object.getOwnPropertyDescriptor?Object.getOwnPropertyDescriptor(e,n):{};r.get||r.set?Object.defineProperty(t,n,r):t[n]=e[n]}return t.default=e,t}(n(646));const o=/https?|ftp|gopher|file/;function i(e){let{auth:t,hostname:n}=e,i=e.protocol||"",a=e.pathname||"",l=e.hash||"",s=e.query||"",u=!1;t=t?encodeURIComponent(t).replace(/%3A/i,":")+"@":"",e.host?u=t+e.host:n&&(u=t+(~n.indexOf(":")?`[${n}]`:n),e.port&&(u+=":"+e.port)),s&&"object"==typeof s&&(s=String(r.urlQueryToSearchParams(s)));let c=e.search||s&&`?${s}`||"";return i&&!i.endsWith(":")&&(i+=":"),e.slashes||(!i||o.test(i))&&!1!==u?(u="//"+(u||""),a&&"/"!==a[0]&&(a="/"+a)):u||(u=""),l&&"#"!==l[0]&&(l="#"+l),c&&"?"!==c[0]&&(c="?"+c),a=a.replace(/[?#]/g,encodeURIComponent),c=c.replace("#","%23"),`${i}${u}${a}${c}${l}`}t.urlObjectKeys=["auth","hash","host","hostname","href","path","pathname","port","protocol","query","search","slashes"]},9983:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t=""){return("/"===e?"/index":/^\/index(\/|$)/.test(e)?`/index${e}`:`${e}`)+t}},2763:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getMiddlewareRegex=function(e,t=!0){const n=r.getParametrizedRoute(e);let o=t?"(?!_next).*":"",i=t?"(?:(/.*)?)":"";return"routeKeys"in n?"/"===n.parameterizedRoute?{groups:{},namedRegex:`^/${o}$`,re:new RegExp(`^/${o}$`),routeKeys:{}}:{groups:n.groups,namedRegex:`^${n.namedParameterizedRoute}${i}$`,re:new RegExp(`^${n.parameterizedRoute}${i}$`),routeKeys:n.routeKeys}:"/"===n.parameterizedRoute?{groups:{},re:new RegExp(`^/${o}$`)}:{groups:{},re:new RegExp(`^${n.parameterizedRoute}${i}$`)}};var r=n(4794)},9150:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"getMiddlewareRegex",{enumerable:!0,get:function(){return r.getMiddlewareRegex}}),Object.defineProperty(t,"getRouteMatcher",{enumerable:!0,get:function(){return o.getRouteMatcher}}),Object.defineProperty(t,"getRouteRegex",{enumerable:!0,get:function(){return i.getRouteRegex}}),Object.defineProperty(t,"getSortedRoutes",{enumerable:!0,get:function(){return a.getSortedRoutes}}),Object.defineProperty(t,"isDynamicRoute",{enumerable:!0,get:function(){return l.isDynamicRoute}});var r=n(2763),o=n(3107),i=n(4794),a=n(9036),l=n(7482)},7482:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.isDynamicRoute=function(e){return n.test(e)};const n=/\/\[[^/]+?\](?=\/|$)/},1577:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.parseRelativeUrl=function(e,t){const n=new URL("undefined"==typeof window?"http://n":r.getLocationOrigin()),i=t?new URL(t,n):n,{pathname:a,searchParams:l,search:s,hash:u,href:c,origin:f}=new URL(e,i);if(f!==n.origin)throw new Error(`invariant: invalid relative URL, router received ${e}`);return{pathname:a,query:o.searchParamsToUrlQuery(l),search:s,hash:u,href:c.slice(n.origin.length)}};var r=n(1624),o=n(646)},2011:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.parseUrl=function(e){if(e.startsWith("/"))return o.parseRelativeUrl(e);const t=new URL(e);return{hash:t.hash,hostname:t.hostname,href:t.href,pathname:t.pathname,port:t.port,protocol:t.protocol,query:r.searchParamsToUrlQuery(t.searchParams),search:t.search}};var r=n(646),o=n(1577)},1095:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getPathMatch=function(e,t){const n=[],o=r.pathToRegexp(e,n,{delimiter:"/",sensitive:!1,strict:null==t?void 0:t.strict}),i=r.regexpToFunction((null==t?void 0:t.regexModifier)?new RegExp(t.regexModifier(o.source),o.flags):o,n);return(e,r)=>{const o=null!=e&&i(e);if(!o)return!1;if(null==t?void 0:t.removeUnnamedParams)for(const e of n)"number"==typeof e.name&&delete o.params[e.name];return{...r,...o.params}}};var r=n(9264)},9716:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.matchHas=function(e,t,n){const r={};return!!t.every((t=>{let o,i=t.key;switch(t.type){case"header":i=i.toLowerCase(),o=e.headers[i];break;case"cookie":o=e.cookies[t.key];break;case"query":o=n[i];break;case"host":{const{host:t}=(null==e?void 0:e.headers)||{};o=null==t?void 0:t.split(":")[0].toLowerCase();break}}if(!t.value&&o)return r[function(e){let t="";for(let n=0;n64&&r<91||r>96&&r<123)&&(t+=e[n])}return t}(i)]=o,!0;if(o){const e=new RegExp(`^${t.value}$`),n=Array.isArray(o)?o.slice(-1)[0].match(e):o.match(e);if(n)return Array.isArray(n)&&(n.groups?Object.keys(n.groups).forEach((e=>{r[e]=n.groups[e]})):"host"===t.type&&n[0]&&(r.host=n[0])),!0}return!1}))&&r},t.compileNonPath=a,t.prepareDestination=function(e){const t=Object.assign({},e.query);delete t.__nextLocale,delete t.__nextDefaultLocale;let n=e.destination;for(const r of Object.keys({...e.params,...t}))s=r,n=n.replace(new RegExp(`:${o.escapeStringRegexp(s)}`,"g"),`__ESC_COLON_${s}`);var s;const u=i.parseUrl(n),c=u.query,f=l(`${u.pathname}${u.hash||""}`),d=l(u.hostname||""),p=[],h=[];r.pathToRegexp(f,p),r.pathToRegexp(d,h);const g=[];p.forEach((e=>g.push(e.name))),h.forEach((e=>g.push(e.name)));const m=r.compile(f,{validate:!1}),v=r.compile(d,{validate:!1});for(const[t,n]of Object.entries(c))Array.isArray(n)?c[t]=n.map((t=>a(l(t),e.params))):c[t]=a(l(n),e.params);let y,b=Object.keys(e.params).filter((e=>"nextInternalLocale"!==e));if(e.appendParamsToQuery&&!b.some((e=>g.includes(e))))for(const t of b)t in c||(c[t]=e.params[t]);try{y=m(e.params);const[t,n]=y.split("#");u.hostname=v(e.params),u.pathname=t,u.hash=`${n?"#":""}${n||""}`,delete u.search}catch(e){if(e.message.match(/Expected .*? to not repeat, but got an array/))throw new Error("To use a multi-match in the destination you must add `*` at the end of the param name to signify it should repeat. https://nextjs.org/docs/messages/invalid-multi-match");throw e}return u.query={...t,...u.query},{newUrl:y,destQuery:c,parsedDestination:u}};var r=n(9264),o=n(8058),i=n(2011);function a(e,t){if(!e.includes(":"))return e;for(const n of Object.keys(t))e.includes(`:${n}`)&&(e=e.replace(new RegExp(`:${n}\\*`,"g"),`:${n}--ESCAPED_PARAM_ASTERISKS`).replace(new RegExp(`:${n}\\?`,"g"),`:${n}--ESCAPED_PARAM_QUESTION`).replace(new RegExp(`:${n}\\+`,"g"),`:${n}--ESCAPED_PARAM_PLUS`).replace(new RegExp(`:${n}(?!\\w)`,"g"),`--ESCAPED_PARAM_COLON${n}`));return e=e.replace(/(:|\*|\?|\+|\(|\)|\{|\})/g,"\\$1").replace(/--ESCAPED_PARAM_PLUS/g,"+").replace(/--ESCAPED_PARAM_COLON/g,":").replace(/--ESCAPED_PARAM_QUESTION/g,"?").replace(/--ESCAPED_PARAM_ASTERISKS/g,"*"),r.compile(`/${e}`,{validate:!1})(t).slice(1)}function l(e){return e.replace(/__ESC_COLON_/gi,":")}},646:(e,t)=>{"use strict";function n(e){return"string"==typeof e||"number"==typeof e&&!isNaN(e)||"boolean"==typeof e?String(e):""}Object.defineProperty(t,"__esModule",{value:!0}),t.searchParamsToUrlQuery=function(e){const t={};return e.forEach(((e,n)=>{void 0===t[n]?t[n]=e:Array.isArray(t[n])?t[n].push(e):t[n]=[t[n],e]})),t},t.urlQueryToSearchParams=function(e){const t=new URLSearchParams;return Object.entries(e).forEach((([e,r])=>{Array.isArray(r)?r.forEach((r=>t.append(e,n(r)))):t.set(e,n(r))})),t},t.assign=function(e,...t){return t.forEach((t=>{Array.from(t.keys()).forEach((t=>e.delete(t))),t.forEach(((t,n)=>e.append(n,t)))})),e}},5317:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t,n,u,c,f){let d,p=!1,h=!1,g=l.parseRelativeUrl(e),m=i.removePathTrailingSlash(a.normalizeLocalePath(s.delBasePath(g.pathname),f).pathname);const v=n=>{let l=r.getPathMatch(n.source,{removeUnnamedParams:!0,strict:!0})(g.pathname);if(n.has&&l){const e=o.matchHas({headers:{host:document.location.hostname},cookies:document.cookie.split("; ").reduce(((e,t)=>{const[n,...r]=t.split("=");return e[n]=r.join("="),e}),{})},n.has,g.query);e?Object.assign(l,e):l=!1}if(l){if(!n.destination)return h=!0,!0;const r=o.prepareDestination({appendParamsToQuery:!0,destination:n.destination,params:l,query:u});if(g=r.parsedDestination,e=r.newUrl,Object.assign(u,r.parsedDestination.query),m=i.removePathTrailingSlash(a.normalizeLocalePath(s.delBasePath(e),f).pathname),t.includes(m))return p=!0,d=m,!0;if(d=c(m),d!==e&&t.includes(d))return p=!0,!0}};let y=!1;for(let e=0;e{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getRouteMatcher=function(e){const{re:t,groups:n}=e;return e=>{const o=t.exec(e);if(!o)return!1;const i=e=>{try{return decodeURIComponent(e)}catch(e){throw new r.DecodeError("failed to decode param")}},a={};return Object.keys(n).forEach((e=>{const t=n[e],r=o[t.pos];void 0!==r&&(a[e]=~r.indexOf("/")?r.split("/").map((e=>i(e))):t.repeat?[i(r)]:i(r))})),a}};var r=n(1624)},4794:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getParametrizedRoute=i,t.getRouteRegex=function(e){const t=i(e);return"routeKeys"in t?{re:new RegExp(`^${t.parameterizedRoute}(?:/)?$`),groups:t.groups,routeKeys:t.routeKeys,namedRegex:`^${t.namedParameterizedRoute}(?:/)?$`}:{re:new RegExp(`^${t.parameterizedRoute}(?:/)?$`),groups:t.groups}};var r=n(8058);function o(e){const t=e.startsWith("[")&&e.endsWith("]");t&&(e=e.slice(1,-1));const n=e.startsWith("...");return n&&(e=e.slice(3)),{key:e,repeat:n,optional:t}}function i(e){const t=(e.replace(/\/$/,"")||"/").slice(1).split("/"),n={};let i=1;const a=t.map((e=>{if(e.startsWith("[")&&e.endsWith("]")){const{key:t,optional:r,repeat:a}=o(e.slice(1,-1));return n[t]={pos:i++,repeat:a,optional:r},a?r?"(?:/(.+?))?":"/(.+?)":"/([^/]+?)"}return`/${r.escapeStringRegexp(e)}`})).join("");if("undefined"==typeof window){let e=97,i=1;const l=()=>{let t="";for(let n=0;n122&&(i++,e=97);return t},s={};return{parameterizedRoute:a,namedParameterizedRoute:t.map((e=>{if(e.startsWith("[")&&e.endsWith("]")){const{key:t,optional:n,repeat:r}=o(e.slice(1,-1));let i=t.replace(/\W/g,""),a=!1;return(0===i.length||i.length>30)&&(a=!0),isNaN(parseInt(i.slice(0,1)))||(a=!0),a&&(i=l()),s[i]=t,r?n?`(?:/(?<${i}>.+?))?`:`/(?<${i}>.+?)`:`/(?<${i}>[^/]+?)`}return`/${r.escapeStringRegexp(e)}`})).join(""),groups:n,routeKeys:s}}return{parameterizedRoute:a,groups:n}}},9036:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getSortedRoutes=function(e){const t=new n;return e.forEach((e=>t.insert(e))),t.smoosh()};class n{insert(e){this._insert(e.split("/").filter(Boolean),[],!1)}smoosh(){return this._smoosh()}_smoosh(e="/"){const t=[...this.children.keys()].sort();null!==this.slugName&&t.splice(t.indexOf("[]"),1),null!==this.restSlugName&&t.splice(t.indexOf("[...]"),1),null!==this.optionalRestSlugName&&t.splice(t.indexOf("[[...]]"),1);const n=t.map((t=>this.children.get(t)._smoosh(`${e}${t}/`))).reduce(((e,t)=>[...e,...t]),[]);if(null!==this.slugName&&n.push(...this.children.get("[]")._smoosh(`${e}[${this.slugName}]/`)),!this.placeholder){const t="/"===e?"/":e.slice(0,-1);if(null!=this.optionalRestSlugName)throw new Error(`You cannot define a route with the same specificity as a optional catch-all route ("${t}" and "${t}[[...${this.optionalRestSlugName}]]").`);n.unshift(t)}return null!==this.restSlugName&&n.push(...this.children.get("[...]")._smoosh(`${e}[...${this.restSlugName}]/`)),null!==this.optionalRestSlugName&&n.push(...this.children.get("[[...]]")._smoosh(`${e}[[...${this.optionalRestSlugName}]]/`)),n}_insert(e,t,r){if(0===e.length)return void(this.placeholder=!1);if(r)throw new Error("Catch-all must be the last part of the URL.");let o=e[0];if(o.startsWith("[")&&o.endsWith("]")){let i=o.slice(1,-1),a=!1;if(i.startsWith("[")&&i.endsWith("]")&&(i=i.slice(1,-1),a=!0),i.startsWith("...")&&(i=i.substring(3),r=!0),i.startsWith("[")||i.endsWith("]"))throw new Error(`Segment names may not start or end with extra brackets ('${i}').`);if(i.startsWith("."))throw new Error(`Segment names may not start with erroneous periods ('${i}').`);function l(e,n){if(null!==e&&e!==n)throw new Error(`You cannot use different slug names for the same dynamic path ('${e}' !== '${n}').`);t.forEach((e=>{if(e===n)throw new Error(`You cannot have the same slug name "${n}" repeat within a single dynamic path`);if(e.replace(/\W/g,"")===o.replace(/\W/g,""))throw new Error(`You cannot have the slug names "${e}" and "${n}" differ only by non-word symbols within a single dynamic path`)})),t.push(n)}if(r)if(a){if(null!=this.restSlugName)throw new Error(`You cannot use both an required and optional catch-all route at the same level ("[...${this.restSlugName}]" and "${e[0]}" ).`);l(this.optionalRestSlugName,i),this.optionalRestSlugName=i,o="[[...]]"}else{if(null!=this.optionalRestSlugName)throw new Error(`You cannot use both an optional and required catch-all route at the same level ("[[...${this.optionalRestSlugName}]]" and "${e[0]}").`);l(this.restSlugName,i),this.restSlugName=i,o="[...]"}else{if(a)throw new Error(`Optional route parameters are not yet supported ("${e[0]}").`);l(this.slugName,i),this.slugName=i,o="[]"}}this.children.has(o)||this.children.set(o,new n),this.children.get(o)._insert(e.slice(1),t,r)}constructor(){this.placeholder=!0,this.children=new Map,this.slugName=null,this.restSlugName=null,this.optionalRestSlugName=null}}},1624:(e,t)=>{"use strict";function n(){const{protocol:e,hostname:t,port:n}=window.location;return`${e}//${t}${n?":"+n:""}`}function r(e){return"string"==typeof e?e:e.displayName||e.name||"Unknown"}function o(e){return e.finished||e.headersSent}Object.defineProperty(t,"__esModule",{value:!0}),t.execOnce=function(e){let t,n=!1;return(...r)=>(n||(n=!0,t=e(...r)),t)},t.getLocationOrigin=n,t.getURL=function(){const{href:e}=window.location,t=n();return e.substring(t.length)},t.getDisplayName=r,t.isResSent=o,t.normalizeRepeatedSlashes=function(e){const t=e.split("?");return t[0].replace(/\\/g,"/").replace(/\/\/+/g,"/")+(t[1]?`?${t.slice(1).join("?")}`:"")},t.loadGetInitialProps=async function e(t,n){const i=n.res||n.ctx&&n.ctx.res;if(!t.getInitialProps)return n.ctx&&n.Component?{pageProps:await e(n.Component,n.ctx)}:{};const a=await t.getInitialProps(n);if(i&&o(i))return a;if(!a){const e=`"${r(t)}.getInitialProps()" should resolve to an object. But found "${a}" instead.`;throw new Error(e)}return a},t.ST=t.SP=t.warnOnce=void 0,t.warnOnce=e=>{};const i="undefined"!=typeof performance;t.SP=i;const a=i&&"function"==typeof performance.mark&&"function"==typeof performance.measure;t.ST=a;class l extends Error{}t.DecodeError=l;class s extends Error{}t.NormalizeError=s},9097:(e,t,n)=>{e.exports=n(4529)},5632:(e,t,n)=>{e.exports=n(9518)},7320:e=>{"use strict";var t=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,r=Object.prototype.propertyIsEnumerable;function o(e){if(null==e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map((function(e){return t[e]})).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach((function(e){r[e]=e})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(e){return!1}}()?Object.assign:function(e,i){for(var a,l,s=o(e),u=1;u{"use strict";var r=n(2784),o=n(7320),i=n(4616);function a(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n