mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #1245 from omnivore-app/android/web-action-data-mutations
Web Action Mutations - Android
This commit is contained in:
commit
c6dde4f6f8
12 changed files with 342 additions and 48 deletions
|
|
@ -0,0 +1,13 @@
|
|||
mutation CreateHighlight($input: CreateHighlightInput!) {
|
||||
createHighlight(input: $input) {
|
||||
... on CreateHighlightSuccess {
|
||||
highlight {
|
||||
...HighlightFields
|
||||
}
|
||||
}
|
||||
|
||||
... on CreateHighlightError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
mutation DeleteHighlight($highlightId: ID!) {
|
||||
deleteHighlight(highlightId: $highlightId) {
|
||||
... on DeleteHighlightSuccess {
|
||||
highlight {
|
||||
id
|
||||
}
|
||||
}
|
||||
... on DeleteHighlightError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
mutation SaveArticleReadingProgress($input: SaveArticleReadingProgressInput!) {
|
||||
saveArticleReadingProgress(input: $input) {
|
||||
... on SaveArticleReadingProgressSuccess {
|
||||
updatedArticle {
|
||||
id
|
||||
readingProgressPercent
|
||||
readingProgressAnchorIndex
|
||||
}
|
||||
}
|
||||
... on SaveArticleReadingProgressError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
mutation UpdateHighlight($input: UpdateHighlightInput!) {
|
||||
updateHighlight(input: $input) {
|
||||
... on UpdateHighlightSuccess {
|
||||
highlight {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
... on UpdateHighlightError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package app.omnivore.omnivore.networking
|
||||
|
||||
import android.util.Log
|
||||
import app.omnivore.omnivore.graphql.generated.CreateHighlightMutation
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import com.google.gson.Gson
|
||||
|
||||
data class CreateHighlightParams(
|
||||
val shortId: String?,
|
||||
val highlightID: String?,
|
||||
val quote: String?,
|
||||
val patch: String?,
|
||||
val articleId: String?,
|
||||
val `annotation`: String?
|
||||
) {
|
||||
fun asCreateHighlightInput() = CreateHighlightInput(
|
||||
annotation = Optional.presentIfNotNull(`annotation`),
|
||||
articleId = articleId ?: "",
|
||||
id = highlightID ?: "",
|
||||
patch = patch ?: "",
|
||||
quote = quote ?: "",
|
||||
shortId = shortId ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun Networker.createHighlight(jsonString: String): Boolean {
|
||||
val input = Gson().fromJson(jsonString, CreateHighlightParams::class.java).asCreateHighlightInput()
|
||||
|
||||
Log.d("Loggo", "created highlight input: $input")
|
||||
|
||||
val result = authenticatedApolloClient().mutation(CreateHighlightMutation(input)).execute()
|
||||
|
||||
val highlight = result.data?.createHighlight?.onCreateHighlightSuccess?.highlight
|
||||
return highlight != null
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package app.omnivore.omnivore.networking
|
||||
|
||||
import app.omnivore.omnivore.graphql.generated.SaveArticleReadingProgressMutation
|
||||
import app.omnivore.omnivore.graphql.generated.type.SaveArticleReadingProgressInput
|
||||
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
|
||||
data class ReadingProgressParams(
|
||||
val id: String?,
|
||||
val readingProgressPercent: Double?,
|
||||
val readingProgressAnchorIndex: Int?
|
||||
) {
|
||||
fun asSaveReadingProgressInput() = SaveArticleReadingProgressInput(
|
||||
id = id ?: "",
|
||||
readingProgressPercent = readingProgressPercent ?: 0.0,
|
||||
readingProgressAnchorIndex = readingProgressAnchorIndex ?: 0
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun Networker.updateReadingProgress(jsonString: String): Boolean {
|
||||
val input = Gson().fromJson(jsonString, ReadingProgressParams::class.java).asSaveReadingProgressInput()
|
||||
|
||||
Log.d("Loggo", "created reading progress input: $input")
|
||||
|
||||
val result = authenticatedApolloClient()
|
||||
.mutation(SaveArticleReadingProgressMutation(input))
|
||||
.execute()
|
||||
|
||||
val articleID = result.data?.saveArticleReadingProgress?.onSaveArticleReadingProgressSuccess?.updatedArticle?.id
|
||||
|
||||
Log.d("Loggo", "updated article with id: $articleID")
|
||||
|
||||
return articleID != null
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ suspend fun Networker.search(
|
|||
)
|
||||
).execute()
|
||||
|
||||
val cursor = result.data?.search?.onSearchSuccess?.pageInfo?.endCursor
|
||||
val newCursor = result.data?.search?.onSearchSuccess?.pageInfo?.endCursor
|
||||
val itemList = result.data?.search?.onSearchSuccess?.edges ?: listOf()
|
||||
|
||||
val items = itemList.map {
|
||||
|
|
@ -49,5 +49,5 @@ suspend fun Networker.search(
|
|||
)
|
||||
}
|
||||
|
||||
return SearchQueryResponse(cursor, items)
|
||||
return SearchQueryResponse(newCursor, items)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
package app.omnivore.omnivore.ui.reader
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
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.unit.dp
|
||||
|
||||
// TODO: better layout and styling for this view
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AnnotationEditView(
|
||||
initialAnnotation: String,
|
||||
onSave: (String) -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
) {
|
||||
val annotation = remember { mutableStateOf(initialAnnotation) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(8.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
) {
|
||||
Text(text = "Note")
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
TextField(
|
||||
value = annotation.value,
|
||||
onValueChange = { annotation.value = it }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.align(Alignment.End)
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
onCancel()
|
||||
}
|
||||
) {
|
||||
Text("Cancel")
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
onSave(annotation.value)
|
||||
}
|
||||
) {
|
||||
Text("Save")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,12 +8,17 @@ import android.view.*
|
|||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -36,6 +41,11 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod
|
|||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Composable
|
||||
fun WebReader(params: WebReaderParams, webReaderViewModel: WebReaderViewModel) {
|
||||
// TODO: maybe handle cases where js can be queued up?
|
||||
val javascriptToExecute = remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val annotation: String? by webReaderViewModel.annotationLiveData.observeAsState(null)
|
||||
|
||||
WebView.setWebContentsDebuggingEnabled(true)
|
||||
|
||||
val webReaderContent = WebReaderContent(
|
||||
|
|
@ -51,39 +61,81 @@ fun WebReader(params: WebReaderParams, webReaderViewModel: WebReaderViewModel) {
|
|||
|
||||
val styledContent = webReaderContent.styledContent()
|
||||
|
||||
AndroidView(factory = {
|
||||
OmnivoreWebView(it).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
Box {
|
||||
AndroidView(factory = {
|
||||
OmnivoreWebView(it).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
|
||||
settings.javaScriptEnabled = true
|
||||
settings.allowContentAccess = true
|
||||
settings.allowFileAccess = true
|
||||
settings.domStorageEnabled = true
|
||||
|
||||
webViewClient = object : WebViewClient() {
|
||||
}
|
||||
|
||||
val javascriptInterface = AndroidWebKitMessenger { actionID, json ->
|
||||
when (actionID) {
|
||||
"existingHighlightTap" -> {
|
||||
isExistingHighlightSelected = true
|
||||
actionTapCoordinates = Gson().fromJson(json, ActionTapCoordinates::class.java)
|
||||
Log.d("Loggo", "receive existing highlight tap action: $actionTapCoordinates")
|
||||
startActionMode(null, ActionMode.TYPE_PRIMARY)
|
||||
}
|
||||
else -> {
|
||||
webReaderViewModel.handleIncomingWebMessage(actionID, json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addJavascriptInterface(javascriptInterface, "AndroidWebKitMessenger")
|
||||
|
||||
loadDataWithBaseURL(
|
||||
"file:///android_asset/",
|
||||
styledContent,
|
||||
"text/html; charset=utf-8",
|
||||
"utf-8",
|
||||
null
|
||||
)
|
||||
}
|
||||
}, update = {
|
||||
if (javascriptToExecute.value != null) {
|
||||
it.evaluateJavascript(javascriptToExecute.value!!, null)
|
||||
}
|
||||
})
|
||||
|
||||
if (annotation != null) {
|
||||
AnnotationEditView(
|
||||
initialAnnotation = annotation!!,
|
||||
onSave = {
|
||||
val script = "var event = new Event('saveAnnotation');event.annotation = '$it';document.dispatchEvent(event);"
|
||||
javascriptToExecute.value = script
|
||||
webReaderViewModel.cancelAnnotationEdit()
|
||||
},
|
||||
onCancel = {
|
||||
webReaderViewModel.cancelAnnotationEdit()
|
||||
}
|
||||
)
|
||||
|
||||
settings.javaScriptEnabled = true
|
||||
settings.allowContentAccess = true
|
||||
settings.allowFileAccess = true
|
||||
settings.domStorageEnabled = true
|
||||
|
||||
webViewClient = object : WebViewClient() {
|
||||
}
|
||||
|
||||
val javascriptInterface = AndroidWebKitMessenger { actionID, json ->
|
||||
webReaderViewModel.handleIncomingWebMessage(actionID, json)
|
||||
}
|
||||
|
||||
addJavascriptInterface(javascriptInterface, "AndroidWebKitMessenger")
|
||||
loadDataWithBaseURL("file:///android_asset/", styledContent, "text/html; charset=utf-8", "utf-8", null);
|
||||
|
||||
}
|
||||
}, update = {
|
||||
it.loadDataWithBaseURL("file:///android_asset/", styledContent, "text/html; charset=utf-8", "utf-8", null);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class OmnivoreWebView(context: Context) : WebView(context) {
|
||||
var isExistingHighlightSelected = false
|
||||
var actionTapCoordinates: ActionTapCoordinates? = null
|
||||
|
||||
private val actionModeCallback = object : ActionMode.Callback2() {
|
||||
// Called when the action mode is created; startActionMode() was called
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
mode.menuInflater.inflate(R.menu.text_selection_menu, menu)
|
||||
if (isExistingHighlightSelected) {
|
||||
mode.menuInflater.inflate(R.menu.highlight_selection_menu, menu)
|
||||
isExistingHighlightSelected = false
|
||||
} else {
|
||||
mode.menuInflater.inflate(R.menu.text_selection_menu, menu)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +149,8 @@ class OmnivoreWebView(context: Context) : WebView(context) {
|
|||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.annotate -> {
|
||||
Log.d("Loggo", "Annotate action selected")
|
||||
val script = "var event = new Event('annotate');document.dispatchEvent(event);"
|
||||
evaluateJavascript(script, null)
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
|
|
@ -108,6 +161,13 @@ class OmnivoreWebView(context: Context) : WebView(context) {
|
|||
mode.finish()
|
||||
true
|
||||
}
|
||||
R.id.delete -> {
|
||||
val script = "var event = new Event('remove');document.dispatchEvent(event);"
|
||||
evaluateJavascript(script, null)
|
||||
clearFocus()
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
else -> {
|
||||
Log.d("Loggo", "${item.itemId} selected")
|
||||
false
|
||||
|
|
@ -117,35 +177,44 @@ class OmnivoreWebView(context: Context) : WebView(context) {
|
|||
|
||||
// Called when the user exits the action mode
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
// actionMode = null
|
||||
Log.d("Loggo", "destroying menu: $mode")
|
||||
isExistingHighlightSelected = false
|
||||
actionTapCoordinates = null
|
||||
}
|
||||
|
||||
override fun onGetContentRect(mode: ActionMode?, view: View?, outRect: Rect?) {
|
||||
Log.d("Loggo", "outRect: $outRect, View: $view")
|
||||
outRect?.set(left, top, right, bottom)
|
||||
}
|
||||
}
|
||||
|
||||
private var currentActionModeCallback: ActionMode.Callback? = actionModeCallback
|
||||
|
||||
override fun startActionMode(callback: ActionMode.Callback?): ActionMode {
|
||||
return super.startActionMode(currentActionModeCallback)
|
||||
return super.startActionMode(actionModeCallback)
|
||||
}
|
||||
|
||||
override fun startActionModeForChild(
|
||||
originalView: View?,
|
||||
callback: ActionMode.Callback?
|
||||
): ActionMode {
|
||||
return super.startActionModeForChild(originalView, currentActionModeCallback)
|
||||
return super.startActionModeForChild(originalView, actionModeCallback)
|
||||
}
|
||||
|
||||
override fun startActionMode(callback: ActionMode.Callback?, type: Int): ActionMode {
|
||||
return super.startActionMode(currentActionModeCallback, type)
|
||||
Log.d("Loggo", "startActionMode:type called")
|
||||
return super.startActionMode(actionModeCallback, type)
|
||||
}
|
||||
}
|
||||
|
||||
class AndroidWebKitMessenger(val messageHandler: (String, JSONObject) -> Unit) {
|
||||
class AndroidWebKitMessenger(val messageHandler: (String, String) -> Unit) {
|
||||
@JavascriptInterface
|
||||
fun handleIdentifiableMessage(actionID: String, jsonString: String) {
|
||||
messageHandler(actionID, JSONObject(jsonString))
|
||||
messageHandler(actionID, jsonString)
|
||||
}
|
||||
}
|
||||
|
||||
data class ActionTapCoordinates(
|
||||
val rectX: Double,
|
||||
val rectY: Double,
|
||||
val rectWidth: Double,
|
||||
val rectHeight: Double,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ 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 app.omnivore.omnivore.networking.*
|
||||
import com.google.gson.Gson
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -19,12 +18,17 @@ data class WebReaderParams(
|
|||
val articleContent: ArticleContent
|
||||
)
|
||||
|
||||
data class AnnotationWebViewMessage(
|
||||
val annotation: String?
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class WebReaderViewModel @Inject constructor(
|
||||
private val datastoreRepo: DatastoreRepository,
|
||||
private val networker: Networker
|
||||
): ViewModel() {
|
||||
val webReaderParamsLiveData = MutableLiveData<WebReaderParams?>(null)
|
||||
val annotationLiveData = MutableLiveData<String?>(null)
|
||||
|
||||
fun loadItem(slug: String) {
|
||||
viewModelScope.launch {
|
||||
|
|
@ -45,37 +49,50 @@ class WebReaderViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun handleIncomingWebMessage(actionID: String, json: JSONObject) {
|
||||
fun handleIncomingWebMessage(actionID: String, jsonString: String) {
|
||||
when (actionID) {
|
||||
"createHighlight" -> {
|
||||
Log.d("Loggo", "receive create highlight action: $json")
|
||||
viewModelScope.launch {
|
||||
val isHighlightSynced = networker.createHighlight(jsonString)
|
||||
Log.d("Network", "isHighlightSynced = $isHighlightSynced")
|
||||
}
|
||||
}
|
||||
"deleteHighlight" -> {
|
||||
// { highlightId }
|
||||
Log.d("Loggo", "receive delete highlight action: $json")
|
||||
Log.d("Loggo", "receive delete highlight action: $jsonString")
|
||||
}
|
||||
"updateHighlight" -> {
|
||||
Log.d("Loggo", "receive update highlight action: $json")
|
||||
Log.d("Loggo", "receive update highlight action: $jsonString")
|
||||
}
|
||||
"articleReadingProgress" -> {
|
||||
Log.d("Loggo", "received article reading progress action: $json")
|
||||
viewModelScope.launch {
|
||||
val isReadingProgressSynced = networker.updateReadingProgress(jsonString)
|
||||
Log.d("Network", "isReadingProgressSynced = $isReadingProgressSynced")
|
||||
}
|
||||
}
|
||||
"annotate" -> {
|
||||
Log.d("Loggo", "received annotate action: $json")
|
||||
}
|
||||
"existingHighlightTap" -> {
|
||||
Log.d("Loggo", "receive existing highlight tap action: $json")
|
||||
viewModelScope.launch {
|
||||
val annotation = Gson()
|
||||
.fromJson(jsonString, AnnotationWebViewMessage::class.java)
|
||||
.annotation ?: ""
|
||||
annotationLiveData.value = annotation
|
||||
}
|
||||
}
|
||||
"shareHighlight" -> {
|
||||
// unimplemented
|
||||
}
|
||||
else -> {
|
||||
Log.d("Loggo", "receive unrecognized action of $actionID with json: $json")
|
||||
Log.d("Loggo", "receive unrecognized action of $actionID with json: $jsonString")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
webReaderParamsLiveData.value = null
|
||||
annotationLiveData.value = null
|
||||
}
|
||||
|
||||
fun cancelAnnotationEdit() {
|
||||
annotationLiveData.value = null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/delete"
|
||||
android:title="@string/delete_highlight_menu_title"
|
||||
app:showAsAction="always">
|
||||
</item>
|
||||
|
||||
<item
|
||||
android:id="@+id/annotate"
|
||||
android:title="@string/annotate_menu_action"
|
||||
app:showAsAction="always">
|
||||
</item>
|
||||
</menu>
|
||||
|
|
@ -5,4 +5,5 @@
|
|||
<string name="welcome_subtitle">Save articles and read them later in our distraction-free reader.</string>
|
||||
<string name="highlight_menu_action">Highlight</string>
|
||||
<string name="annotate_menu_action">Annotate</string>
|
||||
<string name="delete_highlight_menu_title">Delete</string>
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Reference in a new issue