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 + } + } +} 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..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,9 +2,14 @@ package app.omnivore.omnivore.networking import android.util.Log import app.omnivore.omnivore.graphql.generated.CreateHighlightMutation +import app.omnivore.omnivore.graphql.generated.DeleteHighlightMutation +import app.omnivore.omnivore.graphql.generated.MergeHighlightMutation import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput +import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput +import app.omnivore.omnivore.models.Highlight import com.apollographql.apollo3.api.Optional import com.google.gson.Gson +import com.pspdfkit.annotations.HighlightAnnotation data class CreateHighlightParams( val shortId: String?, @@ -24,13 +29,49 @@ data class CreateHighlightParams( ) } -suspend fun Networker.createHighlight(jsonString: String): Boolean { - val input = Gson().fromJson(jsonString, CreateHighlightParams::class.java).asCreateHighlightInput() +suspend fun Networker.deleteHighlights(highlightIDs: List): 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 +} + +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/networking/ReadingProgressMutations.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/ReadingProgressMutations.kt index f6670efe3..b3f7419ca 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/ReadingProgressMutations.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/ReadingProgressMutations.kt @@ -19,8 +19,13 @@ data class ReadingProgressParams( ) } -suspend fun Networker.updateReadingProgress(jsonString: String): Boolean { - val input = Gson().fromJson(jsonString, ReadingProgressParams::class.java).asSaveReadingProgressInput() +suspend fun Networker.updateWebReadingProgress(jsonString: String): Boolean { + val params = Gson().fromJson(jsonString, ReadingProgressParams::class.java) + return updateReadingProgress(params) +} + +suspend fun Networker.updateReadingProgress(params: ReadingProgressParams): Boolean { + val input = params.asSaveReadingProgressInput() Log.d("Loggo", "created reading progress input: $input") 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) 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..2233ec4dc 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,31 +1,73 @@ package app.omnivore.omnivore.ui.reader +import android.annotation.SuppressLint +import android.app.Dialog +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.graphics.PointF import android.graphics.RectF +import android.graphics.drawable.Drawable import android.net.Uri import android.os.Bundle -import android.util.Log +import android.view.Gravity +import android.view.MotionEvent +import android.view.View +import android.widget.Button +import android.widget.EditText +import android.widget.ImageView +import android.widget.PopupMenu import androidx.activity.viewModels -import androidx.annotation.UiThread import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat import androidx.lifecycle.Observer import app.omnivore.omnivore.R -import com.google.gson.Gson +import app.omnivore.omnivore.models.Highlight +import com.pspdfkit.annotations.Annotation import com.pspdfkit.annotations.HighlightAnnotation import com.pspdfkit.configuration.PdfConfiguration +import com.pspdfkit.configuration.activity.ThumbnailBarMode import com.pspdfkit.configuration.page.PageScrollDirection +import com.pspdfkit.datastructures.TextSelection import com.pspdfkit.document.PdfDocument +import com.pspdfkit.document.search.SearchResult import com.pspdfkit.listeners.DocumentListener +import com.pspdfkit.listeners.OnPreparePopupToolbarListener import com.pspdfkit.ui.PdfFragment +import com.pspdfkit.ui.PdfThumbnailBar +import com.pspdfkit.ui.PopupToolbar +import com.pspdfkit.ui.search.PdfSearchViewModular +import com.pspdfkit.ui.search.SimpleSearchResultListener +import com.pspdfkit.ui.special_mode.controller.TextSelectionController +import com.pspdfkit.ui.special_mode.manager.TextSelectionManager +import com.pspdfkit.ui.toolbar.popup.PdfTextSelectionPopupToolbar +import com.pspdfkit.ui.toolbar.popup.PopupToolbarMenuItem +import com.pspdfkit.utils.PdfUtils import dagger.hilt.android.AndroidEntryPoint +import org.json.JSONObject + @AndroidEntryPoint -class PDFReaderActivity: AppCompatActivity(), DocumentListener { +class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionManager.OnTextSelectionChangeListener, TextSelectionManager.OnTextSelectionModeChangeListener, OnPreparePopupToolbarListener { private var hasLoadedHighlights = false + private var pendingHighlightAnnotation: HighlightAnnotation? = null + private var textSelectionController: TextSelectionController? = null + private lateinit var fragment: PdfFragment + private lateinit var thumbnailBar: PdfThumbnailBar + private lateinit var configuration: PdfConfiguration + private lateinit var modularSearchView: PdfSearchViewModular + val viewModel: PDFReaderViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + configuration = PdfConfiguration.Builder() + .scrollDirection(PageScrollDirection.HORIZONTAL) + .build() + setContentView(R.layout.pdf_reader_fragment) // Create the observer which updates the UI. @@ -42,18 +84,26 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener { viewModel.loadItem(slug, this) } - private fun load(params: PDFReaderParams) { - val configuration = PdfConfiguration.Builder() - .scrollDirection(PageScrollDirection.HORIZONTAL) - .build() + // TODO: implement onDestroy to remove listeners? + private fun load(params: PDFReaderParams) { // First, try to restore a previously created fragment. // If no fragment exists, create a new one. fragment = supportFragmentManager.findFragmentById(R.id.fragmentContainer) as PdfFragment? ?: createFragment(params.localFileUri, configuration) + // Initialize all PSPDFKit UI components. + initModularSearchViewAndButton() + initThumbnailBar() + fragment.apply { + setOnPreparePopupToolbarListener(this@PDFReaderActivity) + addOnTextSelectionModeChangeListener(this@PDFReaderActivity) + addOnTextSelectionChangeListener(this@PDFReaderActivity) addDocumentListener(this@PDFReaderActivity) + addDocumentListener(modularSearchView) + addDocumentListener(thumbnailBar.documentListener) + isImmersive = true } } @@ -61,19 +111,14 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener { if (hasLoadedHighlights) return hasLoadedHighlights = true + thumbnailBar.setDocument(document, configuration) + fragment.addDocumentListener(modularSearchView) + modularSearchView.setDocument(document, configuration) + val params = viewModel.pdfReaderParamsLiveData.value params?.let { - for (highlight in it.articleContent.highlights) { - val highlightAnnotation = fragment - .document - ?.annotationProvider - ?.createAnnotationFromInstantJson(highlight.patch) - - highlightAnnotation?.let { - fragment.addAnnotationToPage(highlightAnnotation, true) - } - } + loadHighlights(it.articleContent.highlights) fragment.scrollTo( RectF(0f, 0f, 0f, 0f), @@ -84,6 +129,19 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener { } } + private fun loadHighlights(highlights: List) { + for (highlight in highlights) { + val highlightAnnotation = fragment + .document + ?.annotationProvider + ?.createAnnotationFromInstantJson(highlight.patch) + + highlightAnnotation?.let { + fragment.addAnnotationToPage(highlightAnnotation, true) + } + } + } + private fun createFragment(documentUri: Uri, configuration: PdfConfiguration): PdfFragment { val fragment = PdfFragment.newInstance(documentUri, configuration) supportFragmentManager.beginTransaction() @@ -91,4 +149,256 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener { .commit() return fragment } + + private fun initThumbnailBar() { + thumbnailBar = findViewById(R.id.thumbnailBar) + ?: throw IllegalStateException("Error while loading CustomFragmentActivity. The example layout was missing thumbnail bar view.") + + thumbnailBar.setOnPageChangedListener { _, pageIndex: Int -> fragment.pageIndex = pageIndex } + thumbnailBar.setThumbnailBarMode(ThumbnailBarMode.THUMBNAIL_BAR_MODE_FLOATING) + + val toggleThumbnailButton = findViewById(R.id.toggleThumbnailButton) + ?: throw IllegalStateException( + "Error while loading CustomFragmentActivity. The example layout " + + "was missing the open search button with id `R.id.openThumbnailGridButton`." + ) + + toggleThumbnailButton.apply { + setImageDrawable( + tintDrawable( + drawable, + ContextCompat.getColor(this@PDFReaderActivity, R.color.black) + ) + ) + setOnClickListener { + if (thumbnailBar.visibility == View.VISIBLE) { + thumbnailBar.visibility = View.INVISIBLE + } else { + thumbnailBar.visibility = View.VISIBLE + } + } + } + } + + private fun initModularSearchViewAndButton() { + // The search view is hidden by default (see layout). Set up a click listener that will show the view once pressed. + val openSearchButton = findViewById(R.id.openSearchButton) + ?: throw IllegalStateException( + "Error while loading CustomFragmentActivity. The example layout " + + "was missing the open search button with id `R.id.openSearchButton`." + ) + + val closeSearchButton = findViewById(R.id.closeSearchButton) + ?: throw IllegalStateException( + "Error while loading CustomFragmentActivity. The example layout " + + "was missing the close search button with id `R.id.closeSearchButton`." + ) + + modularSearchView = findViewById(R.id.modularSearchView) + ?: throw IllegalStateException("Error while loading CustomFragmentActivity. The example layout was missing the search view.") + + modularSearchView.setSearchViewListener(object : SimpleSearchResultListener() { + override fun onSearchResultSelected(result: SearchResult?) { + // Pass on the search result to the highlighter. If 'null' the highlighter will clear any selection. + if (result != null) { + closeSearchButton.visibility = View.INVISIBLE + fragment.scrollTo(PdfUtils.createPdfRectUnion(result.textBlock.pageRects), result.pageIndex, 250, false) + } + } + }) + + openSearchButton.apply { + setImageDrawable( + tintDrawable( + drawable, + ContextCompat.getColor(this@PDFReaderActivity, R.color.black) + ) + ) + + setOnClickListener { + closeSearchButton.visibility = View.VISIBLE + modularSearchView.show() + } + } + + closeSearchButton.apply { + setImageDrawable( + tintDrawable( + drawable, + ContextCompat.getColor(this@PDFReaderActivity, R.color.white) + ) + ) + + setOnClickListener { + closeSearchButton.visibility = View.INVISIBLE + modularSearchView.hide() + } + } + } + + override fun onBackPressed() { + when { + modularSearchView.isDisplayed -> { + modularSearchView.hide() + return + } + else -> super.onBackPressed() + } + } + + override fun onPageClick( + document: PdfDocument, + pageIndex: Int, + event: MotionEvent?, + pagePosition: PointF?, + clickedAnnotation: Annotation? + ): Boolean { + if (clickedAnnotation != null) { + showHighlightSelectionPopover(clickedAnnotation) + } + + return super.onPageClick(document, pageIndex, event, pagePosition, clickedAnnotation) + } + + override fun onPageChanged(document: PdfDocument, pageIndex: Int) { + viewModel.syncPageChange(pageIndex, document.pageCount) + super.onPageChanged(document, pageIndex) + } + + private fun showHighlightSelectionPopover(clickedAnnotation: Annotation) { + // TODO: anchor popover at exact position of tap (maybe add an empty view at tap loc and anchor to that?) + val popupMenu = PopupMenu(this, fragment.view, Gravity.CENTER, androidx.appcompat.R.attr.actionOverflowMenuStyle, 0) + + popupMenu.menuInflater.inflate(R.menu.highlight_selection_menu, popupMenu.menu) + + popupMenu.setOnMenuItemClickListener { item -> + when(item.itemId) { + R.id.annotate -> { + viewModel.annotationUnderNoteEdit = clickedAnnotation + // Disabled notes for now since we didn't implement on ios +// showAnnotationView() + } + R.id.delete -> { + viewModel.deleteHighlight(clickedAnnotation) + fragment.document?.annotationProvider?.removeAnnotationFromPage(clickedAnnotation) + } + R.id.copyPdfHighlight -> { + val omnivoreHighlight = clickedAnnotation.customData?.get("omnivoreHighlight") as? JSONObject + val quote = omnivoreHighlight?.get("quote") as? String + quote?.let { + val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText(it, it) + clipboard.setPrimaryClip(clip) + } + } + } + true + } + popupMenu.show() + } + + private fun tintDrawable(drawable: Drawable, tint: Int): Drawable { + val tintedDrawable = DrawableCompat.wrap(drawable) + DrawableCompat.setTint(tintedDrawable, tint) + return tintedDrawable + } + + override fun onBeforeTextSelectionChange(p0: TextSelection?, p1: TextSelection?): Boolean { + return true + } + + override fun onAfterTextSelectionChange(p0: TextSelection?, p1: TextSelection?) { + val textRects = p0?.textBlocks ?: return + val pageIndex = p0.pageIndex + pendingHighlightAnnotation = HighlightAnnotation(pageIndex, textRects) + } + + override fun onEnterTextSelectionMode(p0: TextSelectionController) { + val textRects = p0?.textSelection?.textBlocks ?: return + val pageIndex = p0.textSelection?.pageIndex ?: return + pendingHighlightAnnotation = HighlightAnnotation(pageIndex, textRects) + textSelectionController = p0 + } + + override fun onExitTextSelectionMode(p0: TextSelectionController) { + textSelectionController = null + pendingHighlightAnnotation = null + } + + @SuppressLint("ResourceType") + override fun onPrepareTextSelectionPopupToolbar(p0: PdfTextSelectionPopupToolbar) { + val onClickListener = PopupToolbar.OnPopupToolbarItemClickedListener { it -> + when (it.id) { + 1 -> { + pendingHighlightAnnotation?.let { annotation -> + val quote = textSelectionController?.textSelection?.text ?: "" + val existingAnnotations = fragment.document?.annotationProvider?.getAnnotations(fragment.pageIndex) ?: listOf() + val overlappingAnnotations = viewModel.overlappingAnnotations(annotation, existingAnnotations) + val overlapIDs = overlappingAnnotations.mapNotNull { viewModel.pluckHighlightID(it) } + + for (overlappingAnnotation in overlappingAnnotations) { + fragment.document?.annotationProvider?.removeAnnotationFromPage(overlappingAnnotation) + } + + fragment.addAnnotationToPage(annotation, false) { + viewModel.syncHighlightUpdates(annotation, quote, overlapIDs) + } + } + + textSelectionController?.textSelection = null + p0.dismiss() + return@OnPopupToolbarItemClickedListener true + } +// 2 -> { +// Log.d("pdf", "user selected annotate action") +// textSelectionController?.textSelection = null +// p0.dismiss() +// return@OnPopupToolbarItemClickedListener true +// } + 3 -> { + val text = textSelectionController?.textSelection?.text ?: "" + val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText(text, text) + clipboard.setPrimaryClip(clip) + textSelectionController?.textSelection = null + p0.dismiss() + return@OnPopupToolbarItemClickedListener true + } + else -> { + p0.dismiss() + textSelectionController?.textSelection = null + return@OnPopupToolbarItemClickedListener false + } + } + } + + p0.setOnPopupToolbarItemClickedListener(onClickListener) + + p0.menuItems = listOf( + PopupToolbarMenuItem(1, R.string.pdf_highlight_menu_action), +// PopupToolbarMenuItem(2, R.string.annotate_menu_action), + PopupToolbarMenuItem(3, R.string.pdf_highlight_copy), + ) + } + + private fun showAnnotationView(initialText: String) { + val annotationDialog = Dialog(this) + annotationDialog.setContentView(R.layout.annotation_edit) + + val textField = annotationDialog.findViewById(R.id.highlightNoteTextField) as EditText + textField.setText(initialText) + val confirmButton = annotationDialog.findViewById(R.id.confirmAnnotation) as Button + + confirmButton.setOnClickListener { + val newNoteText = + annotationDialog.dismiss() + } + + val cancelBtn = annotationDialog.findViewById(R.id.cancel) as Button + cancelBtn.setOnClickListener { + annotationDialog.dismiss() + } + + annotationDialog.show() + } } 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..20affa1f0 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,17 +7,24 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.omnivore.omnivore.DatastoreRepository +import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput +import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput import app.omnivore.omnivore.models.LinkedItem -import app.omnivore.omnivore.networking.Networker -import app.omnivore.omnivore.networking.linkedItem +import app.omnivore.omnivore.networking.* +import com.apollographql.apollo3.api.Optional import com.google.gson.Gson import com.pspdfkit.annotations.Annotation +import com.pspdfkit.annotations.HighlightAnnotation import com.pspdfkit.document.download.DownloadJob import com.pspdfkit.document.download.DownloadRequest import com.pspdfkit.document.download.Progress import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import org.json.JSONObject import java.io.File +import java.lang.Double.max +import java.lang.Double.min +import java.util.* import javax.inject.Inject data class PDFReaderParams( @@ -31,8 +38,9 @@ class PDFReaderViewModel @Inject constructor( private val datastoreRepo: DatastoreRepository, private val networker: Networker ): ViewModel() { + var annotationUnderNoteEdit: Annotation? = null val pdfReaderParamsLiveData = MutableLiveData(null) - var annotations: List = listOf() + private var currentReadingProgress = 0.0 fun loadItem(slug: String, context: Context) { viewModelScope.launch { @@ -61,6 +69,7 @@ class PDFReaderViewModel @Inject constructor( labelsJSONString = Gson().toJson(articleQueryResult.labels) ) + currentReadingProgress = article.readingProgress pdfReaderParamsLiveData.postValue(PDFReaderParams(article, articleContent, Uri.fromFile(output))) } @@ -74,4 +83,100 @@ class PDFReaderViewModel @Inject constructor( fun reset() { pdfReaderParamsLiveData.postValue(null) } + + fun syncPageChange(currentPageIndex: Int, totalPages: Int) { + val rawProgress = ((currentPageIndex + 1).toDouble() / totalPages.toDouble()) * 100 + val percent = min(100.0, max(0.0, rawProgress)) + if (percent > currentReadingProgress) { + currentReadingProgress = percent + viewModelScope.launch { + val params = ReadingProgressParams( + id = pdfReaderParamsLiveData.value?.item?.id, + readingProgressPercent = percent, + readingProgressAnchorIndex = currentPageIndex + ) + networker.updateReadingProgress(params) + } + } + } + + fun syncHighlightUpdates(newAnnotation: Annotation, quote: String, overlapIds: List) { + val itemID = pdfReaderParamsLiveData.value?.item?.id ?: return + val highlightID = UUID.randomUUID().toString() + val shortID = UUID.randomUUID().toString().replace("-","").substring(0,8) + + val jsonValues = JSONObject() + .put("id", highlightID) + .put("shortId", shortID) + .put("quote", quote) + .put("articleId", itemID) + + newAnnotation.customData = JSONObject().put("omnivoreHighlight", jsonValues) + + if (overlapIds.isNotEmpty()) { + val input = MergeHighlightInput( + annotation = Optional.presentIfNotNull(newAnnotation.contents), + articleId = itemID, + id = highlightID, + overlapHighlightIdList = overlapIds, + patch = newAnnotation.toInstantJson(), + quote = quote, + shortId = shortID + ) + + viewModelScope.launch { + networker.mergeHighlights(input) + } + } else { + val createHighlightInput = CreateHighlightInput( + annotation = Optional.presentIfNotNull(null), + articleId = itemID, + id = highlightID, + patch = newAnnotation.toInstantJson(), + quote = quote, + shortId = shortID, + ) + + viewModelScope.launch { + networker.createHighlight(createHighlightInput) + } + } + } + + fun deleteHighlight(annotation: Annotation) { + val highlightID = pluckHighlightID(annotation) ?: return + viewModelScope.launch { + networker.deleteHighlights(listOf(highlightID)) + Log.d("network", "deleted $annotation") + } + } + + fun overlappingAnnotations(newAnnotation: Annotation, existingAnnotations: List): List { + val result: MutableList = mutableListOf() + + for (existingAnnotation in existingAnnotations) { + if (hasOverlaps(newAnnotation, existingAnnotation)) { + result.add(existingAnnotation) + } + } + + return result + } + + fun pluckHighlightID(annotation: Annotation): String? { + val omnivoreHighlight = annotation.customData?.get("omnivoreHighlight") as? JSONObject + return omnivoreHighlight?.get("id") as? String + } + + private fun hasOverlaps(leftAnnotation: Annotation, rightAnnotation: Annotation): Boolean { + for (leftRect in (leftAnnotation as? HighlightAnnotation)?.rects ?: listOf()) { + for (rightRect in (rightAnnotation as? HighlightAnnotation)?.rects ?: listOf()) { + if (rightRect.intersect(leftRect)) { + return true + } + } + } + + return false + } } 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..7bec12af3 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") } } @@ -75,7 +75,7 @@ class WebReaderViewModel @Inject constructor( } "articleReadingProgress" -> { viewModelScope.launch { - val isReadingProgressSynced = networker.updateReadingProgress(jsonString) + val isReadingProgressSynced = networker.updateWebReadingProgress(jsonString) Log.d("Network", "isReadingProgressSynced = $isReadingProgressSynced") } } 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/annotation_edit.xml b/android/Omnivore/app/src/main/res/layout/annotation_edit.xml new file mode 100644 index 000000000..3b7a0de18 --- /dev/null +++ b/android/Omnivore/app/src/main/res/layout/annotation_edit.xml @@ -0,0 +1,24 @@ + + + + + + +