diff --git a/android/Omnivore/app/build.gradle b/android/Omnivore/app/build.gradle index db0079448..1e52cc6b1 100644 --- a/android/Omnivore/app/build.gradle +++ b/android/Omnivore/app/build.gradle @@ -17,8 +17,8 @@ android { applicationId "app.omnivore.omnivore" minSdk 26 targetSdk 33 - versionCode 80 - versionName "0.0.80" + versionCode 82 + versionName "0.0.82" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -148,6 +148,7 @@ dependencies { // Room Deps implementation "androidx.room:room-runtime:$room_version" + implementation "androidx.room:room-ktx:$room_version" annotationProcessor "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version" diff --git a/android/Omnivore/app/src/main/graphql/ArticleContent.graphql b/android/Omnivore/app/src/main/graphql/ArticleContent.graphql index 79dcf6456..ef6b812e7 100644 --- a/android/Omnivore/app/src/main/graphql/ArticleContent.graphql +++ b/android/Omnivore/app/src/main/graphql/ArticleContent.graphql @@ -53,6 +53,7 @@ fragment HighlightFields on Highlight { patch annotation createdByMe + createdAt updatedAt sharedAt } diff --git a/android/Omnivore/app/src/main/graphql/schema.graphqls b/android/Omnivore/app/src/main/graphql/schema.graphqls index 420960b75..fd10083a7 100644 --- a/android/Omnivore/app/src/main/graphql/schema.graphqls +++ b/android/Omnivore/app/src/main/graphql/schema.graphqls @@ -1,4 +1,9 @@ -directive @sanitize(allowedTags: [String], maxLength: Int, minLength: Int, pattern: String) on INPUT_FIELD_DEFINITION +directive @sanitize( + allowedTags: [String] + maxLength: Int + minLength: Int + pattern: String +) on INPUT_FIELD_DEFINITION type AddPopularReadError { errorCodes: [AddPopularReadErrorCode!]! @@ -150,7 +155,9 @@ enum ArticleSavingRequestErrorCode { UNAUTHORIZED } -union ArticleSavingRequestResult = ArticleSavingRequestError | ArticleSavingRequestSuccess +union ArticleSavingRequestResult = + ArticleSavingRequestError + | ArticleSavingRequestSuccess enum ArticleSavingRequestStatus { ARCHIVED @@ -248,7 +255,9 @@ input CreateArticleSavingRequestInput { url: String! } -union CreateArticleSavingRequestResult = CreateArticleSavingRequestError | CreateArticleSavingRequestSuccess +union CreateArticleSavingRequestResult = + CreateArticleSavingRequestError + | CreateArticleSavingRequestSuccess type CreateArticleSavingRequestSuccess { articleSavingRequest: ArticleSavingRequest! @@ -329,7 +338,9 @@ input CreateHighlightReplyInput { text: String! } -union CreateHighlightReplyResult = CreateHighlightReplyError | CreateHighlightReplySuccess +union CreateHighlightReplyResult = + CreateHighlightReplyError + | CreateHighlightReplySuccess type CreateHighlightReplySuccess { highlightReply: HighlightReply! @@ -373,7 +384,9 @@ enum CreateNewsletterEmailErrorCode { UNAUTHORIZED } -union CreateNewsletterEmailResult = CreateNewsletterEmailError | CreateNewsletterEmailSuccess +union CreateNewsletterEmailResult = + CreateNewsletterEmailError + | CreateNewsletterEmailSuccess type CreateNewsletterEmailSuccess { newsletterEmail: NewsletterEmail! @@ -481,7 +494,9 @@ enum DeleteHighlightReplyErrorCode { UNAUTHORIZED } -union DeleteHighlightReplyResult = DeleteHighlightReplyError | DeleteHighlightReplySuccess +union DeleteHighlightReplyResult = + DeleteHighlightReplyError + | DeleteHighlightReplySuccess type DeleteHighlightReplySuccess { highlightReply: HighlightReply! @@ -503,7 +518,9 @@ enum DeleteIntegrationErrorCode { UNAUTHORIZED } -union DeleteIntegrationResult = DeleteIntegrationError | DeleteIntegrationSuccess +union DeleteIntegrationResult = + DeleteIntegrationError + | DeleteIntegrationSuccess type DeleteIntegrationSuccess { integration: Integration! @@ -535,7 +552,9 @@ enum DeleteNewsletterEmailErrorCode { UNAUTHORIZED } -union DeleteNewsletterEmailResult = DeleteNewsletterEmailError | DeleteNewsletterEmailSuccess +union DeleteNewsletterEmailResult = + DeleteNewsletterEmailError + | DeleteNewsletterEmailSuccess type DeleteNewsletterEmailSuccess { newsletterEmail: NewsletterEmail! @@ -752,7 +771,9 @@ enum GetUserPersonalizationErrorCode { UNAUTHORIZED } -union GetUserPersonalizationResult = GetUserPersonalizationError | GetUserPersonalizationSuccess +union GetUserPersonalizationResult = + GetUserPersonalizationError + | GetUserPersonalizationSuccess type GetUserPersonalizationSuccess { userPersonalization: UserPersonalization @@ -848,7 +869,9 @@ enum ImportFromIntegrationErrorCode { UNAUTHORIZED } -union ImportFromIntegrationResult = ImportFromIntegrationError | ImportFromIntegrationSuccess +union ImportFromIntegrationResult = + ImportFromIntegrationError + | ImportFromIntegrationSuccess type ImportFromIntegrationSuccess { success: Boolean! @@ -1092,10 +1115,14 @@ type Mutation { addPopularRead(name: String!): AddPopularReadResult! bulkAction(action: BulkActionType!, query: String): BulkActionResult! createArticle(input: CreateArticleInput!): CreateArticleResult! - createArticleSavingRequest(input: CreateArticleSavingRequestInput!): CreateArticleSavingRequestResult! + createArticleSavingRequest( + input: CreateArticleSavingRequestInput! + ): CreateArticleSavingRequestResult! createGroup(input: CreateGroupInput!): CreateGroupResult! createHighlight(input: CreateHighlightInput!): CreateHighlightResult! - createHighlightReply(input: CreateHighlightReplyInput!): CreateHighlightReplyResult! + createHighlightReply( + input: CreateHighlightReplyInput! + ): CreateHighlightReplyResult! createLabel(input: CreateLabelInput!): CreateLabelResult! createNewsletterEmail: CreateNewsletterEmailResult! createReaction(input: CreateReactionInput!): CreateReactionResult! @@ -1124,10 +1151,14 @@ type Mutation { moveLabel(input: MoveLabelInput!): MoveLabelResult! optInFeature(input: OptInFeatureInput!): OptInFeatureResult! recommend(input: RecommendInput!): RecommendResult! - recommendHighlights(input: RecommendHighlightsInput!): RecommendHighlightsResult! + recommendHighlights( + input: RecommendHighlightsInput! + ): RecommendHighlightsResult! reportItem(input: ReportItemInput!): ReportItemResult! revokeApiKey(id: ID!): RevokeApiKeyResult! - saveArticleReadingProgress(input: SaveArticleReadingProgressInput!): SaveArticleReadingProgressResult! + saveArticleReadingProgress( + input: SaveArticleReadingProgressInput! + ): SaveArticleReadingProgressResult! saveFile(input: SaveFileInput!): SaveResult! saveFilter(input: SaveFilterInput!): SaveFilterResult! savePage(input: SavePageInput!): SaveResult! @@ -1142,21 +1173,32 @@ type Mutation { setRule(input: SetRuleInput!): SetRuleResult! setShareArticle(input: SetShareArticleInput!): SetShareArticleResult! setShareHighlight(input: SetShareHighlightInput!): SetShareHighlightResult! - setUserPersonalization(input: SetUserPersonalizationInput!): SetUserPersonalizationResult! + setUserPersonalization( + input: SetUserPersonalizationInput! + ): SetUserPersonalizationResult! setWebhook(input: SetWebhookInput!): SetWebhookResult! subscribe(name: String!): SubscribeResult! unsubscribe(name: String!): UnsubscribeResult! updateHighlight(input: UpdateHighlightInput!): UpdateHighlightResult! - updateHighlightReply(input: UpdateHighlightReplyInput!): UpdateHighlightReplyResult! + updateHighlightReply( + input: UpdateHighlightReplyInput! + ): UpdateHighlightReplyResult! updateLabel(input: UpdateLabelInput!): UpdateLabelResult! - updateLinkShareInfo(input: UpdateLinkShareInfoInput!): UpdateLinkShareInfoResult! + updateLinkShareInfo( + input: UpdateLinkShareInfoInput! + ): UpdateLinkShareInfoResult! updatePage(input: UpdatePageInput!): UpdatePageResult! updateReminder(input: UpdateReminderInput!): UpdateReminderResult! - updateSharedComment(input: UpdateSharedCommentInput!): UpdateSharedCommentResult! + updateSharedComment( + input: UpdateSharedCommentInput! + ): UpdateSharedCommentResult! updateUser(input: UpdateUserInput!): UpdateUserResult! updateUserProfile(input: UpdateUserProfileInput!): UpdateUserProfileResult! uploadFileRequest(input: UploadFileRequestInput!): UploadFileRequestResult! - uploadImportFile(contentType: String!, type: UploadImportFileType!): UploadImportFileResult! + uploadImportFile( + contentType: String! + type: UploadImportFileType! + ): UploadImportFileResult! } type NewsletterEmail { @@ -1281,9 +1323,21 @@ type Query { apiKeys: ApiKeysResult! article(format: String, slug: String!, username: String!): ArticleResult! articleSavingRequest(id: ID, url: String): ArticleSavingRequestResult! - articles(after: String, first: Int, includePending: Boolean, query: String, sharedOnly: Boolean, sort: SortParams): ArticlesResult! + articles( + after: String + first: Int + includePending: Boolean + query: String + sharedOnly: Boolean + sort: SortParams + ): ArticlesResult! deviceTokens: DeviceTokensResult! - feedArticles(after: String, first: Int, sharedByUser: ID, sort: SortParams): FeedArticlesResult! + feedArticles( + after: String + first: Int + sharedByUser: ID + sort: SortParams + ): FeedArticlesResult! filters: FiltersResult! getFollowers(userId: ID): GetFollowersResult! getFollowing(userId: ID): GetFollowingResult! @@ -1298,12 +1352,27 @@ type Query { recentSearches: RecentSearchesResult! reminder(linkId: ID!): ReminderResult! rules(enabled: Boolean): RulesResult! - search(after: String, first: Int, format: String, includeContent: Boolean, query: String): SearchResult! + search( + after: String + first: Int + format: String + includeContent: Boolean + query: String + ): SearchResult! sendInstallInstructions: SendInstallInstructionsResult! - sharedArticle(selectedHighlightId: String, slug: String!, username: String!): SharedArticleResult! + sharedArticle( + selectedHighlightId: String + slug: String! + username: String! + ): SharedArticleResult! subscriptions(sort: SortParams): SubscriptionsResult! typeaheadSearch(first: Int, query: String!): TypeaheadSearchResult! - updatesSince(after: String, first: Int, since: Date!, sort: SortParams): UpdatesSinceResult! + updatesSince( + after: String + first: Int + since: Date! + sort: SortParams + ): UpdatesSinceResult! user(userId: ID, username: String): UserResult! users: UsersResult! validateUsername(username: String!): Boolean! @@ -1409,7 +1478,9 @@ input RecommendHighlightsInput { pageId: ID! } -union RecommendHighlightsResult = RecommendHighlightsError | RecommendHighlightsSuccess +union RecommendHighlightsResult = + RecommendHighlightsError + | RecommendHighlightsSuccess type RecommendHighlightsSuccess { success: Boolean! @@ -1574,7 +1645,9 @@ input SaveArticleReadingProgressInput { readingProgressTopPercent: Float } -union SaveArticleReadingProgressResult = SaveArticleReadingProgressError | SaveArticleReadingProgressSuccess +union SaveArticleReadingProgressResult = + SaveArticleReadingProgressError + | SaveArticleReadingProgressSuccess type SaveArticleReadingProgressSuccess { updatedArticle: Article! @@ -1721,7 +1794,9 @@ enum SendInstallInstructionsErrorCode { UNAUTHORIZED } -union SendInstallInstructionsResult = SendInstallInstructionsError | SendInstallInstructionsSuccess +union SendInstallInstructionsResult = + SendInstallInstructionsError + | SendInstallInstructionsSuccess type SendInstallInstructionsSuccess { sent: Boolean! @@ -1741,7 +1816,9 @@ input SetBookmarkArticleInput { bookmark: Boolean! } -union SetBookmarkArticleResult = SetBookmarkArticleError | SetBookmarkArticleSuccess +union SetBookmarkArticleResult = + SetBookmarkArticleError + | SetBookmarkArticleSuccess type SetBookmarkArticleSuccess { bookmarkedArticle: Article! @@ -1904,7 +1981,9 @@ input SetShareHighlightInput { share: Boolean! } -union SetShareHighlightResult = SetShareHighlightError | SetShareHighlightSuccess +union SetShareHighlightResult = + SetShareHighlightError + | SetShareHighlightSuccess type SetShareHighlightSuccess { highlight: Highlight! @@ -1932,7 +2011,9 @@ input SetUserPersonalizationInput { theme: String } -union SetUserPersonalizationResult = SetUserPersonalizationError | SetUserPersonalizationSuccess +union SetUserPersonalizationResult = + SetUserPersonalizationError + | SetUserPersonalizationSuccess type SetUserPersonalizationSuccess { updatedUserPersonalization: UserPersonalization! @@ -2144,7 +2225,9 @@ input UpdateHighlightReplyInput { text: String! } -union UpdateHighlightReplyResult = UpdateHighlightReplyError | UpdateHighlightReplySuccess +union UpdateHighlightReplyResult = + UpdateHighlightReplyError + | UpdateHighlightReplySuccess type UpdateHighlightReplySuccess { highlightReply: HighlightReply! @@ -2195,7 +2278,9 @@ input UpdateLinkShareInfoInput { title: String! } -union UpdateLinkShareInfoResult = UpdateLinkShareInfoError | UpdateLinkShareInfoSuccess +union UpdateLinkShareInfoResult = + UpdateLinkShareInfoError + | UpdateLinkShareInfoSuccess type UpdateLinkShareInfoSuccess { message: String! @@ -2271,7 +2356,9 @@ input UpdateSharedCommentInput { sharedComment: String! } -union UpdateSharedCommentResult = UpdateSharedCommentError | UpdateSharedCommentSuccess +union UpdateSharedCommentResult = + UpdateSharedCommentError + | UpdateSharedCommentSuccess type UpdateSharedCommentSuccess { articleID: ID! @@ -2313,7 +2400,9 @@ input UpdateUserProfileInput { username: String } -union UpdateUserProfileResult = UpdateUserProfileError | UpdateUserProfileSuccess +union UpdateUserProfileResult = + UpdateUserProfileError + | UpdateUserProfileSuccess type UpdateUserProfileSuccess { user: User! @@ -2357,7 +2446,9 @@ input UploadFileRequestInput { url: String! } -union UploadFileRequestResult = UploadFileRequestError | UploadFileRequestSuccess +union UploadFileRequestResult = + UploadFileRequestError + | UploadFileRequestSuccess type UploadFileRequestSuccess { createdPageId: String @@ -2397,7 +2488,8 @@ type User { followersCount: Int friendsCount: Int id: ID! - isFriend: Boolean @deprecated(reason: "isFriend has been replaced with viewerIsFollowing") + isFriend: Boolean + @deprecated(reason: "isFriend has been replaced with viewerIsFollowing") isFullUser: Boolean name: String! picture: String 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 97d545506..285e0fe80 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 @@ -1,12 +1,16 @@ package app.omnivore.omnivore.dataService +import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput +import app.omnivore.omnivore.graphql.generated.type.HighlightType 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.apollographql.apollo3.api.Optional import com.google.gson.Gson import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import java.util.* suspend fun DataService.createWebHighlight(jsonString: String) { val createHighlightInput = Gson().fromJson(jsonString, CreateHighlightParams::class.java).asCreateHighlightInput() @@ -44,6 +48,53 @@ suspend fun DataService.createWebHighlight(jsonString: String) { } } +suspend fun DataService.createNoteHighlight(savedItemId: String, note: String): String { + val shortId = UUID.randomUUID().toString() + val createHighlightId = UUID.randomUUID().toString() + + withContext(Dispatchers.IO) { + val highlight = Highlight( + type = "NOTE", + highlightId = createHighlightId, + shortId = shortId, + quote = null, + prefix = null, + suffix = null, + patch =null, + annotation = note, + createdAt = null, + updatedAt = null, + createdByMe = true + ) + + highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue + + val crossRef = SavedItemAndHighlightCrossRef( + highlightId = createHighlightId, + savedItemId = savedItemId + ) + + db.highlightDao().insertAll(listOf(highlight)) + db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef)) + + val newHighlight = networker.createHighlight(input = CreateHighlightParams( + type = HighlightType.NOTE, + articleId = savedItemId, + id = createHighlightId, + shortId = shortId, + quote = null, + patch = null, + annotation = note, + ).asCreateHighlightInput()) + + newHighlight?.let { + db.highlightDao().update(it) + } + } + + return createHighlightId +} + suspend fun DataService.mergeWebHighlights(jsonString: String) { val mergeHighlightInput = Gson().fromJson(jsonString, MergeHighlightsParams::class.java).asMergeHighlightInput() 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 4181471e3..a28a71a9b 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 @@ -34,6 +34,10 @@ suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): S val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since) ?: return SavedItemSyncResult.errorResult + if (syncResult.deletedItemIDs.isNotEmpty()) { + db.savedItemDao().deleteByIds(syncResult.deletedItemIDs) + } + val savedItems = syncResult.items.map { val savedItem = SavedItem( savedItemId = it.id, 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 2d272f732..e7793f3f2 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 @@ -6,6 +6,7 @@ import app.omnivore.omnivore.graphql.generated.DeleteHighlightMutation import app.omnivore.omnivore.graphql.generated.MergeHighlightMutation import app.omnivore.omnivore.graphql.generated.UpdateHighlightMutation import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput +import app.omnivore.omnivore.graphql.generated.type.HighlightType import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput import app.omnivore.omnivore.persistence.entities.Highlight @@ -13,6 +14,7 @@ import com.apollographql.apollo3.api.Optional import com.google.gson.Gson data class CreateHighlightParams( + val type: HighlightType, val shortId: String?, val id: String?, val quote: String?, @@ -21,7 +23,8 @@ data class CreateHighlightParams( val `annotation`: String? ) { fun asCreateHighlightInput() = CreateHighlightInput( - annotation = Optional.presentIfNotNull(`annotation`), + type = Optional.presentIfNotNull(type), + annotation = Optional.presentIfNotNull(annotation), articleId = articleId ?: "", id = id ?: "", patch = Optional.presentIfNotNull(patch), @@ -136,8 +139,6 @@ suspend fun Networker.createHighlight(input: CreateHighlightInput): Highlight? { val createdHighlight = result.data?.createHighlight?.onCreateHighlightSuccess?.highlight if (createdHighlight != null) { -// val updatedAtString = createdHighlight.highlightFields.updatedAt as? String - return Highlight( type = createdHighlight.highlightFields.type.toString(), highlightId = createdHighlight.highlightFields.id, @@ -147,8 +148,8 @@ suspend fun Networker.createHighlight(input: CreateHighlightInput): Highlight? { suffix = createdHighlight.highlightFields.suffix, patch = createdHighlight.highlightFields.patch, annotation = createdHighlight.highlightFields.annotation, - createdAt = null, // TODO: update gql query to get this - updatedAt = null, // TODO: fix updatedAtString?.let { LocalDate.parse(it) }, + createdAt = createdHighlight.highlightFields.createdAt.toString(), + updatedAt = createdHighlight.highlightFields.updatedAt.toString(), createdByMe = createdHighlight.highlightFields.createdByMe ) } else { 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 134f09d45..13fc3aed9 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,8 +51,8 @@ suspend fun Networker.savedItem(slug: String): SavedItemQueryResponse { suffix = it.highlightFields.suffix, patch = it.highlightFields.patch, 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 + createdAt = it.highlightFields.createdAt as String?, + updatedAt = it.highlightFields.updatedAt as String?, createdByMe = it.highlightFields.createdByMe ) } 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 fcfab3114..19e23f877 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 @@ -64,7 +64,7 @@ suspend fun Networker.search( savedItemLabelId = label.labelFields.id, name = label.labelFields.name, color = label.labelFields.color, - createdAt = null, + createdAt = label.labelFields.createdAt as String?, labelDescription = null ) }, @@ -81,7 +81,7 @@ suspend fun Networker.search( shortId = highlight.highlightFields.shortId, suffix = highlight.highlightFields.suffix, updatedAt = highlight.highlightFields.updatedAt as String?, - createdAt = null, + createdAt = highlight.highlightFields.createdAt as String?, ) } ) 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 0071697c6..f23cfb04f 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,5 +1,6 @@ package app.omnivore.omnivore.persistence.entities +import androidx.lifecycle.LiveData import androidx.room.* import app.omnivore.omnivore.models.ServerSyncStatus import com.google.gson.annotations.SerializedName @@ -12,7 +13,7 @@ data class Highlight( val type: String, var annotation: String?, val createdAt: String?, - val createdByMe: Boolean, + val createdByMe: Boolean = true, val markedForDeletion: Boolean = false, var patch: String?, var prefix: String?, @@ -21,10 +22,6 @@ data class Highlight( var shortId: String, val suffix: String?, 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( @@ -90,6 +87,10 @@ interface HighlightDao { @Query("SELECT * FROM highlight WHERE highlightId = :highlightId") fun findById(highlightId: String): Highlight? + // Server sync status is passed in here to work around Room compile-time query rules, but should always be NEEDS_UPDATE + @Query("UPDATE highlight SET annotation = :note, serverSyncStatus = :serverSyncStatus WHERE highlightId = :highlightId") + fun updateNote(highlightId: String, note: String, serverSyncStatus: Int = ServerSyncStatus.NEEDS_UPDATE.rawValue) + @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 7ffd74805..863ebe7ae 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 @@ -142,6 +142,9 @@ interface SavedItemDao { @Query("DELETE FROM savedItem WHERE savedItemId = :itemID") fun deleteById(itemID: String) + @Query("DELETE FROM savedItem WHERE savedItemId in (:itemIDs)") + fun deleteByIds(itemIDs: List) + @Update fun update(savedItem: SavedItem) @@ -161,6 +164,22 @@ interface SavedItemDao { ) fun getLibraryItemById(savedItemId: String): LiveData + @Transaction + @Query( + "SELECT ${SavedItemQueryConstants.libraryColumns} " + + "FROM SavedItem " + + "LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " + + "LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " + + + "LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " + + "LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " + + + "WHERE SavedItem.savedItemId = :savedItemId " + + + "GROUP BY SavedItem.savedItemId " + ) + suspend fun getById(savedItemId: String): SavedItemWithLabelsAndHighlights? + @Transaction @Query( "SELECT ${SavedItemQueryConstants.libraryColumns} " + diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/notebook/NotebookView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/notebook/NotebookView.kt index 4bce9ac33..45a7f3aa4 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/notebook/NotebookView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/notebook/NotebookView.kt @@ -3,7 +3,6 @@ package app.omnivore.omnivore.ui.notebook import android.content.ClipData import android.content.ClipboardManager import android.content.Context -import androidx.activity.compose.LocalOnBackPressedDispatcherOwner import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -19,12 +18,16 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TextButton import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext @@ -38,7 +41,7 @@ import app.omnivore.omnivore.ui.library.* import dev.jeziellago.compose.markdowntext.MarkdownText import kotlinx.coroutines.launch import app.omnivore.omnivore.persistence.entities.Highlight - +import app.omnivore.omnivore.ui.theme.OmnivoreTheme fun notebookMD(notes: List, highlights: List): String { @@ -68,15 +71,12 @@ fun notebookMD(notes: List, highlights: List): String { @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @Composable -fun NotebookView(savedItemId: String, viewModel: NotebookViewModel) { +fun NotebookView(savedItemId: String, viewModel: NotebookViewModel, onEditNote: (note: Highlight?) -> Unit) { var isMenuOpen by remember { mutableStateOf(false) } val savedItem = viewModel.getLibraryItemById(savedItemId).observeAsState() val scrollState = rememberScrollState() - val modalBottomSheetState = rememberModalBottomSheetState( - ModalBottomSheetValue.Hidden, - ) val coroutineScope = rememberCoroutineScope() val snackBarHostState = remember { SnackbarHostState() } val clipboard: ClipboardManager? = @@ -85,69 +85,59 @@ fun NotebookView(savedItemId: String, viewModel: NotebookViewModel) { val notes = savedItem.value?.highlights?.filter { it.type == "NOTE" } ?: listOf() val highlights = savedItem.value?.highlights?.filter { it.type == "HIGHLIGHT" } ?: listOf() - ModalBottomSheetLayout( - sheetBackgroundColor = Color.Transparent, - sheetState = modalBottomSheetState, - sheetContent = { - // EditNoteModal() - Spacer(modifier = Modifier.weight(1.0F)) - } - ) { - Scaffold( - topBar = { - TopAppBar( - title = { Text("Notebook") }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.background - ), - actions = { - Box { - IconButton(onClick = { - isMenuOpen = true - }) { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = null - ) - } - if (isMenuOpen) { - DropdownMenu( - expanded = isMenuOpen, - onDismissRequest = { isMenuOpen = false } - ) { - DropdownMenuItem( - text = { Text("Copy") }, - onClick = { - val clip = ClipData.newPlainText("notebook", notebookMD(notes, highlights)) - clipboard?.let { - clipboard?.setPrimaryClip(clip) - } ?: run { - coroutineScope.launch { - snackBarHostState - .showSnackbar("Notebook copied") - } + Scaffold( + topBar = { + TopAppBar( + title = { Text("Notebook") }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.background + ), + actions = { + Box { + IconButton(onClick = { + isMenuOpen = true + }) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = null + ) + } + if (isMenuOpen) { + DropdownMenu( + expanded = isMenuOpen, + onDismissRequest = { isMenuOpen = false } + ) { + DropdownMenuItem( + text = { Text("Copy") }, + onClick = { + val clip = ClipData.newPlainText("notebook", notebookMD(notes, highlights)) + clipboard?.let { + clipboard?.setPrimaryClip(clip) + } ?: run { + coroutineScope.launch { + snackBarHostState + .showSnackbar("Notebook copied") } - isMenuOpen = false } - ) - } + isMenuOpen = false + } + ) } } } - ) - } - ) { paddingValues -> - Column( - modifier = Modifier - .verticalScroll(scrollState) - .fillMaxSize() - ) { - savedItem.value?.let { - if (notes.isNotEmpty()) { - ArticleNotes(it) - } - HighlightsList(it) } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .verticalScroll(scrollState) + .fillMaxSize() + .padding(top = paddingValues.calculateTopPadding()) + ) { + savedItem.value?.let { + ArticleNotes(viewModel, it, onEditNote) + HighlightsList(it, onEditNote) } } } @@ -155,28 +145,31 @@ fun NotebookView(savedItemId: String, viewModel: NotebookViewModel) { @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @Composable -fun EditNoteModal() { - val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher - val annotation = remember { mutableStateOf("") } +fun EditNoteModal(initialValue: String?, onDismiss: (save: Boolean, text: String?) -> Unit) { + val focusRequester = remember { FocusRequester() } + val annotation = remember { mutableStateOf(initialValue ?: "") } BottomSheetUI() { Scaffold( topBar = { - TopAppBar( + CenterAlignedTopAppBar( title = { Text("Note") }, modifier = Modifier.statusBarsPadding(), colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.background ), navigationIcon = { - IconButton(onClick = { - onBackPressedDispatcher?.onBackPressed() + TextButton(onClick = { + onDismiss(false, initialValue) }) { - Icon( - imageVector = androidx.compose.material.icons.Icons.Filled.ArrowBack, - modifier = Modifier, - contentDescription = "Back" - ) + Text(text = "Cancel") + } + }, + actions = { + TextButton(onClick = { + onDismiss(true, annotation.value) + }) { + Text(text = "Save") } } ) @@ -185,26 +178,30 @@ fun EditNoteModal() { TextField( modifier = Modifier .padding(paddingValues) + .focusRequester(focusRequester) .fillMaxSize(), - value = annotation.value, onValueChange = { annotation.value = it } + value = annotation.value, onValueChange = { annotation.value = it }, + colors = TextFieldDefaults.textFieldColors( + textColor = Color.White + ) ) } } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } } @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @Composable -fun ArticleNotes(item: SavedItemWithLabelsAndHighlights) { +fun ArticleNotes(viewModel: NotebookViewModel, item: SavedItemWithLabelsAndHighlights, onEditNote: (note: Highlight?) -> Unit) { val notes = item.highlights?.filter { it.type == "NOTE" } ?: listOf() - val showDialog = remember { mutableStateOf(false) } - val modalBottomSheetState = rememberModalBottomSheetState( - ModalBottomSheetValue.Expanded, - ) - val annotation = remember { mutableStateOf("") } Column(modifier = Modifier .fillMaxWidth() .padding(start = 15.dp) + .padding(top = 20.dp, bottom = 50.dp) ) { Text("Article Notes") Divider(modifier = Modifier.padding(bottom= 15.dp)) @@ -214,14 +211,13 @@ fun ArticleNotes(item: SavedItemWithLabelsAndHighlights) { fontSize = 14.sp, style = TextStyle(lineHeight = 18.sp), color = MaterialTheme.colorScheme.onPrimaryContainer, + onClick = { onEditNote(note) } ) } if (notes.isEmpty()) { Button( onClick = { -// viewModelScope.launch { -// datastoreRepo.clearValue(DatastoreKeys.omnivorePendingUserToken) -// } + onEditNote(null) }, modifier = Modifier .padding(0.dp, end = 15.dp) @@ -232,12 +228,12 @@ fun ArticleNotes(item: SavedItemWithLabelsAndHighlights) { containerColor = MaterialTheme.colorScheme.surfaceVariant ) ) { -// Text( -// text = "Add Notes...", -// style = androidx.compose.material.MaterialTheme.typography.subtitle2, -// modifier = Modifier -// .padding(vertical = 2.dp, horizontal = 0.dp), -// ) + Text( + text = "Add Notes...", + style = androidx.compose.material.MaterialTheme.typography.subtitle2, + modifier = Modifier + .padding(vertical = 2.dp, horizontal = 0.dp), + ) Spacer(Modifier.weight(1f)) } } @@ -246,7 +242,7 @@ fun ArticleNotes(item: SavedItemWithLabelsAndHighlights) { @OptIn(ExperimentalMaterial3Api::class) @Composable -fun HighlightsList(item: SavedItemWithLabelsAndHighlights) { +fun HighlightsList(item: SavedItemWithLabelsAndHighlights, onEditNote: (note: Highlight?) -> Unit) { val highlights = item.highlights?.filter { it.type == "HIGHLIGHT" } ?: listOf() val yellowColor = colorResource(R.color.cta_yellow) @@ -263,98 +259,107 @@ fun HighlightsList(item: SavedItemWithLabelsAndHighlights) { Text("Highlights") Divider(modifier = Modifier.padding(bottom= 10.dp)) highlights.forEach { highlight -> - var isMenuOpen by remember { mutableStateOf(false) } + var isMenuOpen by remember { mutableStateOf(false) } - Row(modifier = Modifier - .fillMaxWidth() - .align(Alignment.End) - .padding(0.dp) - ) { - Spacer(Modifier.weight(1f)) - Box { - IconButton(onClick = { isMenuOpen = true }) { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = null - ) - } - if (isMenuOpen) { - DropdownMenu( - expanded = isMenuOpen, - onDismissRequest = { isMenuOpen = false } - ) { - DropdownMenuItem( - text = { Text("Copy") }, - onClick = { - val clip = ClipData.newPlainText("highlight", highlight.quote) - clipboard?.let { - clipboard?.setPrimaryClip(clip) - } ?: run { - coroutineScope.launch { - snackBarHostState - .showSnackbar("Highlight copied") - } - } - isMenuOpen = false - } - ) - } - } - } - } - - highlight.quote?.let { - Row(modifier = Modifier - .padding(start = 2.dp, end = 15.dp) - .fillMaxWidth() - .drawWithCache { - onDrawWithContent { - // draw behind the content the vertical line on the left - drawLine( - color = yellowColor, - start = Offset.Zero, - end = Offset(0f, this.size.height), - strokeWidth = 10f - ) - - // draw the content - drawContent() - } - }) { - - MarkdownText( - modifier = Modifier - .padding(start = 15.dp, end = 15.dp), - markdown = it, - fontSize = 14.sp, - color = MaterialTheme.colorScheme.onPrimaryContainer, + Row(modifier = Modifier + .fillMaxWidth() + .align(Alignment.End) + .padding(0.dp) + ) { + Spacer(Modifier.weight(1f)) + Box { + IconButton(onClick = { isMenuOpen = true }) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = null ) } + if (isMenuOpen) { + DropdownMenu( + expanded = isMenuOpen, + onDismissRequest = { isMenuOpen = false } + ) { + DropdownMenuItem( + text = { Text("Copy") }, + onClick = { + val clip = ClipData.newPlainText("highlight", highlight.quote) + clipboard?.let { + clipboard?.setPrimaryClip(clip) + } ?: run { + coroutineScope.launch { + snackBarHostState + .showSnackbar("Highlight copied") + } + } + isMenuOpen = false + } + ) + } + } } - highlight.annotation?.let { + } + + highlight.quote?.let { + Row(modifier = Modifier + .padding(start = 2.dp, end = 15.dp) + .fillMaxWidth() + .drawWithCache { + onDrawWithContent { + // draw behind the content the vertical line on the left + drawLine( + color = yellowColor, + start = Offset.Zero, + end = Offset(0f, this.size.height), + strokeWidth = 10f + ) + + // draw the content + drawContent() + } + }) { + MarkdownText( - // modifier = Modifier.padding(paddingValues), + modifier = Modifier + .padding(start = 15.dp, end = 15.dp), markdown = it, fontSize = 14.sp, color = MaterialTheme.colorScheme.onPrimaryContainer, - ) - } ?: run { -// Surface( -// modifier = Modifier -// .padding(0.dp, end = 15.dp, top = 15.dp, bottom = 30.dp) -// .fillMaxWidth(), -// shape = androidx.compose.material.MaterialTheme.shapes.medium, -// color = MaterialTheme.colorScheme.surfaceVariant -// ) { -// Row { -// Text( -// text = "Add Notes...", -// style = androidx.compose.material.MaterialTheme.typography.subtitle2, -// modifier = Modifier.padding(vertical = 10.dp, horizontal = 10.dp) -// ) -// } -// } + ) } + } + highlight.annotation?.let { + MarkdownText( + // modifier = Modifier.padding(paddingValues), + markdown = it, + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onPrimaryContainer, + onClick = { onEditNote(highlight) }, + modifier = Modifier + .padding(top = 15.dp), + ) + } ?: run { + Button( + onClick = { + onEditNote(highlight) + }, + modifier = Modifier + .padding(0.dp, top = 15.dp, end = 15.dp) + .fillMaxWidth(), + shape = androidx.compose.material.MaterialTheme.shapes.medium, + colors = ButtonDefaults.buttonColors( + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) { + Text( + text = "Add Note...", + style = androidx.compose.material.MaterialTheme.typography.subtitle2, + modifier = Modifier + .padding(vertical = 2.dp, horizontal = 0.dp), + ) + Spacer(Modifier.weight(1f)) + } + } } if (highlights.isEmpty()) { Text( @@ -366,18 +371,25 @@ fun HighlightsList(item: SavedItemWithLabelsAndHighlights) { } } +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @Composable -private fun BottomSheetUI(content: @Composable () -> Unit) { - Box( - modifier = Modifier - .wrapContentHeight() - .fillMaxWidth() - .clip(RoundedCornerShape(topEnd = 20.dp, topStart = 20.dp)) - .background(Color.White) - .statusBarsPadding() - .padding(top = 20.dp) - ) { - content() +fun BottomSheetUI(content: @Composable () -> Unit) { + OmnivoreTheme() { + Box( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .clip(RoundedCornerShape(topEnd = 20.dp, topStart = 20.dp)) + .background(Color.Transparent) + .statusBarsPadding() + ) { + Scaffold( + ) { paddingValues -> + Box(modifier = Modifier.fillMaxSize()) { + content() + } + } + } } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/notebook/NotebookViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/notebook/NotebookViewModel.kt index e1095d1ab..7a58837f0 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/notebook/NotebookViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/notebook/NotebookViewModel.kt @@ -1,13 +1,23 @@ package app.omnivore.omnivore.ui.notebook -import androidx.lifecycle.LiveData -import androidx.lifecycle.ViewModel +import androidx.lifecycle.* +import androidx.room.Query import app.omnivore.omnivore.DatastoreRepository import app.omnivore.omnivore.dataService.DataService +import app.omnivore.omnivore.dataService.createNoteHighlight +import app.omnivore.omnivore.dataService.updateWebHighlight +import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput +import app.omnivore.omnivore.models.ServerSyncStatus import app.omnivore.omnivore.networking.Networker +import app.omnivore.omnivore.networking.updateHighlight +import app.omnivore.omnivore.persistence.entities.Highlight import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights import app.omnivore.omnivore.ui.library.SavedItemViewModel +import com.apollographql.apollo3.api.Optional import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject @HiltViewModel @@ -16,8 +26,39 @@ class NotebookViewModel @Inject constructor( private val dataService: DataService, private val datastoreRepo: DatastoreRepository ): ViewModel() { + var highlightUnderEdit: Highlight? = null fun getLibraryItemById(savedItemId: String): LiveData { return dataService.db.savedItemDao().getLibraryItemById(savedItemId) } + + suspend fun addArticleNote(savedItemId: String, note: String) { + withContext(Dispatchers.IO) { + val item = dataService.db.savedItemDao().getById(savedItemId) + item?.let { item -> + val noteHighlight = item.highlights.firstOrNull { it.type == "NOTE" } + noteHighlight?.let { + dataService.db.highlightDao() + .updateNote(highlightId = noteHighlight.highlightId, note = note) + + networker.updateHighlight(input = UpdateHighlightInput( + highlightId = noteHighlight.highlightId, + annotation = Optional.presentIfNotNull(note), + )) + } ?: run { + dataService.createNoteHighlight(savedItemId, note) + } + } + } + } + + suspend fun updateHighlightNote(highlightId: String, note: String?) { + withContext(Dispatchers.IO) { + dataService.db.highlightDao().updateNote(highlightId, note ?: "") + networker.updateHighlight(input = UpdateHighlightInput( + highlightId = highlightId, + annotation = Optional.presentIfNotNull(note), + )) + } + } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/AnnotationEditView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/AnnotationEditView.kt index f7c1605cb..97abd9781 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/AnnotationEditView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/AnnotationEditView.kt @@ -1,37 +1,28 @@ package app.omnivore.omnivore.ui.reader -import android.content.ClipData +import android.R import android.content.DialogInterface import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.compose.foundation.background +import android.view.WindowManager import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.unit.dp import androidx.fragment.app.DialogFragment -import app.omnivore.omnivore.ui.notebook.ArticleNotes -import app.omnivore.omnivore.ui.notebook.HighlightsList -import app.omnivore.omnivore.ui.notebook.notebookMD +import app.omnivore.omnivore.ui.notebook.EditNoteModal import app.omnivore.omnivore.ui.theme.OmnivoreTheme -import kotlinx.coroutines.launch +import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment + class AnnotationEditFragment : DialogFragment() { private var onSave: (String) -> Unit = {} @@ -48,24 +39,38 @@ class AnnotationEditFragment : DialogFragment() { this.onCancel = onCancel } + @OptIn(ExperimentalMaterial3Api::class) override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { + return ComposeView(requireContext()).apply { - // Dispose of the Composition when the view's LifecycleOwner - // is destroyed - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + + (dialog as? BottomSheetDialog)?.let { + it.behavior.skipCollapsed = true + it.behavior.state = STATE_EXPANDED + } + + dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN) + + + + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { - OmnivoreTheme { - AnnotationEditView( - initialAnnotation, - onSave, - onCancel, - // dismissAction = { dismiss() } - ) - } + val annotation = remember { mutableStateOf(initialAnnotation ?: "") } + + OmnivoreTheme { + EditNoteModal(initialValue = initialAnnotation, onDismiss = { save, text -> + if (save) { + onSave(text ?: "") + } else { + onCancel() + } + dismissNow() + }) + } } } } @@ -75,117 +80,3 @@ class AnnotationEditFragment : DialogFragment() { super.onDismiss(dialog) } } - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AnnotationEditView( - initialAnnotation: String, - onSave: (String) -> Unit, - onCancel: () -> Unit, -) { - val annotation = remember { mutableStateOf(initialAnnotation) } - val focusRequester = FocusRequester() - - Scaffold( - topBar = { - TopAppBar( - title = { Text("Notebook") }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.background - ), - navigationIcon = { - IconButton(onClick = { - onCancel() - }) { - Icon( - imageVector = Icons.Filled.ArrowBack, - modifier = Modifier, - contentDescription = "Back", - ) - } - }, - actions = { - TextButton( - onClick = { - onSave(annotation.value) - } - ) { - Text("Save") - } - } - ) - } - ) { paddingValues -> - Column( - modifier = Modifier - .padding(paddingValues) - .fillMaxSize() - ) { - TextField( - value = annotation.value, - onValueChange = { annotation.value = it }, - modifier = Modifier - .focusRequester(focusRequester) - .fillMaxSize() - ) - } - } -} -// -// Column( -// modifier = Modifier.padding(16.dp), -// horizontalAlignment = Alignment.CenterHorizontally, -// ) { -// Row { -// TextButton( -// onClick = { -// onCancel() -// dismissAction() -// } -// ) { -// Text("Cancel") -// } -// -// Spacer(modifier = Modifier.weight(1.0F)) -// -// Text(text = "Note") -// -// Spacer(modifier = Modifier.weight(1.0F)) -// -// TextButton( -// onClick = { -// onSave(annotation.value) -// dismissAction() -// } -// ) { -// Text("Save") -// } -// } -// -// Spacer(modifier = Modifier.height(8.dp)) -// -// -// -// Spacer(modifier = Modifier.height(16.dp)) - -// -// LaunchedEffect(Unit) { -// focusRequester.requestFocus() -// } - -// Row { -// Spacer(modifier = Modifier.weight(0.1F)) -// TextField( -// value = annotation.value, -// onValueChange = { annotation.value = it }, -// modifier = Modifier -// .width(IntrinsicSize.Max) -// .height(IntrinsicSize.Max) -// .weight(1.0F) -// ) -// Spacer(modifier = Modifier.weight(0.1F)) -// } -// } -// -// // } -//} 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 216942a75..fd7ad70e1 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 @@ -4,22 +4,19 @@ import android.annotation.SuppressLint import android.content.ClipData import android.content.ClipboardManager import android.content.Context -import android.graphics.Bitmap +import android.graphics.Color import android.graphics.Rect import android.util.Log import android.view.* import android.view.View.OnScrollChangeListener -import android.view.ViewTreeObserver.OnScrollChangedListener import android.webkit.JavascriptInterface import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.graphics.Color import androidx.compose.ui.viewinterop.AndroidView import app.omnivore.omnivore.R import com.google.gson.Gson @@ -32,7 +29,8 @@ import java.util.* @Composable fun WebReader( styledContent: String, - webReaderViewModel: WebReaderViewModel + webReaderViewModel: WebReaderViewModel, + currentTheme: Themes? ) { val javascriptActionLoopUUID: UUID by webReaderViewModel .javascriptActionLoopUUIDLiveData @@ -55,15 +53,13 @@ fun WebReader( settings.allowFileAccess = true settings.domStorageEnabled = true - alpha = 0.0f + alpha = 1.0f + viewModel?.showNavBar() + currentTheme?.let { theme -> + setBackgroundColor(theme.backgroundColor.toInt()); + } webViewClient = object : WebViewClient() { - override fun onPageFinished(view: WebView?, url: String?) { - super.onPageFinished(view, url) - viewModel?.showNavBar() - view?.animate()?.alpha(1.0f)?.duration = 200 - } - override fun shouldOverrideUrlLoading( view: WebView?, request: WebResourceRequest? diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderLoadingContainer.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderLoadingContainer.kt index 89d92554c..e0864f086 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderLoadingContainer.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderLoadingContainer.kt @@ -3,7 +3,6 @@ package app.omnivore.omnivore.ui.reader import android.content.Intent import android.os.Bundle import android.view.View -import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.LocalOnBackPressedDispatcherOwner import androidx.activity.compose.setContent @@ -15,11 +14,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Home -import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.* -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text @@ -48,7 +44,7 @@ import kotlinx.coroutines.launch import kotlin.math.roundToInt import androidx.compose.material3.Button import androidx.compose.ui.platform.LocalContext - +import app.omnivore.omnivore.ui.notebook.EditNoteModal @AndroidEntryPoint class WebReaderLoadingContainerActivity: ComponentActivity() { @@ -115,6 +111,7 @@ enum class BottomSheetState( NONE(), PREFERENCES(), NOTEBOOK(), + EDITNOTE(), HIGHLIGHTNOTE(), LABELS(), LINK() @@ -127,13 +124,14 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, onLibraryIconTap: (() -> Unit)? = null, webReaderViewModel: WebReaderViewModel, notebookViewModel: NotebookViewModel) { + val currentThemeKey = webReaderViewModel.currentThemeKey.observeAsState() + val currentTheme = Themes.values().find { it.themeKey == currentThemeKey.value } val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher val bottomSheetState: BottomSheetState? by webReaderViewModel.bottomSheetStateLiveData.observeAsState(BottomSheetState.NONE) val webReaderParams: WebReaderParams? by webReaderViewModel.webReaderParamsLiveData.observeAsState(null) val shouldPopView: Boolean by webReaderViewModel.shouldPopViewLiveData.observeAsState(false) - val labels: List by webReaderViewModel.savedItemLabelsLiveData.observeAsState(listOf()) val maxToolbarHeight = 48.dp @@ -151,9 +149,11 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, webReaderContent.styledContent() } ?: null + val modalBottomSheetState = rememberModalBottomSheetState( initialValue = ModalBottomSheetValue.Hidden, - confirmStateChange = { + skipHalfExpanded = bottomSheetState == BottomSheetState.EDITNOTE || bottomSheetState == BottomSheetState.HIGHLIGHTNOTE, + confirmValueChange = { if (it == ModalBottomSheetValue.Hidden) { webReaderViewModel.resetBottomSheet() } @@ -161,6 +161,11 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, } ) + val showMenu = { + coroutineScope.launch { + modalBottomSheetState.show() + } + } when (bottomSheetState) { BottomSheetState.PREFERENCES -> { @@ -170,30 +175,10 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, } } } - BottomSheetState.NOTEBOOK -> { - coroutineScope.launch { - modalBottomSheetState.show() - } - } - BottomSheetState.HIGHLIGHTNOTE -> { - coroutineScope.launch { - modalBottomSheetState.show() - } - } - BottomSheetState.LABELS -> { - coroutineScope.launch { - modalBottomSheetState.show() - } - } - BottomSheetState.LINK -> { - coroutineScope.launch { - modalBottomSheetState.show() - } - } - BottomSheetState.NONE -> { - coroutineScope.launch { - modalBottomSheetState.hide() - } + BottomSheetState.NOTEBOOK, BottomSheetState.EDITNOTE, + BottomSheetState.HIGHLIGHTNOTE, BottomSheetState.LABELS, + BottomSheetState.LINK, -> { + showMenu() } else -> { coroutineScope.launch { @@ -217,31 +202,53 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, BottomSheetState.NOTEBOOK -> { webReaderParams?.let { params -> BottomSheetUI(title = "Notebook") { - NotebookView(savedItemId = params.item.savedItemId, viewModel = notebookViewModel) + NotebookView(savedItemId = params.item.savedItemId, viewModel = notebookViewModel, onEditNote = { + notebookViewModel.highlightUnderEdit = it + webReaderViewModel.setBottomSheet(BottomSheetState.EDITNOTE) + }) } } } - BottomSheetState.HIGHLIGHTNOTE -> { - webReaderViewModel.annotation?.let { annotation -> - BottomSheetUI(title = "Note") { - AnnotationEditView( - initialAnnotation = annotation, - onSave = { - webReaderViewModel.saveAnnotation(it) - coroutineScope.launch { - webReaderViewModel.resetBottomSheet() - } - }, - onCancel = { - webReaderViewModel.cancelAnnotationEdit() - coroutineScope.launch { - webReaderViewModel.resetBottomSheet() + BottomSheetState.EDITNOTE -> { + webReaderParams?.let { params -> + EditNoteModal( + initialValue = notebookViewModel.highlightUnderEdit?.annotation, + onDismiss = { save, note -> + coroutineScope.launch { + if (save) { + notebookViewModel.highlightUnderEdit?.let { highlight -> + notebookViewModel.updateHighlightNote(highlight.highlightId, note) + } ?: run { + if (note != null) { + notebookViewModel.addArticleNote( + savedItemId = params.item.savedItemId, + note = note + ) + } + } } + notebookViewModel.highlightUnderEdit = null } - ) - } + webReaderViewModel.setBottomSheet(BottomSheetState.NOTEBOOK) + }) } } + BottomSheetState.HIGHLIGHTNOTE -> { + EditNoteModal( + initialValue = webReaderViewModel.annotation, + onDismiss = { save, note -> + coroutineScope.launch { + if (save) { + webReaderViewModel.saveAnnotation(note ?: "") + } else { + webReaderViewModel.cancelAnnotation() + } + webReaderViewModel.annotation = null + } + webReaderViewModel.resetBottomSheet() + } + ) + } BottomSheetState.LABELS -> { BottomSheetUI(title = "Notebook") { LabelsSelectionSheetContent( @@ -291,7 +298,8 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, if (styledContent != null) { WebReader( styledContent = styledContent, - webReaderViewModel = webReaderViewModel + webReaderViewModel = webReaderViewModel, + currentTheme = currentTheme, ) } 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 8a8729006..0eb4747c8 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 @@ -319,7 +319,18 @@ class WebReaderViewModel @Inject constructor( } fun saveAnnotation(annotation: String) { - val script = "var event = new Event('saveAnnotation');event.annotation = '$annotation';document.dispatchEvent(event);" + val jsonAnnotation = Gson().toJson(annotation) + val script = "var event = new Event('saveAnnotation');event.annotation = $jsonAnnotation;document.dispatchEvent(event);" + + Log.d("loggo", script) + + enqueueScript(script) + cancelAnnotationEdit() + } + + fun cancelAnnotation() { + val script = "var event = new Event('dismissHighlight');document.dispatchEvent(event);" + enqueueScript(script) cancelAnnotationEdit() } diff --git a/android/Omnivore/app/src/main/res/layout/pdf_reader_fragment.xml b/android/Omnivore/app/src/main/res/layout/pdf_reader_fragment.xml index 401e22ae8..2cf240f6f 100644 --- a/android/Omnivore/app/src/main/res/layout/pdf_reader_fragment.xml +++ b/android/Omnivore/app/src/main/res/layout/pdf_reader_fragment.xml @@ -7,7 +7,7 @@ + android:layout_height="match_parent" />