Merge pull request #1750 from omnivore-app/feature/android-read-now-button

Read Now - Android Share Activity
This commit is contained in:
Satindar Dhillon
2023-01-31 13:23:11 -08:00
committed by GitHub
7 changed files with 318 additions and 139 deletions

View File

@ -51,5 +51,11 @@
android:name=".ui.reader.PDFReaderActivity"
android:theme="@style/Theme.AppCompat.NoActionBar"
android:windowSoftInputMode="adjustNothing" />
<activity
android:name=".ui.reader.WebReaderLoadingContainerActivity"
android:exported="true"
android:theme="@style/Theme.Omnivore"/>
</application>
</manifest>

View File

@ -8,11 +8,12 @@ import app.omnivore.omnivore.persistence.entities.Highlight
data class SavedItemQueryResponse(
val item: SavedItem?,
val highlights: List<Highlight>,
val labels: List<SavedItemLabel>
val labels: List<SavedItemLabel>,
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 = "")
}
}

View File

@ -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(

View File

@ -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<View>(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)
}
}
}

View File

@ -41,20 +41,41 @@ class WebReaderViewModel @Inject constructor(
val annotationLiveData = MutableLiveData<String?>(null)
val javascriptActionLoopUUIDLiveData = MutableLiveData(lastJavascriptActionLoopUUID)
val shouldPopViewLiveData = MutableLiveData<Boolean>(false)
val hasFetchError = MutableLiveData<Boolean>(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)
)

View File

@ -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")
}
}
}
}

View File

@ -29,6 +29,9 @@ class SaveViewModel @Inject constructor(
var message by mutableStateOf<String?>(null)
private set
var clientRequestID by mutableStateOf<String?>(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
)