Merge pull request #1350 from omnivore-app/feature/android-pspdfkit-highlights
PDF Toolbars - Android
This commit is contained in:
23
android/Omnivore/app/src/main/graphql/MergeHighlight.graphql
Normal file
23
android/Omnivore/app/src/main/graphql/MergeHighlight.graphql
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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")
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
1
android/Omnivore/app/src/main/res/drawable-v24/close.xml
Normal file
1
android/Omnivore/app/src/main/res/drawable-v24/close.xml
Normal 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>
|
||||
@ -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>
|
||||
24
android/Omnivore/app/src/main/res/layout/annotation_edit.xml
Normal file
24
android/Omnivore/app/src/main/res/layout/annotation_edit.xml
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user