Skip to content

Commit

Permalink
feat: add google translate feature to text selection
Browse files Browse the repository at this point in the history
  • Loading branch information
plateaukao committed Apr 16, 2023
1 parent f9ef597 commit f54e9b3
Show file tree
Hide file tree
Showing 13 changed files with 337 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import info.plateaukao.einkbro.unit.*
import info.plateaukao.einkbro.unit.BrowserUnit.createDownloadReceiver
import info.plateaukao.einkbro.unit.HelperUnit.toNormalScheme
import info.plateaukao.einkbro.util.Constants.Companion.ACTION_GPT
import info.plateaukao.einkbro.util.Constants.Companion.ACTION_GTRANSLATE
import info.plateaukao.einkbro.util.DebugT
import info.plateaukao.einkbro.view.*
import info.plateaukao.einkbro.view.GestureType.*
Expand All @@ -78,6 +79,7 @@ import info.plateaukao.einkbro.viewmodel.BookmarkViewModelFactory
import info.plateaukao.einkbro.viewmodel.GptViewModel
import info.plateaukao.einkbro.viewmodel.PocketViewModel
import info.plateaukao.einkbro.viewmodel.PocketViewModelFactory
import info.plateaukao.einkbro.viewmodel.TranslationViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -111,6 +113,7 @@ open class BrowserActivity : FragmentActivity(), BrowserController {
private val backupUnit: BackupUnit by lazy { BackupUnit(this) }

private val gptViewModel: GptViewModel by viewModels()
private val translationViewModel: TranslationViewModel by viewModels()

private fun prepareRecord(): Boolean {
val webView = currentAlbumController as NinjaWebView
Expand Down Expand Up @@ -684,6 +687,12 @@ open class BrowserActivity : FragmentActivity(), BrowserController {
}

when (intent.action) {
ACTION_GTRANSLATE -> {
translationViewModel.updateInputMessage(actionModeMenuViewModel.selectedText.value)
TranslateDialogFragment(translationViewModel, actionModeMenuViewModel.clickedPoint.value)
.show(supportFragmentManager, "translateDialog")

}
ACTION_GPT -> {
gptViewModel.updateInputMessage(actionModeMenuViewModel.selectedText.value)
if (gptViewModel.hasApiKey()) {
Expand Down
39 changes: 0 additions & 39 deletions app/src/main/java/info/plateaukao/einkbro/pocket/PocketNetwork.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,45 +70,6 @@ class PocketNetwork {
})
}

fun addUrlToPocket(
accessToken: String,
url: String,
title: String? = null,
tags: String? = null,
callback: (Boolean) -> Unit
) {
val requestBodyBuilder = FormBody.Builder()
.add("url", url)
.add("consumer_key", consumerKey)
.add("access_token", accessToken)

title?.let {
requestBodyBuilder.add("title", it)
}

tags?.let {
requestBodyBuilder.add("tags", it)
}

val requestBody = requestBodyBuilder.build()

val request = Request.Builder()
.url("https://getpocket.com/v3/add")
.post(requestBody)
.build()

client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
// Handle error
callback(false)
}

override fun onResponse(call: Call, response: Response) {
callback(response.isSuccessful)
}
})
}

@OptIn(ExperimentalCoroutinesApi::class)
suspend fun addUrlToPocket(
accessToken: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package info.plateaukao.einkbro.service

import android.net.Uri
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call
import okhttp3.Callback
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okio.IOException
import org.apache.commons.text.StringEscapeUtils
import org.jsoup.Jsoup

class TranslateRepository {
private val client = OkHttpClient()

@OptIn(ExperimentalCoroutinesApi::class)
suspend fun gTranslate(
text: String,
targetLanguage: String = "en",
sourceLanguage: String = "auto",
): String? {
return suspendCancellableCoroutine { continuation ->
val url = HttpUrl.Builder()
.scheme("https")
.host("translate.google.com")
.addPathSegment("m")
.addQueryParameter("tl", targetLanguage)
.addQueryParameter("sl", sourceLanguage)
.addQueryParameter("q", text)
.build()

val request = Request.Builder()
.url(url)
.build()

client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
if (continuation.isActive) {
continuation.resume(null) {}
}
}

override fun onResponse(call: Call, response: Response) {
if (continuation.isActive) {
val body = response.body?.string()
if (body != null) {
Jsoup.parse(body)
.body()
.getElementsByClass("result-container")
.first()?.text()?.let {
continuation.resume(StringEscapeUtils.unescapeJava(Uri.decode(it))) {}
} ?: continuation.resume(null) {}
} else {
continuation.resume(null) {}
}
}
}
})

continuation.invokeOnCancellation {
client.dispatcher.executorService.shutdownNow()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class Constants {
const val MIME_TYPE_FONT = "application/x-font-ttf"
// from https://github.com/Smile4ever/Neat-URL
const val ACTION_GPT = "info.plateaukao.einkbro.gpt"
const val ACTION_GTRANSLATE = "info.plateaukao.einkbro.gtranslate"

const val NEAT_URL_DATA = """
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp

Expand All @@ -18,6 +19,7 @@ fun SelectableText(
modifier: Modifier,
selected: Boolean,
text: String,
textAlign: TextAlign = TextAlign.Start,
onClick: () -> Unit,
) {
val interactionSource = remember { MutableInteractionSource() }
Expand All @@ -28,6 +30,7 @@ fun SelectableText(
style = MaterialTheme.typography.button,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = textAlign,
modifier = modifier
.border(borderWidth, MaterialTheme.colors.onBackground, RoundedCornerShape(7.dp))
.padding(horizontal = 6.dp, vertical = 6.dp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@ class GPTDialogFragment(

override fun setupComposeView() = composeView.setContent {
MyTheme {
GptResponse(gptViewModel) {
dismiss()
}
GptResponse(gptViewModel)
}
}

Expand Down Expand Up @@ -122,10 +120,7 @@ class GPTDialogFragment(
}

@Composable
private fun GptResponse(
gptViewModel: GptViewModel,
onCloseAction: () -> Unit
) {
private fun GptResponse(gptViewModel: GptViewModel) {
val requestMessage by gptViewModel.inputMessage.collectAsState()
val responseMessage by gptViewModel.responseMessage.collectAsState()
val showRequest = remember { mutableStateOf(false) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package info.plateaukao.einkbro.view.dialog.compose

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Point
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import info.plateaukao.einkbro.R
import info.plateaukao.einkbro.view.compose.MyTheme
import info.plateaukao.einkbro.view.compose.SelectableText
import info.plateaukao.einkbro.view.dialog.TranslationLanguageDialog
import info.plateaukao.einkbro.viewmodel.TranslationViewModel

class TranslateDialogFragment(
private val translationViewModel: TranslationViewModel,
private val anchorPoint: Point,
) : ComposeDialogFragment() {

init {
shouldShowInCenter = true
}

override fun setupComposeView() = composeView.setContent {
MyTheme {
TranslateResponse(translationViewModel) {
TranslationLanguageDialog(requireActivity()).show { translationLanguage ->
translationViewModel.updateTranslationLanguage(translationLanguage)
}
}
}
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = super.onCreateView(inflater, container, savedInstanceState)
setupDialogPosition(anchorPoint)

translationViewModel.query()
return view
}

private var initialTouchX: Float = 0f
private var initialTouchY: Float = 0f
private var initialX: Int = 0
private var initialY: Int = 0

@SuppressLint("ClickableViewAccessibility")
private fun setupDialogPosition(position: Point) {
val window = dialog?.window ?: return
window.setGravity(Gravity.TOP or Gravity.LEFT)

if (position.isValid()) {
val params = window.attributes.apply {
x = position.x
y = position.y
}
window.attributes = params
}

supportDragToMove(window)
}

@SuppressLint("ClickableViewAccessibility")
private fun supportDragToMove(window: Window) {
val windowManager =
requireContext().getSystemService(Context.WINDOW_SERVICE) as WindowManager
window.decorView.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// Get the initial touch position and dialog window position
initialTouchX = event.rawX
initialTouchY = event.rawY
initialX = window.attributes.x
initialY = window.attributes.y
true
}

MotionEvent.ACTION_MOVE -> {
// Calculate the new position of the dialog window
val newX = initialX + (event.rawX - initialTouchX).toInt()
val newY = initialY + (event.rawY - initialTouchY).toInt()

// Update the position of the dialog window
window.attributes.x = newX
window.attributes.y = newY
windowManager.updateViewLayout(window.decorView, window.attributes)
true
}

else -> false
}
}
}

private fun Point.isValid() = x != 0 && y != 0
}

@Composable
private fun TranslateResponse(
translationViewModel: TranslationViewModel,
onLanguageClick: () -> Unit
) {
val requestMessage by translationViewModel.inputMessage.collectAsState()
val responseMessage by translationViewModel.responseMessage.collectAsState()
val targetLanguage by translationViewModel.translationLanguage.collectAsState()
val showRequest = remember { mutableStateOf(false) }

Column(
modifier = Modifier
.wrapContentHeight()
.wrapContentWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
SelectableText(
modifier = Modifier
.weight(1f)
.padding(10.dp),
selected = true,
text = targetLanguage.language,
textAlign = TextAlign.Center,
onClick = onLanguageClick
)
Icon(
painter = painterResource(
id = if (showRequest.value) R.drawable.icon_arrow_up_gest else R.drawable.icon_arrow_down_gest
),
contentDescription = "Info Icon",
modifier = Modifier
.size(32.dp)
.clickable { showRequest.value = !showRequest.value }
)
}
if (showRequest.value) {
Text(
text = requestMessage,
modifier = Modifier.padding(10.dp)
)
Divider()
}
Text(
text = responseMessage,
modifier = Modifier.padding(10.dp)
)
}
}
Loading

0 comments on commit f54e9b3

Please sign in to comment.