Merge pull request #1350 from omnivore-app/feature/android-pspdfkit-highlights

PDF Toolbars - Android
This commit is contained in:
Satindar Dhillon
2022-11-15 12:22:27 -08:00
committed by GitHub
14 changed files with 630 additions and 43 deletions

View File

@ -0,0 +1,23 @@
mutation MergeHighlight($input: MergeHighlightInput!) {
mergeHighlight(input: $input) {
... on MergeHighlightSuccess {
highlight {
id
shortId
quote
prefix
suffix
patch
createdAt
updatedAt
annotation
sharedAt
createdByMe
}
overlapHighlightIdList
}
... on MergeHighlightError {
errorCodes
}
}
}

View File

@ -2,9 +2,14 @@ package app.omnivore.omnivore.networking
import android.util.Log
import app.omnivore.omnivore.graphql.generated.CreateHighlightMutation
import app.omnivore.omnivore.graphql.generated.DeleteHighlightMutation
import app.omnivore.omnivore.graphql.generated.MergeHighlightMutation
import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput
import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput
import app.omnivore.omnivore.models.Highlight
import com.apollographql.apollo3.api.Optional
import com.google.gson.Gson
import com.pspdfkit.annotations.HighlightAnnotation
data class CreateHighlightParams(
val shortId: String?,
@ -24,13 +29,49 @@ data class CreateHighlightParams(
)
}
suspend fun Networker.createHighlight(jsonString: String): Boolean {
val input = Gson().fromJson(jsonString, CreateHighlightParams::class.java).asCreateHighlightInput()
suspend fun Networker.deleteHighlights(highlightIDs: List<String>): Boolean {
val statuses: MutableList<Boolean> = mutableListOf()
for (highlightID in highlightIDs) {
val result = authenticatedApolloClient().mutation(DeleteHighlightMutation(highlightID)).execute()
statuses.add(result.data?.deleteHighlight?.onDeleteHighlightSuccess?.highlight != null)
}
val hasFailure = statuses.any { !it }
return !hasFailure
}
suspend fun Networker.mergeHighlights(input: MergeHighlightInput): Boolean {
val result = authenticatedApolloClient().mutation(MergeHighlightMutation(input)).execute()
Log.d("Network", "highlight merge result: $result")
return result.data?.mergeHighlight?.onMergeHighlightSuccess?.highlight != null
}
suspend fun Networker.createWebHighlight(jsonString: String): Boolean {
val input = Gson().fromJson(jsonString, CreateHighlightParams::class.java).asCreateHighlightInput()
return createHighlight(input) != null
}
suspend fun Networker.createHighlight(input: CreateHighlightInput): Highlight? {
Log.d("Loggo", "created highlight input: $input")
val result = authenticatedApolloClient().mutation(CreateHighlightMutation(input)).execute()
val highlight = result.data?.createHighlight?.onCreateHighlightSuccess?.highlight
return highlight != null
val createdHighlight = result.data?.createHighlight?.onCreateHighlightSuccess?.highlight
if (createdHighlight != null) {
return Highlight(
id = createdHighlight.highlightFields.id,
shortId = createdHighlight.highlightFields.shortId,
quote = createdHighlight.highlightFields.quote,
prefix = createdHighlight.highlightFields.prefix,
suffix = createdHighlight.highlightFields.suffix,
patch = createdHighlight.highlightFields.patch,
annotation = createdHighlight.highlightFields.annotation,
createdAt = null, // TODO: update gql query to get this
updatedAt = createdHighlight.highlightFields.updatedAt,
createdByMe = createdHighlight.highlightFields.createdByMe,
)
} else {
return null
}
}

View File

@ -19,8 +19,13 @@ data class ReadingProgressParams(
)
}
suspend fun Networker.updateReadingProgress(jsonString: String): Boolean {
val input = Gson().fromJson(jsonString, ReadingProgressParams::class.java).asSaveReadingProgressInput()
suspend fun Networker.updateWebReadingProgress(jsonString: String): Boolean {
val params = Gson().fromJson(jsonString, ReadingProgressParams::class.java)
return updateReadingProgress(params)
}
suspend fun Networker.updateReadingProgress(params: ReadingProgressParams): Boolean {
val input = params.asSaveReadingProgressInput()
Log.d("Loggo", "created reading progress input: $input")

View File

@ -1,5 +1,9 @@
package app.omnivore.omnivore.ui.reader
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
@ -10,7 +14,34 @@ 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.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment
import app.omnivore.omnivore.ui.theme.OmnivoreTheme
class AnnotationEditFragment : Fragment() {
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)
setContent {
OmnivoreTheme {
AnnotationEditView(
initialAnnotation = "Initial Annotation",
onSave = {},
onCancel = {}
)
}
}
}
}
}
// TODO: better layout and styling for this view
@OptIn(ExperimentalMaterial3Api::class)

View File

@ -1,31 +1,73 @@
package app.omnivore.omnivore.ui.reader
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.PointF
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.ImageView
import android.widget.PopupMenu
import androidx.activity.viewModels
import androidx.annotation.UiThread
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.lifecycle.Observer
import app.omnivore.omnivore.R
import com.google.gson.Gson
import app.omnivore.omnivore.models.Highlight
import com.pspdfkit.annotations.Annotation
import com.pspdfkit.annotations.HighlightAnnotation
import com.pspdfkit.configuration.PdfConfiguration
import com.pspdfkit.configuration.activity.ThumbnailBarMode
import com.pspdfkit.configuration.page.PageScrollDirection
import com.pspdfkit.datastructures.TextSelection
import com.pspdfkit.document.PdfDocument
import com.pspdfkit.document.search.SearchResult
import com.pspdfkit.listeners.DocumentListener
import com.pspdfkit.listeners.OnPreparePopupToolbarListener
import com.pspdfkit.ui.PdfFragment
import com.pspdfkit.ui.PdfThumbnailBar
import com.pspdfkit.ui.PopupToolbar
import com.pspdfkit.ui.search.PdfSearchViewModular
import com.pspdfkit.ui.search.SimpleSearchResultListener
import com.pspdfkit.ui.special_mode.controller.TextSelectionController
import com.pspdfkit.ui.special_mode.manager.TextSelectionManager
import com.pspdfkit.ui.toolbar.popup.PdfTextSelectionPopupToolbar
import com.pspdfkit.ui.toolbar.popup.PopupToolbarMenuItem
import com.pspdfkit.utils.PdfUtils
import dagger.hilt.android.AndroidEntryPoint
import org.json.JSONObject
@AndroidEntryPoint
class PDFReaderActivity: AppCompatActivity(), DocumentListener {
class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionManager.OnTextSelectionChangeListener, TextSelectionManager.OnTextSelectionModeChangeListener, OnPreparePopupToolbarListener {
private var hasLoadedHighlights = false
private var pendingHighlightAnnotation: HighlightAnnotation? = null
private var textSelectionController: TextSelectionController? = null
private lateinit var fragment: PdfFragment
private lateinit var thumbnailBar: PdfThumbnailBar
private lateinit var configuration: PdfConfiguration
private lateinit var modularSearchView: PdfSearchViewModular
val viewModel: PDFReaderViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
configuration = PdfConfiguration.Builder()
.scrollDirection(PageScrollDirection.HORIZONTAL)
.build()
setContentView(R.layout.pdf_reader_fragment)
// Create the observer which updates the UI.
@ -42,18 +84,26 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener {
viewModel.loadItem(slug, this)
}
private fun load(params: PDFReaderParams) {
val configuration = PdfConfiguration.Builder()
.scrollDirection(PageScrollDirection.HORIZONTAL)
.build()
// TODO: implement onDestroy to remove listeners?
private fun load(params: PDFReaderParams) {
// First, try to restore a previously created fragment.
// If no fragment exists, create a new one.
fragment = supportFragmentManager.findFragmentById(R.id.fragmentContainer) as PdfFragment?
?: createFragment(params.localFileUri, configuration)
// Initialize all PSPDFKit UI components.
initModularSearchViewAndButton()
initThumbnailBar()
fragment.apply {
setOnPreparePopupToolbarListener(this@PDFReaderActivity)
addOnTextSelectionModeChangeListener(this@PDFReaderActivity)
addOnTextSelectionChangeListener(this@PDFReaderActivity)
addDocumentListener(this@PDFReaderActivity)
addDocumentListener(modularSearchView)
addDocumentListener(thumbnailBar.documentListener)
isImmersive = true
}
}
@ -61,19 +111,14 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener {
if (hasLoadedHighlights) return
hasLoadedHighlights = true
thumbnailBar.setDocument(document, configuration)
fragment.addDocumentListener(modularSearchView)
modularSearchView.setDocument(document, configuration)
val params = viewModel.pdfReaderParamsLiveData.value
params?.let {
for (highlight in it.articleContent.highlights) {
val highlightAnnotation = fragment
.document
?.annotationProvider
?.createAnnotationFromInstantJson(highlight.patch)
highlightAnnotation?.let {
fragment.addAnnotationToPage(highlightAnnotation, true)
}
}
loadHighlights(it.articleContent.highlights)
fragment.scrollTo(
RectF(0f, 0f, 0f, 0f),
@ -84,6 +129,19 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener {
}
}
private fun loadHighlights(highlights: List<Highlight>) {
for (highlight in highlights) {
val highlightAnnotation = fragment
.document
?.annotationProvider
?.createAnnotationFromInstantJson(highlight.patch)
highlightAnnotation?.let {
fragment.addAnnotationToPage(highlightAnnotation, true)
}
}
}
private fun createFragment(documentUri: Uri, configuration: PdfConfiguration): PdfFragment {
val fragment = PdfFragment.newInstance(documentUri, configuration)
supportFragmentManager.beginTransaction()
@ -91,4 +149,256 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener {
.commit()
return fragment
}
private fun initThumbnailBar() {
thumbnailBar = findViewById(R.id.thumbnailBar)
?: throw IllegalStateException("Error while loading CustomFragmentActivity. The example layout was missing thumbnail bar view.")
thumbnailBar.setOnPageChangedListener { _, pageIndex: Int -> fragment.pageIndex = pageIndex }
thumbnailBar.setThumbnailBarMode(ThumbnailBarMode.THUMBNAIL_BAR_MODE_FLOATING)
val toggleThumbnailButton = findViewById<ImageView>(R.id.toggleThumbnailButton)
?: throw IllegalStateException(
"Error while loading CustomFragmentActivity. The example layout " +
"was missing the open search button with id `R.id.openThumbnailGridButton`."
)
toggleThumbnailButton.apply {
setImageDrawable(
tintDrawable(
drawable,
ContextCompat.getColor(this@PDFReaderActivity, R.color.black)
)
)
setOnClickListener {
if (thumbnailBar.visibility == View.VISIBLE) {
thumbnailBar.visibility = View.INVISIBLE
} else {
thumbnailBar.visibility = View.VISIBLE
}
}
}
}
private fun initModularSearchViewAndButton() {
// The search view is hidden by default (see layout). Set up a click listener that will show the view once pressed.
val openSearchButton = findViewById<ImageView>(R.id.openSearchButton)
?: throw IllegalStateException(
"Error while loading CustomFragmentActivity. The example layout " +
"was missing the open search button with id `R.id.openSearchButton`."
)
val closeSearchButton = findViewById<ImageView>(R.id.closeSearchButton)
?: throw IllegalStateException(
"Error while loading CustomFragmentActivity. The example layout " +
"was missing the close search button with id `R.id.closeSearchButton`."
)
modularSearchView = findViewById(R.id.modularSearchView)
?: throw IllegalStateException("Error while loading CustomFragmentActivity. The example layout was missing the search view.")
modularSearchView.setSearchViewListener(object : SimpleSearchResultListener() {
override fun onSearchResultSelected(result: SearchResult?) {
// Pass on the search result to the highlighter. If 'null' the highlighter will clear any selection.
if (result != null) {
closeSearchButton.visibility = View.INVISIBLE
fragment.scrollTo(PdfUtils.createPdfRectUnion(result.textBlock.pageRects), result.pageIndex, 250, false)
}
}
})
openSearchButton.apply {
setImageDrawable(
tintDrawable(
drawable,
ContextCompat.getColor(this@PDFReaderActivity, R.color.black)
)
)
setOnClickListener {
closeSearchButton.visibility = View.VISIBLE
modularSearchView.show()
}
}
closeSearchButton.apply {
setImageDrawable(
tintDrawable(
drawable,
ContextCompat.getColor(this@PDFReaderActivity, R.color.white)
)
)
setOnClickListener {
closeSearchButton.visibility = View.INVISIBLE
modularSearchView.hide()
}
}
}
override fun onBackPressed() {
when {
modularSearchView.isDisplayed -> {
modularSearchView.hide()
return
}
else -> super.onBackPressed()
}
}
override fun onPageClick(
document: PdfDocument,
pageIndex: Int,
event: MotionEvent?,
pagePosition: PointF?,
clickedAnnotation: Annotation?
): Boolean {
if (clickedAnnotation != null) {
showHighlightSelectionPopover(clickedAnnotation)
}
return super.onPageClick(document, pageIndex, event, pagePosition, clickedAnnotation)
}
override fun onPageChanged(document: PdfDocument, pageIndex: Int) {
viewModel.syncPageChange(pageIndex, document.pageCount)
super.onPageChanged(document, pageIndex)
}
private fun showHighlightSelectionPopover(clickedAnnotation: Annotation) {
// TODO: anchor popover at exact position of tap (maybe add an empty view at tap loc and anchor to that?)
val popupMenu = PopupMenu(this, fragment.view, Gravity.CENTER, androidx.appcompat.R.attr.actionOverflowMenuStyle, 0)
popupMenu.menuInflater.inflate(R.menu.highlight_selection_menu, popupMenu.menu)
popupMenu.setOnMenuItemClickListener { item ->
when(item.itemId) {
R.id.annotate -> {
viewModel.annotationUnderNoteEdit = clickedAnnotation
// Disabled notes for now since we didn't implement on ios
// showAnnotationView()
}
R.id.delete -> {
viewModel.deleteHighlight(clickedAnnotation)
fragment.document?.annotationProvider?.removeAnnotationFromPage(clickedAnnotation)
}
R.id.copyPdfHighlight -> {
val omnivoreHighlight = clickedAnnotation.customData?.get("omnivoreHighlight") as? JSONObject
val quote = omnivoreHighlight?.get("quote") as? String
quote?.let {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(it, it)
clipboard.setPrimaryClip(clip)
}
}
}
true
}
popupMenu.show()
}
private fun tintDrawable(drawable: Drawable, tint: Int): Drawable {
val tintedDrawable = DrawableCompat.wrap(drawable)
DrawableCompat.setTint(tintedDrawable, tint)
return tintedDrawable
}
override fun onBeforeTextSelectionChange(p0: TextSelection?, p1: TextSelection?): Boolean {
return true
}
override fun onAfterTextSelectionChange(p0: TextSelection?, p1: TextSelection?) {
val textRects = p0?.textBlocks ?: return
val pageIndex = p0.pageIndex
pendingHighlightAnnotation = HighlightAnnotation(pageIndex, textRects)
}
override fun onEnterTextSelectionMode(p0: TextSelectionController) {
val textRects = p0?.textSelection?.textBlocks ?: return
val pageIndex = p0.textSelection?.pageIndex ?: return
pendingHighlightAnnotation = HighlightAnnotation(pageIndex, textRects)
textSelectionController = p0
}
override fun onExitTextSelectionMode(p0: TextSelectionController) {
textSelectionController = null
pendingHighlightAnnotation = null
}
@SuppressLint("ResourceType")
override fun onPrepareTextSelectionPopupToolbar(p0: PdfTextSelectionPopupToolbar) {
val onClickListener = PopupToolbar.OnPopupToolbarItemClickedListener { it ->
when (it.id) {
1 -> {
pendingHighlightAnnotation?.let { annotation ->
val quote = textSelectionController?.textSelection?.text ?: ""
val existingAnnotations = fragment.document?.annotationProvider?.getAnnotations(fragment.pageIndex) ?: listOf()
val overlappingAnnotations = viewModel.overlappingAnnotations(annotation, existingAnnotations)
val overlapIDs = overlappingAnnotations.mapNotNull { viewModel.pluckHighlightID(it) }
for (overlappingAnnotation in overlappingAnnotations) {
fragment.document?.annotationProvider?.removeAnnotationFromPage(overlappingAnnotation)
}
fragment.addAnnotationToPage(annotation, false) {
viewModel.syncHighlightUpdates(annotation, quote, overlapIDs)
}
}
textSelectionController?.textSelection = null
p0.dismiss()
return@OnPopupToolbarItemClickedListener true
}
// 2 -> {
// Log.d("pdf", "user selected annotate action")
// textSelectionController?.textSelection = null
// p0.dismiss()
// return@OnPopupToolbarItemClickedListener true
// }
3 -> {
val text = textSelectionController?.textSelection?.text ?: ""
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(text, text)
clipboard.setPrimaryClip(clip)
textSelectionController?.textSelection = null
p0.dismiss()
return@OnPopupToolbarItemClickedListener true
}
else -> {
p0.dismiss()
textSelectionController?.textSelection = null
return@OnPopupToolbarItemClickedListener false
}
}
}
p0.setOnPopupToolbarItemClickedListener(onClickListener)
p0.menuItems = listOf(
PopupToolbarMenuItem(1, R.string.pdf_highlight_menu_action),
// PopupToolbarMenuItem(2, R.string.annotate_menu_action),
PopupToolbarMenuItem(3, R.string.pdf_highlight_copy),
)
}
private fun showAnnotationView(initialText: String) {
val annotationDialog = Dialog(this)
annotationDialog.setContentView(R.layout.annotation_edit)
val textField = annotationDialog.findViewById(R.id.highlightNoteTextField) as EditText
textField.setText(initialText)
val confirmButton = annotationDialog.findViewById(R.id.confirmAnnotation) as Button
confirmButton.setOnClickListener {
val newNoteText =
annotationDialog.dismiss()
}
val cancelBtn = annotationDialog.findViewById(R.id.cancel) as Button
cancelBtn.setOnClickListener {
annotationDialog.dismiss()
}
annotationDialog.show()
}
}

View File

@ -7,17 +7,24 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.omnivore.omnivore.DatastoreRepository
import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput
import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput
import app.omnivore.omnivore.models.LinkedItem
import app.omnivore.omnivore.networking.Networker
import app.omnivore.omnivore.networking.linkedItem
import app.omnivore.omnivore.networking.*
import com.apollographql.apollo3.api.Optional
import com.google.gson.Gson
import com.pspdfkit.annotations.Annotation
import com.pspdfkit.annotations.HighlightAnnotation
import com.pspdfkit.document.download.DownloadJob
import com.pspdfkit.document.download.DownloadRequest
import com.pspdfkit.document.download.Progress
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import org.json.JSONObject
import java.io.File
import java.lang.Double.max
import java.lang.Double.min
import java.util.*
import javax.inject.Inject
data class PDFReaderParams(
@ -31,8 +38,9 @@ class PDFReaderViewModel @Inject constructor(
private val datastoreRepo: DatastoreRepository,
private val networker: Networker
): ViewModel() {
var annotationUnderNoteEdit: Annotation? = null
val pdfReaderParamsLiveData = MutableLiveData<PDFReaderParams?>(null)
var annotations: List<Annotation> = listOf()
private var currentReadingProgress = 0.0
fun loadItem(slug: String, context: Context) {
viewModelScope.launch {
@ -61,6 +69,7 @@ class PDFReaderViewModel @Inject constructor(
labelsJSONString = Gson().toJson(articleQueryResult.labels)
)
currentReadingProgress = article.readingProgress
pdfReaderParamsLiveData.postValue(PDFReaderParams(article, articleContent, Uri.fromFile(output)))
}
@ -74,4 +83,100 @@ class PDFReaderViewModel @Inject constructor(
fun reset() {
pdfReaderParamsLiveData.postValue(null)
}
fun syncPageChange(currentPageIndex: Int, totalPages: Int) {
val rawProgress = ((currentPageIndex + 1).toDouble() / totalPages.toDouble()) * 100
val percent = min(100.0, max(0.0, rawProgress))
if (percent > currentReadingProgress) {
currentReadingProgress = percent
viewModelScope.launch {
val params = ReadingProgressParams(
id = pdfReaderParamsLiveData.value?.item?.id,
readingProgressPercent = percent,
readingProgressAnchorIndex = currentPageIndex
)
networker.updateReadingProgress(params)
}
}
}
fun syncHighlightUpdates(newAnnotation: Annotation, quote: String, overlapIds: List<String>) {
val itemID = pdfReaderParamsLiveData.value?.item?.id ?: return
val highlightID = UUID.randomUUID().toString()
val shortID = UUID.randomUUID().toString().replace("-","").substring(0,8)
val jsonValues = JSONObject()
.put("id", highlightID)
.put("shortId", shortID)
.put("quote", quote)
.put("articleId", itemID)
newAnnotation.customData = JSONObject().put("omnivoreHighlight", jsonValues)
if (overlapIds.isNotEmpty()) {
val input = MergeHighlightInput(
annotation = Optional.presentIfNotNull(newAnnotation.contents),
articleId = itemID,
id = highlightID,
overlapHighlightIdList = overlapIds,
patch = newAnnotation.toInstantJson(),
quote = quote,
shortId = shortID
)
viewModelScope.launch {
networker.mergeHighlights(input)
}
} else {
val createHighlightInput = CreateHighlightInput(
annotation = Optional.presentIfNotNull(null),
articleId = itemID,
id = highlightID,
patch = newAnnotation.toInstantJson(),
quote = quote,
shortId = shortID,
)
viewModelScope.launch {
networker.createHighlight(createHighlightInput)
}
}
}
fun deleteHighlight(annotation: Annotation) {
val highlightID = pluckHighlightID(annotation) ?: return
viewModelScope.launch {
networker.deleteHighlights(listOf(highlightID))
Log.d("network", "deleted $annotation")
}
}
fun overlappingAnnotations(newAnnotation: Annotation, existingAnnotations: List<Annotation>): List<Annotation> {
val result: MutableList<Annotation> = mutableListOf()
for (existingAnnotation in existingAnnotations) {
if (hasOverlaps(newAnnotation, existingAnnotation)) {
result.add(existingAnnotation)
}
}
return result
}
fun pluckHighlightID(annotation: Annotation): String? {
val omnivoreHighlight = annotation.customData?.get("omnivoreHighlight") as? JSONObject
return omnivoreHighlight?.get("id") as? String
}
private fun hasOverlaps(leftAnnotation: Annotation, rightAnnotation: Annotation): Boolean {
for (leftRect in (leftAnnotation as? HighlightAnnotation)?.rects ?: listOf()) {
for (rightRect in (rightAnnotation as? HighlightAnnotation)?.rects ?: listOf()) {
if (rightRect.intersect(leftRect)) {
return true
}
}
}
return false
}
}

View File

@ -62,7 +62,7 @@ class WebReaderViewModel @Inject constructor(
when (actionID) {
"createHighlight" -> {
viewModelScope.launch {
val isHighlightSynced = networker.createHighlight(jsonString)
val isHighlightSynced = networker.createWebHighlight(jsonString)
Log.d("Network", "isHighlightSynced = $isHighlightSynced")
}
}
@ -75,7 +75,7 @@ class WebReaderViewModel @Inject constructor(
}
"articleReadingProgress" -> {
viewModelScope.launch {
val isReadingProgressSynced = networker.updateReadingProgress(jsonString)
val isReadingProgressSynced = networker.updateWebReadingProgress(jsonString)
Log.d("Network", "isReadingProgressSynced = $isReadingProgressSynced")
}
}

View File

@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#000" android:pathData="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"/></vector>

View File

@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#000" android:pathData="M21 4H3C2.45 4 2 4.45 2 5V19C2 19.55 2.45 20 3 20H21C21.55 20 22 19.55 22 19V5C22 4.45 21.55 4 21 4M8 18H4V6H8V18M14 18H10V6H14V18M20 18H16V6H20V18Z"/></vector>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent" android:layout_height="fill_parent"
android:background="#ffffff">
<EditText
android:layout_width="fill_parent"
android:layout_gravity="center"
android:layout_height="wrap_content"
android:hint="Add note."
android:id="@+id/highlightNoteTextField"
android:textColor="#000000"
android:lines="3" />
<LinearLayout android:layout_width="fill_parent"
android:layout_height="fill_parent">
<Button android:id="@+id/confirmAnnotation" android:layout_height="wrap_content"
android:layout_width="wrap_content" android:text="Confirm" android:layout_weight="1" />
<Button android:id="@+id/cancel" android:layout_height="wrap_content"
android:layout_width="wrap_content" android:layout_weight="1"
android:text="Cancel" />
</LinearLayout>
</LinearLayout>

View File

@ -22,15 +22,43 @@
android:elevation="8dp"
android:visibility="visible"/>
<com.pspdfkit.ui.PdfThumbnailGrid
android:id="@+id/thumbnailGrid"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:orientation="horizontal"
android:elevation="16dp"
android:splitMotionEvents="false">
<com.pspdfkit.ui.PdfOutlineView
android:id="@+id/outlineView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible"/>
<ImageView
android:id="@+id/closeSearchButton"
android:layout_width="50dp"
android:layout_height="50dp"
android:padding="12dp"
android:elevation="16dp"
android:visibility="invisible"
android:src="@drawable/close" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:orientation="horizontal"
android:splitMotionEvents="false">
<ImageView
android:id="@+id/openSearchButton"
android:layout_width="50dp"
android:layout_height="50dp"
android:padding="12dp"
android:src="@drawable/pspdf__ic_search" />
<ImageView
android:id="@+id/toggleThumbnailButton"
android:layout_width="50dp"
android:layout_height="50dp"
android:padding="12dp"
android:src="@drawable/pdf_thumbnail_toggle" />
</LinearLayout>
</FrameLayout>

View File

@ -3,13 +3,13 @@
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/delete"
android:title="@string/delete_highlight_menu_title"
android:title="@string/pdf_remove_highlight"
app:showAsAction="always">
</item>
<item
android:id="@+id/annotate"
android:title="@string/annotate_menu_action"
android:id="@+id/copyPdfHighlight"
android:title="@string/pdf_highlight_copy"
app:showAsAction="always">
</item>
</menu>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/createHighlight"
android:title="@string/pdf_highlight_menu_action"
app:showAsAction="always">
</item>
<item
android:id="@+id/copyPdfHighlight"
android:title="@string/pdf_highlight_copy"
app:showAsAction="always">
</item>
</menu>

View File

@ -4,6 +4,9 @@
<string name="learn_more">Learn More</string>
<string name="welcome_subtitle">Save articles and read them later in our distraction-free reader.</string>
<string name="highlight_menu_action">Highlight</string>
<string name="copy_menu_action">Copy</string>
<string name="annotate_menu_action">Annotate</string>
<string name="delete_highlight_menu_title">Delete</string>
<string name="pdf_remove_highlight">Remove</string>
<string name="pdf_highlight_menu_action">Highlight</string>
<string name="pdf_highlight_copy">Copy</string>
</resources>