diff --git a/android/Omnivore/app/src/main/AndroidManifest.xml b/android/Omnivore/app/src/main/AndroidManifest.xml
index a78d2d2f0..3983989ed 100644
--- a/android/Omnivore/app/src/main/AndroidManifest.xml
+++ b/android/Omnivore/app/src/main/AndroidManifest.xml
@@ -51,5 +51,11 @@
android:name=".ui.reader.PDFReaderActivity"
android:theme="@style/Theme.AppCompat.NoActionBar"
android:windowSoftInputMode="adjustNothing" />
+
+
+
diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt
index 9cc667461..bc5ec8840 100644
--- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt
+++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt
@@ -8,11 +8,12 @@ import app.omnivore.omnivore.persistence.entities.Highlight
data class SavedItemQueryResponse(
val item: SavedItem?,
val highlights: List,
- val labels: List
+ val labels: List,
+ val state: String
) {
companion object {
fun emptyResponse(): SavedItemQueryResponse {
- return SavedItemQueryResponse(null, listOf(), listOf())
+ return SavedItemQueryResponse(null, listOf(), listOf(), state = "")
}
}
}
@@ -79,8 +80,8 @@ suspend fun Networker.savedItem(slug: String): SavedItemQueryResponse {
content = article.articleFields.content
)
- return SavedItemQueryResponse(item = savedItem, highlights, labels = savedItemLabels)
+ return SavedItemQueryResponse(item = savedItem, highlights, labels = savedItemLabels, state = article.articleFields.state?.rawValue ?: "")
} catch (e: java.lang.Exception) {
- return SavedItemQueryResponse(item = null, listOf(), labels = listOf())
+ return SavedItemQueryResponse(item = null, listOf(), labels = listOf(), state = "")
}
}
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 ededf42ad..2d38bcef9 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
@@ -41,123 +41,6 @@ import kotlinx.coroutines.launch
import java.util.*
import kotlin.math.roundToInt
-
-@Composable
-fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewModel) {
- val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
-
- var isMenuExpanded by remember { mutableStateOf(false) }
- var showWebPreferencesDialog by remember { mutableStateOf(false ) }
-
- val webReaderParams: WebReaderParams? by webReaderViewModel.webReaderParamsLiveData.observeAsState(null)
- val annotation: String? by webReaderViewModel.annotationLiveData.observeAsState(null)
- val shouldPopView: Boolean by webReaderViewModel.shouldPopViewLiveData.observeAsState(false)
-
- val maxToolbarHeight = 48.dp
- val maxToolbarHeightPx = with(LocalDensity.current) { maxToolbarHeight.roundToPx().toFloat() }
- val toolbarHeightPx = remember { mutableStateOf(maxToolbarHeightPx) }
-
- // Create a connection to the nested scroll system and listen to the scroll happening inside child Column
- val nestedScrollConnection = remember {
- object : NestedScrollConnection {
- override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
- val delta = available.y
- val newHeight = toolbarHeightPx.value + delta
- toolbarHeightPx.value = newHeight.coerceIn(0f, maxToolbarHeightPx)
- return Offset.Zero
- }
- }
- }
-
- if (webReaderParams == null) {
- webReaderViewModel.loadItem(slug = slug)
- }
-
- if (webReaderParams != null) {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .nestedScroll(nestedScrollConnection)
- ) {
- Column(
- modifier = Modifier
- .fillMaxSize()
- .verticalScroll(webReaderViewModel.scrollState)
-
- ) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .requiredHeight(height = maxToolbarHeight)
- ) {
- }
- WebReader(webReaderParams!!, webReaderViewModel.storedWebPreferences(isSystemInDarkTheme()), webReaderViewModel)
- }
-
- TopAppBar(
- modifier = Modifier
- .height(height = with(LocalDensity.current) {
- webReaderViewModel.currentToolbarHeight = toolbarHeightPx.value.toInt()
- toolbarHeightPx.value.roundToInt().toDp()
- } ),
- backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
- title = {},
- actions = {
- // Disabling menu until we implement local persistence
- IconButton(onClick = { isMenuExpanded = true }) {
- Icon(
- imageVector = Icons.Filled.Menu,
- contentDescription = null
- )
- }
- IconButton(onClick = { showWebPreferencesDialog = true }) {
- Icon(
- imageVector = Icons.Filled.Settings, // TODO: set a better icon
- contentDescription = null
- )
- }
- SavedItemContextMenu(
- isExpanded = isMenuExpanded,
- isArchived = webReaderParams!!.item.isArchived,
- onDismiss = { isMenuExpanded = false },
- actionHandler = { webReaderViewModel.handleSavedItemAction(webReaderParams!!.item.savedItemId, it) }
- )
- }
- )
-
- if (showWebPreferencesDialog) {
- WebPreferencesDialog(
- onDismiss = {
- showWebPreferencesDialog = false
- },
- webReaderViewModel = webReaderViewModel
- )
- }
-
- if (annotation != null) {
- AnnotationEditView(
- initialAnnotation = annotation!!,
- onSave = {
- webReaderViewModel.saveAnnotation(it)
- },
- onCancel = {
- webReaderViewModel.cancelAnnotationEdit()
- }
- )
- }
- }
-
- LaunchedEffect(shouldPopView) {
- if (shouldPopView) {
- onBackPressedDispatcher?.onBackPressed()
- }
- }
- } else {
- // TODO: add a proper loading view
- Text("Loading...")
- }
-}
-
@SuppressLint("SetJavaScriptEnabled")
@Composable
fun WebReader(
diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderLoadingContainer.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderLoadingContainer.kt
new file mode 100644
index 000000000..0bee40bc6
--- /dev/null
+++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderLoadingContainer.kt
@@ -0,0 +1,232 @@
+package app.omnivore.omnivore.ui.reader
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
+import androidx.activity.compose.setContent
+import androidx.activity.viewModels
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.List
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.dp
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import app.omnivore.omnivore.MainActivity
+import app.omnivore.omnivore.ui.savedItemViews.SavedItemContextMenu
+import app.omnivore.omnivore.ui.theme.OmnivoreTheme
+import com.google.accompanist.systemuicontroller.rememberSystemUiController
+import dagger.hilt.android.AndroidEntryPoint
+import kotlin.math.roundToInt
+
+
+@AndroidEntryPoint
+class WebReaderLoadingContainerActivity: ComponentActivity() {
+ val viewModel: WebReaderViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val requestID = intent.getStringExtra("SAVED_ITEM_REQUEST_ID") ?: ""
+
+ setContent {
+ val systemUiController = rememberSystemUiController()
+ val useDarkIcons = !isSystemInDarkTheme()
+
+ OmnivoreTheme {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color = Color.Black)
+ .systemBarsPadding()
+ ) {
+ if (viewModel.hasFetchError.value == true) {
+ Text("We were unable to fetch your content.")
+ } else {
+ WebReaderLoadingContainer(
+ requestID = requestID,
+ onLibraryIconTap = { startMainActivity() },
+ webReaderViewModel = viewModel
+ )
+ }
+ }
+ }
+
+ DisposableEffect(systemUiController, useDarkIcons) {
+ systemUiController.setSystemBarsColor(
+ color = Color.Black,
+ darkIcons = false
+ )
+
+ onDispose {}
+ }
+ }
+
+ // animate the view up when keyboard appears
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ val rootView = findViewById(android.R.id.content).rootView
+ ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets ->
+ val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
+ rootView.setPadding(0, 0, 0, imeHeight)
+ insets
+ }
+ }
+
+ private fun startMainActivity() {
+ val intent = Intent(this, MainActivity::class.java)
+ this.startActivity(intent)
+ }
+}
+
+@Composable
+fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, onLibraryIconTap: (() -> Unit)? = null, webReaderViewModel: WebReaderViewModel) {
+ val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
+
+ var isMenuExpanded by remember { mutableStateOf(false) }
+ var showWebPreferencesDialog by remember { mutableStateOf(false ) }
+
+ val webReaderParams: WebReaderParams? by webReaderViewModel.webReaderParamsLiveData.observeAsState(null)
+ val annotation: String? by webReaderViewModel.annotationLiveData.observeAsState(null)
+ val shouldPopView: Boolean by webReaderViewModel.shouldPopViewLiveData.observeAsState(false)
+
+ val maxToolbarHeight = 48.dp
+ val maxToolbarHeightPx = with(LocalDensity.current) { maxToolbarHeight.roundToPx().toFloat() }
+ val toolbarHeightPx = remember { mutableStateOf(maxToolbarHeightPx) }
+
+ // Create a connection to the nested scroll system and listen to the scroll happening inside child Column
+ val nestedScrollConnection = remember {
+ object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ val delta = available.y
+ val newHeight = toolbarHeightPx.value + delta
+ toolbarHeightPx.value = newHeight.coerceIn(0f, maxToolbarHeightPx)
+ return Offset.Zero
+ }
+ }
+ }
+
+ if (webReaderParams == null) {
+ webReaderViewModel.loadItem(slug = slug, requestID = requestID)
+ }
+
+ if (webReaderParams != null) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .nestedScroll(nestedScrollConnection)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(webReaderViewModel.scrollState)
+
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .requiredHeight(height = maxToolbarHeight)
+ ) {
+ }
+ WebReader(webReaderParams!!, webReaderViewModel.storedWebPreferences(isSystemInDarkTheme()), webReaderViewModel)
+ }
+
+ TopAppBar(
+ modifier = Modifier
+ .height(height = with(LocalDensity.current) {
+ webReaderViewModel.currentToolbarHeight = toolbarHeightPx.value.toInt()
+ toolbarHeightPx.value.roundToInt().toDp()
+ } ),
+ backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
+ title = {},
+ actions = {
+ if (onLibraryIconTap != null) {
+ IconButton(onClick = { onLibraryIconTap() }) {
+ Icon(
+ imageVector = Icons.Default.Home,
+ contentDescription = null
+ )
+ }
+ }
+ IconButton(onClick = { isMenuExpanded = true }) {
+ Icon(
+ imageVector = Icons.Filled.Menu,
+ contentDescription = null
+ )
+ }
+ IconButton(onClick = { showWebPreferencesDialog = true }) {
+ Icon(
+ imageVector = Icons.Filled.Settings, // TODO: set a better icon
+ contentDescription = null
+ )
+ }
+ SavedItemContextMenu(
+ isExpanded = isMenuExpanded,
+ isArchived = webReaderParams!!.item.isArchived,
+ onDismiss = { isMenuExpanded = false },
+ actionHandler = { webReaderViewModel.handleSavedItemAction(webReaderParams!!.item.savedItemId, it) }
+ )
+ }
+ )
+
+ if (showWebPreferencesDialog) {
+ WebPreferencesDialog(
+ onDismiss = {
+ showWebPreferencesDialog = false
+ },
+ webReaderViewModel = webReaderViewModel
+ )
+ }
+
+ if (annotation != null) {
+ AnnotationEditView(
+ initialAnnotation = annotation!!,
+ onSave = {
+ webReaderViewModel.saveAnnotation(it)
+ },
+ onCancel = {
+ webReaderViewModel.cancelAnnotationEdit()
+ }
+ )
+ }
+ }
+
+ LaunchedEffect(shouldPopView) {
+ if (shouldPopView) {
+ onBackPressedDispatcher?.onBackPressed()
+ }
+ }
+ } else {
+ Column(
+ verticalArrangement = Arrangement.SpaceAround,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 16.dp)
+ ) {
+ Text("Loading...", color = Color.White)
+ }
+ }
+}
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 affa6166d..4bb3b4fa3 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
@@ -41,20 +41,41 @@ class WebReaderViewModel @Inject constructor(
val annotationLiveData = MutableLiveData(null)
val javascriptActionLoopUUIDLiveData = MutableLiveData(lastJavascriptActionLoopUUID)
val shouldPopViewLiveData = MutableLiveData(false)
+ val hasFetchError = MutableLiveData(false)
var hasTappedExistingHighlight = false
var lastTapCoordinates: TapCoordinates? = null
- fun loadItem(slug: String) {
+ fun loadItem(slug: String?, requestID: String?) {
viewModelScope.launch {
- val webReaderParams = loadItemFromServer(slug)
+ slug?.let { loadItemUsingSlug(it) }
+ requestID?.let { loadItemUsingRequestID(it) }
+ }
+ }
- if (webReaderParams != null) {
- Log.d("sync", "data loaded from server")
- webReaderParamsLiveData.postValue(webReaderParams)
- } else {
- loadItemFromDB(slug)
- }
+ private suspend fun loadItemUsingSlug(slug: String) {
+ val webReaderParams = loadItemFromServer(slug)
+
+ if (webReaderParams != null) {
+ Log.d("sync", "data loaded from server")
+ webReaderParamsLiveData.postValue(webReaderParams)
+ } else {
+ loadItemFromDB(slug)
+ }
+ }
+
+ private suspend fun loadItemUsingRequestID(requestID: String, requestCount: Int = 0) {
+ val webReaderParams = loadItemFromServer(requestID)
+ val isSuccessful = webReaderParams?.articleContent?.contentStatus == "SUCCEEDED"
+
+ if (webReaderParams != null && isSuccessful) {
+ webReaderParamsLiveData.postValue(webReaderParams)
+ } else if (requestCount < 7) {
+ // delay then try again
+ delay(2000L)
+ loadItemUsingRequestID(requestID = requestID, requestCount = requestCount + 1)
+ } else {
+ hasFetchError.postValue(true)
}
}
@@ -87,7 +108,7 @@ class WebReaderViewModel @Inject constructor(
title = article.title,
htmlContent = article.content ?: "",
highlights = articleQueryResult.highlights,
- contentStatus = "SUCCEEDED",
+ contentStatus = articleQueryResult.state,
objectID = "",
labelsJSONString = Gson().toJson(articleQueryResult.labels)
)
diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveContent.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveContent.kt
index ff2146338..e0818befe 100644
--- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveContent.kt
+++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveContent.kt
@@ -1,5 +1,7 @@
package app.omnivore.omnivore.ui.save
+import android.content.Intent
+import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
@@ -11,13 +13,18 @@ import androidx.compose.material.*
import androidx.compose.material.ButtonDefaults
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
+import app.omnivore.omnivore.MainActivity
+import app.omnivore.omnivore.ui.reader.PDFReaderActivity
+import app.omnivore.omnivore.ui.reader.WebReaderLoadingContainerActivity
import kotlinx.coroutines.launch
@Composable
@OptIn(ExperimentalMaterialApi::class)
fun SaveContent(viewModel: SaveViewModel, modalBottomSheetState: ModalBottomSheetState, modifier: Modifier) {
val coroutineScope = rememberCoroutineScope()
+ val context = LocalContext.current
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
Column(
@@ -29,17 +36,41 @@ fun SaveContent(viewModel: SaveViewModel, modalBottomSheetState: ModalBottomShee
.padding(top = 48.dp, bottom = 32.dp)
) {
Text(text = viewModel.message ?: "Saving")
- Button(onClick = {
- coroutineScope.launch {
- modalBottomSheetState.hide()
+ Row {
+ Button(
+ onClick = {
+ coroutineScope.launch {
+ modalBottomSheetState.hide()
+ viewModel.clientRequestID?.let {
+ val intent = Intent(context, WebReaderLoadingContainerActivity::class.java)
+ intent.putExtra("SAVED_ITEM_REQUEST_ID", it)
+ context.startActivity(intent)
+ }
+ }
+ },
+ colors = ButtonDefaults.buttonColors(
+ contentColor = Color(0xFF3D3D3D),
+ backgroundColor = Color.White
+ )
+ ) {
+ Text(text = "Read Now")
}
- },
- colors = ButtonDefaults.buttonColors(
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ Button(
+ onClick = {
+ coroutineScope.launch {
+ modalBottomSheetState.hide()
+ }
+ },
+ colors = ButtonDefaults.buttonColors(
contentColor = Color(0xFF3D3D3D),
backgroundColor = Color(0xffffd234)
- )
- ) {
- Text(text = "Dismiss")
+ )
+ ) {
+ Text(text = "Read Later")
+ }
}
}
}
diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveViewModel.kt
index 0dbdc7222..b02e92f59 100644
--- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveViewModel.kt
+++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveViewModel.kt
@@ -29,6 +29,9 @@ class SaveViewModel @Inject constructor(
var message by mutableStateOf(null)
private set
+ var clientRequestID by mutableStateOf(null)
+ private set
+
private fun getAuthToken(): String? = runBlocking {
datastoreRepo.getString(DatastoreKeys.omnivoreAuthToken)
}
@@ -52,10 +55,12 @@ class SaveViewModel @Inject constructor(
.build()
try {
+ clientRequestID = UUID.randomUUID().toString()
+
val response = apolloClient.mutation(
SaveUrlMutation(
SaveUrlInput(
- clientRequestId = UUID.randomUUID().toString(),
+ clientRequestId = clientRequestID!!,
source = "android",
url = url
)