Reverts to the non-Google kotlin style.
This commit is contained in:
+156
-154
@@ -38,176 +38,178 @@ import java.util.regex.Pattern
|
||||
|
||||
@Immutable
|
||||
data class ResultOrError(
|
||||
val result: String?,
|
||||
val sourceLang: String?,
|
||||
val targetLang: String?,
|
||||
val result: String?,
|
||||
val sourceLang: String?,
|
||||
val targetLang: String?,
|
||||
)
|
||||
|
||||
object LanguageTranslatorService {
|
||||
var executorService = Executors.newScheduledThreadPool(5)
|
||||
var executorService = Executors.newScheduledThreadPool(5)
|
||||
|
||||
private val options =
|
||||
LanguageIdentificationOptions.Builder()
|
||||
.setExecutor(executorService)
|
||||
.setConfidenceThreshold(0.6f)
|
||||
.build()
|
||||
private val languageIdentification = LanguageIdentification.getClient(options)
|
||||
val lnRegex = Pattern.compile("\\blnbc[a-z0-9]+\\b", Pattern.CASE_INSENSITIVE)
|
||||
val tagRegex =
|
||||
Pattern.compile(
|
||||
"(nostr:)?@?(nsec1|npub1|nevent1|naddr1|note1|nprofile1|nrelay1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
)
|
||||
private val options =
|
||||
LanguageIdentificationOptions.Builder()
|
||||
.setExecutor(executorService)
|
||||
.setConfidenceThreshold(0.6f)
|
||||
.build()
|
||||
private val languageIdentification = LanguageIdentification.getClient(options)
|
||||
val lnRegex = Pattern.compile("\\blnbc[a-z0-9]+\\b", Pattern.CASE_INSENSITIVE)
|
||||
val tagRegex =
|
||||
Pattern.compile(
|
||||
"(nostr:)?@?(nsec1|npub1|nevent1|naddr1|note1|nprofile1|nrelay1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
)
|
||||
|
||||
private val translators =
|
||||
object : LruCache<TranslatorOptions, Translator>(3) {
|
||||
override fun create(options: TranslatorOptions): Translator {
|
||||
return Translation.getClient(options)
|
||||
}
|
||||
private val translators =
|
||||
object : LruCache<TranslatorOptions, Translator>(3) {
|
||||
override fun create(options: TranslatorOptions): Translator {
|
||||
return Translation.getClient(options)
|
||||
}
|
||||
|
||||
override fun entryRemoved(
|
||||
evicted: Boolean,
|
||||
key: TranslatorOptions,
|
||||
oldValue: Translator,
|
||||
newValue: Translator?,
|
||||
) {
|
||||
oldValue.close()
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
translators.evictAll()
|
||||
}
|
||||
|
||||
fun identifyLanguage(text: String): Task<String> {
|
||||
return languageIdentification.identifyLanguage(text)
|
||||
}
|
||||
|
||||
fun translate(
|
||||
text: String,
|
||||
source: String,
|
||||
target: String,
|
||||
): Task<ResultOrError> {
|
||||
checkNotInMainThread()
|
||||
val sourceLangCode = TranslateLanguage.fromLanguageTag(source)
|
||||
val targetLangCode = TranslateLanguage.fromLanguageTag(target)
|
||||
|
||||
if (sourceLangCode == null || targetLangCode == null) {
|
||||
return Tasks.forCanceled()
|
||||
}
|
||||
|
||||
val options =
|
||||
TranslatorOptions.Builder()
|
||||
.setExecutor(executorService)
|
||||
.setSourceLanguage(sourceLangCode)
|
||||
.setTargetLanguage(targetLangCode)
|
||||
.build()
|
||||
|
||||
val translator = translators[options]
|
||||
|
||||
return translator.downloadModelIfNeeded().onSuccessTask(executorService) {
|
||||
checkNotInMainThread()
|
||||
|
||||
val tasks = mutableListOf<Task<String>>()
|
||||
val dict = lnDictionary(text) + urlDictionary(text) + tagDictionary(text)
|
||||
|
||||
for (paragraph in encodeDictionary(text, dict).split("\n")) {
|
||||
tasks.add(translator.translate(paragraph))
|
||||
}
|
||||
|
||||
Tasks.whenAll(tasks).continueWith(executorService) {
|
||||
checkNotInMainThread()
|
||||
|
||||
val results: MutableList<String> = ArrayList()
|
||||
for (task in tasks) {
|
||||
val fixedText =
|
||||
task.result.replace("# [", "#[") // fixes tags that always return with a space
|
||||
results.add(decodeDictionary(fixedText, dict))
|
||||
override fun entryRemoved(
|
||||
evicted: Boolean,
|
||||
key: TranslatorOptions,
|
||||
oldValue: Translator,
|
||||
newValue: Translator?,
|
||||
) {
|
||||
oldValue.close()
|
||||
}
|
||||
}
|
||||
ResultOrError(results.joinToString("\n"), source, target)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
translators.evictAll()
|
||||
}
|
||||
}
|
||||
|
||||
private fun encodeDictionary(
|
||||
text: String,
|
||||
dict: Map<String, String>,
|
||||
): String {
|
||||
var newText = text
|
||||
for (pair in dict) {
|
||||
newText = newText.replace(pair.value, pair.key, true)
|
||||
fun identifyLanguage(text: String): Task<String> {
|
||||
return languageIdentification.identifyLanguage(text)
|
||||
}
|
||||
return newText
|
||||
}
|
||||
|
||||
private fun decodeDictionary(
|
||||
text: String,
|
||||
dict: Map<String, String>,
|
||||
): String {
|
||||
var newText = text
|
||||
for (pair in dict) {
|
||||
newText = newText.replace(pair.key, pair.value, true)
|
||||
fun translate(
|
||||
text: String,
|
||||
source: String,
|
||||
target: String,
|
||||
): Task<ResultOrError> {
|
||||
checkNotInMainThread()
|
||||
val sourceLangCode = TranslateLanguage.fromLanguageTag(source)
|
||||
val targetLangCode = TranslateLanguage.fromLanguageTag(target)
|
||||
|
||||
if (sourceLangCode == null || targetLangCode == null) {
|
||||
return Tasks.forCanceled()
|
||||
}
|
||||
|
||||
val options =
|
||||
TranslatorOptions.Builder()
|
||||
.setExecutor(executorService)
|
||||
.setSourceLanguage(sourceLangCode)
|
||||
.setTargetLanguage(targetLangCode)
|
||||
.build()
|
||||
|
||||
val translator = translators[options]
|
||||
|
||||
return translator.downloadModelIfNeeded().onSuccessTask(executorService) {
|
||||
checkNotInMainThread()
|
||||
|
||||
val tasks = mutableListOf<Task<String>>()
|
||||
val dict = lnDictionary(text) + urlDictionary(text) + tagDictionary(text)
|
||||
|
||||
for (paragraph in encodeDictionary(text, dict).split("\n")) {
|
||||
tasks.add(translator.translate(paragraph))
|
||||
}
|
||||
|
||||
Tasks.whenAll(tasks).continueWith(executorService) {
|
||||
checkNotInMainThread()
|
||||
|
||||
val results: MutableList<String> = ArrayList()
|
||||
for (task in tasks) {
|
||||
val fixedText =
|
||||
task.result.replace("# [", "#[") // fixes tags that always return with a space
|
||||
results.add(decodeDictionary(fixedText, dict))
|
||||
}
|
||||
ResultOrError(results.joinToString("\n"), source, target)
|
||||
}
|
||||
}
|
||||
}
|
||||
return newText
|
||||
}
|
||||
|
||||
private fun tagDictionary(text: String): Map<String, String> {
|
||||
val matcher = tagRegex.matcher(text)
|
||||
val returningList = mutableMapOf<String, String>()
|
||||
var counter = 0
|
||||
while (matcher.find()) {
|
||||
try {
|
||||
val tag = matcher.group()
|
||||
val short = "A$counter"
|
||||
counter++
|
||||
returningList.put(short, tag)
|
||||
} catch (_: Exception) {}
|
||||
private fun encodeDictionary(
|
||||
text: String,
|
||||
dict: Map<String, String>,
|
||||
): String {
|
||||
var newText = text
|
||||
for (pair in dict) {
|
||||
newText = newText.replace(pair.value, pair.key, true)
|
||||
}
|
||||
return newText
|
||||
}
|
||||
return returningList
|
||||
}
|
||||
|
||||
private fun lnDictionary(text: String): Map<String, String> {
|
||||
val matcher = lnRegex.matcher(text)
|
||||
val returningList = mutableMapOf<String, String>()
|
||||
var counter = 0
|
||||
while (matcher.find()) {
|
||||
try {
|
||||
val lnInvoice = matcher.group()
|
||||
val short = "A$counter"
|
||||
counter++
|
||||
returningList.put(short, lnInvoice)
|
||||
} catch (_: Exception) {}
|
||||
private fun decodeDictionary(
|
||||
text: String,
|
||||
dict: Map<String, String>,
|
||||
): String {
|
||||
var newText = text
|
||||
for (pair in dict) {
|
||||
newText = newText.replace(pair.key, pair.value, true)
|
||||
}
|
||||
return newText
|
||||
}
|
||||
return returningList
|
||||
}
|
||||
|
||||
private fun urlDictionary(text: String): Map<String, String> {
|
||||
val parser = UrlDetector(text, UrlDetectorOptions.Default)
|
||||
val urlsInText = parser.detect()
|
||||
|
||||
var counter = 0
|
||||
|
||||
return urlsInText
|
||||
.filter { !it.originalUrl.contains(",") && !it.originalUrl.contains("。") }
|
||||
.associate {
|
||||
counter++
|
||||
"A$counter" to it.originalUrl
|
||||
}
|
||||
}
|
||||
|
||||
fun autoTranslate(
|
||||
text: String,
|
||||
dontTranslateFrom: Set<String>,
|
||||
translateTo: String,
|
||||
): Task<ResultOrError> {
|
||||
return identifyLanguage(text).onSuccessTask(executorService) {
|
||||
if (it.equals(translateTo, true)) {
|
||||
Tasks.forCanceled()
|
||||
} else if (it != "und" && !dontTranslateFrom.contains(it)) {
|
||||
translate(text, it, translateTo)
|
||||
} else {
|
||||
Tasks.forCanceled()
|
||||
}
|
||||
private fun tagDictionary(text: String): Map<String, String> {
|
||||
val matcher = tagRegex.matcher(text)
|
||||
val returningList = mutableMapOf<String, String>()
|
||||
var counter = 0
|
||||
while (matcher.find()) {
|
||||
try {
|
||||
val tag = matcher.group()
|
||||
val short = "A$counter"
|
||||
counter++
|
||||
returningList.put(short, tag)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
return returningList
|
||||
}
|
||||
|
||||
private fun lnDictionary(text: String): Map<String, String> {
|
||||
val matcher = lnRegex.matcher(text)
|
||||
val returningList = mutableMapOf<String, String>()
|
||||
var counter = 0
|
||||
while (matcher.find()) {
|
||||
try {
|
||||
val lnInvoice = matcher.group()
|
||||
val short = "A$counter"
|
||||
counter++
|
||||
returningList.put(short, lnInvoice)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
return returningList
|
||||
}
|
||||
|
||||
private fun urlDictionary(text: String): Map<String, String> {
|
||||
val parser = UrlDetector(text, UrlDetectorOptions.Default)
|
||||
val urlsInText = parser.detect()
|
||||
|
||||
var counter = 0
|
||||
|
||||
return urlsInText
|
||||
.filter { !it.originalUrl.contains(",") && !it.originalUrl.contains("。") }
|
||||
.associate {
|
||||
counter++
|
||||
"A$counter" to it.originalUrl
|
||||
}
|
||||
}
|
||||
|
||||
fun autoTranslate(
|
||||
text: String,
|
||||
dontTranslateFrom: Set<String>,
|
||||
translateTo: String,
|
||||
): Task<ResultOrError> {
|
||||
return identifyLanguage(text).onSuccessTask(executorService) {
|
||||
if (it.equals(translateTo, true)) {
|
||||
Tasks.forCanceled()
|
||||
} else if (it != "und" && !dontTranslateFrom.contains(it)) {
|
||||
translate(text, it, translateTo)
|
||||
} else {
|
||||
Tasks.forCanceled()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+45
-45
@@ -31,65 +31,65 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrC
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateZapChannel
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||
import kotlin.time.measureTimedValue
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
class PushNotificationReceiverService : FirebaseMessagingService() {
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private val eventCache = LruCache<String, String>(100)
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private val eventCache = LruCache<String, String>(100)
|
||||
|
||||
// this is called when a message is received
|
||||
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
||||
Log.d("Time", "Notification received $remoteMessage")
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val (value, elapsed) =
|
||||
measureTimedValue { parseMessage(remoteMessage.data)?.let { receiveIfNew(it) } }
|
||||
Log.d("Time", "Notification processed in $elapsed")
|
||||
// this is called when a message is received
|
||||
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
||||
Log.d("Time", "Notification received $remoteMessage")
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val (value, elapsed) =
|
||||
measureTimedValue { parseMessage(remoteMessage.data)?.let { receiveIfNew(it) } }
|
||||
Log.d("Time", "Notification processed in $elapsed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun parseMessage(params: Map<String, String>): GiftWrapEvent? {
|
||||
params["encryptedEvent"]?.let { eventStr ->
|
||||
(Event.fromJson(eventStr) as? GiftWrapEvent)?.let {
|
||||
return it
|
||||
}
|
||||
private suspend fun parseMessage(params: Map<String, String>): GiftWrapEvent? {
|
||||
params["encryptedEvent"]?.let { eventStr ->
|
||||
(Event.fromJson(eventStr) as? GiftWrapEvent)?.let {
|
||||
return it
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private suspend fun receiveIfNew(event: GiftWrapEvent) {
|
||||
if (eventCache.get(event.id) == null) {
|
||||
eventCache.put(event.id, event.id)
|
||||
EventNotificationConsumer(applicationContext).consume(event)
|
||||
private suspend fun receiveIfNew(event: GiftWrapEvent) {
|
||||
if (eventCache.get(event.id) == null) {
|
||||
eventCache.put(event.id, event.id)
|
||||
EventNotificationConsumer(applicationContext).consume(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d("Lifetime Event", "PushNotificationReceiverService.onCreate")
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.d("Lifetime Event", "PushNotificationReceiverService.onDestroy")
|
||||
|
||||
scope.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onNewToken(token: String) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
RegisterAccounts(LocalPreferences.allSavedAccounts()).go(token)
|
||||
notificationManager().getOrCreateZapChannel(applicationContext)
|
||||
notificationManager().getOrCreateDMChannel(applicationContext)
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d("Lifetime Event", "PushNotificationReceiverService.onCreate")
|
||||
}
|
||||
}
|
||||
|
||||
fun notificationManager(): NotificationManager {
|
||||
return ContextCompat.getSystemService(applicationContext, NotificationManager::class.java)
|
||||
as NotificationManager
|
||||
}
|
||||
override fun onDestroy() {
|
||||
Log.d("Lifetime Event", "PushNotificationReceiverService.onDestroy")
|
||||
|
||||
scope.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onNewToken(token: String) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
RegisterAccounts(LocalPreferences.allSavedAccounts()).go(token)
|
||||
notificationManager().getOrCreateZapChannel(applicationContext)
|
||||
notificationManager().getOrCreateDMChannel(applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
fun notificationManager(): NotificationManager {
|
||||
return ContextCompat.getSystemService(applicationContext, NotificationManager::class.java)
|
||||
as NotificationManager
|
||||
}
|
||||
}
|
||||
|
||||
+13
-13
@@ -27,18 +27,18 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.tasks.await
|
||||
|
||||
object PushNotificationUtils {
|
||||
var hasInit: Boolean = false
|
||||
var hasInit: Boolean = false
|
||||
|
||||
suspend fun init(accounts: List<AccountInfo>) =
|
||||
with(Dispatchers.IO) {
|
||||
if (hasInit) {
|
||||
return@with
|
||||
}
|
||||
// get user notification token provided by firebase
|
||||
try {
|
||||
RegisterAccounts(accounts).go(FirebaseMessaging.getInstance().token.await())
|
||||
} catch (e: Exception) {
|
||||
Log.e("Firebase token", "failed to get firebase token", e)
|
||||
}
|
||||
}
|
||||
suspend fun init(accounts: List<AccountInfo>) =
|
||||
with(Dispatchers.IO) {
|
||||
if (hasInit) {
|
||||
return@with
|
||||
}
|
||||
// get user notification token provided by firebase
|
||||
try {
|
||||
RegisterAccounts(accounts).go(FirebaseMessaging.getInstance().token.await())
|
||||
} catch (e: Exception) {
|
||||
Log.e("Firebase token", "failed to get firebase token", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.CheckifItNeedsToRequestNoti
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesViewModel) {
|
||||
CheckifItNeedsToRequestNotificationPermission(sharedPreferencesViewModel)
|
||||
CheckifItNeedsToRequestNotificationPermission(sharedPreferencesViewModel)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+272
-271
@@ -62,316 +62,317 @@ import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun TranslatableRichTextViewer(
|
||||
content: String,
|
||||
canPreview: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
content: String,
|
||||
canPreview: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
var translatedTextState by
|
||||
remember(content) { mutableStateOf(TranslationConfig(content, null, null, false)) }
|
||||
var translatedTextState by
|
||||
remember(content) { mutableStateOf(TranslationConfig(content, null, null, false)) }
|
||||
|
||||
TranslateAndWatchLanguageChanges(content, accountViewModel) { result ->
|
||||
if (
|
||||
!translatedTextState.result.equals(result.result, true) ||
|
||||
translatedTextState.sourceLang != result.sourceLang ||
|
||||
translatedTextState.targetLang != result.targetLang
|
||||
) {
|
||||
translatedTextState = result
|
||||
TranslateAndWatchLanguageChanges(content, accountViewModel) { result ->
|
||||
if (
|
||||
!translatedTextState.result.equals(result.result, true) ||
|
||||
translatedTextState.sourceLang != result.sourceLang ||
|
||||
translatedTextState.targetLang != result.targetLang
|
||||
) {
|
||||
translatedTextState = result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Crossfade(targetState = translatedTextState) {
|
||||
RenderText(
|
||||
it,
|
||||
content,
|
||||
canPreview,
|
||||
modifier,
|
||||
tags,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
Crossfade(targetState = translatedTextState) {
|
||||
RenderText(
|
||||
it,
|
||||
content,
|
||||
canPreview,
|
||||
modifier,
|
||||
tags,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderText(
|
||||
translatedTextState: TranslationConfig,
|
||||
content: String,
|
||||
canPreview: Boolean,
|
||||
modifier: Modifier,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
translatedTextState: TranslationConfig,
|
||||
content: String,
|
||||
canPreview: Boolean,
|
||||
modifier: Modifier,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
var showOriginal by
|
||||
remember(translatedTextState) { mutableStateOf(translatedTextState.showOriginal) }
|
||||
var showOriginal by
|
||||
remember(translatedTextState) { mutableStateOf(translatedTextState.showOriginal) }
|
||||
|
||||
val toBeViewed by
|
||||
remember(translatedTextState) {
|
||||
derivedStateOf { if (showOriginal) content else translatedTextState.result ?: content }
|
||||
val toBeViewed by
|
||||
remember(translatedTextState) {
|
||||
derivedStateOf { if (showOriginal) content else translatedTextState.result ?: content }
|
||||
}
|
||||
|
||||
Column {
|
||||
ExpandableRichTextViewer(
|
||||
toBeViewed,
|
||||
canPreview,
|
||||
modifier,
|
||||
tags,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
|
||||
if (
|
||||
translatedTextState.sourceLang != null &&
|
||||
translatedTextState.targetLang != null &&
|
||||
translatedTextState.sourceLang != translatedTextState.targetLang
|
||||
) {
|
||||
TranslationMessage(
|
||||
translatedTextState.sourceLang,
|
||||
translatedTextState.targetLang,
|
||||
accountViewModel,
|
||||
) {
|
||||
showOriginal = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
ExpandableRichTextViewer(
|
||||
toBeViewed,
|
||||
canPreview,
|
||||
modifier,
|
||||
tags,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
|
||||
if (
|
||||
translatedTextState.sourceLang != null &&
|
||||
translatedTextState.targetLang != null &&
|
||||
translatedTextState.sourceLang != translatedTextState.targetLang
|
||||
) {
|
||||
TranslationMessage(
|
||||
translatedTextState.sourceLang,
|
||||
translatedTextState.targetLang,
|
||||
accountViewModel,
|
||||
) {
|
||||
showOriginal = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TranslationMessage(
|
||||
source: String,
|
||||
target: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
onChangeWhatToShow: (Boolean) -> Unit,
|
||||
source: String,
|
||||
target: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
onChangeWhatToShow: (Boolean) -> Unit,
|
||||
) {
|
||||
var langSettingsPopupExpanded by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
var langSettingsPopupExpanded by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 5.dp),
|
||||
) {
|
||||
val clickableTextStyle = SpanStyle(color = MaterialTheme.colorScheme.lessImportantLink)
|
||||
|
||||
val annotatedTranslationString = buildAnnotatedString {
|
||||
withStyle(clickableTextStyle) {
|
||||
pushStringAnnotation("langSettings", true.toString())
|
||||
append(stringResource(R.string.translations_auto))
|
||||
}
|
||||
|
||||
append("-${stringResource(R.string.translations_translated_from)} ")
|
||||
|
||||
withStyle(clickableTextStyle) {
|
||||
pushStringAnnotation("showOriginal", true.toString())
|
||||
append(Locale(source).displayName)
|
||||
}
|
||||
|
||||
append(" ${stringResource(R.string.translations_to)} ")
|
||||
|
||||
withStyle(clickableTextStyle) {
|
||||
pushStringAnnotation("showOriginal", false.toString())
|
||||
append(Locale(target).displayName)
|
||||
}
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = annotatedTranslationString,
|
||||
style =
|
||||
LocalTextStyle.current.copy(
|
||||
color =
|
||||
MaterialTheme.colorScheme.onSurface.copy(
|
||||
alpha = 0.32f,
|
||||
),
|
||||
),
|
||||
overflow = TextOverflow.Visible,
|
||||
maxLines = 3,
|
||||
) { spanOffset ->
|
||||
annotatedTranslationString.getStringAnnotations(spanOffset, spanOffset).firstOrNull()?.also {
|
||||
span ->
|
||||
if (span.tag == "showOriginal") {
|
||||
onChangeWhatToShow(span.item.toBoolean())
|
||||
} else {
|
||||
langSettingsPopupExpanded = !langSettingsPopupExpanded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = langSettingsPopupExpanded,
|
||||
onDismissRequest = { langSettingsPopupExpanded = false },
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 5.dp),
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
if (source in accountViewModel.account.dontTranslateFrom) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
val clickableTextStyle = SpanStyle(color = MaterialTheme.colorScheme.lessImportantLink)
|
||||
|
||||
Spacer(modifier = Modifier.size(10.dp))
|
||||
val annotatedTranslationString =
|
||||
buildAnnotatedString {
|
||||
withStyle(clickableTextStyle) {
|
||||
pushStringAnnotation("langSettings", true.toString())
|
||||
append(stringResource(R.string.translations_auto))
|
||||
}
|
||||
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.translations_never_translate_from_lang,
|
||||
Locale(source).displayName,
|
||||
),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.dontTranslateFrom(source)
|
||||
langSettingsPopupExpanded = false
|
||||
}
|
||||
},
|
||||
)
|
||||
Divider()
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
if (accountViewModel.account.preferenceBetween(source, target) == source) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
append("-${stringResource(R.string.translations_translated_from)} ")
|
||||
|
||||
Spacer(modifier = Modifier.size(10.dp))
|
||||
withStyle(clickableTextStyle) {
|
||||
pushStringAnnotation("showOriginal", true.toString())
|
||||
append(Locale(source).displayName)
|
||||
}
|
||||
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.translations_show_in_lang_first,
|
||||
Locale(source).displayName,
|
||||
),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.prefer(source, target, source)
|
||||
langSettingsPopupExpanded = false
|
||||
}
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
if (accountViewModel.account.preferenceBetween(source, target) == target) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
append(" ${stringResource(R.string.translations_to)} ")
|
||||
|
||||
Spacer(modifier = Modifier.size(10.dp))
|
||||
withStyle(clickableTextStyle) {
|
||||
pushStringAnnotation("showOriginal", false.toString())
|
||||
append(Locale(target).displayName)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.translations_show_in_lang_first,
|
||||
Locale(target).displayName,
|
||||
),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.prefer(source, target, target)
|
||||
langSettingsPopupExpanded = false
|
||||
}
|
||||
},
|
||||
)
|
||||
Divider()
|
||||
|
||||
val languageList = ConfigurationCompat.getLocales(Resources.getSystem().configuration)
|
||||
for (i in 0 until languageList.size()) {
|
||||
languageList.get(i)?.let { lang ->
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
if (lang.language in accountViewModel.account.translateTo) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(10.dp))
|
||||
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.translations_always_translate_to_lang,
|
||||
lang.displayName,
|
||||
ClickableText(
|
||||
text = annotatedTranslationString,
|
||||
style =
|
||||
LocalTextStyle.current.copy(
|
||||
color =
|
||||
MaterialTheme.colorScheme.onSurface.copy(
|
||||
alpha = 0.32f,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.translateTo(lang)
|
||||
langSettingsPopupExpanded = false
|
||||
}
|
||||
},
|
||||
)
|
||||
overflow = TextOverflow.Visible,
|
||||
maxLines = 3,
|
||||
) { spanOffset ->
|
||||
annotatedTranslationString.getStringAnnotations(spanOffset, spanOffset).firstOrNull()?.also {
|
||||
span ->
|
||||
if (span.tag == "showOriginal") {
|
||||
onChangeWhatToShow(span.item.toBoolean())
|
||||
} else {
|
||||
langSettingsPopupExpanded = !langSettingsPopupExpanded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = langSettingsPopupExpanded,
|
||||
onDismissRequest = { langSettingsPopupExpanded = false },
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
if (source in accountViewModel.account.dontTranslateFrom) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(10.dp))
|
||||
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.translations_never_translate_from_lang,
|
||||
Locale(source).displayName,
|
||||
),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.dontTranslateFrom(source)
|
||||
langSettingsPopupExpanded = false
|
||||
}
|
||||
},
|
||||
)
|
||||
Divider()
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
if (accountViewModel.account.preferenceBetween(source, target) == source) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(10.dp))
|
||||
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.translations_show_in_lang_first,
|
||||
Locale(source).displayName,
|
||||
),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.prefer(source, target, source)
|
||||
langSettingsPopupExpanded = false
|
||||
}
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
if (accountViewModel.account.preferenceBetween(source, target) == target) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(10.dp))
|
||||
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.translations_show_in_lang_first,
|
||||
Locale(target).displayName,
|
||||
),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.prefer(source, target, target)
|
||||
langSettingsPopupExpanded = false
|
||||
}
|
||||
},
|
||||
)
|
||||
Divider()
|
||||
|
||||
val languageList = ConfigurationCompat.getLocales(Resources.getSystem().configuration)
|
||||
for (i in 0 until languageList.size()) {
|
||||
languageList.get(i)?.let { lang ->
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
if (lang.language in accountViewModel.account.translateTo) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(10.dp))
|
||||
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.translations_always_translate_to_lang,
|
||||
lang.displayName,
|
||||
),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.translateTo(lang)
|
||||
langSettingsPopupExpanded = false
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TranslateAndWatchLanguageChanges(
|
||||
content: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
onTranslated: (TranslationConfig) -> Unit,
|
||||
content: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
onTranslated: (TranslationConfig) -> Unit,
|
||||
) {
|
||||
val accountState by accountViewModel.accountLanguagesLiveData.observeAsState()
|
||||
val accountState by accountViewModel.accountLanguagesLiveData.observeAsState()
|
||||
|
||||
LaunchedEffect(accountState) {
|
||||
// This takes some time. Launches as a Composition scope to make sure this gets cancel if this
|
||||
// item gets out of view.
|
||||
launch(Dispatchers.IO) {
|
||||
LanguageTranslatorService.autoTranslate(
|
||||
content,
|
||||
accountViewModel.account.dontTranslateFrom,
|
||||
accountViewModel.account.translateTo,
|
||||
)
|
||||
.addOnCompleteListener { task ->
|
||||
if (task.isSuccessful && !content.equals(task.result.result, true)) {
|
||||
if (task.result.sourceLang != null && task.result.targetLang != null) {
|
||||
val preference =
|
||||
accountViewModel.account.preferenceBetween(
|
||||
task.result.sourceLang!!,
|
||||
task.result.targetLang!!,
|
||||
)
|
||||
val newConfig =
|
||||
TranslationConfig(
|
||||
result = task.result.result,
|
||||
sourceLang = task.result.sourceLang,
|
||||
targetLang = task.result.targetLang,
|
||||
showOriginal = preference == task.result.sourceLang,
|
||||
)
|
||||
LaunchedEffect(accountState) {
|
||||
// This takes some time. Launches as a Composition scope to make sure this gets cancel if this
|
||||
// item gets out of view.
|
||||
launch(Dispatchers.IO) {
|
||||
LanguageTranslatorService.autoTranslate(
|
||||
content,
|
||||
accountViewModel.account.dontTranslateFrom,
|
||||
accountViewModel.account.translateTo,
|
||||
)
|
||||
.addOnCompleteListener { task ->
|
||||
if (task.isSuccessful && !content.equals(task.result.result, true)) {
|
||||
if (task.result.sourceLang != null && task.result.targetLang != null) {
|
||||
val preference =
|
||||
accountViewModel.account.preferenceBetween(
|
||||
task.result.sourceLang!!,
|
||||
task.result.targetLang!!,
|
||||
)
|
||||
val newConfig =
|
||||
TranslationConfig(
|
||||
result = task.result.result,
|
||||
sourceLang = task.result.sourceLang,
|
||||
targetLang = task.result.targetLang,
|
||||
showOriginal = preference == task.result.sourceLang,
|
||||
)
|
||||
|
||||
onTranslated(newConfig)
|
||||
}
|
||||
}
|
||||
onTranslated(newConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user