Merge pull request #2266 from omnivore-app/feat/android-notebooks-ux

WIP: Android notebooks
This commit is contained in:
Jackson Harper
2023-05-31 20:57:13 +08:00
committed by GitHub
17 changed files with 573 additions and 444 deletions

View File

@ -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"

View File

@ -53,6 +53,7 @@ fragment HighlightFields on Highlight {
patch
annotation
createdByMe
createdAt
updatedAt
sharedAt
}

View File

@ -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

View File

@ -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()

View File

@ -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,

View File

@ -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 {

View File

@ -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
)
}

View File

@ -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?,
)
}
)

View File

@ -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)
}

View File

@ -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<String>)
@Update
fun update(savedItem: SavedItem)
@ -161,6 +164,22 @@ interface SavedItemDao {
)
fun getLibraryItemById(savedItemId: String): LiveData<SavedItemWithLabelsAndHighlights>
@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} " +

View File

@ -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<Highlight>, highlights: List<Highlight>): String {
@ -68,15 +71,12 @@ fun notebookMD(notes: List<Highlight>, highlights: List<Highlight>): 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()
}
}
}
}
}

View File

@ -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<SavedItemWithLabelsAndHighlights> {
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),
))
}
}
}

View File

@ -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))
// }
// }
//
// // }
//}

View File

@ -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?

View File

@ -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<SavedItemLabel> 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,
)
}

View File

@ -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()
}

View File

@ -7,7 +7,7 @@
<FrameLayout
android:id="@+id/fragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
android:layout_height="match_parent" />
<com.pspdfkit.ui.PdfThumbnailBar
android:id="@+id/thumbnailBar"