From 270412bc493680c565da5a50faef745b11915796 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 5 Jan 2023 20:50:04 -0800 Subject: [PATCH 01/31] inject networker into data service --- .../app/src/main/java/app/omnivore/omnivore/AppModule.kt | 5 ++++- .../app/src/main/java/app/omnivore/omnivore/DataService.kt | 4 +++- .../java/app/omnivore/omnivore/persistence/AppDatabase.kt | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/AppModule.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/AppModule.kt index a395d831f..c31cf9457 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/AppModule.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/AppModule.kt @@ -31,5 +31,8 @@ object AppModule { @Singleton @Provides - fun provideDataService(@ApplicationContext app: Context) = DataService(app) + fun provideDataService( + @ApplicationContext app: Context, + networker: Networker + ) = DataService(app, networker) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt index 651b7311a..07f449474 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt @@ -2,11 +2,13 @@ package app.omnivore.omnivore import android.content.Context import androidx.room.Room +import app.omnivore.omnivore.networking.Networker import app.omnivore.omnivore.persistence.AppDatabase import javax.inject.Inject class DataService @Inject constructor( - context: Context + context: Context, + private val networker: Networker ) { val db = Room.databaseBuilder( context, 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 7d9d66b94..f78470795 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 @@ -12,7 +12,7 @@ import app.omnivore.omnivore.persistence.entities.ViewerDao Viewer::class, SavedItem::class ], - version = 1 + version = 2 ) abstract class AppDatabase : RoomDatabase() { abstract fun viewerDao(): ViewerDao From 2bb2e806ffff6db3be7b42d32017a92f36a3ebff Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 9 Jan 2023 15:07:37 -0800 Subject: [PATCH 02/31] add updatesSince API to Android --- .../app/src/main/graphql/UpdatesSince.graphql | 56 ++++++++++++++ .../java/app/omnivore/omnivore/Constants.kt | 1 + .../java/app/omnivore/omnivore/DataService.kt | 4 + .../omnivore/models/ServerSyncStatus.kt | 11 +++ .../omnivore/networking/HighlightMutations.kt | 4 +- .../omnivore/networking/SavedItemQuery.kt | 1 - .../omnivore/networking/SearchQuery.kt | 73 ++++++++++++++++++- .../persistence/entities/Highlight.kt | 1 + .../persistence/entities/SavedItem.kt | 3 +- .../persistence/entities/SavedItemLabel.kt | 1 + .../omnivore/ui/library/LibraryViewModel.kt | 24 ++++-- .../App/Views/Home/HomeFeedViewModel.swift | 7 ++ 12 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 android/Omnivore/app/src/main/graphql/UpdatesSince.graphql create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/models/ServerSyncStatus.kt diff --git a/android/Omnivore/app/src/main/graphql/UpdatesSince.graphql b/android/Omnivore/app/src/main/graphql/UpdatesSince.graphql new file mode 100644 index 000000000..e7426f016 --- /dev/null +++ b/android/Omnivore/app/src/main/graphql/UpdatesSince.graphql @@ -0,0 +1,56 @@ +query UpdatesSince($after: String, $first: Int, $since: Date!) { + updatesSince(after: $after, first: $first, since: $since) { + ... on UpdatesSinceSuccess { + edges { + cursor + itemID + updateReason + node { + id + title + slug + url + pageType + contentReader + createdAt + isArchived + readingProgressPercent + readingProgressAnchorIndex + author + image + description + publishedAt + ownedByViewer + originalArticleUrl + uploadFileId + labels { + id + name + color + } + pageId + shortId + quote + annotation + state + siteName + subscription + readAt + savedAt + updatedAt + language + } + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + totalCount + } + } + ... on UpdatesSinceError { + errorCodes + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt index 075eb297b..a7a015770 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt @@ -12,6 +12,7 @@ object DatastoreKeys { const val omnivoreAuthToken = "omnivoreAuthToken" const val omnivoreAuthCookieString = "omnivoreAuthCookieString" const val omnivorePendingUserToken = "omnivorePendingUserToken" + const val libraryLastSyncTimestamp = "libraryLastSyncTimestamp" const val preferredWebFontSize = "preferredWebFontSize" const val preferredWebLineHeight = "preferredWebLineHeight" const val preferredWebMaxWidthPercentage = "preferredWebMaxWidthPercentage" diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt index 07f449474..460c5486c 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt @@ -15,3 +15,7 @@ class DataService @Inject constructor( AppDatabase::class.java, "omnivore-database" ).build() } + +//suspend fun DataService.sync(): Boolean { +// +//} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/models/ServerSyncStatus.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/models/ServerSyncStatus.kt new file mode 100644 index 000000000..e55ffe1ef --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/models/ServerSyncStatus.kt @@ -0,0 +1,11 @@ +package app.omnivore.omnivore.models + +public enum class ServerSyncStatus( + public val rawValue: Int, +) { + IS_SYNCED(0), + IS_SYNCING(1), + NEEDS_DELETION(2), + NEEDS_CREATION(3), + NEEDS_UPDATE(4) +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt index 866b694e3..bf9ea94c6 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt @@ -8,6 +8,7 @@ import app.omnivore.omnivore.graphql.generated.UpdateHighlightMutation import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput +import app.omnivore.omnivore.models.ServerSyncStatus import app.omnivore.omnivore.persistence.entities.Highlight import com.apollographql.apollo3.api.Optional import com.google.gson.Gson @@ -150,8 +151,7 @@ suspend fun Networker.createHighlight(input: CreateHighlightInput): Highlight? { createdAt = null, // TODO: update gql query to get this updatedAt = null, // TODO: fix updatedAtString?.let { LocalDate.parse(it) }, createdByMe = createdHighlight.highlightFields.createdByMe, - markedForDeletion = false, - serverSyncStatus = 1 // TODO: create enum for this + markedForDeletion = false ) } else { return null diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt index 107d17e12..882c3d304 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt @@ -53,7 +53,6 @@ suspend fun Networker.savedItem(slug: String): SavedItemQueryResponse { updatedAt = null, //updatedAtString?.let { str -> LocalDate.parse(str) }, TODO: fix date parsing createdByMe = it.highlightFields.createdByMe, markedForDeletion = false, - serverSyncStatus = 1 // TODO: create enum for this ) } 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 429030f6c..1c705280f 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,6 +2,8 @@ package app.omnivore.omnivore.networking import app.omnivore.omnivore.graphql.generated.SearchQuery import app.omnivore.omnivore.graphql.generated.TypeaheadSearchQuery +import app.omnivore.omnivore.graphql.generated.UpdatesSinceQuery +import app.omnivore.omnivore.graphql.generated.type.UpdateReason import app.omnivore.omnivore.persistence.entities.SavedItem import app.omnivore.omnivore.persistence.entities.SavedItemCardData import com.apollographql.apollo3.api.Optional @@ -11,6 +13,14 @@ data class SearchQueryResponse( val cardsData: List ) +data class SavedItemUpdatesQueryResponse( + val cursor: String?, + val hasMoreItems: Boolean, + val totalCount: Int, + val deletedItemIDs: List, + val items: List +) + suspend fun Networker.typeaheadSearch( query: String ): SearchQueryResponse { @@ -55,7 +65,6 @@ suspend fun Networker.search( ) ).execute() - val newCursor = result.data?.search?.onSearchSuccess?.pageInfo?.endCursor val itemList = result.data?.search?.onSearchSuccess?.edges ?: listOf() @@ -78,3 +87,65 @@ suspend fun Networker.search( return SearchQueryResponse(null, listOf()) } } + +suspend fun Networker.savedItemUpdates( + cursor: String? = null, + limit: Int = 15, + since: String +): SavedItemUpdatesQueryResponse? { + try { + val result = authenticatedApolloClient().query( + UpdatesSinceQuery( + after = Optional.presentIfNotNull(cursor), + first = Optional.presentIfNotNull(limit), + since = since + ) + ).execute() + + val payload = result.data?.updatesSince?.onUpdatesSinceSuccess ?: return null + val itemNodes: MutableList = mutableListOf() + val deletedItemIDs: MutableList = mutableListOf() + + for (edge in payload.edges) { + if (edge.updateReason == UpdateReason.DELETED) { + deletedItemIDs.add(edge.itemID) + } else if (edge.node != null) { + itemNodes.add(edge.node) + } + } + + val savedItems = itemNodes.map { + SavedItem( + id = it.id, + title = it.title, + createdAt = it.createdAt as String, + savedAt = it.savedAt as String, + readAt = it.readAt as String?, + updatedAt = it.updatedAt as String?, + readingProgress = it.readingProgressPercent, + readingProgressAnchor = it.readingProgressAnchorIndex, + imageURLString = it.image, + pageURLString = it.url, + descriptionText = it.description, + publisherURLString = it.originalArticleUrl, + siteName = it.siteName, + author = it.author, + publishDate = it.publishedAt as String?, + slug = it.slug, + isArchived = it.isArchived, + contentReader = it.contentReader.rawValue, + content = null + ) + } + + return SavedItemUpdatesQueryResponse( + cursor = payload.pageInfo.endCursor, + hasMoreItems = payload.pageInfo.hasNextPage, + totalCount = savedItems.size, + deletedItemIDs = deletedItemIDs, + items = savedItems + ) + } catch (e: java.lang.Exception) { + return null + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt index 7b2331330..327f039fc 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt @@ -2,6 +2,7 @@ package app.omnivore.omnivore.persistence.entities import androidx.room.Entity import androidx.room.PrimaryKey +import app.omnivore.omnivore.models.ServerSyncStatus import java.time.LocalDate import java.util.Date diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt index 33cc02ca0..1f0935697 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt @@ -2,6 +2,7 @@ package app.omnivore.omnivore.persistence.entities import androidx.core.net.toUri import androidx.room.* +import app.omnivore.omnivore.models.ServerSyncStatus import app.omnivore.omnivore.persistence.BaseDao @Entity @@ -35,7 +36,7 @@ data class SavedItem( val onDeviceImageURLString: String? = null, val originalHtml: String? = null, @ColumnInfo(typeAffinity = ColumnInfo.BLOB) val pdfData: ByteArray? = null, - val serverSyncStatus: Int = 0, // TODO: implement, + val serverSyncStatus: Int = 0, val tempPDFURL: String? = null // hasMany highlights 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 14a0bbdf0..f13983838 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 @@ -2,6 +2,7 @@ package app.omnivore.omnivore.persistence.entities import androidx.room.Entity import androidx.room.PrimaryKey +import app.omnivore.omnivore.models.ServerSyncStatus @Entity data class SavedItemLabel( 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 e6fecd759..e3b41b37a 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 @@ -8,19 +8,20 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.omnivore.omnivore.DataService +import app.omnivore.omnivore.DatastoreKeys +import app.omnivore.omnivore.DatastoreRepository import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.persistence.entities.SavedItemCardData import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* +import java.time.LocalDateTime import javax.inject.Inject @HiltViewModel class LibraryViewModel @Inject constructor( private val networker: Networker, - private val dataService: DataService + private val dataService: DataService, + private val datastoreRepo: DatastoreRepository ): ViewModel() { private var cursor: String? = null private var items: List = listOf() @@ -51,6 +52,19 @@ class LibraryViewModel @Inject constructor( load(true) } + fun getLastSyncTime(): LocalDateTime? = runBlocking { + datastoreRepo.getString(DatastoreKeys.libraryLastSyncTimestamp)?.let { + LocalDateTime.parse(it) + } + } + + fun syncItems() { + val syncStart = LocalDateTime.now() + val lastSyncDate = getLastSyncTime() ?: LocalDateTime.MIN + +// try? await dataService.syncOfflineItemsWithServerIfNeeded() + } + fun load(clearPreviousSearch: Boolean = false) { if (clearPreviousSearch) { cursor = null diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 8c99130f9..c0f0b1520 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -107,6 +107,13 @@ import Views _ = try? await dataService.labels() } } + + // Sync Items + // 1 - Create start timestamp + // 2 - Retrieve last sync time from datastore + // 3 - Call syncOfflineItemsWithServerIfNeeded (DataService) + // 4 - Call dataService.syncLinkedItems + // func syncItems(dataService: DataService) async { let syncStart = Date.now From 87c1800d705f01efbe0a41918312839624f975f0 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 9 Jan 2023 15:57:03 -0800 Subject: [PATCH 03/31] stub in sync call --- .../java/app/omnivore/omnivore/DataService.kt | 19 ++++- .../omnivore/networking/SearchQuery.kt | 73 ------------------- .../omnivore/ui/library/LibraryViewModel.kt | 12 +-- 3 files changed, 22 insertions(+), 82 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt index 460c5486c..e155ecc82 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt @@ -1,14 +1,16 @@ package app.omnivore.omnivore import android.content.Context +import android.util.Log import androidx.room.Room import app.omnivore.omnivore.networking.Networker +import app.omnivore.omnivore.networking.savedItemUpdates import app.omnivore.omnivore.persistence.AppDatabase import javax.inject.Inject class DataService @Inject constructor( context: Context, - private val networker: Networker + val networker: Networker ) { val db = Room.databaseBuilder( context, @@ -16,6 +18,15 @@ class DataService @Inject constructor( ).build() } -//suspend fun DataService.sync(): Boolean { -// -//} +suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 15): Boolean { + val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since) ?: return false + + Log.d("sync", "count: ${syncResult.totalCount}; sync result: $syncResult") + // TODO: Store items in Room DB + + return true +} + +suspend fun DataService.syncOfflineItemsWithServerIfNeeded() { + // TODO: implement this +} 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 1c705280f..ecb84e728 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,9 +2,6 @@ package app.omnivore.omnivore.networking import app.omnivore.omnivore.graphql.generated.SearchQuery import app.omnivore.omnivore.graphql.generated.TypeaheadSearchQuery -import app.omnivore.omnivore.graphql.generated.UpdatesSinceQuery -import app.omnivore.omnivore.graphql.generated.type.UpdateReason -import app.omnivore.omnivore.persistence.entities.SavedItem import app.omnivore.omnivore.persistence.entities.SavedItemCardData import com.apollographql.apollo3.api.Optional @@ -13,14 +10,6 @@ data class SearchQueryResponse( val cardsData: List ) -data class SavedItemUpdatesQueryResponse( - val cursor: String?, - val hasMoreItems: Boolean, - val totalCount: Int, - val deletedItemIDs: List, - val items: List -) - suspend fun Networker.typeaheadSearch( query: String ): SearchQueryResponse { @@ -87,65 +76,3 @@ suspend fun Networker.search( return SearchQueryResponse(null, listOf()) } } - -suspend fun Networker.savedItemUpdates( - cursor: String? = null, - limit: Int = 15, - since: String -): SavedItemUpdatesQueryResponse? { - try { - val result = authenticatedApolloClient().query( - UpdatesSinceQuery( - after = Optional.presentIfNotNull(cursor), - first = Optional.presentIfNotNull(limit), - since = since - ) - ).execute() - - val payload = result.data?.updatesSince?.onUpdatesSinceSuccess ?: return null - val itemNodes: MutableList = mutableListOf() - val deletedItemIDs: MutableList = mutableListOf() - - for (edge in payload.edges) { - if (edge.updateReason == UpdateReason.DELETED) { - deletedItemIDs.add(edge.itemID) - } else if (edge.node != null) { - itemNodes.add(edge.node) - } - } - - val savedItems = itemNodes.map { - SavedItem( - id = it.id, - title = it.title, - createdAt = it.createdAt as String, - savedAt = it.savedAt as String, - readAt = it.readAt as String?, - updatedAt = it.updatedAt as String?, - readingProgress = it.readingProgressPercent, - readingProgressAnchor = it.readingProgressAnchorIndex, - imageURLString = it.image, - pageURLString = it.url, - descriptionText = it.description, - publisherURLString = it.originalArticleUrl, - siteName = it.siteName, - author = it.author, - publishDate = it.publishedAt as String?, - slug = it.slug, - isArchived = it.isArchived, - contentReader = it.contentReader.rawValue, - content = null - ) - } - - return SavedItemUpdatesQueryResponse( - cursor = payload.pageInfo.endCursor, - hasMoreItems = payload.pageInfo.hasNextPage, - totalCount = savedItems.size, - deletedItemIDs = deletedItemIDs, - items = savedItems - ) - } catch (e: java.lang.Exception) { - return null - } -} 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 e3b41b37a..5fbb65b5e 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 @@ -7,9 +7,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import app.omnivore.omnivore.DataService -import app.omnivore.omnivore.DatastoreKeys -import app.omnivore.omnivore.DatastoreRepository +import app.omnivore.omnivore.* import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.persistence.entities.SavedItemCardData import dagger.hilt.android.lifecycle.HiltViewModel @@ -58,11 +56,14 @@ class LibraryViewModel @Inject constructor( } } - fun syncItems() { + suspend fun syncItems() { val syncStart = LocalDateTime.now() val lastSyncDate = getLastSyncTime() ?: LocalDateTime.MIN -// try? await dataService.syncOfflineItemsWithServerIfNeeded() + withContext(Dispatchers.IO) { + dataService.syncOfflineItemsWithServerIfNeeded() + dataService.sync(since = lastSyncDate.toString(), cursor = null) + } } fun load(clearPreviousSearch: Boolean = false) { @@ -71,6 +72,7 @@ class LibraryViewModel @Inject constructor( } viewModelScope.launch { + syncItems() val thisSearchIdx = searchIdx searchIdx += 1 From 02818ab8f19a1e43edcc08e79e67972630a3130c Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 9 Jan 2023 15:57:32 -0800 Subject: [PATCH 04/31] create SavedItemUpdatesQuery --- .../networking/SavedItemUpdatesQuery.kt | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemUpdatesQuery.kt diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemUpdatesQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemUpdatesQuery.kt new file mode 100644 index 000000000..be79a86da --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemUpdatesQuery.kt @@ -0,0 +1,76 @@ +package app.omnivore.omnivore.networking + +import app.omnivore.omnivore.graphql.generated.UpdatesSinceQuery +import app.omnivore.omnivore.graphql.generated.type.UpdateReason +import app.omnivore.omnivore.persistence.entities.SavedItem +import com.apollographql.apollo3.api.Optional + +data class SavedItemUpdatesQueryResponse( + val cursor: String?, + val hasMoreItems: Boolean, + val totalCount: Int, + val deletedItemIDs: List, + val items: List +) + +suspend fun Networker.savedItemUpdates( + cursor: String? = null, + limit: Int = 15, + since: String +): SavedItemUpdatesQueryResponse? { + try { + val result = authenticatedApolloClient().query( + UpdatesSinceQuery( + after = Optional.presentIfNotNull(cursor), + first = Optional.presentIfNotNull(limit), + since = since + ) + ).execute() + + val payload = result.data?.updatesSince?.onUpdatesSinceSuccess ?: return null + val itemNodes: MutableList = mutableListOf() + val deletedItemIDs: MutableList = mutableListOf() + + for (edge in payload.edges) { + if (edge.updateReason == UpdateReason.DELETED) { + deletedItemIDs.add(edge.itemID) + } else if (edge.node != null) { + itemNodes.add(edge.node) + } + } + + val savedItems = itemNodes.map { + SavedItem( + id = it.id, + title = it.title, + createdAt = it.createdAt as String, + savedAt = it.savedAt as String, + readAt = it.readAt as String?, + updatedAt = it.updatedAt as String?, + readingProgress = it.readingProgressPercent, + readingProgressAnchor = it.readingProgressAnchorIndex, + imageURLString = it.image, + pageURLString = it.url, + descriptionText = it.description, + publisherURLString = it.originalArticleUrl, + siteName = it.siteName, + author = it.author, + publishDate = it.publishedAt as String?, + slug = it.slug, + isArchived = it.isArchived, + contentReader = it.contentReader.rawValue, + content = null + ) + } + + return SavedItemUpdatesQueryResponse( + cursor = payload.pageInfo.endCursor, + hasMoreItems = payload.pageInfo.hasNextPage, + totalCount = savedItems.size, + deletedItemIDs = deletedItemIDs, + items = savedItems + ) + } catch (e: java.lang.Exception) { + return null + } +} From eeddb810b3f3545f3c40c0e6410d2860fc0b8b38 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 9 Jan 2023 16:00:14 -0800 Subject: [PATCH 05/31] remove notes in swift file --- .../Sources/App/Views/Home/HomeFeedViewModel.swift | 7 ------- 1 file changed, 7 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index c0f0b1520..8c99130f9 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -107,13 +107,6 @@ import Views _ = try? await dataService.labels() } } - - // Sync Items - // 1 - Create start timestamp - // 2 - Retrieve last sync time from datastore - // 3 - Call syncOfflineItemsWithServerIfNeeded (DataService) - // 4 - Call dataService.syncLinkedItems - // func syncItems(dataService: DataService) async { let syncStart = Date.now From 8faf23d4bade011b4cb426d5c24ce4c4641f89e2 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 10 Jan 2023 11:04:52 -0800 Subject: [PATCH 06/31] load library list using room db --- .../java/app/omnivore/omnivore/DataService.kt | 25 ++++- .../omnivore/networking/SavedItemQuery.kt | 1 + .../omnivore/ui/library/LibraryViewModel.kt | 97 ++++++++++--------- 3 files changed, 75 insertions(+), 48 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt index e155ecc82..80790f8c1 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt @@ -18,15 +18,32 @@ class DataService @Inject constructor( ).build() } -suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 15): Boolean { - val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since) ?: return false +suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 15): SavedItemSyncMarker { + val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since) ?: return SavedItemSyncMarker.errorResult Log.d("sync", "count: ${syncResult.totalCount}; sync result: $syncResult") - // TODO: Store items in Room DB - return true + db.savedItemDao().insertAll(syncResult.items) + + return SavedItemSyncMarker( + hasError = false, + hasMoreItems = syncResult.hasMoreItems, + cursor = syncResult.cursor, + count = syncResult.items.size + ) } suspend fun DataService.syncOfflineItemsWithServerIfNeeded() { // TODO: implement this } + +data class SavedItemSyncMarker( + val hasError: Boolean, + val hasMoreItems: Boolean, + val count: Int, + val cursor: String? +) { + companion object { + val errorResult = SavedItemSyncMarker(hasError = true, hasMoreItems = true, cursor = null, count = 0) + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt index 882c3d304..0133d3dbd 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt @@ -53,6 +53,7 @@ suspend fun Networker.savedItem(slug: String): SavedItemQueryResponse { updatedAt = null, //updatedAtString?.let { str -> LocalDate.parse(str) }, TODO: fix date parsing createdByMe = it.highlightFields.createdByMe, markedForDeletion = false, + serverSyncStatus = 0 ) } 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 5fbb65b5e..1b43c0d67 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 @@ -56,63 +56,72 @@ class LibraryViewModel @Inject constructor( } } - suspend fun syncItems() { + private suspend fun syncItems() { val syncStart = LocalDateTime.now() val lastSyncDate = getLastSyncTime() ?: LocalDateTime.MIN withContext(Dispatchers.IO) { - dataService.syncOfflineItemsWithServerIfNeeded() - dataService.sync(since = lastSyncDate.toString(), cursor = null) + performItemSync(cursor = null, since = lastSyncDate.toString(), count = 0) } } - fun load(clearPreviousSearch: Boolean = false) { + private suspend fun performItemSync(cursor: String?, since: String, count: Int) { + dataService.syncOfflineItemsWithServerIfNeeded() + val result = dataService.sync(since = since, cursor = cursor) + val totalCount = count + result.count + + Log.d("sync", "grabbed ${result.count} items in this batch") + + if (totalCount < 180 && !result.hasError && result.hasMoreItems && result.cursor != null) { + performItemSync(cursor = result.cursor, since = since, count = totalCount) + } else { + Log.d("sync", "grabbed $count total items") + + val items = dataService.db.savedItemDao().getLibraryData() + + itemsLiveData.postValue(items) + + CoroutineScope(Dispatchers.Main).launch { + isRefreshing = false + } + } + } + + private suspend fun performSearch(clearPreviousSearch: Boolean) { if (clearPreviousSearch) { cursor = null } + val thisSearchIdx = searchIdx + searchIdx += 1 + + // Execute the search + 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. + // For example if a user types 'Canucks', often the search results + // for 'C' are returned after 'Canucks' because it takes the backend + // much longer to compute. + if (thisSearchIdx in 1..receivedIdx) { + return + } + + val previousItems = if (clearPreviousSearch) listOf() else searchedItems + searchedItems = previousItems.plus(searchResult.cardsData) + itemsLiveData.postValue(searchedItems) + + CoroutineScope(Dispatchers.Main).launch { + isRefreshing = false + } + } + + fun load(clearPreviousSearch: Boolean = false) { viewModelScope.launch { - syncItems() - val thisSearchIdx = searchIdx - searchIdx += 1 - - // Execute the search - val searchResult = - if (searchTextLiveData.value != "") { - networker.typeaheadSearch(searchTextLiveData.value ?: "") - } else { - networker.search(cursor = cursor, query = searchQuery()) - } - - // Search results aren't guaranteed to return in order so this - // will discard old results that are returned while a user is typing. - // For example if a user types 'Canucks', often the search results - // for 'C' are returned after 'Canucks' because it takes the backend - // much longer to compute. - if (thisSearchIdx in 1..receivedIdx) { - return@launch - } - - receivedIdx = thisSearchIdx - cursor = searchResult.cursor - - if (searchTextLiveData.value != "" || clearPreviousSearch) { - val previousItems = if (clearPreviousSearch) listOf() else searchedItems - searchedItems = previousItems.plus(searchResult.cardsData) - itemsLiveData.postValue(searchedItems) + if (searchTextLiveData.value != "") { + performSearch(clearPreviousSearch) } else { - items = items.plus(searchResult.cardsData) - itemsLiveData.postValue(items) - } - -// withContext(Dispatchers.IO) { -// dataService.db.savedItemDao().insertAll(items) -// val items = dataService.db.savedItemDao().getLibraryData() -// Log.d("appDatabase", "libraryData: $items") -// } - - CoroutineScope(Dispatchers.Main).launch { - isRefreshing = false + syncItems() } } } From 8349a05dfd73f696fe4694751c476077002385cc Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 10 Jan 2023 12:51:41 -0800 Subject: [PATCH 07/31] discard old search results when new results come in --- .../omnivore/ui/library/LibraryViewModel.kt | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryViewModel.kt index 1b43c0d67..b555199d6 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 @@ -56,10 +56,24 @@ class LibraryViewModel @Inject constructor( } } + fun load(clearPreviousSearch: Boolean = false) { + viewModelScope.launch { + if (searchTextLiveData.value != "") { + performSearch(clearPreviousSearch) + } else { + syncItems() + } + } + } + private suspend fun syncItems() { val syncStart = LocalDateTime.now() val lastSyncDate = getLastSyncTime() ?: LocalDateTime.MIN + CoroutineScope(Dispatchers.Main).launch { + isRefreshing = false + } + withContext(Dispatchers.IO) { performItemSync(cursor = null, since = lastSyncDate.toString(), count = 0) } @@ -80,10 +94,6 @@ class LibraryViewModel @Inject constructor( val items = dataService.db.savedItemDao().getLibraryData() itemsLiveData.postValue(items) - - CoroutineScope(Dispatchers.Main).launch { - isRefreshing = false - } } } @@ -108,7 +118,7 @@ class LibraryViewModel @Inject constructor( } val previousItems = if (clearPreviousSearch) listOf() else searchedItems - searchedItems = previousItems.plus(searchResult.cardsData) + searchedItems = searchResult.cardsData itemsLiveData.postValue(searchedItems) CoroutineScope(Dispatchers.Main).launch { @@ -116,16 +126,6 @@ class LibraryViewModel @Inject constructor( } } - fun load(clearPreviousSearch: Boolean = false) { - viewModelScope.launch { - if (searchTextLiveData.value != "") { - performSearch(clearPreviousSearch) - } else { - syncItems() - } - } - } - fun handleSavedItemAction(itemID: String, action: SavedItemAction) { when (action) { SavedItemAction.Delete -> { From 26e0c78e19ac54fc9ee76a9a5eef69fd22ed6c1c Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 10 Jan 2023 15:23:10 -0800 Subject: [PATCH 08/31] use live data function from room to render library list --- .../java/app/omnivore/omnivore/DataService.kt | 2 -- .../persistence/entities/SavedItem.kt | 11 ++++--- .../omnivore/ui/library/LibraryView.kt | 4 ++- .../omnivore/ui/library/LibraryViewModel.kt | 33 ++++++++----------- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt index 80790f8c1..cac359629 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt @@ -21,8 +21,6 @@ class DataService @Inject constructor( suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 15): SavedItemSyncMarker { val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since) ?: return SavedItemSyncMarker.errorResult - Log.d("sync", "count: ${syncResult.totalCount}; sync result: $syncResult") - db.savedItemDao().insertAll(syncResult.items) return SavedItemSyncMarker( diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt index 1f0935697..0a9a1c58c 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt @@ -1,9 +1,9 @@ package app.omnivore.omnivore.persistence.entities import androidx.core.net.toUri +import androidx.lifecycle.LiveData import androidx.room.* -import app.omnivore.omnivore.models.ServerSyncStatus -import app.omnivore.omnivore.persistence.BaseDao +import java.util.* @Entity data class SavedItem( @@ -105,12 +105,15 @@ data class SavedItemCardData( @Dao interface SavedItemDao { - @Query("SELECT id, slug, publisherURLString, title, author, imageURLString, isArchived, pageURLString, contentReader FROM SavedItem") - fun getLibraryData(): List + @Query("SELECT id, slug, publisherURLString, title, author, imageURLString, isArchived, pageURLString, contentReader FROM SavedItem ORDER BY savedAt DESC") + fun getLibraryLiveData(): LiveData> @Query("SELECT * FROM savedItem") fun getAll(): List @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAll(items: List) + + @Query("DELETE FROM savedItem WHERE id = :itemID") + fun deleteById(itemID: String) } 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 35a16552d..cf6bac4c7 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 @@ -72,6 +72,8 @@ fun LibraryViewContent( ) 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 @@ -87,7 +89,7 @@ fun LibraryViewContent( .fillMaxSize() .padding(horizontal = 6.dp) ) { - items(cardsData) { cardData -> + items(if (searchText.isNotEmpty()) searchedCardsData else cardsData) { cardData -> SavedItemCard( cardData = cardData, onClickHandler = { 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 b555199d6..31226c063 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 @@ -22,8 +22,6 @@ class LibraryViewModel @Inject constructor( private val datastoreRepo: DatastoreRepository ): ViewModel() { private var cursor: String? = null - private var items: List = listOf() - private var searchedItems: List = listOf() // These are used to make sure we handle search result // responses in the right order @@ -32,14 +30,16 @@ class LibraryViewModel @Inject constructor( // Live Data val searchTextLiveData = MutableLiveData("") - val itemsLiveData = MutableLiveData>(listOf()) + val searchItemsLiveData = MutableLiveData>(listOf()) + val itemsLiveData = dataService.db.savedItemDao().getLibraryLiveData() + var isRefreshing by mutableStateOf(false) fun updateSearchText(text: String) { searchTextLiveData.value = text if (text == "") { - itemsLiveData.value = items + searchItemsLiveData.value = listOf() } else { load(clearPreviousSearch = true) } @@ -75,25 +75,21 @@ class LibraryViewModel @Inject constructor( } withContext(Dispatchers.IO) { - performItemSync(cursor = null, since = lastSyncDate.toString(), count = 0) + performItemSync(cursor = null, since = lastSyncDate.toString(), count = 0, startTime = syncStart.toString()) } } - private suspend fun performItemSync(cursor: String?, since: String, count: Int) { + private suspend fun performItemSync(cursor: String?, since: String, count: Int, startTime: String) { dataService.syncOfflineItemsWithServerIfNeeded() val result = dataService.sync(since = since, cursor = cursor) val totalCount = count + result.count - Log.d("sync", "grabbed ${result.count} items in this batch") + Log.d("sync", "fetched ${result.count} items") if (totalCount < 180 && !result.hasError && result.hasMoreItems && result.cursor != null) { - performItemSync(cursor = result.cursor, since = since, count = totalCount) + performItemSync(cursor = result.cursor, since = since, count = totalCount, startTime = startTime) } else { - Log.d("sync", "grabbed $count total items") - - val items = dataService.db.savedItemDao().getLibraryData() - - itemsLiveData.postValue(items) + datastoreRepo.putString(DatastoreKeys.libraryLastSyncTimestamp, startTime) } } @@ -117,9 +113,7 @@ class LibraryViewModel @Inject constructor( return } - val previousItems = if (clearPreviousSearch) listOf() else searchedItems - searchedItems = searchResult.cardsData - itemsLiveData.postValue(searchedItems) + searchItemsLiveData.postValue(searchResult.cardsData) CoroutineScope(Dispatchers.Main).launch { isRefreshing = false @@ -151,9 +145,10 @@ class LibraryViewModel @Inject constructor( } private fun removeItemFromList(itemID: String) { - itemsLiveData.value?.let { - val newList = it.filter { item -> item.id != itemID } - itemsLiveData.postValue(newList) + viewModelScope.launch { + withContext(Dispatchers.IO) { + dataService.db.savedItemDao().deleteById(itemID) + } } } From 985d1ff7ae4cfbb17e0b7cd612c288b2750d31e4 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 11 Jan 2023 10:57:20 -0800 Subject: [PATCH 09/31] stub in syncSavedItemContent function in data service --- .../java/app/omnivore/omnivore/DataService.kt | 22 +++++++++++++------ .../omnivore/networking/HighlightMutations.kt | 3 ++- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt index cac359629..7a127ba1a 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt @@ -1,9 +1,9 @@ package app.omnivore.omnivore import android.content.Context -import android.util.Log import androidx.room.Room import app.omnivore.omnivore.networking.Networker +import app.omnivore.omnivore.networking.savedItem import app.omnivore.omnivore.networking.savedItemUpdates import app.omnivore.omnivore.persistence.AppDatabase import javax.inject.Inject @@ -18,30 +18,38 @@ class DataService @Inject constructor( ).build() } -suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 15): SavedItemSyncMarker { - val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since) ?: return SavedItemSyncMarker.errorResult +suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 15): SavedItemSyncResult { + val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since) ?: return SavedItemSyncResult.errorResult db.savedItemDao().insertAll(syncResult.items) - return SavedItemSyncMarker( + return SavedItemSyncResult( hasError = false, hasMoreItems = syncResult.hasMoreItems, cursor = syncResult.cursor, - count = syncResult.items.size + count = syncResult.items.size, + savedItemSlugs = syncResult.items.map { it.id } ) } +suspend fun DataService.syncSavedItemContent(slug: String) { + val syncResult = networker.savedItem(slug) + +// syncResult.item +} + suspend fun DataService.syncOfflineItemsWithServerIfNeeded() { // TODO: implement this } -data class SavedItemSyncMarker( +data class SavedItemSyncResult( val hasError: Boolean, val hasMoreItems: Boolean, val count: Int, + val savedItemSlugs: List, val cursor: String? ) { companion object { - val errorResult = SavedItemSyncMarker(hasError = true, hasMoreItems = true, cursor = null, count = 0) + val errorResult = SavedItemSyncResult(hasError = true, hasMoreItems = true, cursor = null, count = 0, savedItemSlugs = listOf()) } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt index bf9ea94c6..29e6fce2b 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt @@ -151,7 +151,8 @@ suspend fun Networker.createHighlight(input: CreateHighlightInput): Highlight? { createdAt = null, // TODO: update gql query to get this updatedAt = null, // TODO: fix updatedAtString?.let { LocalDate.parse(it) }, createdByMe = createdHighlight.highlightFields.createdByMe, - markedForDeletion = false + markedForDeletion = false, + serverSyncStatus = 0 ) } else { return null From 23cb7cb2a5fbe8cffbca228af791067ff23e3a5e Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 11 Jan 2023 12:05:24 -0800 Subject: [PATCH 10/31] rename id fields to reflect entity type --- .../java/app/omnivore/omnivore/DataService.kt | 2 +- .../omnivore/networking/HighlightMutations.kt | 4 +-- .../omnivore/networking/SavedItemQuery.kt | 6 ++-- .../networking/SavedItemUpdatesQuery.kt | 2 +- .../omnivore/networking/SearchQuery.kt | 4 +-- .../persistence/entities/Highlight.kt | 3 +- .../persistence/entities/NewsletterEmail.kt | 5 ++- .../persistence/entities/RecentSearchItem.kt | 2 +- .../entities/RecommendationGroup.kt | 2 +- .../persistence/entities/SavedItem.kt | 31 ++++--------------- .../persistence/entities/SavedItemLabel.kt | 3 +- .../omnivore/ui/library/LibraryView.kt | 3 +- .../omnivore/ui/reader/PDFReaderViewModel.kt | 4 +-- .../omnivore/omnivore/ui/reader/WebReader.kt | 2 +- .../omnivore/ui/reader/WebReaderContent.kt | 4 +-- 15 files changed, 26 insertions(+), 51 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt index 7a127ba1a..aadf5ddf9 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt @@ -28,7 +28,7 @@ suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 15): S hasMoreItems = syncResult.hasMoreItems, cursor = syncResult.cursor, count = syncResult.items.size, - savedItemSlugs = syncResult.items.map { it.id } + savedItemSlugs = syncResult.items.map { it.savedItemId } ) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt index 29e6fce2b..10ab31200 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt @@ -8,11 +8,9 @@ import app.omnivore.omnivore.graphql.generated.UpdateHighlightMutation import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput -import app.omnivore.omnivore.models.ServerSyncStatus import app.omnivore.omnivore.persistence.entities.Highlight import com.apollographql.apollo3.api.Optional import com.google.gson.Gson -import java.time.LocalDate data class CreateHighlightParams( val shortId: String?, @@ -141,7 +139,7 @@ suspend fun Networker.createHighlight(input: CreateHighlightInput): Highlight? { // val updatedAtString = createdHighlight.highlightFields.updatedAt as? String return Highlight( - id = createdHighlight.highlightFields.id, + highlightId = createdHighlight.highlightFields.id, shortId = createdHighlight.highlightFields.shortId, quote = createdHighlight.highlightFields.quote, prefix = createdHighlight.highlightFields.prefix, diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt index 0133d3dbd..eb2822697 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt @@ -30,7 +30,7 @@ suspend fun Networker.savedItem(slug: String): SavedItemQueryResponse { val savedItemLabels = labels.map { SavedItemLabel( - id = it.labelFields.id, + savedItemLabelId = it.labelFields.id, name = it.labelFields.name, color = it.labelFields.color, createdAt = it.labelFields.createdAt as String?, @@ -42,7 +42,7 @@ suspend fun Networker.savedItem(slug: String): SavedItemQueryResponse { // val updatedAtString = it.highlightFields.updatedAt as? String Highlight( - id = it.highlightFields.id, + highlightId = it.highlightFields.id, shortId = it.highlightFields.shortId, quote = it.highlightFields.quote, prefix = it.highlightFields.prefix, @@ -60,7 +60,7 @@ suspend fun Networker.savedItem(slug: String): SavedItemQueryResponse { // TODO: handle errors val savedItem = SavedItem( - id = article.articleFields.id, + savedItemId = article.articleFields.id, title = article.articleFields.title, createdAt = article.articleFields.createdAt as String, savedAt = article.articleFields.savedAt as String, diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemUpdatesQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemUpdatesQuery.kt index be79a86da..01c42f9ea 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemUpdatesQuery.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemUpdatesQuery.kt @@ -41,7 +41,7 @@ suspend fun Networker.savedItemUpdates( val savedItems = itemNodes.map { SavedItem( - id = it.id, + savedItemId = it.id, title = it.title, createdAt = it.createdAt as String, savedAt = it.savedAt as String, 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 ecb84e728..f1718846e 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 @@ -22,7 +22,7 @@ suspend fun Networker.typeaheadSearch( val cardsData = itemList.map { SavedItemCardData( - id = it.id, + savedItemId = it.id, slug = it.slug, publisherURLString = "", title = it.title, @@ -59,7 +59,7 @@ suspend fun Networker.search( val cardsData = itemList.map { SavedItemCardData( - id = it.node.id, + savedItemId = it.node.id, slug = it.node.slug, publisherURLString = it.node.originalArticleUrl, title = it.node.title, diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt index 327f039fc..ebe3eed84 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt @@ -2,13 +2,12 @@ package app.omnivore.omnivore.persistence.entities import androidx.room.Entity import androidx.room.PrimaryKey -import app.omnivore.omnivore.models.ServerSyncStatus import java.time.LocalDate import java.util.Date @Entity data class Highlight( - @PrimaryKey val id: String, + @PrimaryKey val highlightId: String, val annotation: String?, val createdAt: Date?, val createdByMe: Boolean, diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/NewsletterEmail.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/NewsletterEmail.kt index 2920367a1..c9b38351e 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/NewsletterEmail.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/NewsletterEmail.kt @@ -5,8 +5,7 @@ import androidx.room.PrimaryKey @Entity data class NewsletterEmail( - @PrimaryKey val userID: String, + @PrimaryKey val newsletterEmailId: String?, val confirmationCode: String?, - val email: String?, - val emailID: String? + val email: String? ) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/RecentSearchItem.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/RecentSearchItem.kt index 2da231121..ec5f41bb4 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/RecentSearchItem.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/RecentSearchItem.kt @@ -5,7 +5,7 @@ import androidx.room.PrimaryKey @Entity data class RecentSearchItem( - @PrimaryKey val id: String, + @PrimaryKey val recentSearchItemId: String, val savedAt: String?, val term: String? ) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/RecommendationGroup.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/RecommendationGroup.kt index 4ea5f5051..f168922f0 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/RecommendationGroup.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/RecommendationGroup.kt @@ -5,7 +5,7 @@ import androidx.room.PrimaryKey @Entity data class RecommendationGroup( - @PrimaryKey val id: String, + @PrimaryKey val recommendationGroupId: String, val name: String?, val canPost: Boolean, val canSeeMembers: Boolean, diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt index 0a9a1c58c..e664df0b1 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt @@ -7,7 +7,7 @@ import java.util.* @Entity data class SavedItem( - @PrimaryKey val id: String, + @PrimaryKey val savedItemId: String, val title: String, val createdAt: String, val savedAt: String, @@ -47,43 +47,24 @@ data class SavedItem( return publisherURLString?.toUri()?.host } - fun isPDF(): Boolean { - val hasPDFSuffix = pageURLString.endsWith("pdf") - return contentReader == "PDF" || hasPDFSuffix - } - - fun asSavedItemCardData(): SavedItemCardData { - return SavedItemCardData( - id = id, - slug = slug, - publisherURLString = publisherURLString, - title = title, - author = author, - imageURLString = imageURLString, - isArchived = isArchived, - pageURLString = pageURLString, - contentReader = contentReader, - ) - } - override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false other as SavedItem - if (id != other.id) return false + if (savedItemId != other.savedItemId) return false return true } override fun hashCode(): Int { - return id.hashCode() + return savedItemId.hashCode() } } data class SavedItemCardData( - val id: String, + val savedItemId: String, val slug: String, val publisherURLString: String?, val title: String, @@ -105,7 +86,7 @@ data class SavedItemCardData( @Dao interface SavedItemDao { - @Query("SELECT id, slug, publisherURLString, title, author, imageURLString, isArchived, pageURLString, contentReader FROM SavedItem ORDER BY savedAt DESC") + @Query("SELECT savedItemId, slug, publisherURLString, title, author, imageURLString, isArchived, pageURLString, contentReader FROM SavedItem ORDER BY savedAt DESC") fun getLibraryLiveData(): LiveData> @Query("SELECT * FROM savedItem") @@ -114,6 +95,6 @@ interface SavedItemDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAll(items: List) - @Query("DELETE FROM savedItem WHERE id = :itemID") + @Query("DELETE FROM savedItem WHERE savedItemId = :itemID") fun deleteById(itemID: String) } 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 f13983838..3cbdb87f8 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 @@ -2,11 +2,10 @@ package app.omnivore.omnivore.persistence.entities import androidx.room.Entity import androidx.room.PrimaryKey -import app.omnivore.omnivore.models.ServerSyncStatus @Entity data class SavedItemLabel( - @PrimaryKey val id: String, + @PrimaryKey val savedItemLabelId: String, val name: String, val color: String, val createdAt: String?, 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 cf6bac4c7..51dd4d74a 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 @@ -20,7 +20,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import app.omnivore.omnivore.Routes -import app.omnivore.omnivore.persistence.entities.SavedItem import app.omnivore.omnivore.persistence.entities.SavedItemCardData import app.omnivore.omnivore.ui.savedItemViews.SavedItemCard import app.omnivore.omnivore.ui.reader.PDFReaderActivity @@ -101,7 +100,7 @@ fun LibraryViewContent( navController.navigate("WebReader/${cardData.slug}") } }, - actionHandler = { libraryViewModel.handleSavedItemAction(cardData.id, it) } + actionHandler = { libraryViewModel.handleSavedItemAction(cardData.savedItemId, it) } ) } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt index d313994f6..f5d37fc62 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt @@ -92,7 +92,7 @@ class PDFReaderViewModel @Inject constructor( currentReadingProgress = percent viewModelScope.launch { val params = ReadingProgressParams( - id = pdfReaderParamsLiveData.value?.item?.id, + id = pdfReaderParamsLiveData.value?.item?.savedItemId, readingProgressPercent = percent, readingProgressAnchorIndex = currentPageIndex ) @@ -102,7 +102,7 @@ class PDFReaderViewModel @Inject constructor( } fun syncHighlightUpdates(newAnnotation: Annotation, quote: String, overlapIds: List, note: String? = null) { - val itemID = pdfReaderParamsLiveData.value?.item?.id ?: return + val itemID = pdfReaderParamsLiveData.value?.item?.savedItemId ?: return val highlightID = UUID.randomUUID().toString() val shortID = UUID.randomUUID().toString().replace("-","").substring(0,8) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt index 25c173aab..596d6b7fa 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt @@ -113,7 +113,7 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod isExpanded = isMenuExpanded, isArchived = webReaderParams!!.item.isArchived, onDismiss = { isMenuExpanded = false }, - actionHandler = { webReaderViewModel.handleSavedItemAction(webReaderParams!!.item.id, it) } + actionHandler = { webReaderViewModel.handleSavedItemAction(webReaderParams!!.item.savedItemId, it) } ) } ) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderContent.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderContent.kt index fccd4038c..f9179a6b8 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderContent.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderContent.kt @@ -77,8 +77,8 @@ data class WebReaderContent( } window.omnivoreArticle = { - id: "${item.id}", - linkId: "${item.id}", + id: "${item.savedItemId}", + linkId: "${item.savedItemId}", slug: "${item.slug}", createdAt: new Date(1662571290735.0).toISOString(), savedAt: new Date(1662571290981.0).toISOString(), From 8b20a0b0be9793cfddb39a2deb205ed7423bff03 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 11 Jan 2023 12:40:19 -0800 Subject: [PATCH 11/31] add cross ref table for saved item and saved item labels --- .../omnivore/persistence/AppDatabase.kt | 6 +++- .../persistence/entities/SavedItem.kt | 4 +++ .../persistence/entities/SavedItemLabel.kt | 29 +++++++++++++++++-- 3 files changed, 36 insertions(+), 3 deletions(-) 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 f78470795..6062c45a2 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 @@ -3,6 +3,8 @@ package app.omnivore.omnivore.persistence import androidx.room.Database import androidx.room.RoomDatabase import app.omnivore.omnivore.persistence.entities.SavedItem +import app.omnivore.omnivore.persistence.entities.SavedItemLabel +import app.omnivore.omnivore.persistence.entities.SavedItemAndSavedItemLabelCrossRef import app.omnivore.omnivore.persistence.entities.SavedItemDao import app.omnivore.omnivore.persistence.entities.Viewer import app.omnivore.omnivore.persistence.entities.ViewerDao @@ -10,7 +12,9 @@ import app.omnivore.omnivore.persistence.entities.ViewerDao @Database( entities = [ Viewer::class, - SavedItem::class + SavedItem::class, + SavedItemLabel::class, + SavedItemAndSavedItemLabelCrossRef::class ], version = 2 ) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt index e664df0b1..33c37d2c1 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt @@ -97,4 +97,8 @@ interface SavedItemDao { @Query("DELETE FROM savedItem WHERE savedItemId = :itemID") fun deleteById(itemID: String) + + @Transaction + @Query("SELECT savedItemId, slug, publisherURLString, title, author, imageURLString, isArchived, pageURLString, contentReader FROM SavedItem ORDER BY savedAt DESC") + fun getLibraryLiveDataWithLabels(): LiveData> } 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 3cbdb87f8..6979aeb90 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,7 +1,6 @@ package app.omnivore.omnivore.persistence.entities -import androidx.room.Entity -import androidx.room.PrimaryKey +import androidx.room.* @Entity data class SavedItemLabel( @@ -13,5 +12,31 @@ data class SavedItemLabel( val serverSyncStatus: Int = 0 ) +@Entity(primaryKeys = ["savedItemLabelId", "savedItemId"]) +data class SavedItemAndSavedItemLabelCrossRef( + val savedItemLabelId: String, + val savedItemId: String +) + +data class SavedItemWithLabels( + @Embedded val savedItem: SavedItem, + @Relation( + parentColumn = "savedItemId", + entityColumn = "savedItemLabelId", + associateBy = Junction(SavedItemAndSavedItemLabelCrossRef::class) + ) + val labels: List +) + +data class SavedItemCardDataWithLabels( + @Embedded val cardData: SavedItemCardData, + @Relation( + parentColumn = "savedItemId", + entityColumn = "savedItemLabelId", + associateBy = Junction(SavedItemAndSavedItemLabelCrossRef::class) + ) + val labels: List +) + // has many highlights // has many savedItems From ffeacab4098a140f9ea79f9f0d75a0ac3e193301 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 11 Jan 2023 22:57:23 -0800 Subject: [PATCH 12/31] save labels when saving items --- .../java/app/omnivore/omnivore/DataService.kt | 58 ++++++++++++++++++- .../networking/SavedItemUpdatesQuery.kt | 30 +--------- .../omnivore/persistence/AppDatabase.kt | 9 +-- .../persistence/entities/SavedItemLabel.kt | 13 +++++ .../omnivore/ui/library/LibraryView.kt | 25 +++++--- .../omnivore/ui/library/LibraryViewModel.kt | 11 +++- 6 files changed, 99 insertions(+), 47 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt index aadf5ddf9..5e1656982 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt @@ -6,6 +6,9 @@ import app.omnivore.omnivore.networking.Networker import app.omnivore.omnivore.networking.savedItem import app.omnivore.omnivore.networking.savedItemUpdates import app.omnivore.omnivore.persistence.AppDatabase +import app.omnivore.omnivore.persistence.entities.SavedItem +import app.omnivore.omnivore.persistence.entities.SavedItemAndSavedItemLabelCrossRef +import app.omnivore.omnivore.persistence.entities.SavedItemLabel import javax.inject.Inject class DataService @Inject constructor( @@ -21,14 +24,65 @@ class DataService @Inject constructor( suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 15): SavedItemSyncResult { val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since) ?: return SavedItemSyncResult.errorResult - db.savedItemDao().insertAll(syncResult.items) + val savedItems = syncResult.items.map { + SavedItem( + savedItemId = it.id, + title = it.title, + createdAt = it.createdAt as String, + savedAt = it.savedAt as String, + readAt = it.readAt as String?, + updatedAt = it.updatedAt as String?, + readingProgress = it.readingProgressPercent, + readingProgressAnchor = it.readingProgressAnchorIndex, + imageURLString = it.image, + pageURLString = it.url, + descriptionText = it.description, + publisherURLString = it.originalArticleUrl, + siteName = it.siteName, + author = it.author, + publishDate = it.publishedAt as String?, + slug = it.slug, + isArchived = it.isArchived, + contentReader = it.contentReader.rawValue, + content = null + ) + } + + db.savedItemDao().insertAll(savedItems) + + val labels: MutableList = mutableListOf() + val crossRefs: MutableList = mutableListOf() + + // save labels + for (item in syncResult.items) { + val itemLabels = (item.labels ?: listOf()).map { + SavedItemLabel( + savedItemLabelId = it.id, + name = it.name, + color = it.color, + createdAt = null, + labelDescription = null + ) + } + + labels.addAll(itemLabels) + + val newCrossRefs = itemLabels.map { + SavedItemAndSavedItemLabelCrossRef(savedItemLabelId = it.savedItemLabelId, savedItemId = item.id) + } + + crossRefs.addAll(newCrossRefs) + } + + db.savedItemLabelDao().insertAll(labels) + db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs) return SavedItemSyncResult( hasError = false, hasMoreItems = syncResult.hasMoreItems, cursor = syncResult.cursor, count = syncResult.items.size, - savedItemSlugs = syncResult.items.map { it.savedItemId } + savedItemSlugs = syncResult.items.map { it.slug } ) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemUpdatesQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemUpdatesQuery.kt index 01c42f9ea..a50c71ce7 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemUpdatesQuery.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemUpdatesQuery.kt @@ -8,9 +8,8 @@ import com.apollographql.apollo3.api.Optional data class SavedItemUpdatesQueryResponse( val cursor: String?, val hasMoreItems: Boolean, - val totalCount: Int, val deletedItemIDs: List, - val items: List + val items: List ) suspend fun Networker.savedItemUpdates( @@ -39,36 +38,11 @@ suspend fun Networker.savedItemUpdates( } } - val savedItems = itemNodes.map { - SavedItem( - savedItemId = it.id, - title = it.title, - createdAt = it.createdAt as String, - savedAt = it.savedAt as String, - readAt = it.readAt as String?, - updatedAt = it.updatedAt as String?, - readingProgress = it.readingProgressPercent, - readingProgressAnchor = it.readingProgressAnchorIndex, - imageURLString = it.image, - pageURLString = it.url, - descriptionText = it.description, - publisherURLString = it.originalArticleUrl, - siteName = it.siteName, - author = it.author, - publishDate = it.publishedAt as String?, - slug = it.slug, - isArchived = it.isArchived, - contentReader = it.contentReader.rawValue, - content = null - ) - } - return SavedItemUpdatesQueryResponse( cursor = payload.pageInfo.endCursor, hasMoreItems = payload.pageInfo.hasNextPage, - totalCount = savedItems.size, deletedItemIDs = deletedItemIDs, - items = savedItems + items = itemNodes ) } catch (e: java.lang.Exception) { return null 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 6062c45a2..2130a2f9a 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 @@ -2,12 +2,7 @@ package app.omnivore.omnivore.persistence import androidx.room.Database import androidx.room.RoomDatabase -import app.omnivore.omnivore.persistence.entities.SavedItem -import app.omnivore.omnivore.persistence.entities.SavedItemLabel -import app.omnivore.omnivore.persistence.entities.SavedItemAndSavedItemLabelCrossRef -import app.omnivore.omnivore.persistence.entities.SavedItemDao -import app.omnivore.omnivore.persistence.entities.Viewer -import app.omnivore.omnivore.persistence.entities.ViewerDao +import app.omnivore.omnivore.persistence.entities.* @Database( entities = [ @@ -21,4 +16,6 @@ import app.omnivore.omnivore.persistence.entities.ViewerDao abstract class AppDatabase : RoomDatabase() { abstract fun viewerDao(): ViewerDao abstract fun savedItemDao(): SavedItemDao + abstract fun savedItemLabelDao(): SavedItemLabelDao + abstract fun savedItemAndSavedItemLabelCrossRefDao(): SavedItemAndSavedItemLabelCrossRefDao } 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 6979aeb90..26ac468a5 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 @@ -12,6 +13,12 @@ data class SavedItemLabel( val serverSyncStatus: Int = 0 ) +@Dao +interface SavedItemLabelDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertAll(items: List) +} + @Entity(primaryKeys = ["savedItemLabelId", "savedItemId"]) data class SavedItemAndSavedItemLabelCrossRef( val savedItemLabelId: String, @@ -38,5 +45,11 @@ data class SavedItemCardDataWithLabels( val labels: List ) +@Dao +interface SavedItemAndSavedItemLabelCrossRefDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertAll(items: List) +} + // has many highlights // has many savedItems 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 51dd4d74a..01ef87ea2 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 @@ -18,9 +18,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController import app.omnivore.omnivore.Routes +import app.omnivore.omnivore.persistence.entities.SavedItemAndSavedItemLabelCrossRef import app.omnivore.omnivore.persistence.entities.SavedItemCardData +import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels import app.omnivore.omnivore.ui.savedItemViews.SavedItemCard import app.omnivore.omnivore.ui.reader.PDFReaderActivity import kotlinx.coroutines.flow.distinctUntilChanged @@ -70,8 +73,8 @@ fun LibraryViewContent( onRefresh = { libraryViewModel.refresh() } ) - val cardsData: List by libraryViewModel.itemsLiveData.observeAsState(listOf()) - val searchedCardsData: List by libraryViewModel.searchItemsLiveData.observeAsState(listOf()) + val cardsData: List by libraryViewModel.itemsLiveData.observeAsState(listOf()) + val searchedCardsData: List by libraryViewModel.searchItemsLiveData.observeAsState(listOf()) val searchText: String by libraryViewModel.searchTextLiveData.observeAsState("") Box( @@ -88,19 +91,25 @@ fun LibraryViewContent( .fillMaxSize() .padding(horizontal = 6.dp) ) { - items(if (searchText.isNotEmpty()) searchedCardsData else cardsData) { cardData -> + items(if (searchText.isNotEmpty()) searchedCardsData else cardsData) { cardDataWithLabels -> SavedItemCard( - cardData = cardData, + cardData = cardDataWithLabels.cardData, onClickHandler = { - if (cardData.isPDF()) { + if (cardDataWithLabels.cardData.isPDF()) { val intent = Intent(context, PDFReaderActivity::class.java) - intent.putExtra("SAVED_ITEM_SLUG", cardData.slug) + intent.putExtra("SAVED_ITEM_SLUG", cardDataWithLabels.cardData.slug) context.startActivity(intent) } else { - navController.navigate("WebReader/${cardData.slug}") + navController.navigate("WebReader/${cardDataWithLabels.cardData.slug}") } }, - actionHandler = { libraryViewModel.handleSavedItemAction(cardData.savedItemId, it) } + actionHandler = { libraryViewModel.handleSavedItemAction(cardDataWithLabels.cardData.savedItemId, it) } + ) + + Text( + text = "${cardDataWithLabels.labels.size.toString()} labels", + style = MaterialTheme.typography.titleMedium, + lineHeight = 20.sp ) } } 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 31226c063..3dc93ad44 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.viewModelScope import app.omnivore.omnivore.* import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.persistence.entities.SavedItemCardData +import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* import java.time.LocalDateTime @@ -30,8 +31,8 @@ class LibraryViewModel @Inject constructor( // Live Data val searchTextLiveData = MutableLiveData("") - val searchItemsLiveData = MutableLiveData>(listOf()) - val itemsLiveData = dataService.db.savedItemDao().getLibraryLiveData() + val searchItemsLiveData = MutableLiveData>(listOf()) + val itemsLiveData = dataService.db.savedItemDao().getLibraryLiveDataWithLabels() var isRefreshing by mutableStateOf(false) @@ -113,7 +114,11 @@ class LibraryViewModel @Inject constructor( return } - searchItemsLiveData.postValue(searchResult.cardsData) + val cardsDataWithLabels = searchResult.cardsData.map { + SavedItemCardDataWithLabels(cardData = it, labels = listOf()) + } + + searchItemsLiveData.postValue(cardsDataWithLabels) CoroutineScope(Dispatchers.Main).launch { isRefreshing = false From 8319fb67ca3efb15cc3fcca1c082c148160e5257 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 12 Jan 2023 13:38:34 -0800 Subject: [PATCH 13/31] load article content from sql --- .../java/app/omnivore/omnivore/DataService.kt | 25 ++++++++- .../omnivore/persistence/AppDatabase.kt | 6 +- .../persistence/entities/Highlight.kt | 43 +++++++++++++-- .../persistence/entities/SavedItem.kt | 6 ++ .../persistence/entities/SavedItemLabel.kt | 1 - .../omnivore/ui/library/LibraryViewModel.kt | 6 ++ .../omnivore/ui/reader/WebReaderViewModel.kt | 55 +++++++++++++------ 7 files changed, 119 insertions(+), 23 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt index 5e1656982..d85be98cf 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt @@ -1,12 +1,14 @@ package app.omnivore.omnivore import android.content.Context +import android.util.Log import androidx.room.Room import app.omnivore.omnivore.networking.Networker import app.omnivore.omnivore.networking.savedItem import app.omnivore.omnivore.networking.savedItemUpdates import app.omnivore.omnivore.persistence.AppDatabase import app.omnivore.omnivore.persistence.entities.SavedItem +import app.omnivore.omnivore.persistence.entities.SavedItemAndHighlightCrossRef import app.omnivore.omnivore.persistence.entities.SavedItemAndSavedItemLabelCrossRef import app.omnivore.omnivore.persistence.entities.SavedItemLabel import javax.inject.Inject @@ -89,7 +91,28 @@ suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 15): S suspend fun DataService.syncSavedItemContent(slug: String) { val syncResult = networker.savedItem(slug) -// syncResult.item + val savedItem = syncResult.item ?: return + db.savedItemDao().insert(savedItem) + + // Persist Labels + db.savedItemLabelDao().insertAll(syncResult.labels) + + val labelCrossRefs = syncResult.labels.map { + SavedItemAndSavedItemLabelCrossRef(savedItemLabelId = it.savedItemLabelId, savedItemId = savedItem.savedItemId) + } + + db.savedItemAndSavedItemLabelCrossRefDao().insertAll(labelCrossRefs) + + // Persist Highlights + db.highlightDao().insertAll(syncResult.highlights) + + val highlightCrossRefs = syncResult.highlights.map { + SavedItemAndHighlightCrossRef(highlightId = it.highlightId, savedItemId = savedItem.savedItemId) + } + + db.savedItemAndHighlightCrossRefDao().insertAll(highlightCrossRefs) + + Log.d("sync", "saved content for item with id: ${savedItem.savedItemId}") } suspend fun DataService.syncOfflineItemsWithServerIfNeeded() { 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 2130a2f9a..56faf0998 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 @@ -9,13 +9,17 @@ import app.omnivore.omnivore.persistence.entities.* Viewer::class, SavedItem::class, SavedItemLabel::class, - SavedItemAndSavedItemLabelCrossRef::class + Highlight::class, + SavedItemAndSavedItemLabelCrossRef::class, + SavedItemAndHighlightCrossRef::class ], version = 2 ) abstract class AppDatabase : RoomDatabase() { abstract fun viewerDao(): ViewerDao abstract fun savedItemDao(): SavedItemDao + abstract fun highlightDao(): HighlightDao abstract fun savedItemLabelDao(): SavedItemLabelDao abstract fun savedItemAndSavedItemLabelCrossRefDao(): SavedItemAndSavedItemLabelCrossRefDao + abstract fun savedItemAndHighlightCrossRefDao(): SavedItemAndHighlightCrossRefDao } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt index ebe3eed84..fcfb76ba6 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt @@ -1,7 +1,6 @@ package app.omnivore.omnivore.persistence.entities -import androidx.room.Entity -import androidx.room.PrimaryKey +import androidx.room.* import java.time.LocalDate import java.util.Date @@ -9,7 +8,7 @@ import java.util.Date data class Highlight( @PrimaryKey val highlightId: String, val annotation: String?, - val createdAt: Date?, + val createdAt: String?, val createdByMe: Boolean, val markedForDeletion: Boolean, // default false val patch: String, @@ -18,9 +17,45 @@ data class Highlight( val serverSyncStatus: Int, // default 0 val shortId: String, val suffix: String?, - val updatedAt: LocalDate? + val updatedAt: String? // has many SavedItemLabels (inverse: labels have many highlights) // has one savedItem (inverse: savedItem has many highlights // has a UserProfile (no inverse) ) + +@Entity(primaryKeys = ["highlightId", "savedItemId"]) +data class SavedItemAndHighlightCrossRef( + val highlightId: String, + val savedItemId: String +) + +@Dao +interface SavedItemAndHighlightCrossRefDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertAll(items: List) +} + +data class SavedItemWithLabelsAndHighlights( + @Embedded val savedItem: SavedItem, + + @Relation( + parentColumn = "savedItemId", + entityColumn = "savedItemLabelId", + associateBy = Junction(SavedItemAndSavedItemLabelCrossRef::class) + ) + val labels: List, + + @Relation( + parentColumn = "savedItemId", + entityColumn = "highlightId", + associateBy = Junction(SavedItemAndHighlightCrossRef::class) + ) + val highlights: List +) + +@Dao +interface HighlightDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertAll(items: List) +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt index 33c37d2c1..440ed2461 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt @@ -92,9 +92,15 @@ interface SavedItemDao { @Query("SELECT * FROM savedItem") fun getAll(): List + @Query("SELECT * FROM savedItem WHERE slug = :slug") + fun getSavedItemWithLabelsAndHighlights(slug: String): SavedItemWithLabelsAndHighlights? + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAll(items: List) + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(item: SavedItem) + @Query("DELETE FROM savedItem WHERE savedItemId = :itemID") fun deleteById(itemID: String) 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 26ac468a5..512c1c564 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,6 +1,5 @@ package app.omnivore.omnivore.persistence.entities -import androidx.lifecycle.LiveData import androidx.room.* @Entity 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 3dc93ad44..a67afa781 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 @@ -83,6 +83,12 @@ class LibraryViewModel @Inject constructor( private suspend fun performItemSync(cursor: String?, since: String, count: Int, startTime: String) { dataService.syncOfflineItemsWithServerIfNeeded() val result = dataService.sync(since = since, cursor = cursor) + + // TODO: Defer this until later? + for (slug in result.savedItemSlugs) { + dataService.syncSavedItemContent(slug) + } + val totalCount = count + result.count Log.d("sync", "fetched ${result.count} items") 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 fe6c1b103..21835f088 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 @@ -5,6 +5,7 @@ import androidx.compose.foundation.ScrollState import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.omnivore.omnivore.DataService import app.omnivore.omnivore.DatastoreKeys import app.omnivore.omnivore.DatastoreRepository import app.omnivore.omnivore.persistence.entities.SavedItem @@ -12,10 +13,7 @@ import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.ui.library.SavedItemAction import com.google.gson.Gson import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.* import java.util.* import javax.inject.Inject @@ -31,6 +29,7 @@ data class AnnotationWebViewMessage( @HiltViewModel class WebReaderViewModel @Inject constructor( private val datastoreRepo: DatastoreRepository, + private val dataService: DataService, private val networker: Networker ): ViewModel() { var lastJavascriptActionLoopUUID: UUID = UUID.randomUUID() @@ -47,23 +46,47 @@ class WebReaderViewModel @Inject constructor( fun loadItem(slug: String) { viewModelScope.launch { - val articleQueryResult = networker.savedItem(slug) + // Attempt to load from db first + withContext(Dispatchers.IO) { + val persistedItem = dataService.db.savedItemDao().getSavedItemWithLabelsAndHighlights(slug) - val article = articleQueryResult.item ?: return@launch + if (persistedItem?.savedItem != null) { + val articleContent = ArticleContent( + title = persistedItem.savedItem.title, + htmlContent = persistedItem.savedItem.content ?: "", + highlights = persistedItem.highlights, + contentStatus = "SUCCEEDED", + objectID = "", + labelsJSONString = Gson().toJson(persistedItem.labels) + ) - val articleContent = ArticleContent( - title = article.title, - htmlContent = article.content ?: "", - highlights = articleQueryResult.highlights, - contentStatus = "SUCCEEDED", - objectID = "", - labelsJSONString = Gson().toJson(articleQueryResult.labels) - ) - - webReaderParamsLiveData.value = WebReaderParams(article, articleContent) + Log.d("sync", "data loaded from db") + webReaderParamsLiveData.postValue(WebReaderParams(persistedItem.savedItem, articleContent)) + } else { + loadItemFromServer(slug) + } + } } } + private suspend fun loadItemFromServer(slug: String) { + val articleQueryResult = networker.savedItem(slug) + + val article = articleQueryResult.item ?: return + + val articleContent = ArticleContent( + title = article.title, + htmlContent = article.content ?: "", + highlights = articleQueryResult.highlights, + contentStatus = "SUCCEEDED", + objectID = "", + labelsJSONString = Gson().toJson(articleQueryResult.labels) + ) + + Log.d("sync", "data loaded from server") + webReaderParamsLiveData.postValue(WebReaderParams(article, articleContent)) + } + fun handleSavedItemAction(itemID: String, action: SavedItemAction) { when (action) { SavedItemAction.Delete -> { From d7c76bf9c56885d47e20ba2e3bf45aa7589873a9 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 13 Jan 2023 10:54:57 -0800 Subject: [PATCH 14/31] add foreign keys to cross ref tables --- .../omnivore/persistence/entities/Highlight.kt | 18 +++++++++++++++++- .../persistence/entities/SavedItemLabel.kt | 18 +++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt index fcfb76ba6..31ecae2cd 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt @@ -24,7 +24,23 @@ data class Highlight( // has a UserProfile (no inverse) ) -@Entity(primaryKeys = ["highlightId", "savedItemId"]) +@Entity( + primaryKeys = ["highlightId", "savedItemId"], + foreignKeys = [ + ForeignKey( + entity = Highlight::class, + parentColumns = arrayOf("highlightId"), + childColumns = arrayOf("highlightId"), + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = SavedItem::class, + parentColumns = arrayOf("savedItemId"), + childColumns = arrayOf("savedItemId"), + onDelete = ForeignKey.CASCADE + ) + ] +) data class SavedItemAndHighlightCrossRef( val highlightId: String, val savedItemId: String 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 512c1c564..ea3b3aa40 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 @@ -18,7 +18,23 @@ interface SavedItemLabelDao { fun insertAll(items: List) } -@Entity(primaryKeys = ["savedItemLabelId", "savedItemId"]) +@Entity( + primaryKeys = ["savedItemLabelId", "savedItemId"], + foreignKeys = [ + ForeignKey( + entity = SavedItem::class, + parentColumns = arrayOf("savedItemId"), + childColumns = arrayOf("savedItemId"), + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = SavedItemLabel::class, + parentColumns = arrayOf("savedItemLabelId"), + childColumns = arrayOf("savedItemLabelId"), + onDelete = ForeignKey.CASCADE + ) + ] +) data class SavedItemAndSavedItemLabelCrossRef( val savedItemLabelId: String, val savedItemId: String From 3f14cb08ae7beefbbbaccaf0f1f09a22b60d9a23 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 16 Jan 2023 08:27:55 -0800 Subject: [PATCH 15/31] set defaults values for serverSyncSTatus and isDeleted values --- .../omnivore/omnivore/networking/HighlightMutations.kt | 4 +--- .../app/omnivore/omnivore/networking/SavedItemQuery.kt | 4 +--- .../omnivore/omnivore/persistence/entities/Highlight.kt | 5 +++-- .../app/omnivore/omnivore/ui/library/LibraryViewModel.kt | 8 ++++---- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt index 10ab31200..97260c72d 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt @@ -148,9 +148,7 @@ suspend fun Networker.createHighlight(input: CreateHighlightInput): Highlight? { annotation = createdHighlight.highlightFields.annotation, createdAt = null, // TODO: update gql query to get this updatedAt = null, // TODO: fix updatedAtString?.let { LocalDate.parse(it) }, - createdByMe = createdHighlight.highlightFields.createdByMe, - markedForDeletion = false, - serverSyncStatus = 0 + createdByMe = createdHighlight.highlightFields.createdByMe ) } else { return null diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt index eb2822697..9cc667461 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt @@ -51,9 +51,7 @@ suspend fun Networker.savedItem(slug: String): SavedItemQueryResponse { annotation = it.highlightFields.annotation, createdAt = null, // TODO: update gql query to get this updatedAt = null, //updatedAtString?.let { str -> LocalDate.parse(str) }, TODO: fix date parsing - createdByMe = it.highlightFields.createdByMe, - markedForDeletion = false, - serverSyncStatus = 0 + createdByMe = it.highlightFields.createdByMe ) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt index 31ecae2cd..0c4588741 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt @@ -1,6 +1,7 @@ package app.omnivore.omnivore.persistence.entities import androidx.room.* +import app.omnivore.omnivore.models.ServerSyncStatus import java.time.LocalDate import java.util.Date @@ -10,11 +11,11 @@ data class Highlight( val annotation: String?, val createdAt: String?, val createdByMe: Boolean, - val markedForDeletion: Boolean, // default false + val markedForDeletion: Boolean = false, val patch: String, val prefix: String?, val quote: String, - val serverSyncStatus: Int, // default 0 + val serverSyncStatus: Int = ServerSyncStatus.IS_SYNCED.rawValue, val shortId: String, val suffix: String?, val updatedAt: String? 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 a67afa781..84da2262a 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 @@ -84,10 +84,10 @@ class LibraryViewModel @Inject constructor( dataService.syncOfflineItemsWithServerIfNeeded() val result = dataService.sync(since = since, cursor = cursor) - // TODO: Defer this until later? - for (slug in result.savedItemSlugs) { - dataService.syncSavedItemContent(slug) - } + // TODO: Defer this until later? /fix - this results in a 429 server error +// for (slug in result.savedItemSlugs) { +// dataService.syncSavedItemContent(slug) +// } val totalCount = count + result.count From a0d00028a70bd4be3f16e9432a44e45e1be982f2 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 16 Jan 2023 10:01:37 -0800 Subject: [PATCH 16/31] sync offline savedItem updates before syncing library list --- .../java/app/omnivore/omnivore/DataService.kt | 72 ++++++++++++++++--- .../omnivore/networking/SavedItemMutations.kt | 2 +- .../persistence/entities/Highlight.kt | 9 ++- .../persistence/entities/SavedItem.kt | 8 ++- 4 files changed, 79 insertions(+), 12 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt index d85be98cf..6b561c864 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt @@ -3,14 +3,10 @@ package app.omnivore.omnivore import android.content.Context import android.util.Log import androidx.room.Room -import app.omnivore.omnivore.networking.Networker -import app.omnivore.omnivore.networking.savedItem -import app.omnivore.omnivore.networking.savedItemUpdates +import app.omnivore.omnivore.models.ServerSyncStatus +import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.persistence.AppDatabase -import app.omnivore.omnivore.persistence.entities.SavedItem -import app.omnivore.omnivore.persistence.entities.SavedItemAndHighlightCrossRef -import app.omnivore.omnivore.persistence.entities.SavedItemAndSavedItemLabelCrossRef -import app.omnivore.omnivore.persistence.entities.SavedItemLabel +import app.omnivore.omnivore.persistence.entities.* import javax.inject.Inject class DataService @Inject constructor( @@ -116,7 +112,67 @@ suspend fun DataService.syncSavedItemContent(slug: String) { } suspend fun DataService.syncOfflineItemsWithServerIfNeeded() { - // TODO: implement this + val unSyncedSavedItems = db.savedItemDao().getUnSynced() + val unSyncedHighlights = db.highlightDao().getUnSynced() + + for (savedItem in unSyncedSavedItems) { + syncSavedItem(savedItem) + } + + for (highlight in unSyncedHighlights) { + syncHighlight(highlight) + } +} + +private suspend fun DataService.syncSavedItem(item: SavedItem) { + fun updateSyncStatus(status: ServerSyncStatus) { + item.serverSyncStatus = status.rawValue + db.savedItemDao().update(item) + } + + when (item.serverSyncStatus) { + ServerSyncStatus.NEEDS_DELETION.rawValue -> { + updateSyncStatus(ServerSyncStatus.IS_SYNCING) + + val isDeletedOnServer = networker.deleteSavedItem(item.savedItemId) + + if (isDeletedOnServer) { + db.savedItemDao().deleteById(item.savedItemId) + } else { + updateSyncStatus(ServerSyncStatus.NEEDS_DELETION) + } + } + ServerSyncStatus.NEEDS_UPDATE.rawValue -> { + updateSyncStatus(ServerSyncStatus.IS_SYNCING) + + val isArchiveServerSynced = networker.updateArchiveStatusSavedItem(itemID = item.savedItemId, setAsArchived = item.isArchived) + + val isReadingProgressSynced = networker.updateReadingProgress( + ReadingProgressParams( + id = item.savedItemId, + readingProgressPercent = item.readingProgress, + readingProgressAnchorIndex = item.readingProgressAnchor + ) + ) + + if (isArchiveServerSynced && isReadingProgressSynced) { + updateSyncStatus(ServerSyncStatus.IS_SYNCED) + } else { + updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE) + } + } + ServerSyncStatus.NEEDS_CREATION.rawValue -> { + // TODO: implement when we are able to create content on device + // updateSyncStatus(ServerSyncStatus.IS_SYNCING) + // send update to server + // update db + } + else -> return + } +} + +private suspend fun DataService.syncHighlight(item: Highlight) { + // TODO: update } data class SavedItemSyncResult( diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemMutations.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemMutations.kt index 67ead474e..391124e2e 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemMutations.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemMutations.kt @@ -23,7 +23,7 @@ suspend fun Networker.unarchiveSavedItem(itemID: String): Boolean { return updateArchiveStatusSavedItem(itemID, false) } -private suspend fun Networker.updateArchiveStatusSavedItem(itemID: String, setAsArchived: Boolean): Boolean { +suspend fun Networker.updateArchiveStatusSavedItem(itemID: String, setAsArchived: Boolean): Boolean { return try { val input = ArchiveLinkInput(setAsArchived, itemID) val result = authenticatedApolloClient().mutation(SetLinkArchivedMutation(input)).execute() diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt index 0c4588741..c734af753 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt @@ -2,8 +2,7 @@ package app.omnivore.omnivore.persistence.entities import androidx.room.* import app.omnivore.omnivore.models.ServerSyncStatus -import java.time.LocalDate -import java.util.Date + @Entity data class Highlight( @@ -73,6 +72,12 @@ data class SavedItemWithLabelsAndHighlights( @Dao interface HighlightDao { + @Query("SELECT * FROM highlight WHERE serverSyncStatus != 0") + fun getUnSynced(): List + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAll(items: List) + + @Update + fun update(highlight: Highlight) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt index 440ed2461..00965a85d 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt @@ -36,7 +36,7 @@ data class SavedItem( val onDeviceImageURLString: String? = null, val originalHtml: String? = null, @ColumnInfo(typeAffinity = ColumnInfo.BLOB) val pdfData: ByteArray? = null, - val serverSyncStatus: Int = 0, + var serverSyncStatus: Int = 0, val tempPDFURL: String? = null // hasMany highlights @@ -92,6 +92,9 @@ interface SavedItemDao { @Query("SELECT * FROM savedItem") fun getAll(): List + @Query("SELECT * FROM savedItem WHERE serverSyncStatus != 0") + fun getUnSynced(): List + @Query("SELECT * FROM savedItem WHERE slug = :slug") fun getSavedItemWithLabelsAndHighlights(slug: String): SavedItemWithLabelsAndHighlights? @@ -104,6 +107,9 @@ interface SavedItemDao { @Query("DELETE FROM savedItem WHERE savedItemId = :itemID") fun deleteById(itemID: String) + @Update + fun update(savedItem: SavedItem) + @Transaction @Query("SELECT savedItemId, slug, publisherURLString, title, author, imageURLString, isArchived, pageURLString, contentReader FROM SavedItem ORDER BY savedAt DESC") fun getLibraryLiveDataWithLabels(): LiveData> From 8cd14c75405b884555aad99cfd3118ea489c7d97 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 16 Jan 2023 14:53:31 -0800 Subject: [PATCH 17/31] sync offline highlight changes with serevr before syncing library --- .../java/app/omnivore/omnivore/DataService.kt | 65 ++++++++++++++++++- .../persistence/entities/Highlight.kt | 8 ++- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt index 6b561c864..cfe12257c 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt @@ -3,10 +3,13 @@ package app.omnivore.omnivore import android.content.Context import android.util.Log import androidx.room.Room +import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput +import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput import app.omnivore.omnivore.models.ServerSyncStatus import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.persistence.AppDatabase import app.omnivore.omnivore.persistence.entities.* +import com.apollographql.apollo3.api.Optional import javax.inject.Inject class DataService @Inject constructor( @@ -171,8 +174,66 @@ private suspend fun DataService.syncSavedItem(item: SavedItem) { } } -private suspend fun DataService.syncHighlight(item: Highlight) { - // TODO: update +private suspend fun DataService.syncHighlight(highlight: Highlight) { + fun updateSyncStatus(status: ServerSyncStatus) { + highlight.serverSyncStatus = status.rawValue + db.highlightDao().update(highlight) + } + + when (highlight.serverSyncStatus) { + ServerSyncStatus.NEEDS_DELETION.rawValue -> { + updateSyncStatus(ServerSyncStatus.IS_SYNCING) + + val isDeletedOnServer = networker.deleteHighlights(listOf(highlight.highlightId)) + + if (isDeletedOnServer) { + db.highlightDao().deleteById(highlight.highlightId) + } else { + updateSyncStatus(ServerSyncStatus.NEEDS_DELETION) + } + } + ServerSyncStatus.NEEDS_UPDATE.rawValue -> { + updateSyncStatus(ServerSyncStatus.IS_SYNCING) + + val isUpdatedOnServer = networker.updateHighlight( + UpdateHighlightInput( + annotation = Optional.presentIfNotNull(highlight.annotation), + highlightId = highlight.highlightId ?: "", + sharedAt = Optional.absent() + ) + ) + + if (isUpdatedOnServer) { + updateSyncStatus(ServerSyncStatus.IS_SYNCED) + } else { + updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE) + } + } + ServerSyncStatus.NEEDS_CREATION.rawValue -> { + updateSyncStatus(ServerSyncStatus.IS_SYNCING) + + val savedItemID = db.savedItemAndHighlightCrossRefDao() + .associatedSavedItemID(highlightId = highlight.highlightId) + + val isCreatedOnServer = networker.createHighlight( + CreateHighlightInput( + annotation = Optional.presentIfNotNull(highlight.annotation), + articleId = savedItemID ?: "", + id = highlight.highlightId, + patch = highlight.patch ?: "", + quote = highlight.quote ?: "", + shortId = highlight.shortId ?: "" + ) + ) + + if (isCreatedOnServer != null) { + updateSyncStatus(ServerSyncStatus.IS_SYNCED) + } else { + updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE) + } + } + else -> return + } } data class SavedItemSyncResult( diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt index c734af753..c69d6bf5e 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt @@ -14,7 +14,7 @@ data class Highlight( val patch: String, val prefix: String?, val quote: String, - val serverSyncStatus: Int = ServerSyncStatus.IS_SYNCED.rawValue, + var serverSyncStatus: Int = ServerSyncStatus.IS_SYNCED.rawValue, val shortId: String, val suffix: String?, val updatedAt: String? @@ -50,6 +50,9 @@ data class SavedItemAndHighlightCrossRef( interface SavedItemAndHighlightCrossRefDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAll(items: List) + + @Query("SELECT savedItemId FROM savedItemAndHighlightCrossRef WHERE highlightId = :highlightId") + fun associatedSavedItemID(highlightId: String): String? } data class SavedItemWithLabelsAndHighlights( @@ -78,6 +81,9 @@ interface HighlightDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAll(items: List) + @Query("DELETE FROM highlight WHERE highlightId = :highlightId") + fun deleteById(highlightId: String) + @Update fun update(highlight: Highlight) } From 245c40e6856b40f4fb64a2ad9e8dc4976084f9f3 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 16 Jan 2023 15:05:30 -0800 Subject: [PATCH 18/31] split data service file into more files --- .../java/app/omnivore/omnivore/AppModule.kt | 1 + .../omnivore/dataService/DataService.kt | 17 +++ .../omnivore/dataService/LibrarySync.kt | 109 +++++++++++++++ .../SyncOfflineChanges.kt} | 124 +----------------- .../omnivore/ui/library/LibraryViewModel.kt | 3 + .../omnivore/ui/reader/WebReaderViewModel.kt | 2 +- 6 files changed, 134 insertions(+), 122 deletions(-) create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/DataService.kt create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt rename android/Omnivore/app/src/main/java/app/omnivore/omnivore/{DataService.kt => dataService/SyncOfflineChanges.kt} (52%) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/AppModule.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/AppModule.kt index c31cf9457..a84f737f5 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/AppModule.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/AppModule.kt @@ -2,6 +2,7 @@ package app.omnivore.omnivore import android.content.Context import androidx.room.Room +import app.omnivore.omnivore.dataService.DataService import app.omnivore.omnivore.networking.Networker import app.omnivore.omnivore.persistence.AppDatabase import dagger.Module 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 new file mode 100644 index 000000000..f0143e4d2 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/DataService.kt @@ -0,0 +1,17 @@ +package app.omnivore.omnivore.dataService + +import android.content.Context +import androidx.room.Room +import app.omnivore.omnivore.networking.* +import app.omnivore.omnivore.persistence.AppDatabase +import javax.inject.Inject + +class DataService @Inject constructor( + context: Context, + val networker: Networker +) { + val db = Room.databaseBuilder( + context, + AppDatabase::class.java, "omnivore-database" + ).build() +} 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 new file mode 100644 index 000000000..a1030378f --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt @@ -0,0 +1,109 @@ +package app.omnivore.omnivore.dataService + +import android.util.Log +import app.omnivore.omnivore.networking.* +import app.omnivore.omnivore.persistence.entities.* + +suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 15): SavedItemSyncResult { + val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since) ?: return SavedItemSyncResult.errorResult + + val savedItems = syncResult.items.map { + SavedItem( + savedItemId = it.id, + title = it.title, + createdAt = it.createdAt as String, + savedAt = it.savedAt as String, + readAt = it.readAt as String?, + updatedAt = it.updatedAt as String?, + readingProgress = it.readingProgressPercent, + readingProgressAnchor = it.readingProgressAnchorIndex, + imageURLString = it.image, + pageURLString = it.url, + descriptionText = it.description, + publisherURLString = it.originalArticleUrl, + siteName = it.siteName, + author = it.author, + publishDate = it.publishedAt as String?, + slug = it.slug, + isArchived = it.isArchived, + contentReader = it.contentReader.rawValue, + content = null + ) + } + + db.savedItemDao().insertAll(savedItems) + + val labels: MutableList = mutableListOf() + val crossRefs: MutableList = mutableListOf() + + // save labels + for (item in syncResult.items) { + val itemLabels = (item.labels ?: listOf()).map { + SavedItemLabel( + savedItemLabelId = it.id, + name = it.name, + color = it.color, + createdAt = null, + labelDescription = null + ) + } + + labels.addAll(itemLabels) + + val newCrossRefs = itemLabels.map { + SavedItemAndSavedItemLabelCrossRef(savedItemLabelId = it.savedItemLabelId, savedItemId = item.id) + } + + crossRefs.addAll(newCrossRefs) + } + + db.savedItemLabelDao().insertAll(labels) + db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs) + + return SavedItemSyncResult( + hasError = false, + hasMoreItems = syncResult.hasMoreItems, + cursor = syncResult.cursor, + count = syncResult.items.size, + savedItemSlugs = syncResult.items.map { it.slug } + ) +} + +suspend fun DataService.syncSavedItemContent(slug: String) { + val syncResult = networker.savedItem(slug) + + val savedItem = syncResult.item ?: return + db.savedItemDao().insert(savedItem) + + // Persist Labels + db.savedItemLabelDao().insertAll(syncResult.labels) + + val labelCrossRefs = syncResult.labels.map { + SavedItemAndSavedItemLabelCrossRef(savedItemLabelId = it.savedItemLabelId, savedItemId = savedItem.savedItemId) + } + + db.savedItemAndSavedItemLabelCrossRefDao().insertAll(labelCrossRefs) + + // Persist Highlights + db.highlightDao().insertAll(syncResult.highlights) + + val highlightCrossRefs = syncResult.highlights.map { + SavedItemAndHighlightCrossRef(highlightId = it.highlightId, savedItemId = savedItem.savedItemId) + } + + db.savedItemAndHighlightCrossRefDao().insertAll(highlightCrossRefs) + + Log.d("sync", "saved content for item with id: ${savedItem.savedItemId}") +} + +data class SavedItemSyncResult( + val hasError: Boolean, + val hasMoreItems: Boolean, + val count: Int, + val savedItemSlugs: List, + val cursor: String? +) { + companion object { + val errorResult = SavedItemSyncResult(hasError = true, hasMoreItems = true, cursor = null, count = 0, savedItemSlugs = listOf()) + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SyncOfflineChanges.kt similarity index 52% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SyncOfflineChanges.kt index cfe12257c..c3a6fd263 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DataService.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SyncOfflineChanges.kt @@ -1,118 +1,12 @@ -package app.omnivore.omnivore +package app.omnivore.omnivore.dataService -import android.content.Context -import android.util.Log -import androidx.room.Room import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput import app.omnivore.omnivore.models.ServerSyncStatus import app.omnivore.omnivore.networking.* -import app.omnivore.omnivore.persistence.AppDatabase -import app.omnivore.omnivore.persistence.entities.* +import app.omnivore.omnivore.persistence.entities.Highlight +import app.omnivore.omnivore.persistence.entities.SavedItem import com.apollographql.apollo3.api.Optional -import javax.inject.Inject - -class DataService @Inject constructor( - context: Context, - val networker: Networker -) { - val db = Room.databaseBuilder( - context, - AppDatabase::class.java, "omnivore-database" - ).build() -} - -suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 15): SavedItemSyncResult { - val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since) ?: return SavedItemSyncResult.errorResult - - val savedItems = syncResult.items.map { - SavedItem( - savedItemId = it.id, - title = it.title, - createdAt = it.createdAt as String, - savedAt = it.savedAt as String, - readAt = it.readAt as String?, - updatedAt = it.updatedAt as String?, - readingProgress = it.readingProgressPercent, - readingProgressAnchor = it.readingProgressAnchorIndex, - imageURLString = it.image, - pageURLString = it.url, - descriptionText = it.description, - publisherURLString = it.originalArticleUrl, - siteName = it.siteName, - author = it.author, - publishDate = it.publishedAt as String?, - slug = it.slug, - isArchived = it.isArchived, - contentReader = it.contentReader.rawValue, - content = null - ) - } - - db.savedItemDao().insertAll(savedItems) - - val labels: MutableList = mutableListOf() - val crossRefs: MutableList = mutableListOf() - - // save labels - for (item in syncResult.items) { - val itemLabels = (item.labels ?: listOf()).map { - SavedItemLabel( - savedItemLabelId = it.id, - name = it.name, - color = it.color, - createdAt = null, - labelDescription = null - ) - } - - labels.addAll(itemLabels) - - val newCrossRefs = itemLabels.map { - SavedItemAndSavedItemLabelCrossRef(savedItemLabelId = it.savedItemLabelId, savedItemId = item.id) - } - - crossRefs.addAll(newCrossRefs) - } - - db.savedItemLabelDao().insertAll(labels) - db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs) - - return SavedItemSyncResult( - hasError = false, - hasMoreItems = syncResult.hasMoreItems, - cursor = syncResult.cursor, - count = syncResult.items.size, - savedItemSlugs = syncResult.items.map { it.slug } - ) -} - -suspend fun DataService.syncSavedItemContent(slug: String) { - val syncResult = networker.savedItem(slug) - - val savedItem = syncResult.item ?: return - db.savedItemDao().insert(savedItem) - - // Persist Labels - db.savedItemLabelDao().insertAll(syncResult.labels) - - val labelCrossRefs = syncResult.labels.map { - SavedItemAndSavedItemLabelCrossRef(savedItemLabelId = it.savedItemLabelId, savedItemId = savedItem.savedItemId) - } - - db.savedItemAndSavedItemLabelCrossRefDao().insertAll(labelCrossRefs) - - // Persist Highlights - db.highlightDao().insertAll(syncResult.highlights) - - val highlightCrossRefs = syncResult.highlights.map { - SavedItemAndHighlightCrossRef(highlightId = it.highlightId, savedItemId = savedItem.savedItemId) - } - - db.savedItemAndHighlightCrossRefDao().insertAll(highlightCrossRefs) - - Log.d("sync", "saved content for item with id: ${savedItem.savedItemId}") -} suspend fun DataService.syncOfflineItemsWithServerIfNeeded() { val unSyncedSavedItems = db.savedItemDao().getUnSynced() @@ -235,15 +129,3 @@ private suspend fun DataService.syncHighlight(highlight: Highlight) { else -> return } } - -data class SavedItemSyncResult( - val hasError: Boolean, - val hasMoreItems: Boolean, - val count: Int, - val savedItemSlugs: List, - val cursor: String? -) { - companion object { - val errorResult = SavedItemSyncResult(hasError = true, hasMoreItems = true, cursor = null, count = 0, savedItemSlugs = 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 84da2262a..1031fc40e 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 @@ -8,6 +8,9 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.omnivore.omnivore.* +import app.omnivore.omnivore.dataService.DataService +import app.omnivore.omnivore.dataService.sync +import app.omnivore.omnivore.dataService.syncOfflineItemsWithServerIfNeeded import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.persistence.entities.SavedItemCardData import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels 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 21835f088..3a911562d 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 @@ -5,9 +5,9 @@ import androidx.compose.foundation.ScrollState import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import app.omnivore.omnivore.DataService import app.omnivore.omnivore.DatastoreKeys import app.omnivore.omnivore.DatastoreRepository +import app.omnivore.omnivore.dataService.DataService import app.omnivore.omnivore.persistence.entities.SavedItem import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.ui.library.SavedItemAction From 8aec8005ce645252c0593e9fe1f0504968fadfeb Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 16 Jan 2023 15:07:20 -0800 Subject: [PATCH 19/31] check if content value is present when opening web reader from db --- .../app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 3a911562d..4b347c733 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 @@ -50,10 +50,10 @@ class WebReaderViewModel @Inject constructor( withContext(Dispatchers.IO) { val persistedItem = dataService.db.savedItemDao().getSavedItemWithLabelsAndHighlights(slug) - if (persistedItem?.savedItem != null) { + if (persistedItem?.savedItem?.content != null) { val articleContent = ArticleContent( title = persistedItem.savedItem.title, - htmlContent = persistedItem.savedItem.content ?: "", + htmlContent = persistedItem.savedItem.content, highlights = persistedItem.highlights, contentStatus = "SUCCEEDED", objectID = "", From 89099d443148debed8d92fd86a5c80c2012c06a5 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 17 Jan 2023 12:45:48 -0800 Subject: [PATCH 20/31] use db to save archive status changes and handle deletion from web reader --- .../SavedItemMenuActionHandlers.kt | 56 +++++++++++++++++++ .../persistence/entities/SavedItem.kt | 5 +- .../omnivore/omnivore/ui/reader/WebReader.kt | 13 +++-- .../omnivore/ui/reader/WebReaderViewModel.kt | 9 ++- 4 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SavedItemMenuActionHandlers.kt diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SavedItemMenuActionHandlers.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SavedItemMenuActionHandlers.kt new file mode 100644 index 000000000..10988a3cc --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SavedItemMenuActionHandlers.kt @@ -0,0 +1,56 @@ +package app.omnivore.omnivore.dataService + +import app.omnivore.omnivore.models.ServerSyncStatus +import app.omnivore.omnivore.networking.archiveSavedItem +import app.omnivore.omnivore.networking.deleteSavedItem +import app.omnivore.omnivore.networking.unarchiveSavedItem +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +suspend fun DataService.deleteSavedItem(itemID: String) { + withContext(Dispatchers.IO) { + val savedItem = db.savedItemDao().findById(itemID = itemID) ?: return@withContext + savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue + db.savedItemDao().update(savedItem) + + val isUpdatedOnServer = networker.deleteSavedItem(itemID) + + if (isUpdatedOnServer) { + db.savedItemDao().deleteById(itemID) + } + } +} + +suspend fun DataService.archiveSavedItem(itemID: String) { + withContext(Dispatchers.IO) { + val savedItem = db.savedItemDao().findById(itemID = itemID) ?: return@withContext + + savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue + savedItem.isArchived = true + db.savedItemDao().update(savedItem) + + val isUpdatedOnServer = networker.archiveSavedItem(itemID) + + if (isUpdatedOnServer) { + savedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue + db.savedItemDao().update(savedItem) + } + } +} + +suspend fun DataService.unarchiveSavedItem(itemID: String) { + withContext(Dispatchers.IO) { + val savedItem = db.savedItemDao().findById(itemID = itemID) ?: return@withContext + + savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue + savedItem.isArchived = false + db.savedItemDao().update(savedItem) + + val isUpdatedOnServer = networker.unarchiveSavedItem(itemID) + + if (isUpdatedOnServer) { + savedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue + db.savedItemDao().update(savedItem) + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt index 00965a85d..91b3096ad 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt @@ -23,7 +23,7 @@ data class SavedItem( val author: String?, val publishDate: String?, val slug: String, - val isArchived: Boolean, + var isArchived: Boolean, val contentReader: String? = null, val content: String? = null, val createdId: String? = null, @@ -92,6 +92,9 @@ interface SavedItemDao { @Query("SELECT * FROM savedItem") fun getAll(): List + @Query("SELECT * FROM savedItem WHERE savedItemId = :itemID") + fun findById(itemID: String): SavedItem? + @Query("SELECT * FROM savedItem WHERE serverSyncStatus != 0") fun getUnSynced(): List diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt index 596d6b7fa..fbb10bfdf 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.verticalScroll import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.* import androidx.compose.runtime.* @@ -97,12 +98,12 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod title = {}, actions = { // Disabling menu until we implement local persistence -// IconButton(onClick = { isMenuExpanded = true }) { -// Icon( -// imageVector = Icons.Filled.Menu, -// contentDescription = null -// ) -// } + IconButton(onClick = { isMenuExpanded = true }) { + Icon( + imageVector = Icons.Filled.Menu, + contentDescription = null + ) + } IconButton(onClick = { showWebPreferencesDialog = true }) { Icon( imageVector = Icons.Filled.Settings, // TODO: set a better icon 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 4b347c733..ac9fc7f21 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 @@ -8,6 +8,9 @@ import androidx.lifecycle.viewModelScope import app.omnivore.omnivore.DatastoreKeys import app.omnivore.omnivore.DatastoreRepository import app.omnivore.omnivore.dataService.DataService +import app.omnivore.omnivore.dataService.archiveSavedItem +import app.omnivore.omnivore.dataService.deleteSavedItem +import app.omnivore.omnivore.dataService.unarchiveSavedItem import app.omnivore.omnivore.persistence.entities.SavedItem import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.ui.library.SavedItemAction @@ -91,19 +94,19 @@ class WebReaderViewModel @Inject constructor( when (action) { SavedItemAction.Delete -> { viewModelScope.launch { - networker.deleteSavedItem(itemID) + dataService.deleteSavedItem(itemID) popToLibraryView(itemID) } } SavedItemAction.Archive -> { viewModelScope.launch { - networker.archiveSavedItem(itemID) + dataService.archiveSavedItem(itemID) popToLibraryView(itemID) } } SavedItemAction.Unarchive -> { viewModelScope.launch { - networker.unarchiveSavedItem(itemID) + dataService.unarchiveSavedItem(itemID) popToLibraryView(itemID) } } From 7d20c6ca91b20d4911fa501b3b8dd038b22edbd9 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 17 Jan 2023 12:52:07 -0800 Subject: [PATCH 21/31] use data service item action to handle library list changes --- .../omnivore/ui/library/LibraryViewModel.kt | 32 +++---------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryViewModel.kt index 1031fc40e..20adabb6a 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 @@ -8,9 +8,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.omnivore.omnivore.* -import app.omnivore.omnivore.dataService.DataService -import app.omnivore.omnivore.dataService.sync -import app.omnivore.omnivore.dataService.syncOfflineItemsWithServerIfNeeded +import app.omnivore.omnivore.dataService.* import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.persistence.entities.SavedItemCardData import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels @@ -137,44 +135,22 @@ class LibraryViewModel @Inject constructor( fun handleSavedItemAction(itemID: String, action: SavedItemAction) { when (action) { SavedItemAction.Delete -> { - removeItemFromList(itemID) - viewModelScope.launch { - networker.deleteSavedItem(itemID) + dataService.deleteSavedItem(itemID) } } SavedItemAction.Archive -> { - removeItemFromList(itemID) viewModelScope.launch { - networker.archiveSavedItem(itemID) + dataService.archiveSavedItem(itemID) } } SavedItemAction.Unarchive -> { - removeItemFromList(itemID) viewModelScope.launch { - networker.unarchiveSavedItem(itemID) + dataService.unarchiveSavedItem(itemID) } } } } - - private fun removeItemFromList(itemID: String) { - viewModelScope.launch { - withContext(Dispatchers.IO) { - dataService.db.savedItemDao().deleteById(itemID) - } - } - } - - private fun searchQuery(): String { - var query = "in:inbox sort:saved" - - if (searchTextLiveData.value != "") { - query = query.plus(" ${searchTextLiveData.value}") - } - - return query - } } enum class SavedItemAction { From 23f1ccde1e7a871ca89c41baff1e2938db51e9f0 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 17 Jan 2023 13:24:56 -0800 Subject: [PATCH 22/31] pop web reader when un/archive or delete action is selected from the reader view --- .../app/omnivore/omnivore/ui/reader/WebReader.kt | 12 ++++++++++++ .../omnivore/ui/reader/WebReaderViewModel.kt | 13 +++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt index fbb10bfdf..ededf42ad 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt @@ -10,9 +10,11 @@ import android.view.* import android.webkit.JavascriptInterface import android.webkit.WebView import android.webkit.WebViewClient +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.foundation.verticalScroll +import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Menu @@ -30,6 +32,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import app.omnivore.omnivore.R +import app.omnivore.omnivore.ui.save.SaveSheetActivityBase import app.omnivore.omnivore.ui.savedItemViews.SavedItemContextMenu import com.google.gson.Gson import kotlinx.coroutines.CoroutineScope @@ -41,11 +44,14 @@ import kotlin.math.roundToInt @Composable fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewModel) { + val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher + var isMenuExpanded by remember { mutableStateOf(false) } var showWebPreferencesDialog by remember { mutableStateOf(false ) } val webReaderParams: WebReaderParams? by webReaderViewModel.webReaderParamsLiveData.observeAsState(null) val annotation: String? by webReaderViewModel.annotationLiveData.observeAsState(null) + val shouldPopView: Boolean by webReaderViewModel.shouldPopViewLiveData.observeAsState(false) val maxToolbarHeight = 48.dp val maxToolbarHeightPx = with(LocalDensity.current) { maxToolbarHeight.roundToPx().toFloat() } @@ -140,6 +146,12 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod ) } } + + LaunchedEffect(shouldPopView) { + if (shouldPopView) { + onBackPressedDispatcher?.onBackPressed() + } + } } else { // TODO: add a proper loading view Text("Loading...") 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 ac9fc7f21..3cde695de 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 @@ -43,6 +43,7 @@ class WebReaderViewModel @Inject constructor( val webReaderParamsLiveData = MutableLiveData(null) val annotationLiveData = MutableLiveData(null) val javascriptActionLoopUUIDLiveData = MutableLiveData(lastJavascriptActionLoopUUID) + val shouldPopViewLiveData = MutableLiveData(false) var hasTappedExistingHighlight = false var lastTapCoordinates: TapCoordinates? = null @@ -95,28 +96,27 @@ class WebReaderViewModel @Inject constructor( SavedItemAction.Delete -> { viewModelScope.launch { dataService.deleteSavedItem(itemID) - popToLibraryView(itemID) + popToLibraryView() } } SavedItemAction.Archive -> { viewModelScope.launch { dataService.archiveSavedItem(itemID) - popToLibraryView(itemID) + popToLibraryView() } } SavedItemAction.Unarchive -> { viewModelScope.launch { dataService.unarchiveSavedItem(itemID) - popToLibraryView(itemID) + popToLibraryView() } } } } - private fun popToLibraryView(itemID: String) { + private fun popToLibraryView() { CoroutineScope(Dispatchers.Main).launch { - // TODO: pop to library - Log.d("maxx", "should pop to library and remove item with ID: $itemID") + shouldPopViewLiveData.postValue(true) } } @@ -172,6 +172,7 @@ class WebReaderViewModel @Inject constructor( } fun reset() { + shouldPopViewLiveData.postValue(false) webReaderParamsLiveData.value = null annotationLiveData.value = null scrollState = ScrollState(0) From bb1efac33670353022006482e6c543f121947801 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 17 Jan 2023 21:00:06 -0800 Subject: [PATCH 23/31] update db for saved item property changes before sending updates to serevr --- .../dataService/HighlightActionHandlers.kt | 118 ++++++++++++++++++ .../ReadingProgressChangeHandler.kt | 28 +++++ .../persistence/entities/Highlight.kt | 13 +- .../persistence/entities/SavedItem.kt | 4 +- .../omnivore/ui/reader/WebReaderViewModel.kt | 20 +-- 5 files changed, 162 insertions(+), 21 deletions(-) create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/HighlightActionHandlers.kt create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/ReadingProgressChangeHandler.kt diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/HighlightActionHandlers.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/HighlightActionHandlers.kt new file mode 100644 index 000000000..4fc89f1f6 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/HighlightActionHandlers.kt @@ -0,0 +1,118 @@ +package app.omnivore.omnivore.dataService + +import app.omnivore.omnivore.models.ServerSyncStatus +import app.omnivore.omnivore.networking.* +import app.omnivore.omnivore.persistence.entities.Highlight +import app.omnivore.omnivore.persistence.entities.SavedItemAndHighlightCrossRef +import com.google.gson.Gson +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +suspend fun DataService.createWebHighlight(jsonString: String) { + val createHighlightInput = Gson().fromJson(jsonString, CreateHighlightParams::class.java).asCreateHighlightInput() + + withContext(Dispatchers.IO) { + val highlight = Highlight( + highlightId = createHighlightInput.id, + shortId = createHighlightInput.shortId, + quote = createHighlightInput.quote, + prefix = null, + suffix = null, + patch = createHighlightInput.patch, + annotation = createHighlightInput.annotation.getOrNull(), + createdAt = null, + updatedAt = null, + createdByMe = false + ) + + highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue + + val crossRef = SavedItemAndHighlightCrossRef( + highlightId = createHighlightInput.id, + savedItemId = createHighlightInput.articleId + ) + + db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef)) + db.highlightDao().update(highlight) + + val newHighlight = networker.createHighlight(createHighlightInput) + + newHighlight?.let { + db.highlightDao().update(it) + } + } +} + +suspend fun DataService.mergeWebHighlights(jsonString: String) { + val mergeHighlightInput = Gson().fromJson(jsonString, MergeHighlightsParams::class.java).asMergeHighlightInput() + + withContext(Dispatchers.IO) { + val highlight = db.highlightDao().findById(highlightId = mergeHighlightInput.id) ?: return@withContext + highlight.shortId = mergeHighlightInput.shortId + highlight.quote = mergeHighlightInput.quote + highlight.patch = mergeHighlightInput.patch + highlight.prefix = mergeHighlightInput.prefix.getOrNull() + highlight.annotation = mergeHighlightInput.annotation.getOrNull() + highlight.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue + + for (highlightID in mergeHighlightInput.overlapHighlightIdList) { + deleteHighlight(highlightID) + } + + val crossRef = SavedItemAndHighlightCrossRef( + highlightId = mergeHighlightInput.id, + savedItemId = mergeHighlightInput.articleId + ) + + db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef)) + db.highlightDao().update(highlight) + + val isUpdatedOnServer = networker.mergeHighlights(mergeHighlightInput) + + if (isUpdatedOnServer) { + highlight.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue + db.highlightDao().update(highlight) + } + } +} + +suspend fun DataService.updateWebHighlight(jsonString: String) { + val updateHighlightParams = Gson().fromJson(jsonString, UpdateHighlightParams::class.java).asUpdateHighlightInput() + + withContext(Dispatchers.IO) { + val highlight = db.highlightDao().findById(highlightId = updateHighlightParams.highlightId) ?: return@withContext + + highlight.annotation = updateHighlightParams.annotation.getOrNull() + highlight.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue + db.highlightDao().update(highlight) + + val isUpdatedOnServer = networker.updateHighlight(updateHighlightParams) + + if (isUpdatedOnServer) { + highlight.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue + db.highlightDao().update(highlight) + } + } +} + +suspend fun DataService.deleteHighlights(jsonString: String) { + val highlightIDs = Gson().fromJson(jsonString, DeleteHighlightParams::class.java).asIdList() + + for (highlightID in highlightIDs) { + deleteHighlight(highlightID) + } +} + +private suspend fun DataService.deleteHighlight(highlightID: String) { + withContext(Dispatchers.IO) { + val highlight = db.highlightDao().findById(highlightId = highlightID) ?: return@withContext + highlight.serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue + db.highlightDao().update(highlight) + + val isUpdatedOnServer = networker.deleteHighlights(listOf(highlightID)) + + if (isUpdatedOnServer) { + db.highlightDao().deleteById(highlightId = highlightID) + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/ReadingProgressChangeHandler.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/ReadingProgressChangeHandler.kt new file mode 100644 index 000000000..98e175ddd --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/ReadingProgressChangeHandler.kt @@ -0,0 +1,28 @@ +package app.omnivore.omnivore.dataService + +import app.omnivore.omnivore.models.ServerSyncStatus +import app.omnivore.omnivore.networking.ReadingProgressParams +import app.omnivore.omnivore.networking.updateReadingProgress +import com.google.gson.Gson +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +suspend fun DataService.updateWebReadingProgress(jsonString: String) { + val readingProgressParams = Gson().fromJson(jsonString, ReadingProgressParams::class.java) + val savedItemId = readingProgressParams.id ?: return + + withContext(Dispatchers.IO) { + val savedItem = db.savedItemDao().findById(savedItemId) ?: return@withContext + savedItem.readingProgress = readingProgressParams.readingProgressPercent ?: 0.0 + savedItem.readingProgressAnchor = readingProgressParams.readingProgressAnchorIndex ?: 0 + savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue + db.savedItemDao().update(savedItem) + + val isUpdatedOnServer = networker.updateReadingProgress(readingProgressParams) + + if (isUpdatedOnServer) { + savedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue + db.savedItemDao().update(savedItem) + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt index c69d6bf5e..757a8ee42 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt @@ -7,15 +7,15 @@ import app.omnivore.omnivore.models.ServerSyncStatus @Entity data class Highlight( @PrimaryKey val highlightId: String, - val annotation: String?, + var annotation: String?, val createdAt: String?, val createdByMe: Boolean, val markedForDeletion: Boolean = false, - val patch: String, - val prefix: String?, - val quote: String, + var patch: String, + var prefix: String?, + var quote: String, var serverSyncStatus: Int = ServerSyncStatus.IS_SYNCED.rawValue, - val shortId: String, + var shortId: String, val suffix: String?, val updatedAt: String? @@ -84,6 +84,9 @@ interface HighlightDao { @Query("DELETE FROM highlight WHERE highlightId = :highlightId") fun deleteById(highlightId: String) + @Query("SELECT * FROM highlight WHERE highlightId = :highlightId") + fun findById(highlightId: String): Highlight? + @Update fun update(highlight: Highlight) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt index 91b3096ad..506534964 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt @@ -13,8 +13,8 @@ data class SavedItem( val savedAt: String, val readAt: String?, val updatedAt: String?, - val readingProgress: Double, - val readingProgressAnchor: Int, + var readingProgress: Double, + var readingProgressAnchor: Int, val imageURLString: String?, val pageURLString: String, val descriptionText: String?, 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 3cde695de..dfb472e8d 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt @@ -7,10 +7,7 @@ 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.dataService.archiveSavedItem -import app.omnivore.omnivore.dataService.deleteSavedItem -import app.omnivore.omnivore.dataService.unarchiveSavedItem +import app.omnivore.omnivore.dataService.* import app.omnivore.omnivore.persistence.entities.SavedItem import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.ui.library.SavedItemAction @@ -124,28 +121,24 @@ class WebReaderViewModel @Inject constructor( when (actionID) { "createHighlight" -> { viewModelScope.launch { - val isHighlightSynced = networker.createWebHighlight(jsonString) - Log.d("Network", "isHighlightSynced = $isHighlightSynced") + dataService.createWebHighlight(jsonString) } } "deleteHighlight" -> { Log.d("Loggo", "receive delete highlight action: $jsonString") viewModelScope.launch { - val isHighlightDeletionSynced = networker.deleteHighlight(jsonString) - Log.d("Network", "isHighlightDeletionSynced = $isHighlightDeletionSynced") + dataService.deleteHighlights(jsonString) } } "updateHighlight" -> { Log.d("Loggo", "receive update highlight action: $jsonString") viewModelScope.launch { - val isHighlightUpdateSynced = networker.updateWebHighlight(jsonString) - Log.d("Network", "isHighlightUpdateSynced = $isHighlightUpdateSynced") + dataService.updateWebHighlight(jsonString) } } "articleReadingProgress" -> { viewModelScope.launch { - val isReadingProgressSynced = networker.updateWebReadingProgress(jsonString) - Log.d("Network", "isReadingProgressSynced = $isReadingProgressSynced") + dataService.updateWebReadingProgress(jsonString) } } "annotate" -> { @@ -161,8 +154,7 @@ class WebReaderViewModel @Inject constructor( } "mergeHighlight" -> { viewModelScope.launch { - val isHighlightSynced = networker.mergeWebHighlights(jsonString) - Log.d("Network", "isMergedHighlightSynced = $isHighlightSynced") + dataService.mergeWebHighlights(jsonString) } } else -> { From f352f2a3de1e89196aae8fdfba63a67da8f02afb Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 18 Jan 2023 08:59:08 -0800 Subject: [PATCH 24/31] fetch cards list then content for first 20 updated items --- .../omnivore/dataService/LibrarySync.kt | 2 +- .../omnivore/ui/library/LibraryViewModel.kt | 22 ++++++++++++------- 2 files changed, 15 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 a1030378f..fb73fe3ed 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,7 +4,7 @@ import android.util.Log import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.persistence.entities.* -suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 15): SavedItemSyncResult { +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 val savedItems = syncResult.items.map { 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 20adabb6a..35b185efd 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 @@ -81,23 +81,29 @@ class LibraryViewModel @Inject constructor( } } - private suspend fun performItemSync(cursor: String?, since: String, count: Int, startTime: String) { + private suspend fun performItemSync(cursor: String?, since: String, count: Int, startTime: String, fetchContentSlugs: List = listOf()) { dataService.syncOfflineItemsWithServerIfNeeded() val result = dataService.sync(since = since, cursor = cursor) - // TODO: Defer this until later? /fix - this results in a 429 server error -// for (slug in result.savedItemSlugs) { -// dataService.syncSavedItemContent(slug) -// } - val totalCount = count + result.count Log.d("sync", "fetched ${result.count} items") - if (totalCount < 180 && !result.hasError && result.hasMoreItems && result.cursor != null) { - performItemSync(cursor = result.cursor, since = since, count = totalCount, startTime = startTime) + if (!result.hasError && result.hasMoreItems && result.cursor != null) { + performItemSync( + cursor = result.cursor, + since = since, + count = totalCount, + startTime = startTime, + fetchContentSlugs = fetchContentSlugs.ifEmpty { result.savedItemSlugs } + + ) } else { datastoreRepo.putString(DatastoreKeys.libraryLastSyncTimestamp, startTime) + + for (slug in result.savedItemSlugs) { + dataService.syncSavedItemContent(slug) + } } } From 3f1c9d709374f948baf92f3e2f56757b17a0cac3 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 18 Jan 2023 09:09:47 -0800 Subject: [PATCH 25/31] use insert rather than update when creating a highlight --- .../omnivore/omnivore/dataService/HighlightActionHandlers.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/HighlightActionHandlers.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/HighlightActionHandlers.kt index 4fc89f1f6..6a354fed4 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/HighlightActionHandlers.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/HighlightActionHandlers.kt @@ -32,8 +32,8 @@ suspend fun DataService.createWebHighlight(jsonString: String) { savedItemId = createHighlightInput.articleId ) + db.highlightDao().insertAll(listOf(highlight)) db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef)) - db.highlightDao().update(highlight) val newHighlight = networker.createHighlight(createHighlightInput) From a02f1efaf494d881feb2327805d917ed5b1e78de Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 18 Jan 2023 09:35:53 -0800 Subject: [PATCH 26/31] remove old web reader --- .../main/java/app/omnivore/omnivore/Routes.kt | 1 - .../ReadingProgressChangeHandler.kt | 3 ++ .../omnivore/ui/reader/WebAppReader.kt | 45 ------------------- .../omnivore/ui/reader/WebReaderViewModel.kt | 1 + .../app/omnivore/omnivore/ui/root/RootView.kt | 8 ---- 5 files changed, 4 insertions(+), 54 deletions(-) delete mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebAppReader.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 d30d337a6..a1e5ece80 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 @@ -2,6 +2,5 @@ package app.omnivore.omnivore sealed class Routes(val route: String) { object Library : Routes("Library") - object WebAppReader : Routes("WebAppReader") object Settings: Routes("Settings") } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/ReadingProgressChangeHandler.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/ReadingProgressChangeHandler.kt index 98e175ddd..3138048d7 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/ReadingProgressChangeHandler.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/ReadingProgressChangeHandler.kt @@ -1,5 +1,6 @@ package app.omnivore.omnivore.dataService +import android.util.Log import app.omnivore.omnivore.models.ServerSyncStatus import app.omnivore.omnivore.networking.ReadingProgressParams import app.omnivore.omnivore.networking.updateReadingProgress @@ -12,6 +13,7 @@ suspend fun DataService.updateWebReadingProgress(jsonString: String) { val savedItemId = readingProgressParams.id ?: return withContext(Dispatchers.IO) { + Log.d("sync", "updating progress") val savedItem = db.savedItemDao().findById(savedItemId) ?: return@withContext savedItem.readingProgress = readingProgressParams.readingProgressPercent ?: 0.0 savedItem.readingProgressAnchor = readingProgressParams.readingProgressAnchorIndex ?: 0 @@ -22,6 +24,7 @@ suspend fun DataService.updateWebReadingProgress(jsonString: String) { if (isUpdatedOnServer) { savedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue + Log.d("sync", "saved item progress: ${savedItem.readingProgress}") db.savedItemDao().update(savedItem) } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebAppReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebAppReader.kt deleted file mode 100644 index ab77cfd32..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebAppReader.kt +++ /dev/null @@ -1,45 +0,0 @@ -package app.omnivore.omnivore.ui.reader - -import android.annotation.SuppressLint -import android.view.ViewGroup -import android.webkit.CookieManager -import android.webkit.WebView -import android.webkit.WebViewClient -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.viewinterop.AndroidView -import app.omnivore.omnivore.BuildConfig - -@SuppressLint("SetJavaScriptEnabled") -@Composable -fun ArticleWebView(slug: String, authCookieString: String) { - WebView.setWebContentsDebuggingEnabled(true) - - val url = BuildConfig.OMNIVORE_WEB_URL + "/app/me/$slug" - - AndroidView(factory = { - WebView(it).apply { - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - - settings.javaScriptEnabled = true - settings.allowContentAccess = true - settings.allowFileAccess = true - settings.domStorageEnabled = true - - webViewClient = object : WebViewClient() { - } - - CookieManager.getInstance().setAcceptThirdPartyCookies(this, true) - CookieManager.getInstance().setAcceptCookie(true) - CookieManager.getInstance().setCookie(BuildConfig.OMNIVORE_API_URL, authCookieString) - CookieManager.getInstance().setCookie(BuildConfig.OMNIVORE_WEB_URL, authCookieString) { - loadUrl(url) - } - } - }, update = { - it.loadUrl(url) - }) -} 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 dfb472e8d..6f6f013aa 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 @@ -137,6 +137,7 @@ class WebReaderViewModel @Inject constructor( } } "articleReadingProgress" -> { + Log.d("sync", "articleReadingProgressMesssageReceived") viewModelScope.launch { dataService.updateWebReadingProgress(jsonString) } 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 e22d3ac18..76be9653d 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 @@ -79,14 +79,6 @@ fun PrimaryNavigator( ) } - // TODO: delete this route and views - composable("WebAppReader/{slug}") { - ArticleWebView( - it.arguments?.getString("slug") ?: "", - authCookieString = loginViewModel.getAuthCookieString() ?: "" - ) - } - composable("WebReader/{slug}") { webReaderViewModel.reset() // clear previously loaded item From 83897e63e3893adfcc337fd294eb83b602b9b7a3 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 18 Jan 2023 09:37:05 -0800 Subject: [PATCH 27/31] update web bundles --- android/Omnivore/app/src/main/assets/bundle.js | 2 +- apple/OmnivoreKit/Sources/Views/Resources/bundle.js | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/android/Omnivore/app/src/main/assets/bundle.js b/android/Omnivore/app/src/main/assets/bundle.js index 769938d7b..481891c34 100644 --- a/android/Omnivore/app/src/main/assets/bundle.js +++ b/android/Omnivore/app/src/main/assets/bundle.js @@ -1,2 +1,2 @@ /*! For license information please see bundle.js.LICENSE.txt */ -(()=>{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 d(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 f(e){this.map={},e instanceof f?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 f(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 f(this.headers),url:this.url})},x.error=function(){var e=new x(null,{status:0,statusText:""});return e.type="error",e};var k=[301,302,303,307,308];x.redirect=function(e,t){if(-1===k.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 E(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 f,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)}))}E.polyfill=!0,e.fetch||(e.fetch=E,e.Headers=f,e.Request=b,e.Response=x),t.Headers=f,t.Request=b,t.Response=x,t.fetch=E,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 d=c[0],f=c[1],p=c[2],h=c[3],g=c[4],m=this.diff_main(d,p,o,i),v=this.diff_main(f,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,d="",f="";s=1&&c>=1){l.splice(s-u-c,u+c),s=s-u-c;for(var p=this.diff_main(d,f,!1,o),h=p.length-1;h>=0;h--)l.splice(s,0,p[h]);s+=p.length}c=0,u=0,d="",f=""}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),d=new Array(u),f=0;fo);b++){for(var w=-b+g;w<=b-m;w+=2){for(var x=s+w,k=(O=w==-b||w!=b&&c[x-1]i)m+=2;else if(k>a)g+=2;else if(h&&(_=s+p-w)>=0&&_=(S=i-d[_]))return this.diff_bisectSplit_(e,r,O,k,o)}for(var E=-b+v;E<=b-y;E+=2){for(var S,_=s+E,C=(S=E==-b||E!=b&&d[_-1]i)y+=2;else if(C>a)v+=2;else if(!h){var O;if((x=s+p-E)>=0&&x=(S=i-S))return this.diff_bisectSplit_(e,r,O,k,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,d=i(n,r,Math.ceil(n.length/4)),f=i(n,r,Math.ceil(n.length/2));return d||f?(a=f?d&&d[4].length>f[4].length?d:f:d,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,d=0;l0?o[i-1]:-1,s=0,u=0,c=0,d=0,a=null,r=!0)),l++;for(r&&this.diff_cleanupMerge(e),this.diff_cleanupSemanticLossless(e),l=1;l=g?(h>=f.length/2||h>=p.length/2)&&(e.splice(l,0,new t.Diff(0,p.substring(0,h))),e[l-1][1]=f.substring(0,f.length-h),e[l+1][1]=p.substring(h),l++):(g>=f.length/2||g>=p.length/2)&&(e.splice(l,0,new t.Diff(0,f.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]=f.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_),d=u&&e.match(t.blanklineEndRegex_),f=c&&n.match(t.blanklineStartRegex_);return d||f?5:u||c?4:i&&!l&&s?3:l||s?2:i||a?1:0}for(var r=1;r=f&&(f=p,u=o,c=i,d=a)}e[r-1][1]!=u&&(u?e[r-1][1]=u:(e.splice(r-1,1),r--),e[r][1]=c,d?e[r+1][1]=d:(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,d=!1;l0?o[i-1]:-1,c=d=!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|(d[v+1]|d[v])<<1|1|d[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;d=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,d=0,f=i,p=i,h=0;h=2*this.Patch_Margin&&u&&(this.patch_addContext_(s,f),l.push(s),s=new t.patch_obj,u=0,f=p,c=d)}1!==g&&(c+=m.length),g!==n&&(d+=m.length)}return u&&(this.patch_addContext_(s,f),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==(d=this.match_main(t,c.substring(c.length-this.Match_MaxBits),u+c.length-this.Match_MaxBits))||l>=d)&&(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==d?t.substring(l,l+c.length):t.substring(l,d+this.Match_MaxBits)))t=t.substring(0,l)+this.diff_text2(e[a].diffs)+t.substring(l+c.length);else{var f=this.diff_main(c,s,!1);if(c.length>this.Match_MaxBits&&this.diff_levenshtein(f)/c.length>this.Patch_DeleteThreshold)i[a]=!1;else{this.diff_cleanupSemanticLossless(f);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+=f.length,a+=f.length,c=!1,u.diffs.push(new t.Diff(d,f)),i.diffs.shift()):(f=f.substring(0,r-u.length1-this.Patch_Margin),u.length1+=f.length,a+=f.length,0===d?(u.length2+=f.length,l+=f.length):c=!1,u.diffs.push(new t.Diff(d,f)),f==i.diffs[0][1]?i.diffs.shift():i.diffs[0][1]=i.diffs[0][1].substring(f.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 d={},f=0;return s.forEach((function(e){d[++f]=e})),c.append("map",JSON.stringify(d)),f=0,s.forEach((function(e,t){c.append(""+ ++f,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+(f(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+(f(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 d(e){return-1!==e.indexOf("\n")}function f(e){return null!=e&&e.some(d)}},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],d=-1,f=[],p=void 0,h=void 0,g=void 0,m=[],v=[],y=e;do{var b=++d===c.length,w=b&&0!==f.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={},k=0,E=Object.keys(p);k{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,d,f,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,d=e.apply(r,n)}function b(e){return h=e,f=setTimeout(x,t),g?y(e):d}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 k(e);f=setTimeout(x,function(e){var n=t-(e-p);return m?l(n,c-(e-h)):n}(e))}function k(e){return f=void 0,v&&s?y(e):(s=u=void 0,d)}function E(){var e=o(),n=w(e);if(s=arguments,u=this,p=e,n){if(void 0===f)return b(p);if(m)return clearTimeout(f),f=setTimeout(x,t),y(p)}return void 0===f&&(f=setTimeout(x,t)),d}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),E.cancel=function(){void 0!==f&&clearTimeout(f),h=0,s=p=u=f=void 0},E.flush=function(){return void 0===f?d:k(o())},E}},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)},104:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){var{src:t,sizes:n,unoptimized:r=!1,priority:o=!1,loading:c,lazyRoot:d=null,lazyBoundary:v="200px",className:k,quality:S,width:_,height:C,style:O,objectFit:L,objectPosition:j,onLoadingComplete:P,placeholder:R="empty",blurDataURL:T}=e,A=p(e,["src","sizes","unoptimized","priority","loading","lazyRoot","lazyBoundary","className","quality","width","height","style","objectFit","objectPosition","onLoadingComplete","placeholder","blurDataURL"]);const M=i.useContext(u.ImageConfigContext),I=i.useMemo((()=>{const e=h||M||l.imageConfigDefault,t=[...e.deviceSizes,...e.imageSizes].sort(((e,t)=>e-t)),n=e.deviceSizes.sort(((e,t)=>e-t));return f({},e,{allSizes:t,deviceSizes:n})}),[M]);let D=A,F=n?"responsive":"intrinsic";"layout"in D&&(D.layout&&(F=D.layout),delete D.layout);let N=x;if("loader"in D){if(D.loader){const e=D.loader;N=t=>{const{config:n}=t,r=p(t,["config"]);return e(r)}}delete D.loader}let z="";if(function(e){return"object"==typeof e&&(y(e)||function(e){return void 0!==e.src}(e))}(t)){const e=y(t)?t.default:t;if(!e.src)throw new Error(`An object should only be passed to the image component src parameter if it comes from a static image import. It must include src. Received ${JSON.stringify(e)}`);if(T=T||e.blurDataURL,z=e.src,!(F&&"fill"===F||(C=C||e.height,_=_||e.width,e.height&&e.width)))throw new Error(`An object should only be passed to the image component src parameter if it comes from a static image import. It must include height and width. Received ${JSON.stringify(e)}`)}t="string"==typeof t?t:z;const $=w(_),B=w(C),W=w(S);let H=!o&&("lazy"===c||void 0===c);(t.startsWith("data:")||t.startsWith("blob:"))&&(r=!0,H=!1),"undefined"!=typeof window&&g.has(t)&&(H=!1);const[U,V]=i.useState(!1),[q,G,K]=s.useIntersection({rootRef:d,rootMargin:v,disabled:!H}),X=!H||G,Y={boxSizing:"border-box",display:"block",overflow:"hidden",width:"initial",height:"initial",background:"none",opacity:1,border:0,margin:0,padding:0},Z={boxSizing:"border-box",display:"block",width:"initial",height:"initial",background:"none",opacity:1,border:0,margin:0,padding:0};let Q,J=!1;const ee={position:"absolute",top:0,left:0,bottom:0,right:0,boxSizing:"border-box",padding:0,border:"none",margin:"auto",display:"block",width:0,height:0,minWidth:"100%",maxWidth:"100%",minHeight:"100%",maxHeight:"100%",objectFit:L,objectPosition:j},te=Object.assign({},O,"raw"===F?{}:ee),ne="blur"!==R||U?{}:{filter:"blur(20px)",backgroundSize:L||"cover",backgroundImage:`url("${T}")`,backgroundPosition:j||"0% 0%"};if("fill"===F)Y.display="block",Y.position="absolute",Y.top=0,Y.left=0,Y.bottom=0,Y.right=0;else if(void 0!==$&&void 0!==B){const e=B/$,t=isNaN(e)?"100%":100*e+"%";"responsive"===F?(Y.display="block",Y.position="relative",J=!0,Z.paddingTop=t):"intrinsic"===F?(Y.display="inline-block",Y.position="relative",Y.maxWidth="100%",J=!0,Z.maxWidth="100%",Q=`data:image/svg+xml,%3csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20version=%271.1%27%20width=%27${$}%27%20height=%27${B}%27/%3e`):"fixed"===F&&(Y.display="inline-block",Y.position="relative",Y.width=$,Y.height=B)}let re={src:m,srcSet:void 0,sizes:void 0};X&&(re=b({config:I,src:t,unoptimized:r,layout:F,width:$,quality:W,sizes:n,loader:N}));let oe=t,ie="imagesrcset",ae="imagesizes";window.omnivoreEnv.__NEXT_REACT_ROOT&&(ie="imageSrcSet",ae="imageSizes");const le={[ie]:re.srcSet,[ae]:re.sizes},se="undefined"==typeof window?i.default.useEffect:i.default.useLayoutEffect,ue=i.useRef(P),ce=i.useRef(t);i.useEffect((()=>{ue.current=P}),[P]),se((()=>{ce.current!==t&&(K(),ce.current=t)}),[K,t]);const de=f({isLazy:H,imgAttributes:re,heightInt:B,widthInt:$,qualityInt:W,layout:F,className:k,imgStyle:te,blurStyle:ne,loading:c,config:I,unoptimized:r,placeholder:R,loader:N,srcString:oe,onLoadingCompleteRef:ue,setBlurComplete:V,setIntersection:q,isVisible:X},D);return i.default.createElement(i.default.Fragment,null,"raw"===F?i.default.createElement(E,Object.assign({},de)):i.default.createElement("span",{style:Y},J?i.default.createElement("span",{style:Z},Q?i.default.createElement("img",{style:{display:"block",maxWidth:"100%",width:"initial",height:"initial",background:"none",opacity:1,border:0,margin:0,padding:0},alt:"","aria-hidden":!0,src:Q}):null):null,i.default.createElement(E,Object.assign({},de))),o?i.default.createElement(a.default,null,i.default.createElement("link",Object.assign({key:"__nimg-"+re.src+re.srcSet+re.sizes,rel:"preload",as:"image",href:re.srcSet?void 0:re.src},le))):null)};var r,o,i=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)),a=(r=n(5977))&&r.__esModule?r:{default:r},l=n(8467),s=n(3321),u=n(4329),c=(n(1624),n(1119));function d(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function f(e){for(var t=1;t=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}null===(o=window.omnivoreEnv.__NEXT_IMAGE_OPTS)||void 0===o||o.experimentalLayoutRaw;const h=window.omnivoreEnv.__NEXT_IMAGE_OPTS,g=new Set;new Map;const m="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";"undefined"==typeof window&&(n.g.__NEXT_IMAGE_IMPORTED=!0);const v=new Map([["default",function({config:e,src:t,width:n,quality:r}){return t.endsWith(".svg")&&!e.dangerouslyAllowSVG?t:`${c.normalizePathTrailingSlash(e.path)}?url=${encodeURIComponent(t)}&w=${n}&q=${r||75}`}],["imgix",function({config:e,src:t,width:n,quality:r}){const o=new URL(`${e.path}${S(t)}`),i=o.searchParams;return i.set("auto",i.get("auto")||"format"),i.set("fit",i.get("fit")||"max"),i.set("w",i.get("w")||n.toString()),r&&i.set("q",r.toString()),o.href}],["cloudinary",function({config:e,src:t,width:n,quality:r}){const o=["f_auto","c_limit","w_"+n,"q_"+(r||"auto")].join(",")+"/";return`${e.path}${o}${S(t)}`}],["akamai",function({config:e,src:t,width:n}){return`${e.path}${S(t)}?imwidth=${n}`}],["custom",function({src:e}){throw new Error(`Image with src "${e}" is missing "loader" prop.\nRead more: https://nextjs.org/docs/messages/next-image-missing-loader`)}]]);function y(e){return void 0!==e.default}function b({config:e,src:t,unoptimized:n,layout:r,width:o,quality:i,sizes:a,loader:l}){if(n)return{src:t,srcSet:void 0,sizes:void 0};const{widths:s,kind:u}=function({deviceSizes:e,allSizes:t},n,r,o){if(o&&("fill"===r||"responsive"===r||"raw"===r)){const n=/(^|\s)(1?\d?\d)vw/g,r=[];for(let e;e=n.exec(o);e)r.push(parseInt(e[2]));if(r.length){const n=.01*Math.min(...r);return{widths:t.filter((t=>t>=e[0]*n)),kind:"w"}}return{widths:t,kind:"w"}}return"number"!=typeof n||"fill"===r||"responsive"===r?{widths:e,kind:"w"}:{widths:[...new Set([n,2*n].map((e=>t.find((t=>t>=e))||t[t.length-1])))],kind:"x"}}(e,o,r,a),c=s.length-1;return{sizes:a||"w"!==u?a:"100vw",srcSet:s.map(((n,r)=>`${l({config:e,src:t,quality:i,width:n})} ${"w"===u?n:r+1}${u}`)).join(", "),src:l({config:e,src:t,quality:i,width:s[c]})}}function w(e){return"number"==typeof e?e:"string"==typeof e?parseInt(e,10):void 0}function x(e){var t;const n=(null===(t=e.config)||void 0===t?void 0:t.loader)||"default",r=v.get(n);if(r)return r(e);throw new Error(`Unknown "loader" found in "next.config.js". Expected: ${l.VALID_LOADERS.join(", ")}. Received: ${n}`)}function k(e,t,n,r,o,i){e&&e.src!==m&&e["data-loaded-src"]!==t&&(e["data-loaded-src"]=t,("decode"in e?e.decode():Promise.resolve()).catch((()=>{})).then((()=>{if(e.parentNode&&(g.add(t),"blur"===r&&i(!0),null==o?void 0:o.current)){const{naturalWidth:t,naturalHeight:n}=e;o.current({naturalWidth:t,naturalHeight:n})}})))}const E=e=>{var{imgAttributes:t,heightInt:n,widthInt:r,qualityInt:o,layout:a,className:l,imgStyle:s,blurStyle:u,isLazy:c,placeholder:d,loading:h,srcString:g,config:m,unoptimized:v,loader:y,onLoadingCompleteRef:w,setBlurComplete:x,setIntersection:E,onLoad:S,onError:_,isVisible:C}=e,O=p(e,["imgAttributes","heightInt","widthInt","qualityInt","layout","className","imgStyle","blurStyle","isLazy","placeholder","loading","srcString","config","unoptimized","loader","onLoadingCompleteRef","setBlurComplete","setIntersection","onLoad","onError","isVisible"]);return i.default.createElement(i.default.Fragment,null,i.default.createElement("img",Object.assign({},O,t,"raw"===a?{height:n,width:r}:{},{decoding:"async","data-nimg":a,className:l,style:f({},s,u),ref:i.useCallback((e=>{E(e),(null==e?void 0:e.complete)&&k(e,g,0,d,w,x)}),[E,g,a,d,w,x]),onLoad:e=>{k(e.currentTarget,g,0,d,w,x),S&&S(e)},onError:e=>{"blur"===d&&x(!0),_&&_(e)}})),(c||"blur"===d)&&i.default.createElement("noscript",null,i.default.createElement("img",Object.assign({},O,b({config:m,src:g,unoptimized:v,layout:a,width:r,quality:o,sizes:t.sizes,loader:y}),"raw"===a?{height:n,width:r}:{},{decoding:"async","data-nimg":a,style:s,className:l,loading:h||"lazy"}))))};function S(e){return"/"===e[0]?e.slice(1):e}("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:d,children:f,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=f,n&&"string"==typeof r&&(r=o.default.createElement("a",null,r));const k=!1!==p,E=a.useRouter(),{href:S,as:_}=o.default.useMemo((()=>{const[e,t]=i.resolveHref(E,c,!0);return{href:e,as:d?i.resolveHref(E,d):t||e}}),[E,c,d]),C=o.default.useRef(S),O=o.default.useRef(_);let L;n&&(L=o.default.Children.only(r));const j=n?L&&"object"==typeof L&&L.ref:t,[P,R,T]=l.useIntersection({rootMargin:"200px"}),A=o.default.useCallback((e=>{O.current===_&&C.current===S||(T(),O.current=_,C.current=S),P(e),j&&("function"==typeof j?j(e):"object"==typeof j&&(j.current=e))}),[_,j,S,T,P]);o.default.useEffect((()=>{const e=R&&k&&i.isLocalURL(S),t=void 0!==y?y:E&&E.locale,n=s[S+"%"+_+(t?"%"+t:"")];e&&!n&&u(E,S,_,{locale:t})}),[_,S,R,y,k,E]);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,E,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(E,S,_,{priority:!0})}};if(!n||h||"a"===L.type&&!("href"in L.props)){const e=void 0!==y?y:E&&E.locale,t=E&&E.isLocaleDomain&&i.getDomainLocale(_,e,E&&E.locales,E&&E.domainLocales);M.href=t||i.addBasePath(i.addLocale(_,e,E&&E.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=d,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 d(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(f(e,n).then((({scripts:e,css:r})=>Promise.all([t.has(n)?[]:Promise.all(e.map(d)),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():f(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 d(){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 f(e,t){return d().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,d.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"],d=["push","replace","reload","back","prefetch","beforePopState"];function f(){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:()=>f()[e]})})),d.forEach((e=>{u[e]=(...t)=>f()[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,f=n||t;if(f&&c.has(f))return;if(u.has(t))return c.add(f),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(f),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||d.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((()=>f(e)))})):f(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:d,scripts:p,getIsSsr:h}=r.useContext(o.HeadManagerContext);return r.useEffect((()=>{"afterInteractive"===i?f(e):"lazyOnload"===i&&function(e){"complete"===document.readyState?a.requestIdleCallback((()=>f(e))):window.addEventListener("load",(()=>{a.requestIdleCallback((()=>f(e)))}))}(e)}),[e,i]),"beforeInteractive"!==i&&"worker"!==i||(d?(p[i]=(p[i]||[]).concat([s({src:t,onLoad:n,onError:l},u)]),d(p)):h&&h()?c.add(u.id||t):h&&!h()&&f(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,d]=r.useState(!1),[f,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&&d(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:f,rootMargin:t}))}),[s,f,t,c]),g=r.useCallback((()=>{d(!1)}),[]);return r.useEffect((()=>{if(!i&&!c){const e=o.requestIdleCallback((()=>d(!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="",d=function(e){if(u-1:void 0===k;o||(g+="(?:"+h+"(?="+p+"))?"),E||(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}},1719:(e,t,n)=>{"use strict";var r;Object.defineProperty(t,"__esModule",{value:!0}),t.AmpStateContext=void 0;const o=((r=n(2784))&&r.__esModule?r:{default:r}).default.createContext({});t.AmpStateContext=o},9739:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.isInAmpMode=a,t.useAmp=function(){return a(o.default.useContext(i.AmpStateContext))};var r,o=(r=n(2784))&&r.__esModule?r:{default:r},i=n(1719);function a({ampFirst:e=!1,hybrid:t=!1,hasQuery:n=!1}={}){return e||t&&n}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&(Object.assign(t.default,t),e.exports=t.default)},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},5977:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.defaultHead=u,t.default=void 0;var r,o=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)),i=(r=n(1889))&&r.__esModule?r:{default:r},a=n(1719),l=n(7177),s=n(9739);function u(e=!1){const t=[o.default.createElement("meta",{charSet:"utf-8"})];return e||t.push(o.default.createElement("meta",{name:"viewport",content:"width=device-width"})),t}function c(e,t){return"string"==typeof t||"number"==typeof t?e:t.type===o.default.Fragment?e.concat(o.default.Children.toArray(t.props.children).reduce(((e,t)=>"string"==typeof t||"number"==typeof t?e:e.concat(t)),[])):e.concat(t)}n(1624);const d=["name","httpEquiv","charSet","itemProp"];function f(e,t){return e.reduce(((e,t)=>{const n=o.default.Children.toArray(t.props.children);return e.concat(n)}),[]).reduce(c,[]).reverse().concat(u(t.inAmpMode)).filter(function(){const e=new Set,t=new Set,n=new Set,r={};return o=>{let i=!0,a=!1;if(o.key&&"number"!=typeof o.key&&o.key.indexOf("$")>0){a=!0;const t=o.key.slice(o.key.indexOf("$")+1);e.has(t)?i=!1:e.add(t)}switch(o.type){case"title":case"base":t.has(o.type)?i=!1:t.add(o.type);break;case"meta":for(let e=0,t=d.length;e{const r=e.key||n;if(window.omnivoreEnv.__NEXT_OPTIMIZE_FONTS&&!t.inAmpMode&&"link"===e.type&&e.props.href&&["https://fonts.googleapis.com/css","https://use.typekit.net/"].some((t=>e.props.href.startsWith(t)))){const t={...e.props||{}};return t["data-href"]=t.href,t.href=void 0,t["data-optimized-fonts"]=!0,o.default.cloneElement(e,t)}return o.default.cloneElement(e,{key:r})}))}t.default=function({children:e}){const t=o.useContext(a.AmpStateContext),n=o.useContext(l.HeadManagerContext);return o.default.createElement(i.default,{reduceComponentsToState:f,headManager:n,inAmpMode:s.isInAmpMode(t)},e)},("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&(Object.assign(t.default,t),e.exports=t.default)},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}}},4329:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ImageConfigContext=void 0;var r,o=(r=n(2784))&&r.__esModule?r:{default:r},i=n(8467);const a=o.default.createContext(i.imageConfigDefault);t.ImageConfigContext=a},8467:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.imageConfigDefault=t.VALID_LOADERS=void 0,t.VALID_LOADERS=["default","imgix","cloudinary","akamai","custom"];t.imageConfigDefault={deviceSizes:[640,750,828,1080,1200,1920,2048,3840],imageSizes:[16,32,48,64,96,128,256,384],path:"/_next/image",loader:"default",domains:[],disableStaticImages:!1,minimumCacheTTL:60,formats:["image/webp"],dangerouslyAllowSVG:!1,contentSecurityPolicy:"script-src 'none'; frame-src 'none'; sandbox;"}},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=j,t.delBasePath=P,t.isLocalURL=R,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),d=n(7482),f=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 k(){return Object.assign(new Error("Route Cancelled"),{cancelled:!0})}function E(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 E(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 j(e){return E(e,x)}function P(e){return(e=e.slice(x.length)).startsWith("/")||(e=`/${e}`),e}function R(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(!R(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(d.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:j(r),u=n?I(M(e,n)):o||r;return{url:s,as:l?u:j(u)}}function F(e,t){const n=r.removePathTrailingSlash(l.denormalizePagePath(e));return"/404"===n||"/_error"===n?e:(t.includes(n)||t.some((t=>{if(d.isDynamicRoute(t)&&m.getRouteRegex(t).re.test(n))return e=t,!0})),r.removePathTrailingSlash(e))}const N=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 $(e,t,n){return fetch(e,{credentials:"same-origin"}).then((r=>{if(!r.ok){if(t>1&&r.status>=500)return $(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 B(e,t,n,r,i){const{href:a}=new URL(e,window.location.href);return void 0!==r[a]?r[a]:r[a]=$(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:k,isRsc:E}){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:j(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&&N&&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}=f.parseRelativeUrl(r);this.isSsr&&o===j(this.asPath)&&l===j(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:!!E}),this.components["/_app"]={Component:a,styleSheets:[]},this.events=W.events,this.pageLoader=i;const _=d.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:!!k,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:j(e),query:t}),c.getURL(),r)}window.addEventListener("popstate",this.onPopState),window.omnivoreEnv.__NEXT_SCROLL_RESTORATION&&N&&(window.history.scrollRestoration="manual")}}reload(){window.location.reload()}back(){window.history.back()}push(e,t,n={}){if(window.omnivoreEnv.__NEXT_SCROLL_RESTORATION&&N)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(!R(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=f.parseRelativeUrl(L(n)?P(n):n),r=s.normalizeLocalePath(e.pathname,this.locales);r.detectedLocale&&(v.locale=r.detectedLocale,e.pathname=j(e.pathname),n=y.formatWithValidation(e),t=j(s.normalizeLocalePath(L(t)?P(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=P(n);window.location.href=`http${i.http?"":"s"}://${i.domain}${j(`${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:k=!1,scroll:E=!0}=l,S={shallow:k};this._inFlightRoute&&this.abortComponentLoad(this._inFlightRoute,S),n=j(_(L(n)?P(n):n,l.locale,this.defaultLocale));const M=C(L(n)?P(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}),E&&this.scrollToHash(M),this.set(v,this.components[v.route],null),W.events.emit("hashChangeComplete",n,S),!0;let N,$,B=f.parseRelativeUrl(t),{pathname:H,query:U}=B;try{[N,{__rewrites:$}]=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(P(H)):H,p&&"/_error"!==H)if(l._shouldResolveHref=!0,window.omnivoreEnv.__NEXT_HAS_REWRITES&&n.startsWith("/")){const e=h.default(j(_(M,v.locale)),N,$,U,(e=>F(e,N)),this.locales);if(e.externalDest)return location.href=n,!0;V=e.asPath,e.matchedPage&&e.resolvedHref&&(H=e.resolvedHref,B.pathname=j(H),t=y.formatWithValidation(B))}else B.pathname=F(H,N),B.pathname!==H&&(H=B.pathname,B.pathname=j(H),t=y.formatWithValidation(B));if(!R(n))return window.location.href=n,!1;if(V=C(P(V),v.locale),(!l.shallow||1===l._h)&&(1!==l._h||d.isDynamicRoute(r.removePathTrailingSlash(H)))){const r=await this._preflightRequest({as:n,cache:!0,pages:N,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,B.pathname=r.resolvedHref,t=y.formatWithValidation(B);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(d.isDynamicRoute(q)){const e=f.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 G,K;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 d=r.Component;if(d&&d.unstable_scriptLoader&&[].concat(d.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=f.parseRelativeUrl(t);n.pathname=F(n.pathname,N);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===(G=self.__NEXT_DATA__.props)||void 0===G||null===(K=G.pageProps)||void 0===K?void 0:K.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,k();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:d,__N_SSG:f,__N_SSP:p,__N_RSC:h}=c;let g;const m=p&&h;(f||p||h)&&(g=this.pageLoader.getDataHref({href:y.formatWithValidation({pathname:t,query:n}),asPath:o,ssg:f,flight:m,locale:l}));const v=await this._getData((()=>(f||p||h)&&!m?B(g,this.isSsr,!1,f?this.sdc:this.sdr,!!f&&!s):this.getInitialProps(d,{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=f.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=f.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(j(_(t,this.locale)),u,n,i.query,(e=>F(e,u)),this.locales);if(r.externalDest)return;c=C(P(r.asPath),this.locale),r.matchedPage&&r.resolvedHref&&(a=r.resolvedHref,i.pathname=a,e=y.formatWithValidation(i))}else i.pathname=F(i.pathname,u),i.pathname!==a&&(a=i.pathname,i.pathname=a,e=y.formatWithValidation(i));const d=await this._preflightRequest({as:j(t),cache:!0,pages:u,pathname:a,query:l,locale:this.locale,isPreview:this.isPreview});"rewrite"===d.type&&(i.pathname=d.resolvedHref,a=d.resolvedHref,l={...l,...d.parsedAs.query},c=d.asPath,e=y.formatWithValidation(i));const p=r.removePathTrailingSlash(a);await Promise.all([this.pageLoader._isSsg(p).then((t=>!!t&&B(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 B(e,!0,!0,this.sdc,!1).then((e=>({data:e})))}async _preflightRequest(e){const t=O(e.as),n=C(L(t)?P(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=f.parseRelativeUrl(s.normalizeLocalePath(L(i.rewrite)?P(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=F(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)?P(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",k(),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:d}=new URL(e,i);if(d!==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,d=l(`${u.pathname}${u.hash||""}`),f=l(u.hostname||""),p=[],h=[];r.pathToRegexp(d,p),r.pathToRegexp(f,h);const g=[];p.forEach((e=>g.push(e.name))),h.forEach((e=>g.push(e.name)));const m=r.compile(d,{validate:!1}),v=r.compile(f,{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,d){let f,p=!1,h=!1,g=l.parseRelativeUrl(e),m=i.removePathTrailingSlash(a.normalizeLocalePath(s.delBasePath(g.pathname),d).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),d).pathname),t.includes(m))return p=!0,f=m,!0;if(f=c(m),f!==e&&t.includes(f))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}}},1889:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),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));const o="undefined"==typeof window;class i extends r.Component{constructor(e){super(e),this.emitChange=()=>{this._hasHeadManager&&this.props.headManager.updateHead(this.props.reduceComponentsToState([...this.props.headManager.mountedInstances],this.props))},this._hasHeadManager=this.props.headManager&&this.props.headManager.mountedInstances,o&&this._hasHeadManager&&(this.props.headManager.mountedInstances.add(this),this.emitChange())}componentDidMount(){this._hasHeadManager&&this.props.headManager.mountedInstances.add(this),this.emitChange()}componentDidUpdate(){this.emitChange()}componentWillUnmount(){this._hasHeadManager&&this.props.headManager.mountedInstances.delete(this),this.emitChange()}render(){return null}}t.default=i},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},6577:(e,t,n)=>{n(104)},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