diff --git a/android/Omnivore/app/build.gradle b/android/Omnivore/app/build.gradle index a7a6c744b..66cccad8f 100644 --- a/android/Omnivore/app/build.gradle +++ b/android/Omnivore/app/build.gradle @@ -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 { diff --git a/android/Omnivore/app/src/main/AndroidManifest.xml b/android/Omnivore/app/src/main/AndroidManifest.xml index e0b3e5563..0bbedccff 100644 --- a/android/Omnivore/app/src/main/AndroidManifest.xml +++ b/android/Omnivore/app/src/main/AndroidManifest.xml @@ -15,6 +15,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Omnivore" + android:largeHeap="true" tools:targetApi="31"> + + + + diff --git a/android/Omnivore/app/src/main/assets/test.pdf b/android/Omnivore/app/src/main/assets/test.pdf new file mode 100644 index 000000000..2887e5b76 Binary files /dev/null and b/android/Omnivore/app/src/main/assets/test.pdf differ diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt index 2a07a427b..ff2d4f155 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/models/Highlight.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/models/Highlight.kt index f7c4fe365..0a7c90423 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/models/Highlight.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/models/Highlight.kt @@ -12,3 +12,4 @@ data class Highlight( val updatedAt: Any?, val createdByMe : Boolean, ) + diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/models/LinkedItem.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/models/LinkedItem.kt index 0d5a75efd..960348b8b 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/models/LinkedItem.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/models/LinkedItem.kt @@ -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 + } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/home/HomeView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/home/HomeView.kt index ed17d858a..54e6ab667 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/home/HomeView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/home/HomeView.kt @@ -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 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}") + } } ) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/home/HomeViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/home/HomeViewModel.kt index e07e2c09d..6fc76e977 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/home/HomeViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/home/HomeViewModel.kt @@ -24,7 +24,7 @@ class HomeViewModel @Inject constructor( private var receivedIdx = 0 // Live Data - val searchTextLiveData = MutableLiveData("") + val searchTextLiveData = MutableLiveData("") val itemsLiveData = MutableLiveData>(listOf()) fun updateSearchText(text: String) { 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 new file mode 100644 index 000000000..d8a4c3612 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt @@ -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 { 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 + } +} 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 new file mode 100644 index 000000000..c673cfdd2 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt @@ -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(null) + var annotations: List = 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) + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt index c141a78ef..20e3e0439 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReader.kt @@ -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 diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderContent.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderContent.kt index a3342fb35..599c55078 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderContent.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderContent.kt @@ -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, 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 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 cb0908ed5..e9a6f4aaf 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 @@ -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) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt index 60619015c..c669be8b6 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt @@ -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 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 new file mode 100644 index 000000000..c1c022b28 --- /dev/null +++ b/android/Omnivore/app/src/main/res/layout/pdf_reader_fragment.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + diff --git a/android/Omnivore/settings.gradle b/android/Omnivore/settings.gradle index 78303f486..8a45271fe 100644 --- a/android/Omnivore/settings.gradle +++ b/android/Omnivore/settings.gradle @@ -10,6 +10,9 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven { + url = uri("https://customers.pspdfkit.com/maven") + } } } rootProject.name = "Omnivore"