From 44c4c3b8f1d4dd7207d82cd571b8428b0db48df7 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 25 Oct 2022 12:33:15 -0700 Subject: [PATCH 01/23] attempt to add pspdfkit toolbars --- .../omnivore/omnivore/ui/reader/PDFReader.kt | 234 +++++++++++++++++- .../main/res/layout/pdf_reader_fragment.xml | 33 +++ 2 files changed, 262 insertions(+), 5 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt index d8a4c3612..d2d3ddd13 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt @@ -1,27 +1,56 @@ package app.omnivore.omnivore.ui.reader +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.HapticFeedbackConstants +import android.view.MotionEvent +import android.widget.ImageView +import android.widget.Toast import androidx.activity.viewModels -import androidx.annotation.UiThread +import androidx.annotation.IntRange 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 com.pspdfkit.annotations.HighlightAnnotation +import com.pspdfkit.annotations.Annotation +import com.pspdfkit.annotations.LinkAnnotation +import com.pspdfkit.annotations.actions.ActionType +import com.pspdfkit.annotations.actions.UriAction import com.pspdfkit.configuration.PdfConfiguration import com.pspdfkit.configuration.page.PageScrollDirection +import com.pspdfkit.document.DocumentSaveOptions import com.pspdfkit.document.PdfDocument +import com.pspdfkit.document.search.SearchResult import com.pspdfkit.listeners.DocumentListener +import com.pspdfkit.listeners.OnDocumentLongPressListener import com.pspdfkit.ui.PdfFragment +import com.pspdfkit.ui.PdfOutlineView +import com.pspdfkit.ui.PdfThumbnailBar +import com.pspdfkit.ui.PdfThumbnailGrid +import com.pspdfkit.ui.outline.DefaultBookmarkAdapter +import com.pspdfkit.ui.outline.DefaultOutlineViewListener +import com.pspdfkit.ui.search.PdfSearchViewModular +import com.pspdfkit.ui.search.SearchResultHighlighter +import com.pspdfkit.ui.search.SimpleSearchResultListener +import com.pspdfkit.utils.PdfUtils import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class PDFReaderActivity: AppCompatActivity(), DocumentListener { +class PDFReaderActivity: AppCompatActivity(), DocumentListener, OnDocumentLongPressListener { private var hasLoadedHighlights = false + private lateinit var fragment: PdfFragment + private lateinit var thumbnailBar: PdfThumbnailBar + private lateinit var configuration: PdfConfiguration + private lateinit var modularSearchView: PdfSearchViewModular + private lateinit var thumbnailGrid: PdfThumbnailGrid + private lateinit var highlighter: SearchResultHighlighter + private lateinit var pdfOutlineView: PdfOutlineView + val viewModel: PDFReaderViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { @@ -52,8 +81,18 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener { fragment = supportFragmentManager.findFragmentById(R.id.fragmentContainer) as PdfFragment? ?: createFragment(params.localFileUri, configuration) + // Initialize all PSPDFKit UI components. + initModularSearchViewAndButton() + initOutlineViewAndButton() + initThumbnailBar() + initThumbnailGridAndButton() + fragment.apply { addDocumentListener(this@PDFReaderActivity) + addDocumentListener(modularSearchView) + addDocumentListener(thumbnailBar.documentListener) + addDocumentListener(thumbnailGrid) + setOnDocumentLongPressListener(this@PDFReaderActivity) } } @@ -91,4 +130,189 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener { .commit() return fragment } + + private fun initThumbnailGridAndButton() { + thumbnailGrid = findViewById(R.id.thumbnailGrid) + ?: throw IllegalStateException("Error while loading CustomFragmentActivity. The example layout was missing the thumbnail grid view.") + + thumbnailGrid.setOnPageClickListener { view, pageIndex -> + fragment.pageIndex = pageIndex + view.hide() + } + + // The thumbnail grid is hidden by default. Set up a click listener to show it. + val openThumbnailGridButton = findViewById(R.id.openThumbnailGridButton) + ?: throw IllegalStateException( + "Error while loading CustomFragmentActivity. The example layout" + + " was missing the open thumbnail grid button with id `R.id.openThumbnailGridButton`." + ) + + openThumbnailGridButton.apply { + setImageDrawable( + tintDrawable( + openThumbnailGridButton.drawable, + ContextCompat.getColor(this@PDFReaderActivity, R.color.white) + ) + ) + setOnClickListener { + if (thumbnailGrid.isShown) thumbnailGrid.hide() else thumbnailGrid.show() + } + } + } + + 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 } + } + + private fun initOutlineViewAndButton() { + pdfOutlineView = findViewById(R.id.outlineView) + ?: throw IllegalStateException("Error while loading CustomFragmentActivity. The example layout was missing the outline view.") + + pdfOutlineView.apply { + val outlineViewListener = DefaultOutlineViewListener(fragment) + setOnAnnotationTapListener(outlineViewListener) + setOnOutlineElementTapListener(outlineViewListener) + setBookmarkAdapter(DefaultBookmarkAdapter(fragment)) + } + + val openOutlineButton = findViewById(R.id.openOutlineButton) + ?: throw IllegalStateException( + "Error while loading CustomFragmentActivity. The example layout " + + "was missing the open outline view button with id `R.id.openOutlineButton`." + ) + + openOutlineButton.apply { + setImageDrawable( + tintDrawable( + openOutlineButton.drawable, + ContextCompat.getColor(this@PDFReaderActivity, R.color.white) + ) + ) + setOnClickListener { + if (pdfOutlineView.isShown) pdfOutlineView.hide() else pdfOutlineView.show() + } + } + } + + private fun initModularSearchViewAndButton() { + // The search result highlighter will highlight any selected result. + highlighter = SearchResultHighlighter(this).also { + fragment.addDrawableProvider(it) + } + + 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 onMoreSearchResults(results: List) { + highlighter.addSearchResults(results) + } + + override fun onSearchCleared() { + highlighter.clearSearchResults() + } + + override fun onSearchResultSelected(result: SearchResult?) { + // Pass on the search result to the highlighter. If 'null' the highlighter will clear any selection. + highlighter.setSelectedSearchResult(result) + if (result != null) { + fragment.scrollTo(PdfUtils.createPdfRectUnion(result.textBlock.pageRects), result.pageIndex, 250, false) + } + } + }) + + // The search view is hidden by default (see layout). Set up a click listener that will show the view once pressed. + val openSearchButton = findViewById(R.id.openSearchButton) + ?: throw IllegalStateException( + "Error while loading CustomFragmentActivity. The example layout " + + "was missing the open search button with id `R.id.openSearchButton`." + ) + + openSearchButton.apply { + setImageDrawable( + tintDrawable( + drawable, + ContextCompat.getColor(this@PDFReaderActivity, R.color.white) + ) + ) + setOnClickListener { + if (modularSearchView.isShown) modularSearchView.hide() else modularSearchView.show() + } + } + } + + override fun onBackPressed() { + when { + modularSearchView.isDisplayed -> { + modularSearchView.hide() + return + } + thumbnailGrid.isDisplayed -> { + thumbnailGrid.hide() + return + } + pdfOutlineView.isDisplayed -> { + pdfOutlineView.hide() + return + } + else -> super.onBackPressed() + } + } + + override fun onDocumentLongPress( + document: PdfDocument, + @IntRange(from = 0) pageIndex: Int, + event: MotionEvent?, + pagePosition: PointF?, + longPressedAnnotation: Annotation? + ): Boolean { + // This code showcases how to handle long click gesture on the document links. + fragment.view?.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + + if (longPressedAnnotation is LinkAnnotation) { + val action = longPressedAnnotation.action + if (action?.type == ActionType.URI) { + val uri = (action as UriAction).uri ?: return true + Toast.makeText(this@PDFReaderActivity, uri, Toast.LENGTH_LONG).show() + return true + } + } + return false + } + + // Rest of the `DocumentListener` methods are unused. + override fun onDocumentLoadFailed(exception: Throwable) = Unit + + override fun onDocumentSave(document: PdfDocument, saveOptions: DocumentSaveOptions): Boolean = true + + override fun onDocumentSaved(document: PdfDocument) = Unit + + override fun onDocumentSaveFailed(document: PdfDocument, exception: Throwable) = Unit + + override fun onDocumentSaveCancelled(document: PdfDocument) = Unit + + override fun onPageClick( + document: PdfDocument, + @IntRange(from = 0) pageIndex: Int, + event: MotionEvent?, + pagePosition: PointF?, + clickedAnnotation: Annotation? + ): Boolean = false + + override fun onDocumentClick(): Boolean = false + + override fun onPageChanged(document: PdfDocument, @IntRange(from = 0) pageIndex: Int) = Unit + + override fun onDocumentZoomed(document: PdfDocument, @IntRange(from = 0) pageIndex: Int, scaleFactor: Float) = Unit + + override fun onPageUpdated(document: PdfDocument, @IntRange(from = 0) pageIndex: Int) = Unit + + private fun tintDrawable(drawable: Drawable, tint: Int): Drawable { + val tintedDrawable = DrawableCompat.wrap(drawable) + DrawableCompat.setTint(tintedDrawable, tint) + return tintedDrawable + } } diff --git a/android/Omnivore/app/src/main/res/layout/pdf_reader_fragment.xml b/android/Omnivore/app/src/main/res/layout/pdf_reader_fragment.xml index c1c022b28..a25c9702e 100644 --- a/android/Omnivore/app/src/main/res/layout/pdf_reader_fragment.xml +++ b/android/Omnivore/app/src/main/res/layout/pdf_reader_fragment.xml @@ -33,4 +33,37 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="invisible"/> + + + + + + + + + From 5060380b058649b034498be7e4ef12750bb19bb2 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 26 Oct 2022 10:55:19 -0700 Subject: [PATCH 02/23] add pdf thumbnail view --- .../omnivore/omnivore/ui/reader/PDFReader.kt | 137 ++---------------- .../main/res/layout/pdf_reader_fragment.xml | 18 +-- 2 files changed, 14 insertions(+), 141 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt index d2d3ddd13..77d9e897a 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt @@ -21,6 +21,7 @@ import com.pspdfkit.annotations.LinkAnnotation import com.pspdfkit.annotations.actions.ActionType import com.pspdfkit.annotations.actions.UriAction import com.pspdfkit.configuration.PdfConfiguration +import com.pspdfkit.configuration.activity.ThumbnailBarMode import com.pspdfkit.configuration.page.PageScrollDirection import com.pspdfkit.document.DocumentSaveOptions import com.pspdfkit.document.PdfDocument @@ -40,21 +41,24 @@ import com.pspdfkit.utils.PdfUtils import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class PDFReaderActivity: AppCompatActivity(), DocumentListener, OnDocumentLongPressListener { +class PDFReaderActivity: AppCompatActivity(), DocumentListener { private var hasLoadedHighlights = false private lateinit var fragment: PdfFragment private lateinit var thumbnailBar: PdfThumbnailBar private lateinit var configuration: PdfConfiguration private lateinit var modularSearchView: PdfSearchViewModular - private lateinit var thumbnailGrid: PdfThumbnailGrid private lateinit var highlighter: SearchResultHighlighter - private lateinit var pdfOutlineView: PdfOutlineView 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. @@ -72,10 +76,6 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, OnDocumentLongPr } private fun load(params: PDFReaderParams) { - val configuration = PdfConfiguration.Builder() - .scrollDirection(PageScrollDirection.HORIZONTAL) - .build() - // First, try to restore a previously created fragment. // If no fragment exists, create a new one. fragment = supportFragmentManager.findFragmentById(R.id.fragmentContainer) as PdfFragment? @@ -83,16 +83,13 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, OnDocumentLongPr // Initialize all PSPDFKit UI components. initModularSearchViewAndButton() - initOutlineViewAndButton() initThumbnailBar() - initThumbnailGridAndButton() fragment.apply { addDocumentListener(this@PDFReaderActivity) addDocumentListener(modularSearchView) addDocumentListener(thumbnailBar.documentListener) - addDocumentListener(thumbnailGrid) - setOnDocumentLongPressListener(this@PDFReaderActivity) + isImmersive = true } } @@ -100,6 +97,8 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, OnDocumentLongPr if (hasLoadedHighlights) return hasLoadedHighlights = true + thumbnailBar.setDocument(document, configuration) + val params = viewModel.pdfReaderParamsLiveData.value params?.let { @@ -131,70 +130,12 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, OnDocumentLongPr return fragment } - private fun initThumbnailGridAndButton() { - thumbnailGrid = findViewById(R.id.thumbnailGrid) - ?: throw IllegalStateException("Error while loading CustomFragmentActivity. The example layout was missing the thumbnail grid view.") - - thumbnailGrid.setOnPageClickListener { view, pageIndex -> - fragment.pageIndex = pageIndex - view.hide() - } - - // The thumbnail grid is hidden by default. Set up a click listener to show it. - val openThumbnailGridButton = findViewById(R.id.openThumbnailGridButton) - ?: throw IllegalStateException( - "Error while loading CustomFragmentActivity. The example layout" + - " was missing the open thumbnail grid button with id `R.id.openThumbnailGridButton`." - ) - - openThumbnailGridButton.apply { - setImageDrawable( - tintDrawable( - openThumbnailGridButton.drawable, - ContextCompat.getColor(this@PDFReaderActivity, R.color.white) - ) - ) - setOnClickListener { - if (thumbnailGrid.isShown) thumbnailGrid.hide() else thumbnailGrid.show() - } - } - } - 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 } - } - - private fun initOutlineViewAndButton() { - pdfOutlineView = findViewById(R.id.outlineView) - ?: throw IllegalStateException("Error while loading CustomFragmentActivity. The example layout was missing the outline view.") - - pdfOutlineView.apply { - val outlineViewListener = DefaultOutlineViewListener(fragment) - setOnAnnotationTapListener(outlineViewListener) - setOnOutlineElementTapListener(outlineViewListener) - setBookmarkAdapter(DefaultBookmarkAdapter(fragment)) - } - - val openOutlineButton = findViewById(R.id.openOutlineButton) - ?: throw IllegalStateException( - "Error while loading CustomFragmentActivity. The example layout " + - "was missing the open outline view button with id `R.id.openOutlineButton`." - ) - - openOutlineButton.apply { - setImageDrawable( - tintDrawable( - openOutlineButton.drawable, - ContextCompat.getColor(this@PDFReaderActivity, R.color.white) - ) - ) - setOnClickListener { - if (pdfOutlineView.isShown) pdfOutlineView.hide() else pdfOutlineView.show() - } - } + thumbnailBar.setThumbnailBarMode(ThumbnailBarMode.THUMBNAIL_BAR_MODE_FLOATING) } private fun initModularSearchViewAndButton() { @@ -250,66 +191,10 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, OnDocumentLongPr modularSearchView.hide() return } - thumbnailGrid.isDisplayed -> { - thumbnailGrid.hide() - return - } - pdfOutlineView.isDisplayed -> { - pdfOutlineView.hide() - return - } else -> super.onBackPressed() } } - override fun onDocumentLongPress( - document: PdfDocument, - @IntRange(from = 0) pageIndex: Int, - event: MotionEvent?, - pagePosition: PointF?, - longPressedAnnotation: Annotation? - ): Boolean { - // This code showcases how to handle long click gesture on the document links. - fragment.view?.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - - if (longPressedAnnotation is LinkAnnotation) { - val action = longPressedAnnotation.action - if (action?.type == ActionType.URI) { - val uri = (action as UriAction).uri ?: return true - Toast.makeText(this@PDFReaderActivity, uri, Toast.LENGTH_LONG).show() - return true - } - } - return false - } - - // Rest of the `DocumentListener` methods are unused. - override fun onDocumentLoadFailed(exception: Throwable) = Unit - - override fun onDocumentSave(document: PdfDocument, saveOptions: DocumentSaveOptions): Boolean = true - - override fun onDocumentSaved(document: PdfDocument) = Unit - - override fun onDocumentSaveFailed(document: PdfDocument, exception: Throwable) = Unit - - override fun onDocumentSaveCancelled(document: PdfDocument) = Unit - - override fun onPageClick( - document: PdfDocument, - @IntRange(from = 0) pageIndex: Int, - event: MotionEvent?, - pagePosition: PointF?, - clickedAnnotation: Annotation? - ): Boolean = false - - override fun onDocumentClick(): Boolean = false - - override fun onPageChanged(document: PdfDocument, @IntRange(from = 0) pageIndex: Int) = Unit - - override fun onDocumentZoomed(document: PdfDocument, @IntRange(from = 0) pageIndex: Int, scaleFactor: Float) = Unit - - override fun onPageUpdated(document: PdfDocument, @IntRange(from = 0) pageIndex: Int) = Unit - private fun tintDrawable(drawable: Drawable, tint: Int): Drawable { val tintedDrawable = DrawableCompat.wrap(drawable) DrawableCompat.setTint(tintedDrawable, tint) diff --git a/android/Omnivore/app/src/main/res/layout/pdf_reader_fragment.xml b/android/Omnivore/app/src/main/res/layout/pdf_reader_fragment.xml index a25c9702e..b68907f07 100644 --- a/android/Omnivore/app/src/main/res/layout/pdf_reader_fragment.xml +++ b/android/Omnivore/app/src/main/res/layout/pdf_reader_fragment.xml @@ -22,24 +22,12 @@ android:elevation="8dp" android:visibility="visible"/> - - - - Date: Wed, 26 Oct 2022 13:14:13 -0700 Subject: [PATCH 03/23] add toggle button for thumbnail view --- .../omnivore/omnivore/ui/reader/PDFReader.kt | 26 ++++++++++++++++++- .../main/res/layout/pdf_reader_fragment.xml | 13 +--------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt index 77d9e897a..4481aebde 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt @@ -5,8 +5,10 @@ import android.graphics.RectF import android.graphics.drawable.Drawable import android.net.Uri import android.os.Bundle +import android.util.Log import android.view.HapticFeedbackConstants import android.view.MotionEvent +import android.view.View import android.widget.ImageView import android.widget.Toast import androidx.activity.viewModels @@ -136,6 +138,28 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener { thumbnailBar.setOnPageChangedListener { _, pageIndex: Int -> fragment.pageIndex = pageIndex } thumbnailBar.setThumbnailBarMode(ThumbnailBarMode.THUMBNAIL_BAR_MODE_FLOATING) + + val toggleThumbnailButton = findViewById(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() { @@ -176,7 +200,7 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener { setImageDrawable( tintDrawable( drawable, - ContextCompat.getColor(this@PDFReaderActivity, R.color.white) + ContextCompat.getColor(this@PDFReaderActivity, R.color.black) ) ) setOnClickListener { diff --git a/android/Omnivore/app/src/main/res/layout/pdf_reader_fragment.xml b/android/Omnivore/app/src/main/res/layout/pdf_reader_fragment.xml index b68907f07..03f6af258 100644 --- a/android/Omnivore/app/src/main/res/layout/pdf_reader_fragment.xml +++ b/android/Omnivore/app/src/main/res/layout/pdf_reader_fragment.xml @@ -26,7 +26,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end" - android:background="@color/gray_B7B7B7" android:orientation="horizontal" android:splitMotionEvents="false"> @@ -34,23 +33,13 @@ android:id="@+id/openSearchButton" android:layout_width="50dp" android:layout_height="50dp" - android:background="?android:attr/selectableItemBackground" android:padding="12dp" android:src="@drawable/pspdf__ic_search" /> - - From dcfe5a6ac51778fcc4241c6c7be66c55fdb7026a Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 26 Oct 2022 13:19:06 -0700 Subject: [PATCH 04/23] listen for changes in pdf search view --- .../src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt index 4481aebde..c4c4326cb 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt @@ -100,6 +100,8 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener { hasLoadedHighlights = true thumbnailBar.setDocument(document, configuration) + fragment.addDocumentListener(modularSearchView) + modularSearchView.setDocument(document, configuration) val params = viewModel.pdfReaderParamsLiveData.value From f5e3153c668682c7e0b149685c1087d86a81f543 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 27 Oct 2022 21:37:32 -0700 Subject: [PATCH 05/23] add hooks for annotation changes in pdf --- .../omnivore/omnivore/ui/reader/PDFReader.kt | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt index c4c4326cb..aa6ae2e38 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt @@ -1,41 +1,28 @@ package app.omnivore.omnivore.ui.reader -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.HapticFeedbackConstants -import android.view.MotionEvent import android.view.View import android.widget.ImageView -import android.widget.Toast import androidx.activity.viewModels -import androidx.annotation.IntRange 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.pspdfkit.annotations.Annotation -import com.pspdfkit.annotations.LinkAnnotation -import com.pspdfkit.annotations.actions.ActionType -import com.pspdfkit.annotations.actions.UriAction +import com.pspdfkit.annotations.AnnotationProvider import com.pspdfkit.configuration.PdfConfiguration import com.pspdfkit.configuration.activity.ThumbnailBarMode import com.pspdfkit.configuration.page.PageScrollDirection -import com.pspdfkit.document.DocumentSaveOptions import com.pspdfkit.document.PdfDocument import com.pspdfkit.document.search.SearchResult import com.pspdfkit.listeners.DocumentListener -import com.pspdfkit.listeners.OnDocumentLongPressListener import com.pspdfkit.ui.PdfFragment -import com.pspdfkit.ui.PdfOutlineView import com.pspdfkit.ui.PdfThumbnailBar -import com.pspdfkit.ui.PdfThumbnailGrid -import com.pspdfkit.ui.outline.DefaultBookmarkAdapter -import com.pspdfkit.ui.outline.DefaultOutlineViewListener import com.pspdfkit.ui.search.PdfSearchViewModular import com.pspdfkit.ui.search.SearchResultHighlighter import com.pspdfkit.ui.search.SimpleSearchResultListener @@ -92,6 +79,28 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener { addDocumentListener(modularSearchView) addDocumentListener(thumbnailBar.documentListener) isImmersive = true + + addOnAnnotationUpdatedListener(object: AnnotationProvider.OnAnnotationUpdatedListener { + override fun onAnnotationCreated(annotation: Annotation) { + Log.i("anno", "The annotation was created. $annotation") + } + + override fun onAnnotationUpdated(annotation: Annotation) { + Log.i("anno", "The annotation was updated. $annotation") + } + + override fun onAnnotationRemoved(annotation: Annotation) { + Log.i("anno", "The annotation was removed. $annotation") + } + + override fun onAnnotationZOrderChanged( + p0: Int, + p1: MutableList, + p2: MutableList + ) { + // Unimplemented + } + }) } } From 33e45ebba9dd7e5c5e64c0c0aa69b954aaaa8b72 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 2 Nov 2022 20:48:54 -0700 Subject: [PATCH 06/23] save highlight when created from pspdfkit --- .../omnivore/networking/HighlightMutations.kt | 6 ++- .../omnivore/omnivore/ui/reader/PDFReader.kt | 45 +++++++++++++------ .../omnivore/ui/reader/PDFReaderViewModel.kt | 31 +++++++++++++ .../omnivore/ui/reader/WebReaderViewModel.kt | 2 +- 4 files changed, 69 insertions(+), 15 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt index 8c418d63e..f41344c7b 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt @@ -3,6 +3,7 @@ package app.omnivore.omnivore.networking import android.util.Log import app.omnivore.omnivore.graphql.generated.CreateHighlightMutation import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput +import app.omnivore.omnivore.graphql.generated.type.Highlight import com.apollographql.apollo3.api.Optional import com.google.gson.Gson @@ -24,9 +25,12 @@ data class CreateHighlightParams( ) } -suspend fun Networker.createHighlight(jsonString: String): Boolean { +suspend fun Networker.createWebHighlight(jsonString: String): Boolean { val input = Gson().fromJson(jsonString, CreateHighlightParams::class.java).asCreateHighlightInput() + return createHighlight(input) +} +suspend fun Networker.createHighlight(input: CreateHighlightInput): Boolean { Log.d("Loggo", "created highlight input: $input") val result = authenticatedApolloClient().mutation(CreateHighlightMutation(input)).execute() diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt index aa6ae2e38..615be9117 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt @@ -13,6 +13,7 @@ import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.DrawableCompat import androidx.lifecycle.Observer import app.omnivore.omnivore.R +import app.omnivore.omnivore.models.Highlight import com.pspdfkit.annotations.Annotation import com.pspdfkit.annotations.AnnotationProvider import com.pspdfkit.configuration.PdfConfiguration @@ -28,6 +29,10 @@ import com.pspdfkit.ui.search.SearchResultHighlighter import com.pspdfkit.ui.search.SimpleSearchResultListener import com.pspdfkit.utils.PdfUtils import dagger.hilt.android.AndroidEntryPoint +import java.time.Duration +import java.time.LocalDateTime +import java.util.* +import kotlin.time.Duration.Companion.seconds @AndroidEntryPoint class PDFReaderActivity: AppCompatActivity(), DocumentListener { @@ -82,15 +87,17 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener { addOnAnnotationUpdatedListener(object: AnnotationProvider.OnAnnotationUpdatedListener { override fun onAnnotationCreated(annotation: Annotation) { - Log.i("anno", "The annotation was created. $annotation") + if (isNewAnnotation(annotation)) { + viewModel.createHighlight(annotation, params.item.id) + } } override fun onAnnotationUpdated(annotation: Annotation) { - Log.i("anno", "The annotation was updated. $annotation") + viewModel.updateHighlight(annotation) } override fun onAnnotationRemoved(annotation: Annotation) { - Log.i("anno", "The annotation was removed. $annotation") + viewModel.deleteHighlight(annotation) } override fun onAnnotationZOrderChanged( @@ -104,6 +111,14 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener { } } + // If created time is less than 2 seconds then we consider it a newly created annotation + private fun isNewAnnotation(annotation: Annotation): Boolean { + val currentTime = Calendar.getInstance().time.time + val createdTime = annotation.createdDate?.time ?: 0 + val duration = currentTime - createdTime + return duration < 2000 + } + override fun onDocumentLoaded(document: PdfDocument) { if (hasLoadedHighlights) return hasLoadedHighlights = true @@ -115,16 +130,7 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener { 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), @@ -135,6 +141,19 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener { } } + private fun loadHighlights(highlights: List) { + 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() diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt index c673cfdd2..b1b374287 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt @@ -7,9 +7,14 @@ 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.models.Highlight import app.omnivore.omnivore.models.LinkedItem import app.omnivore.omnivore.networking.Networker +import app.omnivore.omnivore.networking.createHighlight +import app.omnivore.omnivore.networking.createWebHighlight import app.omnivore.omnivore.networking.linkedItem +import com.apollographql.apollo3.api.Optional import com.google.gson.Gson import com.pspdfkit.annotations.Annotation import com.pspdfkit.document.download.DownloadJob @@ -18,6 +23,7 @@ import com.pspdfkit.document.download.Progress import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import java.io.File +import java.util.* import javax.inject.Inject data class PDFReaderParams( @@ -74,4 +80,29 @@ class PDFReaderViewModel @Inject constructor( fun reset() { pdfReaderParamsLiveData.postValue(null) } + + fun createHighlight(annotation: Annotation, articleID: String) { + // TODO: Check for overlapping highlights + val createHighlightInput = CreateHighlightInput( + annotation = Optional.presentIfNotNull(null), + articleId = articleID, + id = UUID.randomUUID().toString(), + patch = annotation.toInstantJson(), + quote = annotation.contents ?: "", + shortId = UUID.randomUUID().toString().replace("-","").substring(0,8), + ) + + viewModelScope.launch { + val isHighlightSynced = networker.createHighlight(createHighlightInput) + Log.d("Network", "isHighlightSynced = $isHighlightSynced") + } + } + + fun updateHighlight(annotation: Annotation) { + + } + + fun deleteHighlight(annotation: Annotation) { + + } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt index fa816a96f..c30c036ff 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt @@ -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") } } From cb8cecff47587079f8187f9499414a1460d37f8d Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 2 Nov 2022 21:33:50 -0700 Subject: [PATCH 07/23] attempt to detect overlapping highlights --- .../omnivore/ui/reader/PDFReaderViewModel.kt | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt index b1b374287..776a6294c 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt @@ -1,6 +1,7 @@ package app.omnivore.omnivore.ui.reader import android.content.Context +import android.graphics.RectF import android.net.Uri import android.util.Log import androidx.lifecycle.MutableLiveData @@ -10,10 +11,7 @@ import app.omnivore.omnivore.DatastoreRepository import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput import app.omnivore.omnivore.models.Highlight import app.omnivore.omnivore.models.LinkedItem -import app.omnivore.omnivore.networking.Networker -import app.omnivore.omnivore.networking.createHighlight -import app.omnivore.omnivore.networking.createWebHighlight -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 @@ -92,6 +90,9 @@ class PDFReaderViewModel @Inject constructor( shortId = UUID.randomUUID().toString().replace("-","").substring(0,8), ) +// val ggg = overlappingHighlights(annotation) +// Log.d("annny", "has ${ggg.count()} overlapping highlights") + viewModelScope.launch { val isHighlightSynced = networker.createHighlight(createHighlightInput) Log.d("Network", "isHighlightSynced = $isHighlightSynced") @@ -99,10 +100,37 @@ class PDFReaderViewModel @Inject constructor( } fun updateHighlight(annotation: Annotation) { - + Log.d("annny", "updated $annotation") } fun deleteHighlight(annotation: Annotation) { + Log.d("annny", "deleted $annotation") + } + private fun overlappingHighlights(annotation: Annotation): List { + var result: MutableList = mutableListOf() + + for (highlight in pdfReaderParamsLiveData.value?.articleContent?.highlights ?: listOf()) { + if (hasOverlappingHighlights(highlight, annotation)) { + result.add(highlight) + } + } + + return result + } + + private fun hasOverlappingHighlights(highlight: Highlight, annotation: Annotation): Boolean { + val highlightRects = Gson().fromJson(highlight.patch, HighlightRects::class.java).rects + + for (rect in highlightRects) { + if (rect.intersect(annotation.boundingBox)) { + return true + } + } + return false } } + +data class HighlightRects( + val rects: List +) From 062f98908f8fe6afa5c79380210327ebddb7ae59 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Sun, 6 Nov 2022 22:02:02 -0800 Subject: [PATCH 08/23] detect merge overlaps properly --- .../omnivore/networking/HighlightMutations.kt | 26 ++++- .../omnivore/omnivore/ui/reader/PDFReader.kt | 4 +- .../omnivore/ui/reader/PDFReaderViewModel.kt | 95 ++++++++++++++----- 3 files changed, 93 insertions(+), 32 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt index f41344c7b..725c81011 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt @@ -3,7 +3,7 @@ package app.omnivore.omnivore.networking import android.util.Log import app.omnivore.omnivore.graphql.generated.CreateHighlightMutation import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput -import app.omnivore.omnivore.graphql.generated.type.Highlight +import app.omnivore.omnivore.models.Highlight import com.apollographql.apollo3.api.Optional import com.google.gson.Gson @@ -27,14 +27,30 @@ data class CreateHighlightParams( suspend fun Networker.createWebHighlight(jsonString: String): Boolean { val input = Gson().fromJson(jsonString, CreateHighlightParams::class.java).asCreateHighlightInput() - return createHighlight(input) + return createHighlight(input) != null } -suspend fun Networker.createHighlight(input: CreateHighlightInput): Boolean { +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 + } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt index 615be9117..f436eca2a 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt @@ -16,6 +16,7 @@ import app.omnivore.omnivore.R import app.omnivore.omnivore.models.Highlight import com.pspdfkit.annotations.Annotation import com.pspdfkit.annotations.AnnotationProvider +import com.pspdfkit.annotations.HighlightAnnotation import com.pspdfkit.configuration.PdfConfiguration import com.pspdfkit.configuration.activity.ThumbnailBarMode import com.pspdfkit.configuration.page.PageScrollDirection @@ -93,7 +94,8 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener { } override fun onAnnotationUpdated(annotation: Annotation) { - viewModel.updateHighlight(annotation) + val highlightAnnotation = annotation as? HighlightAnnotation ?: return + viewModel.syncUpdatedAnnotationHighlight(highlightAnnotation, articleID = params.item.id) } override fun onAnnotationRemoved(annotation: Annotation) { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt index 776a6294c..5f45b49bd 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt @@ -15,11 +15,15 @@ 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.flow.merge import kotlinx.coroutines.launch +import org.json.JSONArray +import org.json.JSONObject import java.io.File import java.util.* import javax.inject.Inject @@ -36,7 +40,8 @@ class PDFReaderViewModel @Inject constructor( private val networker: Networker ): ViewModel() { val pdfReaderParamsLiveData = MutableLiveData(null) - var annotations: List = listOf() + var annotations: List = listOf() + var documentHighlights: MutableList = mutableListOf() fun loadItem(slug: String, context: Context) { viewModelScope.launch { @@ -56,6 +61,8 @@ class PDFReaderViewModel @Inject constructor( } override fun onComplete(output: File) { + documentHighlights.addAll(0, articleQueryResult.highlights) + val articleContent = ArticleContent( title = article.title, htmlContent = article.content ?: "", @@ -79,58 +86,94 @@ class PDFReaderViewModel @Inject constructor( pdfReaderParamsLiveData.postValue(null) } - fun createHighlight(annotation: Annotation, articleID: String) { - // TODO: Check for overlapping highlights + fun createHighlight(annotation: Annotation, articleID: String, updateDoc: Boolean = true) { + val highlightID = UUID.randomUUID().toString() + val shortID = UUID.randomUUID().toString().replace("-","").substring(0,8) + val quote = annotation.contents ?: "" + + val jsonValues = JSONObject() + .put("id", highlightID) + .put("shortId", shortID) + .put("quote", quote) + .put("articleId", articleID) + + val omnivoreHighlight = JSONObject() + .put("omnivoreHighlight", jsonValues) + + annotation.customData = omnivoreHighlight + val createHighlightInput = CreateHighlightInput( annotation = Optional.presentIfNotNull(null), articleId = articleID, - id = UUID.randomUUID().toString(), + id = highlightID, patch = annotation.toInstantJson(), - quote = annotation.contents ?: "", - shortId = UUID.randomUUID().toString().replace("-","").substring(0,8), + quote = quote, + shortId = shortID, ) -// val ggg = overlappingHighlights(annotation) -// Log.d("annny", "has ${ggg.count()} overlapping highlights") - viewModelScope.launch { - val isHighlightSynced = networker.createHighlight(createHighlightInput) - Log.d("Network", "isHighlightSynced = $isHighlightSynced") + val highlight = networker.createHighlight(createHighlightInput) + if (highlight != null) { + documentHighlights.add(highlight) + } } } - fun updateHighlight(annotation: Annotation) { - Log.d("annny", "updated $annotation") + fun syncUpdatedAnnotationHighlight(annotation: HighlightAnnotation, articleID: String) { + val overlapList = overlappingHighlightIDs(annotation) + // TODO: Delete the overlap list + // Calling create highlight creates a loop... +// createHighlight(annotation, articleID) } fun deleteHighlight(annotation: Annotation) { Log.d("annny", "deleted $annotation") } - private fun overlappingHighlights(annotation: Annotation): List { - var result: MutableList = mutableListOf() + private fun overlappingHighlightIDs(annotation: HighlightAnnotation): List { + val result: MutableList = mutableListOf() + val highlightID = pluckHighlightID(annotation) ?: return listOf() - for (highlight in pdfReaderParamsLiveData.value?.articleContent?.highlights ?: listOf()) { - if (hasOverlappingHighlights(highlight, annotation)) { - result.add(highlight) + val pageHighlights = documentHighlights.filter { + Gson().fromJson(it.patch, HighlightPatch::class.java).pageIndex == annotation.pageIndex + } + + for (highlight in pageHighlights) { + if (highlight.id == highlightID) { + continue + } + + val rects = Gson().fromJson(highlight.patch, HighlightPatch::class.java).rects + if (hasOverlaps(annotation.rects, rects)) { + result.add(highlight.id) } } + result.add(highlightID) + return result } - private fun hasOverlappingHighlights(highlight: Highlight, annotation: Annotation): Boolean { - val highlightRects = Gson().fromJson(highlight.patch, HighlightRects::class.java).rects - - for (rect in highlightRects) { - if (rect.intersect(annotation.boundingBox)) { - return true + private fun hasOverlaps(leftRects: List, rightRects: List>): Boolean { + for (leftRect in leftRects) { + for (rightRect in rightRects) { + val transformedRect = RectF(rightRect[0].toFloat(), rightRect[1].toFloat(), rightRect[2].toFloat(), rightRect[3].toFloat()) + if (transformedRect.intersect(leftRect)) { + return true + } } } + return false } + + private fun pluckHighlightID(annotation: Annotation): String? { + val omnivoreHighlight = annotation.customData?.get("omnivoreHighlight") as? JSONObject + return omnivoreHighlight?.get("id") as? String + } } -data class HighlightRects( - val rects: List +data class HighlightPatch( + val rects: List>, + val pageIndex: Int ) From 51d6c3d31c7adb94c11d328622cf69be35d50718 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 7 Nov 2022 15:57:35 -0800 Subject: [PATCH 09/23] use merge highlight mutation --- .../omnivore/networking/HighlightMutations.kt | 21 +++++ .../omnivore/omnivore/ui/reader/PDFReader.kt | 1 + .../omnivore/ui/reader/PDFReaderViewModel.kt | 81 +++++++++++++++---- 3 files changed, 87 insertions(+), 16 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt index 725c81011..eba0d5baa 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt @@ -2,10 +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?, @@ -25,6 +29,23 @@ data class CreateHighlightParams( ) } +suspend fun Networker.deleteHighlights(highlightIDs: List): Boolean { + val statuses: MutableList = 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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt index f436eca2a..7cabf172b 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt @@ -94,6 +94,7 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener { } override fun onAnnotationUpdated(annotation: Annotation) { + if (!isNewAnnotation(annotation)) { return } val highlightAnnotation = annotation as? HighlightAnnotation ?: return viewModel.syncUpdatedAnnotationHighlight(highlightAnnotation, articleID = params.item.id) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt index 5f45b49bd..0fa7387e1 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt @@ -9,6 +9,7 @@ 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.Highlight import app.omnivore.omnivore.models.LinkedItem import app.omnivore.omnivore.networking.* @@ -86,21 +87,12 @@ class PDFReaderViewModel @Inject constructor( pdfReaderParamsLiveData.postValue(null) } - fun createHighlight(annotation: Annotation, articleID: String, updateDoc: Boolean = true) { + fun createHighlight(annotation: Annotation, articleID: String) { val highlightID = UUID.randomUUID().toString() val shortID = UUID.randomUUID().toString().replace("-","").substring(0,8) val quote = annotation.contents ?: "" - val jsonValues = JSONObject() - .put("id", highlightID) - .put("shortId", shortID) - .put("quote", quote) - .put("articleId", articleID) - - val omnivoreHighlight = JSONObject() - .put("omnivoreHighlight", jsonValues) - - annotation.customData = omnivoreHighlight + annotation.customData = createCustomData(highlightID, shortID, quote, articleID) val createHighlightInput = CreateHighlightInput( annotation = Optional.presentIfNotNull(null), @@ -119,15 +111,68 @@ class PDFReaderViewModel @Inject constructor( } } + private fun createCustomData( + highlightID: String, + shortID: String, + quote: String, + articleID: String + ): JSONObject { + val jsonValues = JSONObject() + .put("id", highlightID) + .put("shortId", shortID) + .put("quote", quote) + .put("articleId", articleID) + + return JSONObject() + .put("omnivoreHighlight", jsonValues) + } + fun syncUpdatedAnnotationHighlight(annotation: HighlightAnnotation, articleID: String) { val overlapList = overlappingHighlightIDs(annotation) - // TODO: Delete the overlap list - // Calling create highlight creates a loop... + val highlightID = pluckHighlightID(annotation) ?: return + val shortID = pluckShortID(annotation) ?: return + + Log.d("Network", "overlaps: $overlapList") + + if (overlapList.isNotEmpty()) { + val quote = annotation.contents ?: "" + + annotation.customData = createCustomData( + highlightID = highlightID, + shortID = shortID, + quote = quote, + articleID = articleID + ) + + val input = MergeHighlightInput( + annotation = Optional.presentIfNotNull(annotation.contents), + articleId = articleID, + id = highlightID, + overlapHighlightIdList = overlapList, + patch = annotation.toInstantJson(), + quote = quote, + shortId = shortID + ) + + documentHighlights.removeAll { overlapList.contains(it.id) } + + viewModelScope.launch { + networker.mergeHighlights(input) + Log.d("network", "merged annotations with input: $input") + } + + return + } + // createHighlight(annotation, articleID) } fun deleteHighlight(annotation: Annotation) { - Log.d("annny", "deleted $annotation") + val highlightID = pluckHighlightID(annotation) ?: return + viewModelScope.launch { + networker.deleteHighlights(listOf(highlightID)) + Log.d("network", "deleted $annotation") + } } private fun overlappingHighlightIDs(annotation: HighlightAnnotation): List { @@ -149,14 +194,13 @@ class PDFReaderViewModel @Inject constructor( } } - result.add(highlightID) - return result } private fun hasOverlaps(leftRects: List, rightRects: List>): Boolean { for (leftRect in leftRects) { for (rightRect in rightRects) { + Log.d("rect", "left: $leftRect, right: $rightRect") val transformedRect = RectF(rightRect[0].toFloat(), rightRect[1].toFloat(), rightRect[2].toFloat(), rightRect[3].toFloat()) if (transformedRect.intersect(leftRect)) { return true @@ -171,6 +215,11 @@ class PDFReaderViewModel @Inject constructor( val omnivoreHighlight = annotation.customData?.get("omnivoreHighlight") as? JSONObject return omnivoreHighlight?.get("id") as? String } + + private fun pluckShortID(annotation: Annotation): String? { + val omnivoreHighlight = annotation.customData?.get("omnivoreHighlight") as? JSONObject + return omnivoreHighlight?.get("shortId") as? String + } } data class HighlightPatch( From d0ceda4489b3357908a8055c7a0c2692aa31afb7 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 7 Nov 2022 15:57:55 -0800 Subject: [PATCH 10/23] add merge highlight graphql file --- .../src/main/graphql/MergeHighlight.graphql | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 android/Omnivore/app/src/main/graphql/MergeHighlight.graphql diff --git a/android/Omnivore/app/src/main/graphql/MergeHighlight.graphql b/android/Omnivore/app/src/main/graphql/MergeHighlight.graphql new file mode 100644 index 000000000..7765de8d7 --- /dev/null +++ b/android/Omnivore/app/src/main/graphql/MergeHighlight.graphql @@ -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 + } + } +} From 5a96cad4a9e3eaf93f962fdb381027d60ded68a7 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 8 Nov 2022 10:16:40 -0800 Subject: [PATCH 11/23] add hook to detect taps on an existing annotation --- .../omnivore/omnivore/ui/reader/PDFReader.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt index 7cabf172b..8ad7c68a9 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt @@ -1,10 +1,12 @@ package app.omnivore.omnivore.ui.reader +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.MotionEvent import android.view.View import android.widget.ImageView import androidx.activity.viewModels @@ -252,6 +254,21 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener { } } + override fun onPageClick( + document: PdfDocument, + pageIndex: Int, + event: MotionEvent?, + pagePosition: PointF?, + clickedAnnotation: Annotation? + ): Boolean { + if (clickedAnnotation != null) { + // TODO: show menu with delete and add note buttons + Log.d("pdf", "clicked annotation: $clickedAnnotation") + } + + return super.onPageClick(document, pageIndex, event, pagePosition, clickedAnnotation) + } + private fun tintDrawable(drawable: Drawable, tint: Int): Drawable { val tintedDrawable = DrawableCompat.wrap(drawable) DrawableCompat.setTint(tintedDrawable, tint) From ec71c580aa68d5ce45d2579a07d47ad25fc680a8 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 8 Nov 2022 11:14:41 -0800 Subject: [PATCH 12/23] use TextSelectionManager to create highlights --- .../omnivore/omnivore/ui/reader/PDFReader.kt | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt index 8ad7c68a9..7a4b0ef39 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt @@ -22,6 +22,7 @@ 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 @@ -30,6 +31,8 @@ import com.pspdfkit.ui.PdfThumbnailBar import com.pspdfkit.ui.search.PdfSearchViewModular import com.pspdfkit.ui.search.SearchResultHighlighter 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.utils.PdfUtils import dagger.hilt.android.AndroidEntryPoint import java.time.Duration @@ -38,8 +41,9 @@ import java.util.* import kotlin.time.Duration.Companion.seconds @AndroidEntryPoint -class PDFReaderActivity: AppCompatActivity(), DocumentListener { +class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionManager.OnTextSelectionChangeListener, TextSelectionManager.OnTextSelectionModeChangeListener { private var hasLoadedHighlights = false + private var pendingHighlightAnnotation: HighlightAnnotation? = null private lateinit var fragment: PdfFragment private lateinit var thumbnailBar: PdfThumbnailBar @@ -83,6 +87,8 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener { initThumbnailBar() fragment.apply { + addOnTextSelectionModeChangeListener(this@PDFReaderActivity) + addOnTextSelectionChangeListener(this@PDFReaderActivity) addDocumentListener(this@PDFReaderActivity) addDocumentListener(modularSearchView) addDocumentListener(thumbnailBar.documentListener) @@ -274,4 +280,28 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener { 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) + Log.d("pdf", "created annotation: $pendingHighlightAnnotation") +// fragment.addAnnotationToPage(highlightAnnotation, false) + } + + override fun onEnterTextSelectionMode(p0: TextSelectionController) { + val textRects = p0?.textSelection?.textBlocks ?: return + val pageIndex = p0.textSelection?.pageIndex ?: return + pendingHighlightAnnotation = HighlightAnnotation(pageIndex, textRects) + Log.d("pdf", "updated annotation via mode listener: $pendingHighlightAnnotation") + } + + override fun onExitTextSelectionMode(p0: TextSelectionController) { + pendingHighlightAnnotation = null + Log.d("pdf", "destroyed pending highlight") + } } From e287a5084436684e5ea5f4d3479e0996468c43f4 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 9 Nov 2022 11:17:38 -0800 Subject: [PATCH 13/23] tap into text selection popover listener --- .../app/omnivore/omnivore/ui/reader/PDFReader.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt index 7a4b0ef39..fd5281bf1 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt @@ -26,6 +26,7 @@ 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.search.PdfSearchViewModular @@ -33,15 +34,13 @@ import com.pspdfkit.ui.search.SearchResultHighlighter 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.utils.PdfUtils import dagger.hilt.android.AndroidEntryPoint -import java.time.Duration -import java.time.LocalDateTime import java.util.* -import kotlin.time.Duration.Companion.seconds @AndroidEntryPoint -class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionManager.OnTextSelectionChangeListener, TextSelectionManager.OnTextSelectionModeChangeListener { +class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionManager.OnTextSelectionChangeListener, TextSelectionManager.OnTextSelectionModeChangeListener, OnPreparePopupToolbarListener { private var hasLoadedHighlights = false private var pendingHighlightAnnotation: HighlightAnnotation? = null @@ -76,6 +75,8 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionMan viewModel.loadItem(slug, this) } + // 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. @@ -87,6 +88,7 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionMan initThumbnailBar() fragment.apply { + setOnPreparePopupToolbarListener(this@PDFReaderActivity) addOnTextSelectionModeChangeListener(this@PDFReaderActivity) addOnTextSelectionChangeListener(this@PDFReaderActivity) addDocumentListener(this@PDFReaderActivity) @@ -304,4 +306,8 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionMan pendingHighlightAnnotation = null Log.d("pdf", "destroyed pending highlight") } + + override fun onPrepareTextSelectionPopupToolbar(p0: PdfTextSelectionPopupToolbar) { + Log.d("pdf", "popup toolbar action") + } } From 0d32117c755a0422e3a9a25cf0121a8192d3517c Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 9 Nov 2022 12:15:48 -0800 Subject: [PATCH 14/23] implement popup menu item listeners --- .../omnivore/omnivore/ui/reader/PDFReader.kt | 47 ++++++++++++++++++- .../app/src/main/res/values/strings.xml | 1 + 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt index fd5281bf1..34a8be717 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt @@ -1,5 +1,8 @@ package app.omnivore.omnivore.ui.reader +import android.annotation.SuppressLint +import android.content.ContentValues +import android.content.Intent import android.graphics.PointF import android.graphics.RectF import android.graphics.drawable.Drawable @@ -29,12 +32,16 @@ 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.SearchResultHighlighter 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.ContextualToolbar +import com.pspdfkit.ui.toolbar.ToolbarCoordinatorLayout 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 java.util.* @@ -43,6 +50,7 @@ import java.util.* 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 @@ -299,15 +307,52 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionMan val textRects = p0?.textSelection?.textBlocks ?: return val pageIndex = p0.textSelection?.pageIndex ?: return pendingHighlightAnnotation = HighlightAnnotation(pageIndex, textRects) + textSelectionController = p0 Log.d("pdf", "updated annotation via mode listener: $pendingHighlightAnnotation") } override fun onExitTextSelectionMode(p0: TextSelectionController) { + textSelectionController = null pendingHighlightAnnotation = null Log.d("pdf", "destroyed pending highlight") } + @SuppressLint("ResourceType") override fun onPrepareTextSelectionPopupToolbar(p0: PdfTextSelectionPopupToolbar) { - Log.d("pdf", "popup toolbar action") + val onClickListener = PopupToolbar.OnPopupToolbarItemClickedListener { + when (it.id) { + 1 -> { + Log.d("pdf", "user selected highlight action") + 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 -> { + Log.d("pdf", "user selected copy action") + 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.highlight_menu_action), + PopupToolbarMenuItem(2, R.string.annotate_menu_action), + PopupToolbarMenuItem(3, R.string.copy_menu_action), + ) } } diff --git a/android/Omnivore/app/src/main/res/values/strings.xml b/android/Omnivore/app/src/main/res/values/strings.xml index b6ed5d009..dcefd6457 100644 --- a/android/Omnivore/app/src/main/res/values/strings.xml +++ b/android/Omnivore/app/src/main/res/values/strings.xml @@ -4,6 +4,7 @@ Learn More Save articles and read them later in our distraction-free reader. Highlight + Copy Annotate Delete From c102bcfc7c1dc34744812d574f057e1bfb3ec352 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 9 Nov 2022 15:30:15 -0800 Subject: [PATCH 15/23] finally get highlight merging working right --- .../omnivore/omnivore/ui/reader/PDFReader.kt | 55 ++------ .../omnivore/ui/reader/PDFReaderViewModel.kt | 130 ++++++------------ 2 files changed, 54 insertions(+), 131 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt index 34a8be717..61b10f493 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt @@ -103,43 +103,9 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionMan addDocumentListener(modularSearchView) addDocumentListener(thumbnailBar.documentListener) isImmersive = true - - addOnAnnotationUpdatedListener(object: AnnotationProvider.OnAnnotationUpdatedListener { - override fun onAnnotationCreated(annotation: Annotation) { - if (isNewAnnotation(annotation)) { - viewModel.createHighlight(annotation, params.item.id) - } - } - - override fun onAnnotationUpdated(annotation: Annotation) { - if (!isNewAnnotation(annotation)) { return } - val highlightAnnotation = annotation as? HighlightAnnotation ?: return - viewModel.syncUpdatedAnnotationHighlight(highlightAnnotation, articleID = params.item.id) - } - - override fun onAnnotationRemoved(annotation: Annotation) { - viewModel.deleteHighlight(annotation) - } - - override fun onAnnotationZOrderChanged( - p0: Int, - p1: MutableList, - p2: MutableList - ) { - // Unimplemented - } - }) } } - // If created time is less than 2 seconds then we consider it a newly created annotation - private fun isNewAnnotation(annotation: Annotation): Boolean { - val currentTime = Calendar.getInstance().time.time - val createdTime = annotation.createdDate?.time ?: 0 - val duration = currentTime - createdTime - return duration < 2000 - } - override fun onDocumentLoaded(document: PdfDocument) { if (hasLoadedHighlights) return hasLoadedHighlights = true @@ -299,8 +265,6 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionMan val textRects = p0?.textBlocks ?: return val pageIndex = p0.pageIndex pendingHighlightAnnotation = HighlightAnnotation(pageIndex, textRects) - Log.d("pdf", "created annotation: $pendingHighlightAnnotation") -// fragment.addAnnotationToPage(highlightAnnotation, false) } override fun onEnterTextSelectionMode(p0: TextSelectionController) { @@ -308,21 +272,32 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionMan val pageIndex = p0.textSelection?.pageIndex ?: return pendingHighlightAnnotation = HighlightAnnotation(pageIndex, textRects) textSelectionController = p0 - Log.d("pdf", "updated annotation via mode listener: $pendingHighlightAnnotation") } override fun onExitTextSelectionMode(p0: TextSelectionController) { textSelectionController = null pendingHighlightAnnotation = null - Log.d("pdf", "destroyed pending highlight") } @SuppressLint("ResourceType") override fun onPrepareTextSelectionPopupToolbar(p0: PdfTextSelectionPopupToolbar) { - val onClickListener = PopupToolbar.OnPopupToolbarItemClickedListener { + val onClickListener = PopupToolbar.OnPopupToolbarItemClickedListener { it -> when (it.id) { 1 -> { - Log.d("pdf", "user selected highlight action") + pendingHighlightAnnotation?.let { annotation -> + 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, overlapIDs) + } + } + textSelectionController?.textSelection = null p0.dismiss() return@OnPopupToolbarItemClickedListener true diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt index 0fa7387e1..597523414 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt @@ -41,8 +41,6 @@ class PDFReaderViewModel @Inject constructor( private val networker: Networker ): ViewModel() { val pdfReaderParamsLiveData = MutableLiveData(null) - var annotations: List = listOf() - var documentHighlights: MutableList = mutableListOf() fun loadItem(slug: String, context: Context) { viewModelScope.launch { @@ -62,8 +60,6 @@ class PDFReaderViewModel @Inject constructor( } override fun onComplete(output: File) { - documentHighlights.addAll(0, articleQueryResult.highlights) - val articleContent = ArticleContent( title = article.title, htmlContent = article.content ?: "", @@ -87,26 +83,40 @@ class PDFReaderViewModel @Inject constructor( pdfReaderParamsLiveData.postValue(null) } - fun createHighlight(annotation: Annotation, articleID: String) { + fun syncHighlightUpdates(newAnnotation: Annotation, overlapIds: List) { + val itemID = pdfReaderParamsLiveData.value?.item?.id ?: return val highlightID = UUID.randomUUID().toString() val shortID = UUID.randomUUID().toString().replace("-","").substring(0,8) - val quote = annotation.contents ?: "" + val quote = newAnnotation.contents ?: "" - annotation.customData = createCustomData(highlightID, shortID, quote, articleID) + newAnnotation.customData = createCustomData(highlightID, shortID, quote, itemID) - val createHighlightInput = CreateHighlightInput( - annotation = Optional.presentIfNotNull(null), - articleId = articleID, - id = highlightID, - patch = annotation.toInstantJson(), - quote = quote, - shortId = shortID, - ) + 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 { - val highlight = networker.createHighlight(createHighlightInput) - if (highlight != null) { - documentHighlights.add(highlight) + 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) } } } @@ -127,46 +137,6 @@ class PDFReaderViewModel @Inject constructor( .put("omnivoreHighlight", jsonValues) } - fun syncUpdatedAnnotationHighlight(annotation: HighlightAnnotation, articleID: String) { - val overlapList = overlappingHighlightIDs(annotation) - val highlightID = pluckHighlightID(annotation) ?: return - val shortID = pluckShortID(annotation) ?: return - - Log.d("Network", "overlaps: $overlapList") - - if (overlapList.isNotEmpty()) { - val quote = annotation.contents ?: "" - - annotation.customData = createCustomData( - highlightID = highlightID, - shortID = shortID, - quote = quote, - articleID = articleID - ) - - val input = MergeHighlightInput( - annotation = Optional.presentIfNotNull(annotation.contents), - articleId = articleID, - id = highlightID, - overlapHighlightIdList = overlapList, - patch = annotation.toInstantJson(), - quote = quote, - shortId = shortID - ) - - documentHighlights.removeAll { overlapList.contains(it.id) } - - viewModelScope.launch { - networker.mergeHighlights(input) - Log.d("network", "merged annotations with input: $input") - } - - return - } - -// createHighlight(annotation, articleID) - } - fun deleteHighlight(annotation: Annotation) { val highlightID = pluckHighlightID(annotation) ?: return viewModelScope.launch { @@ -175,34 +145,22 @@ class PDFReaderViewModel @Inject constructor( } } - private fun overlappingHighlightIDs(annotation: HighlightAnnotation): List { - val result: MutableList = mutableListOf() - val highlightID = pluckHighlightID(annotation) ?: return listOf() + fun overlappingAnnotations(newAnnotation: Annotation, existingAnnotations: List): List { + val result: MutableList = mutableListOf() - val pageHighlights = documentHighlights.filter { - Gson().fromJson(it.patch, HighlightPatch::class.java).pageIndex == annotation.pageIndex - } - - for (highlight in pageHighlights) { - if (highlight.id == highlightID) { - continue - } - - val rects = Gson().fromJson(highlight.patch, HighlightPatch::class.java).rects - if (hasOverlaps(annotation.rects, rects)) { - result.add(highlight.id) + for (existingAnnotation in existingAnnotations) { + if (hasOverlaps(newAnnotation, existingAnnotation)) { + result.add(existingAnnotation) } } return result } - private fun hasOverlaps(leftRects: List, rightRects: List>): Boolean { - for (leftRect in leftRects) { - for (rightRect in rightRects) { - Log.d("rect", "left: $leftRect, right: $rightRect") - val transformedRect = RectF(rightRect[0].toFloat(), rightRect[1].toFloat(), rightRect[2].toFloat(), rightRect[3].toFloat()) - if (transformedRect.intersect(leftRect)) { + 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 } } @@ -211,18 +169,8 @@ class PDFReaderViewModel @Inject constructor( return false } - private fun pluckHighlightID(annotation: Annotation): String? { + fun pluckHighlightID(annotation: Annotation): String? { val omnivoreHighlight = annotation.customData?.get("omnivoreHighlight") as? JSONObject return omnivoreHighlight?.get("id") as? String } - - private fun pluckShortID(annotation: Annotation): String? { - val omnivoreHighlight = annotation.customData?.get("omnivoreHighlight") as? JSONObject - return omnivoreHighlight?.get("shortId") as? String - } } - -data class HighlightPatch( - val rects: List>, - val pageIndex: Int -) From 2ef7da59b465844fcae85d57f27e7ae662c01760 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 9 Nov 2022 20:12:12 -0800 Subject: [PATCH 16/23] remove unused code --- .../omnivore/ui/reader/PDFReaderViewModel.kt | 38 ++++++------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt index 597523414..367de0d80 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt @@ -1,7 +1,6 @@ package app.omnivore.omnivore.ui.reader import android.content.Context -import android.graphics.RectF import android.net.Uri import android.util.Log import androidx.lifecycle.MutableLiveData @@ -10,7 +9,6 @@ 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.Highlight import app.omnivore.omnivore.models.LinkedItem import app.omnivore.omnivore.networking.* import com.apollographql.apollo3.api.Optional @@ -21,9 +19,7 @@ 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.flow.merge import kotlinx.coroutines.launch -import org.json.JSONArray import org.json.JSONObject import java.io.File import java.util.* @@ -89,7 +85,13 @@ class PDFReaderViewModel @Inject constructor( val shortID = UUID.randomUUID().toString().replace("-","").substring(0,8) val quote = newAnnotation.contents ?: "" - newAnnotation.customData = createCustomData(highlightID, shortID, quote, itemID) + 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( @@ -121,22 +123,6 @@ class PDFReaderViewModel @Inject constructor( } } - private fun createCustomData( - highlightID: String, - shortID: String, - quote: String, - articleID: String - ): JSONObject { - val jsonValues = JSONObject() - .put("id", highlightID) - .put("shortId", shortID) - .put("quote", quote) - .put("articleId", articleID) - - return JSONObject() - .put("omnivoreHighlight", jsonValues) - } - fun deleteHighlight(annotation: Annotation) { val highlightID = pluckHighlightID(annotation) ?: return viewModelScope.launch { @@ -157,6 +143,11 @@ class PDFReaderViewModel @Inject constructor( 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()) { @@ -168,9 +159,4 @@ class PDFReaderViewModel @Inject constructor( return false } - - fun pluckHighlightID(annotation: Annotation): String? { - val omnivoreHighlight = annotation.customData?.get("omnivoreHighlight") as? JSONObject - return omnivoreHighlight?.get("id") as? String - } } From 1e977aa62f6e2902a67bb65f3d05f501c59bb286 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 14 Nov 2022 10:45:25 -0800 Subject: [PATCH 17/23] add ability to delete annotations --- .../omnivore/omnivore/ui/reader/PDFReader.kt | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt index 61b10f493..eec27c4fb 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt @@ -1,17 +1,17 @@ package app.omnivore.omnivore.ui.reader import android.annotation.SuppressLint -import android.content.ContentValues -import android.content.Intent 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.ImageView +import android.widget.PopupMenu import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat @@ -20,7 +20,6 @@ import androidx.lifecycle.Observer import app.omnivore.omnivore.R import app.omnivore.omnivore.models.Highlight import com.pspdfkit.annotations.Annotation -import com.pspdfkit.annotations.AnnotationProvider import com.pspdfkit.annotations.HighlightAnnotation import com.pspdfkit.configuration.PdfConfiguration import com.pspdfkit.configuration.activity.ThumbnailBarMode @@ -38,13 +37,11 @@ import com.pspdfkit.ui.search.SearchResultHighlighter 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.ContextualToolbar -import com.pspdfkit.ui.toolbar.ToolbarCoordinatorLayout 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 java.util.* + @AndroidEntryPoint class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionManager.OnTextSelectionChangeListener, TextSelectionManager.OnTextSelectionModeChangeListener, OnPreparePopupToolbarListener { @@ -244,13 +241,32 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionMan clickedAnnotation: Annotation? ): Boolean { if (clickedAnnotation != null) { - // TODO: show menu with delete and add note buttons - Log.d("pdf", "clicked annotation: $clickedAnnotation") + showHighlightSelectionPopover(clickedAnnotation) } return super.onPageClick(document, pageIndex, event, pagePosition, clickedAnnotation) } + 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(PopupMenu.OnMenuItemClickListener { item -> + when(item.itemId) { + R.id.annotate -> + Log.d("pdf", "annotate button tapped") + R.id.delete -> { + viewModel.deleteHighlight(clickedAnnotation) + fragment.document?.annotationProvider?.removeAnnotationFromPage(clickedAnnotation) + } + } + true + }) + popupMenu.show() + } + private fun tintDrawable(drawable: Drawable, tint: Int): Drawable { val tintedDrawable = DrawableCompat.wrap(drawable) DrawableCompat.setTint(tintedDrawable, tint) From 31ac8f602312ee2db0a1b3ac5c3784567de3d331 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 14 Nov 2022 11:01:55 -0800 Subject: [PATCH 18/23] wrap edit annotation view in fragment --- .../omnivore/ui/reader/AnnotationEditView.kt | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/AnnotationEditView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/AnnotationEditView.kt index e308250e2..0ebdc63f9 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/AnnotationEditView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/AnnotationEditView.kt @@ -1,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) From 6747ea4191a46d03154749576e1757f69cada916 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 14 Nov 2022 13:13:46 -0800 Subject: [PATCH 19/23] show close icon on pdf search view --- .../omnivore/omnivore/ui/reader/PDFReader.kt | 64 +++++++++++-------- .../app/src/main/res/drawable-v24/close.xml | 1 + .../res/drawable-v24/pdf_thumbnail_toggle.xml | 1 + .../main/res/layout/pdf_reader_fragment.xml | 20 +++++- 4 files changed, 57 insertions(+), 29 deletions(-) create mode 100644 android/Omnivore/app/src/main/res/drawable-v24/close.xml create mode 100644 android/Omnivore/app/src/main/res/drawable-v24/pdf_thumbnail_toggle.xml diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt index eec27c4fb..348849246 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt @@ -53,7 +53,6 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionMan private lateinit var thumbnailBar: PdfThumbnailBar private lateinit var configuration: PdfConfiguration private lateinit var modularSearchView: PdfSearchViewModular - private lateinit var highlighter: SearchResultHighlighter val viewModel: PDFReaderViewModel by viewModels() @@ -177,32 +176,6 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionMan } private fun initModularSearchViewAndButton() { - // The search result highlighter will highlight any selected result. - highlighter = SearchResultHighlighter(this).also { - fragment.addDrawableProvider(it) - } - - 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 onMoreSearchResults(results: List) { - highlighter.addSearchResults(results) - } - - override fun onSearchCleared() { - highlighter.clearSearchResults() - } - - override fun onSearchResultSelected(result: SearchResult?) { - // Pass on the search result to the highlighter. If 'null' the highlighter will clear any selection. - highlighter.setSelectedSearchResult(result) - if (result != null) { - fragment.scrollTo(PdfUtils.createPdfRectUnion(result.textBlock.pageRects), result.pageIndex, 250, false) - } - } - }) - // The search view is hidden by default (see layout). Set up a click listener that will show the view once pressed. val openSearchButton = findViewById(R.id.openSearchButton) ?: throw IllegalStateException( @@ -210,6 +183,25 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionMan "was missing the open search button with id `R.id.openSearchButton`." ) + val closeSearchButton = findViewById(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( @@ -217,8 +209,24 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionMan ContextCompat.getColor(this@PDFReaderActivity, R.color.black) ) ) + setOnClickListener { - if (modularSearchView.isShown) modularSearchView.hide() else modularSearchView.show() + 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() } } } diff --git a/android/Omnivore/app/src/main/res/drawable-v24/close.xml b/android/Omnivore/app/src/main/res/drawable-v24/close.xml new file mode 100644 index 000000000..a4f627f4b --- /dev/null +++ b/android/Omnivore/app/src/main/res/drawable-v24/close.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/android/Omnivore/app/src/main/res/drawable-v24/pdf_thumbnail_toggle.xml b/android/Omnivore/app/src/main/res/drawable-v24/pdf_thumbnail_toggle.xml new file mode 100644 index 000000000..772d8270e --- /dev/null +++ b/android/Omnivore/app/src/main/res/drawable-v24/pdf_thumbnail_toggle.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/android/Omnivore/app/src/main/res/layout/pdf_reader_fragment.xml b/android/Omnivore/app/src/main/res/layout/pdf_reader_fragment.xml index 03f6af258..401e22ae8 100644 --- a/android/Omnivore/app/src/main/res/layout/pdf_reader_fragment.xml +++ b/android/Omnivore/app/src/main/res/layout/pdf_reader_fragment.xml @@ -22,6 +22,24 @@ android:elevation="8dp" android:visibility="visible"/> + + + + + + android:src="@drawable/pdf_thumbnail_toggle" /> From 0f5336b743fa393c32657c17b9b7170b24e7c7c9 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 14 Nov 2022 13:18:51 -0800 Subject: [PATCH 20/23] copy text to clipboard when user selects copy action --- .../java/app/omnivore/omnivore/ui/reader/PDFReader.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt index 348849246..d48f7a8e1 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt @@ -1,6 +1,10 @@ package app.omnivore.omnivore.ui.reader +import android.R.attr.label import android.annotation.SuppressLint +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 @@ -33,7 +37,6 @@ 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.SearchResultHighlighter import com.pspdfkit.ui.search.SimpleSearchResultListener import com.pspdfkit.ui.special_mode.controller.TextSelectionController import com.pspdfkit.ui.special_mode.manager.TextSelectionManager @@ -333,7 +336,10 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionMan return@OnPopupToolbarItemClickedListener true } 3 -> { - Log.d("pdf", "user selected copy action") + 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 From 31c7b705ed5125d0c864145e6b07aa502d494f3f Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 14 Nov 2022 21:30:19 -0800 Subject: [PATCH 21/23] add an edit annotation dialog to pdf viewer --- .../omnivore/omnivore/ui/reader/PDFReader.kt | 24 +++++++++++++++++-- .../src/main/res/layout/annotation_edit.xml | 23 ++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 android/Omnivore/app/src/main/res/layout/annotation_edit.xml diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt index d48f7a8e1..ca65d8940 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt @@ -1,7 +1,7 @@ package app.omnivore.omnivore.ui.reader -import android.R.attr.label import android.annotation.SuppressLint +import android.app.Dialog import android.content.ClipData import android.content.ClipboardManager import android.content.Context @@ -14,6 +14,7 @@ import android.util.Log import android.view.Gravity import android.view.MotionEvent import android.view.View +import android.widget.Button import android.widget.ImageView import android.widget.PopupMenu import androidx.activity.viewModels @@ -266,8 +267,10 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionMan popupMenu.setOnMenuItemClickListener(PopupMenu.OnMenuItemClickListener { item -> when(item.itemId) { - R.id.annotate -> + R.id.annotate -> { + showAnnotationView() Log.d("pdf", "annotate button tapped") + } R.id.delete -> { viewModel.deleteHighlight(clickedAnnotation) fragment.document?.annotationProvider?.removeAnnotationFromPage(clickedAnnotation) @@ -360,4 +363,21 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionMan PopupToolbarMenuItem(3, R.string.copy_menu_action), ) } + + private fun showAnnotationView() { + val annotationDialog = Dialog(this) + annotationDialog.setContentView(R.layout.annotation_edit) + + val confirmButton = annotationDialog.findViewById(R.id.confirmAnnotation) as Button + confirmButton.setOnClickListener { + annotationDialog.dismiss() + } + + val cancelBtn = annotationDialog.findViewById(R.id.cancel) as Button + cancelBtn.setOnClickListener { + annotationDialog.dismiss() + } + + annotationDialog.show() + } } diff --git a/android/Omnivore/app/src/main/res/layout/annotation_edit.xml b/android/Omnivore/app/src/main/res/layout/annotation_edit.xml new file mode 100644 index 000000000..2dfad5f81 --- /dev/null +++ b/android/Omnivore/app/src/main/res/layout/annotation_edit.xml @@ -0,0 +1,23 @@ + + + + + + +