Merge pull request #1266 from omnivore-app/feature/pspdfkit-android-integration
PSPDFKit Android
This commit is contained in:
@ -156,6 +156,7 @@ dependencies {
|
||||
implementation 'io.coil-kt:coil-compose:2.2.0'
|
||||
|
||||
implementation 'com.google.code.gson:gson:2.8.6'
|
||||
implementation 'com.pspdfkit:pspdfkit:8.4.1'
|
||||
}
|
||||
|
||||
apollo {
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Omnivore"
|
||||
android:largeHeap="true"
|
||||
tools:targetApi="31">
|
||||
|
||||
<activity
|
||||
@ -38,5 +39,15 @@
|
||||
<data android:mimeType="text/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.pspdfkit.ui.PdfActivity"
|
||||
android:theme="@style/Theme.AppCompat.NoActionBar"
|
||||
android:windowSoftInputMode="adjustNothing" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.reader.PDFReaderActivity"
|
||||
android:theme="@style/Theme.AppCompat.NoActionBar"
|
||||
android:windowSoftInputMode="adjustNothing" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
BIN
android/Omnivore/app/src/main/assets/test.pdf
Normal file
BIN
android/Omnivore/app/src/main/assets/test.pdf
Normal file
Binary file not shown.
@ -16,6 +16,7 @@ import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import app.omnivore.omnivore.ui.auth.LoginViewModel
|
||||
import app.omnivore.omnivore.ui.home.HomeViewModel
|
||||
import app.omnivore.omnivore.ui.reader.PDFReaderViewModel
|
||||
import app.omnivore.omnivore.ui.reader.WebReaderViewModel
|
||||
import app.omnivore.omnivore.ui.root.RootView
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@ -12,3 +12,4 @@ data class Highlight(
|
||||
val updatedAt: Any?,
|
||||
val createdByMe : Boolean,
|
||||
)
|
||||
|
||||
|
||||
@ -26,4 +26,9 @@ data class LinkedItem(
|
||||
fun publisherDisplayName(): String? {
|
||||
return publisherURLString?.toUri()?.host
|
||||
}
|
||||
|
||||
fun isPDF(): Boolean {
|
||||
val hasPDFSuffix = pageURLString.endsWith("pdf")
|
||||
return contentReader == "PDF" || hasPDFSuffix
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package app.omnivore.omnivore.ui.home
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
@ -11,10 +12,12 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import app.omnivore.omnivore.Routes
|
||||
import app.omnivore.omnivore.models.LinkedItem
|
||||
import app.omnivore.omnivore.ui.reader.PDFReaderActivity
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
|
||||
@ -53,6 +56,7 @@ fun HomeViewContent(
|
||||
navController: NavHostController,
|
||||
modifier: Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val listState = rememberLazyListState()
|
||||
val linkedItems: List<LinkedItem> by homeViewModel.itemsLiveData.observeAsState(listOf())
|
||||
|
||||
@ -69,7 +73,13 @@ fun HomeViewContent(
|
||||
LinkedItemCard(
|
||||
item = item,
|
||||
onClickHandler = {
|
||||
navController.navigate("WebReader/${item.slug}")
|
||||
if (item.isPDF()) {
|
||||
val intent = Intent(context, PDFReaderActivity::class.java)
|
||||
intent.putExtra("LINKED_ITEM_SLUG", item.slug)
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
navController.navigate("WebReader/${item.slug}")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ class HomeViewModel @Inject constructor(
|
||||
private var receivedIdx = 0
|
||||
|
||||
// Live Data
|
||||
val searchTextLiveData = MutableLiveData<String>("")
|
||||
val searchTextLiveData = MutableLiveData("")
|
||||
val itemsLiveData = MutableLiveData<List<LinkedItem>>(listOf())
|
||||
|
||||
fun updateSearchText(text: String) {
|
||||
|
||||
@ -0,0 +1,94 @@
|
||||
package app.omnivore.omnivore.ui.reader
|
||||
|
||||
import android.graphics.RectF
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.Observer
|
||||
import app.omnivore.omnivore.R
|
||||
import com.google.gson.Gson
|
||||
import com.pspdfkit.annotations.HighlightAnnotation
|
||||
import com.pspdfkit.configuration.PdfConfiguration
|
||||
import com.pspdfkit.configuration.page.PageScrollDirection
|
||||
import com.pspdfkit.document.PdfDocument
|
||||
import com.pspdfkit.listeners.DocumentListener
|
||||
import com.pspdfkit.ui.PdfFragment
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PDFReaderActivity: AppCompatActivity(), DocumentListener {
|
||||
private var hasLoadedHighlights = false
|
||||
private lateinit var fragment: PdfFragment
|
||||
val viewModel: PDFReaderViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.pdf_reader_fragment)
|
||||
|
||||
// Create the observer which updates the UI.
|
||||
val pdfParamsObserver = Observer<PDFReaderParams?> { params ->
|
||||
if (params != null) {
|
||||
load(params)
|
||||
}
|
||||
}
|
||||
|
||||
// Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
|
||||
viewModel.pdfReaderParamsLiveData.observe(this, pdfParamsObserver)
|
||||
|
||||
val slug = intent.getStringExtra("LINKED_ITEM_SLUG") ?: ""
|
||||
viewModel.loadItem(slug, this)
|
||||
}
|
||||
|
||||
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?
|
||||
?: createFragment(params.localFileUri, configuration)
|
||||
|
||||
fragment.apply {
|
||||
addDocumentListener(this@PDFReaderActivity)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDocumentLoaded(document: PdfDocument) {
|
||||
if (hasLoadedHighlights) return
|
||||
hasLoadedHighlights = true
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fragment.scrollTo(
|
||||
RectF(0f, 0f, 0f, 0f),
|
||||
params.item.readingProgressAnchor,
|
||||
0,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createFragment(documentUri: Uri, configuration: PdfConfiguration): PdfFragment {
|
||||
val fragment = PdfFragment.newInstance(documentUri, configuration)
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragmentContainer, fragment)
|
||||
.commit()
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
package app.omnivore.omnivore.ui.reader
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.omnivore.omnivore.DatastoreRepository
|
||||
import app.omnivore.omnivore.models.LinkedItem
|
||||
import app.omnivore.omnivore.networking.Networker
|
||||
import app.omnivore.omnivore.networking.linkedItem
|
||||
import com.google.gson.Gson
|
||||
import com.pspdfkit.annotations.Annotation
|
||||
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 java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
data class PDFReaderParams(
|
||||
val item: LinkedItem,
|
||||
val articleContent: ArticleContent,
|
||||
val localFileUri: Uri
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class PDFReaderViewModel @Inject constructor(
|
||||
private val datastoreRepo: DatastoreRepository,
|
||||
private val networker: Networker
|
||||
): ViewModel() {
|
||||
val pdfReaderParamsLiveData = MutableLiveData<PDFReaderParams?>(null)
|
||||
var annotations: List<Annotation> = listOf()
|
||||
|
||||
fun loadItem(slug: String, context: Context) {
|
||||
viewModelScope.launch {
|
||||
val articleQueryResult = networker.linkedItem(slug)
|
||||
|
||||
val article = articleQueryResult.item ?: return@launch
|
||||
|
||||
val request = DownloadRequest.Builder(context)
|
||||
.uri(article.pageURLString)
|
||||
.build()
|
||||
|
||||
val job = DownloadJob.startDownload(request)
|
||||
|
||||
job.setProgressListener(object : DownloadJob.ProgressListenerAdapter() {
|
||||
override fun onProgress(progress: Progress) {
|
||||
// progressBar.setProgress((100f * progress.bytesReceived / progress.totalBytes).toInt())
|
||||
}
|
||||
|
||||
override fun onComplete(output: File) {
|
||||
val articleContent = ArticleContent(
|
||||
title = article.title,
|
||||
htmlContent = article.content ?: "",
|
||||
highlights = articleQueryResult.highlights,
|
||||
contentStatus = "SUCCEEDED",
|
||||
objectID = "",
|
||||
labelsJSONString = Gson().toJson(articleQueryResult.labels)
|
||||
)
|
||||
|
||||
pdfReaderParamsLiveData.postValue(PDFReaderParams(article, articleContent, Uri.fromFile(output)))
|
||||
}
|
||||
|
||||
override fun onError(exception: Throwable) {
|
||||
// handleDownloadError(exception)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
pdfReaderParamsLiveData.postValue(null)
|
||||
}
|
||||
}
|
||||
@ -17,9 +17,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.networking.ReadingProgressParams
|
||||
import com.google.gson.Gson
|
||||
import org.json.JSONObject
|
||||
|
||||
|
||||
@Composable
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
package app.omnivore.omnivore.ui.reader
|
||||
|
||||
import android.util.Log
|
||||
import app.omnivore.omnivore.models.Highlight
|
||||
import app.omnivore.omnivore.models.LinkedItem
|
||||
import com.google.gson.Gson
|
||||
|
||||
enum class WebFont(val displayText: String, val rawValue: String) {
|
||||
INTER("Inter", "Inter"),
|
||||
@ -26,11 +28,15 @@ enum class ArticleContentStatus(val rawValue: String) {
|
||||
data class ArticleContent(
|
||||
val title: String,
|
||||
val htmlContent: String,
|
||||
val highlightsJSONString: String,
|
||||
val highlights: List<Highlight>,
|
||||
val contentStatus: String, // ArticleContentStatus,
|
||||
val objectID: String?, // whatever the Room Equivalent of objectID is
|
||||
val labelsJSONString: String
|
||||
)
|
||||
) {
|
||||
fun highlightsJSONString(): String {
|
||||
return Gson().toJson(highlights)
|
||||
}
|
||||
}
|
||||
|
||||
data class WebReaderContent(
|
||||
val textFontSize: Int,
|
||||
@ -86,7 +92,7 @@ data class WebReaderContent(
|
||||
readingProgressPercent: ${item.readingProgress},
|
||||
readingProgressAnchorIndex: ${item.readingProgressAnchor},
|
||||
labels: ${articleContent.labelsJSONString},
|
||||
highlights: ${articleContent.highlightsJSONString},
|
||||
highlights: ${articleContent.highlightsJSONString()},
|
||||
}
|
||||
|
||||
window.fontSize = $textFontSize
|
||||
|
||||
@ -39,7 +39,7 @@ class WebReaderViewModel @Inject constructor(
|
||||
val articleContent = ArticleContent(
|
||||
title = article.title,
|
||||
htmlContent = article.content ?: "",
|
||||
highlightsJSONString = Gson().toJson(articleQueryResult.highlights),
|
||||
highlights = articleQueryResult.highlights,
|
||||
contentStatus = "SUCCEEDED",
|
||||
objectID = "",
|
||||
labelsJSONString = Gson().toJson(articleQueryResult.labels)
|
||||
|
||||
@ -18,10 +18,7 @@ import app.omnivore.omnivore.ui.auth.LoginViewModel
|
||||
import app.omnivore.omnivore.ui.auth.WelcomeScreen
|
||||
import app.omnivore.omnivore.ui.home.HomeView
|
||||
import app.omnivore.omnivore.ui.home.HomeViewModel
|
||||
import app.omnivore.omnivore.ui.reader.ArticleWebView
|
||||
import app.omnivore.omnivore.ui.reader.WebReader
|
||||
import app.omnivore.omnivore.ui.reader.WebReaderLoadingContainer
|
||||
import app.omnivore.omnivore.ui.reader.WebReaderViewModel
|
||||
import app.omnivore.omnivore.ui.reader.*
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
|
||||
@Composable
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fragmentContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<com.pspdfkit.ui.PdfThumbnailBar
|
||||
android:id="@+id/thumbnailBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"/>
|
||||
|
||||
<com.pspdfkit.ui.search.PdfSearchViewModular
|
||||
android:id="@+id/modularSearchView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:elevation="8dp"
|
||||
android:visibility="visible"/>
|
||||
|
||||
<com.pspdfkit.ui.PdfThumbnailGrid
|
||||
android:id="@+id/thumbnailGrid"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="invisible"/>
|
||||
|
||||
<com.pspdfkit.ui.PdfOutlineView
|
||||
android:id="@+id/outlineView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="invisible"/>
|
||||
</FrameLayout>
|
||||
@ -10,6 +10,9 @@ dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = uri("https://customers.pspdfkit.com/maven")
|
||||
}
|
||||
}
|
||||
}
|
||||
rootProject.name = "Omnivore"
|
||||
|
||||
Reference in New Issue
Block a user