From ea9448e144f7c686654c0ea8a41379f1218eca28 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 20 Jan 2026 15:57:29 +0100 Subject: [PATCH 01/25] feat(assistant): translate Signed-off-by: alperozturk96 # Conflicts: # gradle/libs.versions.toml # Conflicts: # app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt --- .../client/assistant/AssistantViewModel.kt | 2 + .../assistant/model/AssistantScreenState.kt | 2 + .../assistant/translate/TranslationScreen.kt | 190 ++++++++++++++++++ app/src/main/res/drawable/ic_translate.xml | 9 + app/src/main/res/values/strings.xml | 6 + gradle/verification-metadata.xml | 8 + 6 files changed, 217 insertions(+) create mode 100644 app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt create mode 100644 app/src/main/res/drawable/ic_translate.xml diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index 820bbafb63e7..5ef49cdd55ca 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -168,9 +168,11 @@ class AssistantViewModel( _filteredTaskList ) { selectedTask, chats, tasks -> val isChat = selectedTask?.isChat() == true + val isTranslation = selectedTask?.isTranslate() == true when { selectedTask == null -> AssistantScreenState.Loading + isTranslation -> AssistantScreenState.Translation isChat && chats.isEmpty() -> AssistantScreenState.emptyChatList() isChat -> AssistantScreenState.ChatContent !isChat && (tasks == null || tasks.isEmpty()) -> AssistantScreenState.emptyTaskList() diff --git a/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt b/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt index 80d2f1a29fb1..eb9b508c9e48 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt @@ -16,6 +16,8 @@ sealed class AssistantScreenState { data object ChatContent : AssistantScreenState() + data object Translation : AssistantScreenState() + data class EmptyContent(val iconId: Int?, val titleId: Int?, val descriptionId: Int?) : AssistantScreenState() companion object { diff --git a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt new file mode 100644 index 000000000000..7d57a12c88f4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt @@ -0,0 +1,190 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.assistant.translate + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.owncloud.android.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TranslationScreen(textToTranslate: String) { + var originText by remember { mutableStateOf(textToTranslate) } + var originLanguage by remember { mutableStateOf("English") } + var showOriginDropdownMenu by remember { mutableStateOf(false) } + + var targetText by remember { mutableStateOf("") } + var targetLanguage by remember { mutableStateOf("Spanish") } + var showTargetDropdownMenu by remember { mutableStateOf(false) } + + val languages = listOf("English", "Spanish", "French", "German", "Turkish", "Japanese") + + Scaffold( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .padding(top = 32.dp), floatingActionButton = { + FloatingActionButton(onClick = { + + }, content = { + Icon( + painter = painterResource(R.drawable.ic_translate), + contentDescription = "translate button" + ) + }) + }) { + LazyColumn( + modifier = Modifier.padding(it) + ) { + item { + LanguageSelector( + title = originLanguage, + languages = languages, + titleId = R.string.translation_screen_label_from, + expanded = showOriginDropdownMenu, + expand = { + showOriginDropdownMenu = it + }, onLanguageSelect = { + originLanguage = it + } + ) + + TranslationTextField(titleId = R.string.translation_screen_hint_source, originText, onValueChange = { + originText = it + }) + } + + item { + HorizontalDivider( + modifier = Modifier.padding(vertical = 16.dp), + thickness = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant + ) + } + + item { + LanguageSelector( + title = targetLanguage, + languages = languages, + titleId = R.string.translation_screen_label_to, + expanded = showTargetDropdownMenu, + expand = { + showTargetDropdownMenu = it + }, onLanguageSelect = { + targetLanguage = it + } + ) + + TranslationTextField(titleId = R.string.translation_screen_hint_target, targetText, onValueChange = { + targetText = it + }) + } + } + } +} + +@Composable +private fun TranslationTextField(titleId: Int, value: String, onValueChange: (String) -> Unit) { + TextField( + value = value, + onValueChange = { + onValueChange(it) + }, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 120.dp, max = 240.dp), + placeholder = { + Text( + text = stringResource(titleId), + style = MaterialTheme.typography.headlineSmall + ) + }, + textStyle = MaterialTheme.typography.headlineSmall, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ) + ) +} + +@Composable +private fun LanguageSelector( + title: String, + languages: List, + titleId: Int, + expanded: Boolean, + expand: (Boolean) -> Unit, + onLanguageSelect: (String) -> Unit +) { + Row( + modifier = Modifier + .padding(16.dp) + .clickable(onClick = { + expand(!expanded) + }) + ) { + Text( + text = stringResource(titleId, title), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = title, + style = MaterialTheme.typography.labelLarge, + ) + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expand(false) } + ) { + languages.forEach { language -> + DropdownMenuItem( + text = { Text(language) }, + onClick = { + expand(false) + onLanguageSelect(language) + } + ) + } + } + } +} diff --git a/app/src/main/res/drawable/ic_translate.xml b/app/src/main/res/drawable/ic_translate.xml new file mode 100644 index 000000000000..0fbfe69210f6 --- /dev/null +++ b/app/src/main/res/drawable/ic_translate.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4395d77f9c47..4d89a045e066 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -80,6 +80,12 @@ successful failed + + Source Language: + Translate to: + Enter text to translate… + Translation will appear here… + Conversations No conversations yet diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index c1b61818d38c..4b5bc87fa745 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -21127,6 +21127,14 @@ + + + + + + + + From d13778a1dc6f21f4a6a4633f9b0137ea80c0a477 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 21 Jan 2026 11:00:20 +0100 Subject: [PATCH 02/25] fix git conflict Signed-off-by: alperozturk96 # Conflicts: # gradle/libs.versions.toml --- app/src/main/res/drawable/ic_translate.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/drawable/ic_translate.xml b/app/src/main/res/drawable/ic_translate.xml index 0fbfe69210f6..fa98b6cc47b9 100644 --- a/app/src/main/res/drawable/ic_translate.xml +++ b/app/src/main/res/drawable/ic_translate.xml @@ -1,3 +1,9 @@ + Date: Wed, 21 Jan 2026 11:18:06 +0100 Subject: [PATCH 03/25] copy selected text to input bar Signed-off-by: alperozturk96 --- .../main/java/com/nextcloud/client/assistant/AssistantScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index b51dd1d9f927..52b664217cfb 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -190,7 +190,7 @@ fun AssistantScreen( } }, bottomBar = { - if (!taskTypes.isNullOrEmpty()) { + if (!taskTypes.isNullOrEmpty() && selectedTaskType?.isTranslate() == false) { InputBar( sessionId, selectedTaskType, From dab2c20ed707511de817b7848486d0b67440d0b2 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 21 Jan 2026 12:27:32 +0100 Subject: [PATCH 04/25] add translation model Signed-off-by: alperozturk96 --- .../client/assistant/AssistantScreen.kt | 5 ++ .../client/assistant/AssistantViewModel.kt | 2 +- .../assistant/translate/TranslationScreen.kt | 57 +++++++++++-------- app/src/main/res/values/strings.xml | 2 +- 4 files changed, 41 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index 52b664217cfb..623db7d36846 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -65,6 +65,7 @@ import com.nextcloud.client.assistant.repository.local.MockAssistantLocalReposit import com.nextcloud.client.assistant.repository.remote.MockAssistantRemoteRepository import com.nextcloud.client.assistant.task.TaskView import com.nextcloud.client.assistant.taskTypes.TaskTypesRow +import com.nextcloud.client.assistant.translate.TranslationScreen import com.nextcloud.ui.composeActivity.ComposeActivity import com.nextcloud.ui.composeActivity.ComposeViewModel import com.nextcloud.ui.composeComponents.alertDialog.SimpleAlertDialog @@ -229,6 +230,10 @@ fun AssistantScreen( ) } + AssistantScreenState.Translation -> { + TranslationScreen(selectedTaskType, viewModel,selectedText ?: "") + } + else -> EmptyContent( paddingValues, iconId = R.drawable.spinner_inner, diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index 5ef49cdd55ca..10b814f7dca8 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -44,7 +44,7 @@ class AssistantViewModel( private const val POLLING_INTERVAL_MS = 15_000L } - private val _inputBarText = MutableStateFlow("") + private val _inputBarText = MutableStateFlow("") val inputBarText: StateFlow = _inputBarText private val _screenState = MutableStateFlow(null) diff --git a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt index 7d57a12c88f4..a272c97ad073 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt @@ -37,21 +37,26 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.nextcloud.client.assistant.AssistantViewModel import com.owncloud.android.R +import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData +import com.owncloud.android.lib.resources.assistant.v2.model.TranslationLanguage +import com.owncloud.android.lib.resources.assistant.v2.model.TranslationLanguages +import com.owncloud.android.lib.resources.assistant.v2.model.toTranslationLanguages @OptIn(ExperimentalMaterial3Api::class) @Composable -fun TranslationScreen(textToTranslate: String) { +fun TranslationScreen(task: TaskTypeData?, viewModel: AssistantViewModel, textToTranslate: String) { var originText by remember { mutableStateOf(textToTranslate) } - var originLanguage by remember { mutableStateOf("English") } + val languages = task?.toTranslationLanguages() ?: TranslationLanguages(listOf(), listOf()) + + var originLanguage by remember { mutableStateOf(languages.originLanguages.first()) } var showOriginDropdownMenu by remember { mutableStateOf(false) } var targetText by remember { mutableStateOf("") } - var targetLanguage by remember { mutableStateOf("Spanish") } + var targetLanguage by remember { mutableStateOf(languages.targetLanguages.first()) } var showTargetDropdownMenu by remember { mutableStateOf(false) } - val languages = listOf("English", "Spanish", "French", "German", "Turkish", "Japanese") - Scaffold( modifier = Modifier .fillMaxSize() @@ -72,19 +77,22 @@ fun TranslationScreen(textToTranslate: String) { item { LanguageSelector( title = originLanguage, - languages = languages, + languages = languages.originLanguages, titleId = R.string.translation_screen_label_from, expanded = showOriginDropdownMenu, expand = { showOriginDropdownMenu = it - }, onLanguageSelect = { - originLanguage = it + }, onLanguageSelect = { newLanguage -> + originLanguage = newLanguage } ) - TranslationTextField(titleId = R.string.translation_screen_hint_source, originText, onValueChange = { - originText = it - }) + TranslationTextField( + titleId = R.string.translation_screen_hint_source, + originText, + onValueChange = { updatedText -> + originText = updatedText + }) } item { @@ -98,19 +106,22 @@ fun TranslationScreen(textToTranslate: String) { item { LanguageSelector( title = targetLanguage, - languages = languages, + languages = languages.targetLanguages, titleId = R.string.translation_screen_label_to, expanded = showTargetDropdownMenu, expand = { showTargetDropdownMenu = it - }, onLanguageSelect = { - targetLanguage = it + }, onLanguageSelect = { newLanguage -> + targetLanguage = newLanguage } ) - TranslationTextField(titleId = R.string.translation_screen_hint_target, targetText, onValueChange = { - targetText = it - }) + TranslationTextField( + titleId = R.string.translation_screen_hint_target, + targetText, + onValueChange = { updatedText -> + targetText = updatedText + }) } } } @@ -145,12 +156,12 @@ private fun TranslationTextField(titleId: Int, value: String, onValueChange: (St @Composable private fun LanguageSelector( - title: String, - languages: List, + title: TranslationLanguage, + languages: List, titleId: Int, expanded: Boolean, expand: (Boolean) -> Unit, - onLanguageSelect: (String) -> Unit + onLanguageSelect: (TranslationLanguage) -> Unit ) { Row( modifier = Modifier @@ -160,7 +171,7 @@ private fun LanguageSelector( }) ) { Text( - text = stringResource(titleId, title), + text = stringResource(titleId), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary, ) @@ -168,7 +179,7 @@ private fun LanguageSelector( Spacer(modifier = Modifier.width(8.dp)) Text( - text = title, + text = title.name, style = MaterialTheme.typography.labelLarge, ) @@ -178,7 +189,7 @@ private fun LanguageSelector( ) { languages.forEach { language -> DropdownMenuItem( - text = { Text(language) }, + text = { Text(language.name) }, onClick = { expand(false) onLanguageSelect(language) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4d89a045e066..cef263cc863d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -81,7 +81,7 @@ failed - Source Language: + Source language: Translate to: Enter text to translate… Translation will appear here… From f52a0a491c6b1ba4c9bb707e3303795ca6c3ee00 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 21 Jan 2026 13:38:14 +0100 Subject: [PATCH 05/25] add translation logic Signed-off-by: alperozturk96 # Conflicts: # app/src/main/res/values/strings.xml --- .../client/assistant/AssistantViewModel.kt | 32 +++++++++++++++++++ .../assistant/translate/TranslationScreen.kt | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index 10b814f7dca8..52052647fd53 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -21,6 +21,9 @@ import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessage import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessageRequest import com.owncloud.android.lib.resources.assistant.v2.model.Task import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData +import com.owncloud.android.lib.resources.assistant.v2.model.TranslationLanguage +import com.owncloud.android.lib.resources.assistant.v2.model.TranslationRequest +import com.owncloud.android.lib.resources.assistant.v2.model.toTranslationModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -184,6 +187,35 @@ class AssistantViewModel( } } + fun translate(textToTranslate: String, originLanguage: TranslationLanguage, targetLanguage: TranslationLanguage) { + val task = _selectedTaskType.value + if (task == null) { + _snackbarMessageId.update { + R.string.assistant_screen_select_task + } + return + } + + val model = task.toTranslationModel() + + if (model == null) { + _snackbarMessageId.update { + R.string.translation_screen_error_message + } + return + } + + val input = TranslationRequest( + input = textToTranslate, + originLanguage = originLanguage.code, + targetLanguage = targetLanguage.code, + maxTokens = model.maxTokens, + model = model.model + ).toJson() + + createTask(input, task) + } + // region chat fun sendChatMessage(content: String, sessionId: Long) = viewModelScope.launch(Dispatchers.IO) { val request = ChatMessageRequest( diff --git a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt index a272c97ad073..21e2e3f799bc 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt @@ -63,7 +63,7 @@ fun TranslationScreen(task: TaskTypeData?, viewModel: AssistantViewModel, textTo .padding(16.dp) .padding(top = 32.dp), floatingActionButton = { FloatingActionButton(onClick = { - + viewModel.translate(textToTranslate, originLanguage, targetLanguage) }, content = { Icon( painter = painterResource(R.drawable.ic_translate), From a6ebfd55a882bba68fec15416fa53cbc74069479 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 21 Jan 2026 13:57:52 +0100 Subject: [PATCH 06/25] add translation logic Signed-off-by: alperozturk96 --- .../client/assistant/AssistantScreen.kt | 2 +- .../assistant/translate/TranslationScreen.kt | 170 +++++++----------- .../translate/TranslationSideState.kt | 16 ++ 3 files changed, 86 insertions(+), 102 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/client/assistant/translate/TranslationSideState.kt diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index 623db7d36846..531631266e6b 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -231,7 +231,7 @@ fun AssistantScreen( } AssistantScreenState.Translation -> { - TranslationScreen(selectedTaskType, viewModel,selectedText ?: "") + TranslationScreen(selectedTaskType, viewModel, selectedText ?: "") } else -> EmptyContent( diff --git a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt index 21e2e3f799bc..ed3df1e028e6 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt @@ -41,58 +41,51 @@ import com.nextcloud.client.assistant.AssistantViewModel import com.owncloud.android.R import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData import com.owncloud.android.lib.resources.assistant.v2.model.TranslationLanguage -import com.owncloud.android.lib.resources.assistant.v2.model.TranslationLanguages import com.owncloud.android.lib.resources.assistant.v2.model.toTranslationLanguages @OptIn(ExperimentalMaterial3Api::class) @Composable fun TranslationScreen(task: TaskTypeData?, viewModel: AssistantViewModel, textToTranslate: String) { - var originText by remember { mutableStateOf(textToTranslate) } - val languages = task?.toTranslationLanguages() ?: TranslationLanguages(listOf(), listOf()) + val languages = remember(task) { task?.toTranslationLanguages() } - var originLanguage by remember { mutableStateOf(languages.originLanguages.first()) } - var showOriginDropdownMenu by remember { mutableStateOf(false) } - - var targetText by remember { mutableStateOf("") } - var targetLanguage by remember { mutableStateOf(languages.targetLanguages.first()) } - var showTargetDropdownMenu by remember { mutableStateOf(false) } + var sourceState by remember { + mutableStateOf( + TranslationSideState( + text = textToTranslate, + language = languages?.originLanguages?.firstOrNull() + ) + ) + } + var targetState by remember { + mutableStateOf(TranslationSideState(language = languages?.targetLanguages?.firstOrNull())) + } Scaffold( modifier = Modifier .fillMaxSize() .padding(16.dp) - .padding(top = 32.dp), floatingActionButton = { + .padding(top = 32.dp), + floatingActionButton = { FloatingActionButton(onClick = { - viewModel.translate(textToTranslate, originLanguage, targetLanguage) + val originLang = sourceState.language + val targetLang = targetState.language + if (originLang != null && targetLang != null) { + viewModel.translate(sourceState.text, originLang, targetLang) + } }, content = { - Icon( - painter = painterResource(R.drawable.ic_translate), - contentDescription = "translate button" - ) + Icon(painter = painterResource(R.drawable.ic_translate), contentDescription = "translate button") }) - }) { - LazyColumn( - modifier = Modifier.padding(it) - ) { + } + ) { paddingValues -> + LazyColumn(modifier = Modifier.padding(paddingValues)) { item { - LanguageSelector( - title = originLanguage, - languages = languages.originLanguages, - titleId = R.string.translation_screen_label_from, - expanded = showOriginDropdownMenu, - expand = { - showOriginDropdownMenu = it - }, onLanguageSelect = { newLanguage -> - originLanguage = newLanguage - } + TranslationSection( + labelId = R.string.translation_screen_label_from, + hintId = R.string.translation_screen_hint_source, + state = sourceState, + availableLanguages = languages?.originLanguages ?: emptyList(), + onStateChange = { sourceState = it } ) - - TranslationTextField( - titleId = R.string.translation_screen_hint_source, - originText, - onValueChange = { updatedText -> - originText = updatedText - }) } item { @@ -104,98 +97,73 @@ fun TranslationScreen(task: TaskTypeData?, viewModel: AssistantViewModel, textTo } item { - LanguageSelector( - title = targetLanguage, - languages = languages.targetLanguages, - titleId = R.string.translation_screen_label_to, - expanded = showTargetDropdownMenu, - expand = { - showTargetDropdownMenu = it - }, onLanguageSelect = { newLanguage -> - targetLanguage = newLanguage - } + TranslationSection( + labelId = R.string.translation_screen_label_to, + hintId = R.string.translation_screen_hint_target, + state = targetState, + availableLanguages = languages?.targetLanguages ?: emptyList(), + onStateChange = { targetState = it } ) - - TranslationTextField( - titleId = R.string.translation_screen_hint_target, - targetText, - onValueChange = { updatedText -> - targetText = updatedText - }) } } } } @Composable -private fun TranslationTextField(titleId: Int, value: String, onValueChange: (String) -> Unit) { - TextField( - value = value, - onValueChange = { - onValueChange(it) - }, - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 120.dp, max = 240.dp), - placeholder = { - Text( - text = stringResource(titleId), - style = MaterialTheme.typography.headlineSmall - ) - }, - textStyle = MaterialTheme.typography.headlineSmall, - colors = TextFieldDefaults.colors( - focusedContainerColor = Color.Transparent, - unfocusedContainerColor = Color.Transparent, - disabledContainerColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent - ) - ) -} - -@Composable -private fun LanguageSelector( - title: TranslationLanguage, - languages: List, - titleId: Int, - expanded: Boolean, - expand: (Boolean) -> Unit, - onLanguageSelect: (TranslationLanguage) -> Unit +private fun TranslationSection( + labelId: Int, + hintId: Int, + state: TranslationSideState, + availableLanguages: List, + onStateChange: (TranslationSideState) -> Unit ) { Row( modifier = Modifier .padding(16.dp) - .clickable(onClick = { - expand(!expanded) - }) + .clickable { onStateChange(state.copy(isExpanded = !state.isExpanded)) } ) { Text( - text = stringResource(titleId), + text = stringResource(labelId), style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.primary, + color = MaterialTheme.colorScheme.primary ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = title.name, - style = MaterialTheme.typography.labelLarge, + text = state.language?.name ?: "", + style = MaterialTheme.typography.labelLarge ) DropdownMenu( - expanded = expanded, - onDismissRequest = { expand(false) } + expanded = state.isExpanded, + onDismissRequest = { onStateChange(state.copy(isExpanded = false)) } ) { - languages.forEach { language -> + availableLanguages.forEach { language -> DropdownMenuItem( text = { Text(language.name) }, onClick = { - expand(false) - onLanguageSelect(language) + onStateChange(state.copy(language = language, isExpanded = false)) } ) } } } + + TextField( + value = state.text, + onValueChange = { onStateChange(state.copy(text = it)) }, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 120.dp, max = 240.dp), + placeholder = { + Text(text = stringResource(hintId), style = MaterialTheme.typography.headlineSmall) + }, + textStyle = MaterialTheme.typography.headlineSmall, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ) + ) } diff --git a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationSideState.kt b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationSideState.kt new file mode 100644 index 000000000000..96a7e0cad782 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationSideState.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.assistant.translate + +import com.owncloud.android.lib.resources.assistant.v2.model.TranslationLanguage + +data class TranslationSideState( + val text: String = "", + val language: TranslationLanguage? = null, + val isExpanded: Boolean = false +) From d3c4155872324dd0c44569878450430035c06513 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 21 Jan 2026 14:23:20 +0100 Subject: [PATCH 07/25] add translation logic Signed-off-by: alperozturk96 --- .../client/assistant/AssistantViewModel.kt | 48 +++++++++++-------- .../remote/AssistantRemoteRepository.kt | 3 ++ .../remote/AssistantRemoteRepositoryImpl.kt | 7 +++ .../remote/MockAssistantRemoteRepository.kt | 5 +- 4 files changed, 42 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index 52052647fd53..52d7a8feb96e 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -16,6 +16,7 @@ import com.nextcloud.client.assistant.repository.remote.AssistantRemoteRepositor import com.nextcloud.utils.TimeConstants.MILLIS_PER_SECOND import com.nextcloud.utils.extensions.isHuman import com.owncloud.android.R +import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessage import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessageRequest @@ -188,32 +189,35 @@ class AssistantViewModel( } fun translate(textToTranslate: String, originLanguage: TranslationLanguage, targetLanguage: TranslationLanguage) { - val task = _selectedTaskType.value - if (task == null) { - _snackbarMessageId.update { - R.string.assistant_screen_select_task + viewModelScope.launch(Dispatchers.IO) { + val task = _selectedTaskType.value + if (task == null) { + _snackbarMessageId.update { + R.string.assistant_screen_select_task + } + return@launch } - return - } - val model = task.toTranslationModel() + val model = task.toTranslationModel() - if (model == null) { - _snackbarMessageId.update { - R.string.translation_screen_error_message + if (model == null) { + _snackbarMessageId.update { + R.string.translation_screen_error_message + } + return@launch } - return - } - val input = TranslationRequest( - input = textToTranslate, - originLanguage = originLanguage.code, - targetLanguage = targetLanguage.code, - maxTokens = model.maxTokens, - model = model.model - ).toJson() + val input = TranslationRequest( + input = textToTranslate, + originLanguage = originLanguage.code, + targetLanguage = targetLanguage.code, + maxTokens = model.maxTokens, + model = model.model + ) - createTask(input, task) + val result = remoteRepository.translate(input, task) + handleTaskCreation(result) + } } // region chat @@ -261,6 +265,10 @@ class AssistantViewModel( // region task fun createTask(input: String, taskType: TaskTypeData) = viewModelScope.launch(Dispatchers.IO) { val result = remoteRepository.createTask(input, taskType) + handleTaskCreation(result) + } + + private suspend fun handleTaskCreation(result: RemoteOperationResult<*>) { val message = if (result.isSuccess) { R.string.assistant_screen_task_create_success_message } else { diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepository.kt index 3eb48968bb78..8b1f2397d421 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepository.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepository.kt @@ -15,6 +15,7 @@ import com.owncloud.android.lib.resources.assistant.chat.model.Session import com.owncloud.android.lib.resources.assistant.chat.model.SessionTask import com.owncloud.android.lib.resources.assistant.v2.model.Task import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData +import com.owncloud.android.lib.resources.assistant.v2.model.TranslationRequest interface AssistantRemoteRepository { suspend fun getTaskTypes(): List? @@ -36,4 +37,6 @@ interface AssistantRemoteRepository { suspend fun generateSession(sessionId: String): SessionTask? suspend fun checkGeneration(taskId: String, sessionId: String): ChatMessage? + + suspend fun translate(input: TranslationRequest, taskType: TaskTypeData): RemoteOperationResult } diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt index 3030ecbe779d..923f689bd0e9 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt @@ -27,11 +27,13 @@ import com.owncloud.android.lib.resources.assistant.v1.GetTaskListRemoteOperatio import com.owncloud.android.lib.resources.assistant.v1.GetTaskTypesRemoteOperationV1 import com.owncloud.android.lib.resources.assistant.v1.model.toV2 import com.owncloud.android.lib.resources.assistant.v2.CreateTaskRemoteOperationV2 +import com.owncloud.android.lib.resources.assistant.v2.CreateTranslationTaskRemoteOperation import com.owncloud.android.lib.resources.assistant.v2.DeleteTaskRemoteOperationV2 import com.owncloud.android.lib.resources.assistant.v2.GetTaskListRemoteOperationV2 import com.owncloud.android.lib.resources.assistant.v2.GetTaskTypesRemoteOperationV2 import com.owncloud.android.lib.resources.assistant.v2.model.Task import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData +import com.owncloud.android.lib.resources.assistant.v2.model.TranslationRequest import com.owncloud.android.lib.resources.status.NextcloudVersion import com.owncloud.android.lib.resources.status.OCCapability import kotlinx.coroutines.Dispatchers @@ -124,4 +126,9 @@ class AssistantRemoteRepositoryImpl(private val client: NextcloudClient, capabil val result = CheckGenerationRemoteOperation(taskId, sessionId).execute(client) if (result.isSuccess) result.resultData else null } + + override suspend fun translate(input: TranslationRequest, taskType: TaskTypeData): RemoteOperationResult = + withContext(Dispatchers.IO) { + CreateTranslationTaskRemoteOperation(input, taskType).execute(client) + } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt index 930489812e85..e11ec5d1a873 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt @@ -18,6 +18,7 @@ import com.owncloud.android.lib.resources.assistant.v2.model.Task import com.owncloud.android.lib.resources.assistant.v2.model.TaskInput import com.owncloud.android.lib.resources.assistant.v2.model.TaskOutput import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData +import com.owncloud.android.lib.resources.assistant.v2.model.TranslationRequest @Suppress("MagicNumber") class MockAssistantRemoteRepository(private val giveEmptyTasks: Boolean = false) : AssistantRemoteRepository { @@ -68,7 +69,7 @@ class MockAssistantRemoteRepository(private val giveEmptyTasks: Boolean = false) } override suspend fun deleteTask(id: Long): RemoteOperationResult = - RemoteOperationResult(RemoteOperationResult.ResultCode.OK) + RemoteOperationResult(RemoteOperationResult.ResultCode.OK) override suspend fun fetchChatMessages(id: Long): List = emptyList() override suspend fun sendChatMessage(request: ChatMessageRequest): ChatMessage? = null override suspend fun createConversation(title: String): CreateConversation? = null @@ -76,4 +77,6 @@ class MockAssistantRemoteRepository(private val giveEmptyTasks: Boolean = false) override suspend fun generateSession(sessionId: String): SessionTask? = null override suspend fun checkGeneration(taskId: String, sessionId: String): ChatMessage? = null + override suspend fun translate(input: TranslationRequest, taskType: TaskTypeData): RemoteOperationResult = + RemoteOperationResult(RemoteOperationResult.ResultCode.OK) } From d36b9b140501e4e44d52a8ba02d8f95d41333953 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 21 Jan 2026 14:41:25 +0100 Subject: [PATCH 08/25] add translation logic Signed-off-by: alperozturk96 --- gradle/verification-metadata.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 4b5bc87fa745..24b51de1c4b4 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -20823,6 +20823,14 @@ + + + + + + + + From 664521cad4ded561f19da9edee844fc8ce03c1a5 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 21 Jan 2026 15:50:35 +0100 Subject: [PATCH 09/25] add translation task select Signed-off-by: alperozturk96 --- app/build.gradle.kts | 1 + .../client/assistant/AssistantScreen.kt | 15 +++++- .../client/assistant/AssistantViewModel.kt | 51 +++++++++++++------ .../assistant/model/AssistantScreenState.kt | 3 +- .../local/AssistantLocalRepository.kt | 4 +- .../local/AssistantLocalRepositoryImpl.kt | 8 +-- .../local/MockAssistantLocalRepository.kt | 5 +- .../client/assistant/task/TaskView.kt | 10 +++- .../assistant/translate/TranslationScreen.kt | 11 +++- .../client/database/dao/AssistantDao.kt | 4 +- gradle/libs.versions.toml | 1 + 11 files changed, 81 insertions(+), 32 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ef2466fd2d2b..8e3717619d62 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -347,6 +347,7 @@ dependencies { implementation(libs.compose.ui) implementation(libs.compose.ui.graphics) implementation(libs.compose.material3) + implementation(libs.compose.activity) implementation(libs.compose.ui.tooling.preview) implementation(libs.foundation) debugImplementation(libs.compose.ui.tooling) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index 531631266e6b..9086d09803dc 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -161,6 +161,7 @@ fun AssistantScreen( } }) } + AssistantPage.Content.id -> { Scaffold( modifier = Modifier.pullToRefresh( @@ -230,8 +231,15 @@ fun AssistantScreen( ) } - AssistantScreenState.Translation -> { - TranslationScreen(selectedTaskType, viewModel, selectedText ?: "") + is AssistantScreenState.Translation -> { + val task = (screenState as AssistantScreenState.Translation).task + val textToTranslate = task?.input?.input ?: selectedText ?: "" + + TranslationScreen( + selectedTaskType, + viewModel, + textToTranslate + ) } else -> EmptyContent( @@ -380,6 +388,9 @@ private fun TaskContent( showTaskActions = { val newState = ScreenOverlayState.TaskActions(task) viewModel.updateScreenOverlayState(newState) + }, + showTranslateScreen = { + viewModel.updateScreenState(AssistantScreenState.Translation(it)) } ) Spacer(modifier = Modifier.height(8.dp)) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index 52d7a8feb96e..0237b78356e3 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -127,12 +127,13 @@ class AssistantViewModel( // endregion private suspend fun pollTaskList() { - val cachedTasks = localRepository.getCachedTasks(accountName) + val taskType = _selectedTaskType.value?.id ?: return + + val cachedTasks = localRepository.getCachedTasks(accountName, taskType) if (cachedTasks.isNotEmpty()) { _filteredTaskList.value = cachedTasks.sortedByDescending { it.id } } - val taskType = _selectedTaskType.value?.id ?: return val result = remoteRepository.getTaskList(taskType) if (result != null) { taskList = result @@ -172,11 +173,9 @@ class AssistantViewModel( _filteredTaskList ) { selectedTask, chats, tasks -> val isChat = selectedTask?.isChat() == true - val isTranslation = selectedTask?.isTranslate() == true when { selectedTask == null -> AssistantScreenState.Loading - isTranslation -> AssistantScreenState.Translation isChat && chats.isEmpty() -> AssistantScreenState.emptyChatList() isChat -> AssistantScreenState.ChatContent !isChat && (tasks == null || tasks.isEmpty()) -> AssistantScreenState.emptyTaskList() @@ -282,17 +281,27 @@ class AssistantViewModel( fun selectTaskType(task: TaskTypeData) { Log_OC.d(TAG, "Task type changed: ${task.name}, session id: ${_sessionId.value}") + + // clear task list immediately when task type change + if (_selectedTaskType.value != task) { + _filteredTaskList.update { + listOf() + } + } + updateTaskType(task) + if (!task.isChat()) { + fetchTaskList() + return + } + + // only task chat type needs to be handled differently val sessionId = _sessionId.value ?: return - if (task.isChat()) { - if (_chatMessages.value.isEmpty()) { - fetchChatMessages(sessionId) - } else { - fetchNewChatMessage(sessionId) - } + if (_chatMessages.value.isEmpty()) { + fetchChatMessages(sessionId) } else { - fetchTaskList() + fetchNewChatMessage(sessionId) } } @@ -310,12 +319,16 @@ class AssistantViewModel( } fun fetchTaskList() = viewModelScope.launch(Dispatchers.IO) { - val cached = localRepository.getCachedTasks(accountName) + val taskType = _selectedTaskType.value ?: return@launch + + val cached = localRepository.getCachedTasks(accountName, taskType.name) if (cached.isNotEmpty()) { - _filteredTaskList.value = cached.sortedByDescending { it.id } + _filteredTaskList.update { + cached.sortedByDescending { it.id } + } } - _selectedTaskType.value?.id?.let { typeId -> + taskType.id?.let { typeId -> remoteRepository.getTaskList(typeId)?.let { result -> taskList = result _filteredTaskList.value = result.sortedByDescending { it.id } @@ -334,9 +347,11 @@ class AssistantViewModel( } updateSnackbarMessage(message) + + val taskType = _selectedTaskType.value ?: return@launch if (result.isSuccess) { removeTaskFromList(id) - localRepository.deleteTask(id, accountName) + localRepository.deleteTask(id, accountName, taskType.name) } } // endregion @@ -365,6 +380,12 @@ class AssistantViewModel( } } + fun updateScreenState(state: AssistantScreenState) { + _screenState.update { + state + } + } + private fun removeTaskFromList(id: Long) { _filteredTaskList.update { currentList -> currentList?.filter { it.id != id } diff --git a/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt b/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt index eb9b508c9e48..8f0ad7bfd011 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt @@ -8,6 +8,7 @@ package com.nextcloud.client.assistant.model import com.owncloud.android.R +import com.owncloud.android.lib.resources.assistant.v2.model.Task sealed class AssistantScreenState { data object Loading : AssistantScreenState() @@ -16,7 +17,7 @@ sealed class AssistantScreenState { data object ChatContent : AssistantScreenState() - data object Translation : AssistantScreenState() + data class Translation(val task: Task?) : AssistantScreenState() data class EmptyContent(val iconId: Int?, val titleId: Int?, val descriptionId: Int?) : AssistantScreenState() diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/local/AssistantLocalRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/local/AssistantLocalRepository.kt index 070c0c74a9c1..76568959583e 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/local/AssistantLocalRepository.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/local/AssistantLocalRepository.kt @@ -11,7 +11,7 @@ import com.owncloud.android.lib.resources.assistant.v2.model.Task interface AssistantLocalRepository { suspend fun cacheTasks(tasks: List, accountName: String) - suspend fun getCachedTasks(accountName: String): List + suspend fun getCachedTasks(accountName: String, type: String): List suspend fun insertTask(task: Task, accountName: String) - suspend fun deleteTask(id: Long, accountName: String) + suspend fun deleteTask(id: Long, accountName: String, type: String) } diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/local/AssistantLocalRepositoryImpl.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/local/AssistantLocalRepositoryImpl.kt index ef6ba9365606..70fb3398cee9 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/local/AssistantLocalRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/local/AssistantLocalRepositoryImpl.kt @@ -20,8 +20,8 @@ class AssistantLocalRepositoryImpl(private val assistantDao: AssistantDao) : Ass assistantDao.insertAssistantTasks(entities) } - override suspend fun getCachedTasks(accountName: String): List { - val entities = assistantDao.getAssistantTasksByAccount(accountName) + override suspend fun getCachedTasks(accountName: String, type: String): List { + val entities = assistantDao.getAssistantTasksByAccount(accountName, type) return entities.map { it.toTask() } } @@ -29,8 +29,8 @@ class AssistantLocalRepositoryImpl(private val assistantDao: AssistantDao) : Ass assistantDao.insertAssistantTask(task.toEntity(accountName)) } - override suspend fun deleteTask(id: Long, accountName: String) { - val cached = assistantDao.getAssistantTasksByAccount(accountName).firstOrNull { it.id == id } ?: return + override suspend fun deleteTask(id: Long, accountName: String, type: String) { + val cached = assistantDao.getAssistantTasksByAccount(accountName, type).firstOrNull { it.id == id } ?: return assistantDao.deleteAssistantTask(cached) } diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/local/MockAssistantLocalRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/local/MockAssistantLocalRepository.kt index c09065a5b867..231534a674a6 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/local/MockAssistantLocalRepository.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/local/MockAssistantLocalRepository.kt @@ -23,13 +23,14 @@ class MockAssistantLocalRepository : AssistantLocalRepository { } } - override suspend fun getCachedTasks(accountName: String): List = mutex.withLock { tasks.toList() } + override suspend fun getCachedTasks(accountName: String, type: String): List = + mutex.withLock { tasks.toList() } override suspend fun insertTask(task: Task, accountName: String) { mutex.withLock { tasks.add(task) } } - override suspend fun deleteTask(id: Long, accountName: String) { + override suspend fun deleteTask(id: Long, accountName: String, type: String) { mutex.withLock { tasks.removeAll { it.id == id } } } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt b/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt index 7cefa122fb9c..63c1ffad1a32 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt @@ -49,7 +49,7 @@ import com.owncloud.android.lib.resources.status.OCCapability @Suppress("LongMethod", "MagicNumber") @Composable -fun TaskView(task: Task, capability: OCCapability, showTaskActions: () -> Unit) { +fun TaskView(task: Task, capability: OCCapability, showTaskActions: () -> Unit, showTranslateScreen: (Task) -> Unit) { var showTaskDetailBottomSheet by remember { mutableStateOf(false) } Box { @@ -59,7 +59,11 @@ fun TaskView(task: Task, capability: OCCapability, showTaskActions: () -> Unit) .clip(RoundedCornerShape(8.dp)) .background(color = colorResource(R.color.task_container)) .clickable { - showTaskDetailBottomSheet = true + if (task.type == "core:text2text:translate") { + showTranslateScreen(task) + } else { + showTaskDetailBottomSheet = true + } } .padding(16.dp) ) { @@ -146,6 +150,8 @@ private fun TaskViewPreview() { versionMayor = 30 }, showTaskActions = { + }, + showTranslateScreen = { } ) } diff --git a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt index ed3df1e028e6..03206fb42261 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt @@ -7,6 +7,7 @@ package com.nextcloud.client.assistant.translate +import androidx.activity.compose.BackHandler import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -38,15 +39,17 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.nextcloud.client.assistant.AssistantViewModel +import com.nextcloud.client.assistant.model.AssistantScreenState import com.owncloud.android.R import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData import com.owncloud.android.lib.resources.assistant.v2.model.TranslationLanguage import com.owncloud.android.lib.resources.assistant.v2.model.toTranslationLanguages +@Suppress("LongMethod") @OptIn(ExperimentalMaterial3Api::class) @Composable -fun TranslationScreen(task: TaskTypeData?, viewModel: AssistantViewModel, textToTranslate: String) { - val languages = remember(task) { task?.toTranslationLanguages() } +fun TranslationScreen(selectedTaskType: TaskTypeData?, viewModel: AssistantViewModel, textToTranslate: String) { + val languages = remember(selectedTaskType) { selectedTaskType?.toTranslationLanguages() } var sourceState by remember { mutableStateOf( @@ -60,6 +63,10 @@ fun TranslationScreen(task: TaskTypeData?, viewModel: AssistantViewModel, textTo mutableStateOf(TranslationSideState(language = languages?.targetLanguages?.firstOrNull())) } + BackHandler { + viewModel.updateScreenState(AssistantScreenState.TaskContent) + } + Scaffold( modifier = Modifier .fillMaxSize() diff --git a/app/src/main/java/com/nextcloud/client/database/dao/AssistantDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/AssistantDao.kt index 9ba9012ea2a6..f674ae4f3c6c 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/AssistantDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/AssistantDao.kt @@ -33,9 +33,9 @@ interface AssistantDao { @Query( """ SELECT * FROM ${ProviderMeta.ProviderTableMeta.ASSISTANT_TABLE_NAME} - WHERE accountName = :accountName + WHERE accountName = :accountName AND type = :taskType ORDER BY lastUpdated DESC """ ) - suspend fun getAssistantTasksByAccount(accountName: String): List + suspend fun getAssistantTasksByAccount(accountName: String, taskType: String): List } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5c9a358fab26..0d616a6e3279 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -182,6 +182,7 @@ compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } compose-material3 = { module = "androidx.compose.material3:material3" } +compose-activity = { module = "androidx.activity:activity-compose" } # Media3 media3-datasource = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3" } From 814a5df0488146e79a7d8092a46098176af8ea39 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 21 Jan 2026 16:04:00 +0100 Subject: [PATCH 10/25] fix layout update Signed-off-by: alperozturk96 --- .../client/assistant/AssistantScreen.kt | 6 ++---- .../client/assistant/AssistantViewModel.kt | 19 ++++++++++++++++--- .../client/assistant/task/TaskView.kt | 12 ++++++++---- .../assistant/translate/TranslationScreen.kt | 13 ++++++++++++- app/src/main/res/values/strings.xml | 2 +- 5 files changed, 39 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index 9086d09803dc..256e42388fdd 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -384,13 +384,11 @@ private fun TaskContent( items(taskList, key = { it.id }) { task -> TaskView( task, + viewModel, capability, showTaskActions = { val newState = ScreenOverlayState.TaskActions(task) viewModel.updateScreenOverlayState(newState) - }, - showTranslateScreen = { - viewModel.updateScreenState(AssistantScreenState.Translation(it)) } ) Spacer(modifier = Modifier.height(8.dp)) @@ -484,7 +482,7 @@ private fun getMockConversationViewModel(): ConversationViewModel { ) } -private fun getMockAssistantViewModel(giveEmptyTasks: Boolean): AssistantViewModel { +fun getMockAssistantViewModel(giveEmptyTasks: Boolean): AssistantViewModel { val mockLocalRepository = MockAssistantLocalRepository() val mockRemoteRepository = MockAssistantRemoteRepository(giveEmptyTasks) return AssistantViewModel( diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index 0237b78356e3..10b958ad3068 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -63,6 +63,9 @@ class AssistantViewModel( private val _snackbarMessageId = MutableStateFlow(null) val snackbarMessageId: StateFlow = _snackbarMessageId + private val _selectedTask = MutableStateFlow(null) + val selectedTask: StateFlow = _selectedTask + private val _selectedTaskType = MutableStateFlow(null) val selectedTaskType: StateFlow = _selectedTaskType @@ -168,14 +171,18 @@ class AssistantViewModel( private fun observeScreenState() { viewModelScope.launch { combine( + _selectedTask, _selectedTaskType, _chatMessages, _filteredTaskList - ) { selectedTask, chats, tasks -> - val isChat = selectedTask?.isChat() == true + ) { selectedTask, selectedTaskType, chats, tasks -> + val isChat = selectedTaskType?.isChat() == true + val isTranslation = + selectedTaskType?.isTranslate() == true && selectedTask?.type == "core:text2text:translate" when { - selectedTask == null -> AssistantScreenState.Loading + selectedTaskType == null -> AssistantScreenState.Loading + isTranslation -> AssistantScreenState.Translation(selectedTask) isChat && chats.isEmpty() -> AssistantScreenState.emptyChatList() isChat -> AssistantScreenState.ChatContent !isChat && (tasks == null || tasks.isEmpty()) -> AssistantScreenState.emptyTaskList() @@ -362,6 +369,12 @@ class AssistantViewModel( } } + fun selectTask(task: Task) { + _selectedTask.update { + task + } + } + fun updateSnackbarMessage(value: Int?) { _snackbarMessageId.update { value diff --git a/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt b/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt index 63c1ffad1a32..b8c9b4ee0541 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt @@ -39,6 +39,9 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.nextcloud.client.assistant.AssistantViewModel +import com.nextcloud.client.assistant.getMockAssistantViewModel +import com.nextcloud.client.assistant.model.AssistantScreenState import com.nextcloud.client.assistant.taskDetail.TaskDetailBottomSheet import com.nextcloud.utils.extensions.truncateWithEllipsis import com.owncloud.android.R @@ -49,7 +52,7 @@ import com.owncloud.android.lib.resources.status.OCCapability @Suppress("LongMethod", "MagicNumber") @Composable -fun TaskView(task: Task, capability: OCCapability, showTaskActions: () -> Unit, showTranslateScreen: (Task) -> Unit) { +fun TaskView(task: Task, viewModel: AssistantViewModel, capability: OCCapability, showTaskActions: () -> Unit) { var showTaskDetailBottomSheet by remember { mutableStateOf(false) } Box { @@ -59,8 +62,10 @@ fun TaskView(task: Task, capability: OCCapability, showTaskActions: () -> Unit, .clip(RoundedCornerShape(8.dp)) .background(color = colorResource(R.color.task_container)) .clickable { + viewModel.selectTask(task) + if (task.type == "core:text2text:translate") { - showTranslateScreen(task) + viewModel.updateScreenState(AssistantScreenState.Translation(task)) } else { showTaskDetailBottomSheet = true } @@ -146,12 +151,11 @@ private fun TaskViewPreview() { 1707692337, 1707692337 ), + viewModel = getMockAssistantViewModel(true), OCCapability().apply { versionMayor = 30 }, showTaskActions = { - }, - showTranslateScreen = { } ) } diff --git a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt index 03206fb42261..61b6c15d7bad 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.DropdownMenu @@ -33,6 +34,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource @@ -127,7 +129,8 @@ private fun TranslationSection( Row( modifier = Modifier .padding(16.dp) - .clickable { onStateChange(state.copy(isExpanded = !state.isExpanded)) } + .clickable { onStateChange(state.copy(isExpanded = !state.isExpanded)) }, + verticalAlignment = Alignment.CenterVertically ) { Text( text = stringResource(labelId), @@ -139,6 +142,14 @@ private fun TranslationSection( text = state.language?.name ?: "", style = MaterialTheme.typography.labelLarge ) + Icon( + painter = painterResource(R.drawable.ic_baseline_arrow_drop_down_24), + contentDescription = "dropdown icon", + modifier = Modifier + .padding(start = 4.dp) + .size(16.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) DropdownMenu( expanded = state.isExpanded, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cef263cc863d..9ce215b888b7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -81,7 +81,7 @@ failed - Source language: + Translate from: Translate to: Enter text to translate… Translation will appear here… From 1508f93c714ff7540f310d061a45be8f567c652e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 21 Jan 2026 16:08:57 +0100 Subject: [PATCH 11/25] fix layout update Signed-off-by: alperozturk96 --- .../com/nextcloud/client/assistant/AssistantScreen.kt | 9 +++++++-- .../client/assistant/translate/TranslationScreen.kt | 7 ++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index 256e42388fdd..3bcbe1064ee1 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -192,7 +192,7 @@ fun AssistantScreen( } }, bottomBar = { - if (!taskTypes.isNullOrEmpty() && selectedTaskType?.isTranslate() == false) { + if (!taskTypes.isNullOrEmpty()) { InputBar( sessionId, selectedTaskType, @@ -317,7 +317,12 @@ private fun InputBar(sessionId: Long?, selectedTaskType: TaskTypeData?, viewMode viewModel.createConversation(text) } } else { - viewModel.createTask(input = text, taskType = taskType) + if (taskType.isTranslate()) { + // TODO: + viewModel.translate() + } else { + viewModel.createTask(input = text, taskType = taskType) + } } scope.launch { diff --git a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt index 61b6c15d7bad..aaf6e569875d 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.nextcloud.client.assistant.AssistantViewModel import com.nextcloud.client.assistant.model.AssistantScreenState @@ -76,6 +77,7 @@ fun TranslationScreen(selectedTaskType: TaskTypeData?, viewModel: AssistantViewM .padding(top = 32.dp), floatingActionButton = { FloatingActionButton(onClick = { + // TODO: val originLang = sourceState.language val targetLang = targetState.language if (originLang != null && targetLang != null) { @@ -93,6 +95,7 @@ fun TranslationScreen(selectedTaskType: TaskTypeData?, viewModel: AssistantViewM hintId = R.string.translation_screen_hint_source, state = sourceState, availableLanguages = languages?.originLanguages ?: emptyList(), + maxDp = 120.dp, onStateChange = { sourceState = it } ) } @@ -111,6 +114,7 @@ fun TranslationScreen(selectedTaskType: TaskTypeData?, viewModel: AssistantViewM hintId = R.string.translation_screen_hint_target, state = targetState, availableLanguages = languages?.targetLanguages ?: emptyList(), + maxDp = Dp.Unspecified, onStateChange = { targetState = it } ) } @@ -124,6 +128,7 @@ private fun TranslationSection( hintId: Int, state: TranslationSideState, availableLanguages: List, + maxDp: Dp, onStateChange: (TranslationSideState) -> Unit ) { Row( @@ -171,7 +176,7 @@ private fun TranslationSection( onValueChange = { onStateChange(state.copy(text = it)) }, modifier = Modifier .fillMaxWidth() - .heightIn(min = 120.dp, max = 240.dp), + .heightIn(min = 120.dp, max = maxDp), placeholder = { Text(text = stringResource(hintId), style = MaterialTheme.typography.headlineSmall) }, From 3ecd1b7137a1d67e94e30b8d66123944e0d48f32 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 22 Jan 2026 10:44:11 +0100 Subject: [PATCH 12/25] handle task select Signed-off-by: alperozturk96 --- .../com/nextcloud/client/assistant/AssistantScreen.kt | 2 +- .../com/nextcloud/client/assistant/AssistantViewModel.kt | 4 ++-- .../java/com/nextcloud/client/assistant/task/TaskView.kt | 4 +++- .../client/assistant/translate/TranslationScreen.kt | 9 +++++++++ gradle/verification-metadata.xml | 8 ++++++++ 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index 3bcbe1064ee1..a2f2858ebd0f 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -319,7 +319,7 @@ private fun InputBar(sessionId: Long?, selectedTaskType: TaskTypeData?, viewMode } else { if (taskType.isTranslate()) { // TODO: - viewModel.translate() + // viewModel.translate() } else { viewModel.createTask(input = text, taskType = taskType) } diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index 10b958ad3068..5b43a1ca2c03 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -178,7 +178,7 @@ class AssistantViewModel( ) { selectedTask, selectedTaskType, chats, tasks -> val isChat = selectedTaskType?.isChat() == true val isTranslation = - selectedTaskType?.isTranslate() == true && selectedTask?.type == "core:text2text:translate" + selectedTaskType?.isTranslate() == true && selectedTask?.isTranslate() == true when { selectedTaskType == null -> AssistantScreenState.Loading @@ -369,7 +369,7 @@ class AssistantViewModel( } } - fun selectTask(task: Task) { + fun selectTask(task: Task?) { _selectedTask.update { task } diff --git a/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt b/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt index b8c9b4ee0541..652986c7b11a 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt @@ -64,7 +64,7 @@ fun TaskView(task: Task, viewModel: AssistantViewModel, capability: OCCapability .clickable { viewModel.selectTask(task) - if (task.type == "core:text2text:translate") { + if (task.isTranslate()) { viewModel.updateScreenState(AssistantScreenState.Translation(task)) } else { showTaskDetailBottomSheet = true @@ -111,6 +111,8 @@ fun TaskView(task: Task, viewModel: AssistantViewModel, capability: OCCapability showTaskDetailBottomSheet = false showTaskActions() }) { + // task is unselected + viewModel.selectTask(null) showTaskDetailBottomSheet = false } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt index aaf6e569875d..2e91b0cdf8a6 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt @@ -30,6 +30,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -67,9 +68,17 @@ fun TranslationScreen(selectedTaskType: TaskTypeData?, viewModel: AssistantViewM } BackHandler { + viewModel.selectTask(null) viewModel.updateScreenState(AssistantScreenState.TaskContent) } + // task is unselected + DisposableEffect(Unit) { + onDispose { + viewModel.selectTask(null) + } + } + Scaffold( modifier = Modifier .fillMaxSize() diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 24b51de1c4b4..d567b861b812 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -21591,6 +21591,14 @@ + + + + + + + + From 2644578039835aa55a0bc30d48a2245a7272efe7 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 22 Jan 2026 10:48:06 +0100 Subject: [PATCH 13/25] handle task select Signed-off-by: alperozturk96 --- .../java/com/nextcloud/client/assistant/AssistantViewModel.kt | 2 +- .../main/java/com/nextcloud/client/assistant/task/TaskView.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index 5b43a1ca2c03..f99ac7722261 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -178,7 +178,7 @@ class AssistantViewModel( ) { selectedTask, selectedTaskType, chats, tasks -> val isChat = selectedTaskType?.isChat() == true val isTranslation = - selectedTaskType?.isTranslate() == true && selectedTask?.isTranslate() == true + selectedTaskType?.isTranslate() == true && selectedTask?.type == "core:text2text:translate" when { selectedTaskType == null -> AssistantScreenState.Loading diff --git a/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt b/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt index 652986c7b11a..5f50229f4a39 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt @@ -64,7 +64,7 @@ fun TaskView(task: Task, viewModel: AssistantViewModel, capability: OCCapability .clickable { viewModel.selectTask(task) - if (task.isTranslate()) { + if (task.type == "core:text2text:translate") { viewModel.updateScreenState(AssistantScreenState.Translation(task)) } else { showTaskDetailBottomSheet = true From 673091bf45ec93f4396761cb940080e0eda64a2f Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 22 Jan 2026 15:21:43 +0100 Subject: [PATCH 14/25] fix translation flow Signed-off-by: alperozturk96 --- .../client/assistant/AssistantScreen.kt | 29 ++++++++++++++----- .../client/assistant/AssistantViewModel.kt | 17 ++++++++++- .../client/assistant/task/TaskView.kt | 1 + .../assistant/translate/TranslationScreen.kt | 3 +- 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index a2f2858ebd0f..010d91625b75 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator @@ -96,6 +97,7 @@ fun AssistantScreen( val messageId by viewModel.snackbarMessageId.collectAsState() val screenOverlayState by viewModel.screenOverlayState.collectAsState() val selectedTaskType by viewModel.selectedTaskType.collectAsState() + val isTranslationTask by viewModel.isTranslationTask.collectAsState() val filteredTaskList by viewModel.filteredTaskList.collectAsState() val screenState by viewModel.screenState.collectAsState() val taskTypes by viewModel.taskTypes.collectAsState() @@ -192,7 +194,7 @@ fun AssistantScreen( } }, bottomBar = { - if (!taskTypes.isNullOrEmpty()) { + if (!taskTypes.isNullOrEmpty() && selectedTaskType?.isTranslate() != true) { InputBar( sessionId, selectedTaskType, @@ -202,6 +204,24 @@ fun AssistantScreen( }, snackbarHost = { SnackbarHost(snackbarHostState) + }, + floatingActionButton = { + if (selectedTaskType?.isTranslate() == true && !isTranslationTask) { + FloatingActionButton(onClick = { + viewModel.updateTranslationTaskState(true) + viewModel.updateScreenState(AssistantScreenState.Translation(null)) + }, content = { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(16.dp) + ) { + Icon( + painter = painterResource(R.drawable.ic_plus), + contentDescription = "translate button" + ) + } + }) + } } ) { paddingValues -> when (screenState) { @@ -317,12 +337,7 @@ private fun InputBar(sessionId: Long?, selectedTaskType: TaskTypeData?, viewMode viewModel.createConversation(text) } } else { - if (taskType.isTranslate()) { - // TODO: - // viewModel.translate() - } else { - viewModel.createTask(input = text, taskType = taskType) - } + viewModel.createTask(input = text, taskType = taskType) } scope.launch { diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index f99ac7722261..e81e40255229 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -63,6 +63,9 @@ class AssistantViewModel( private val _snackbarMessageId = MutableStateFlow(null) val snackbarMessageId: StateFlow = _snackbarMessageId + private val _isTranslationTask = MutableStateFlow(false) + val isTranslationTask: StateFlow = _isTranslationTask + private val _selectedTask = MutableStateFlow(null) val selectedTask: StateFlow = _selectedTask @@ -186,7 +189,13 @@ class AssistantViewModel( isChat && chats.isEmpty() -> AssistantScreenState.emptyChatList() isChat -> AssistantScreenState.ChatContent !isChat && (tasks == null || tasks.isEmpty()) -> AssistantScreenState.emptyTaskList() - else -> AssistantScreenState.TaskContent + else -> { + if (!_isTranslationTask.value) { + AssistantScreenState.TaskContent + } else { + _screenState.value + } + } } }.collect { newState -> _screenState.value = newState @@ -399,6 +408,12 @@ class AssistantViewModel( } } + fun updateTranslationTaskState(value: Boolean) { + _isTranslationTask.update { + value + } + } + private fun removeTaskFromList(id: Long) { _filteredTaskList.update { currentList -> currentList?.filter { it.id != id } diff --git a/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt b/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt index 5f50229f4a39..e94a3336c67b 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt @@ -65,6 +65,7 @@ fun TaskView(task: Task, viewModel: AssistantViewModel, capability: OCCapability viewModel.selectTask(task) if (task.type == "core:text2text:translate") { + viewModel.updateTranslationTaskState(true) viewModel.updateScreenState(AssistantScreenState.Translation(task)) } else { showTaskDetailBottomSheet = true diff --git a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt index 2e91b0cdf8a6..c1e79bd142d8 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt @@ -68,6 +68,7 @@ fun TranslationScreen(selectedTaskType: TaskTypeData?, viewModel: AssistantViewM } BackHandler { + viewModel.updateTranslationTaskState(false) viewModel.selectTask(null) viewModel.updateScreenState(AssistantScreenState.TaskContent) } @@ -75,6 +76,7 @@ fun TranslationScreen(selectedTaskType: TaskTypeData?, viewModel: AssistantViewM // task is unselected DisposableEffect(Unit) { onDispose { + viewModel.updateTranslationTaskState(false) viewModel.selectTask(null) } } @@ -86,7 +88,6 @@ fun TranslationScreen(selectedTaskType: TaskTypeData?, viewModel: AssistantViewM .padding(top = 32.dp), floatingActionButton = { FloatingActionButton(onClick = { - // TODO: val originLang = sourceState.language val targetLang = targetState.language if (originLang != null && targetLang != null) { From 8cd39cfb100e6a38b5a888e5ed4e05e087dd947e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 22 Jan 2026 15:38:10 +0100 Subject: [PATCH 15/25] fix translation flow Signed-off-by: alperozturk96 --- .../assistant/translate/TranslationScreen.kt | 23 +++++++++++++++++-- .../translate/TranslationSideState.kt | 3 ++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt index c1e79bd142d8..2c9db41d59af 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -38,16 +39,19 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.nextcloud.client.assistant.AssistantViewModel import com.nextcloud.client.assistant.model.AssistantScreenState +import com.nextcloud.utils.extensions.getActivity import com.owncloud.android.R import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData import com.owncloud.android.lib.resources.assistant.v2.model.TranslationLanguage import com.owncloud.android.lib.resources.assistant.v2.model.toTranslationLanguages +import com.owncloud.android.utils.ClipboardUtil @Suppress("LongMethod") @OptIn(ExperimentalMaterial3Api::class) @@ -59,12 +63,13 @@ fun TranslationScreen(selectedTaskType: TaskTypeData?, viewModel: AssistantViewM mutableStateOf( TranslationSideState( text = textToTranslate, - language = languages?.originLanguages?.firstOrNull() + language = languages?.originLanguages?.firstOrNull(), + isTarget = false ) ) } var targetState by remember { - mutableStateOf(TranslationSideState(language = languages?.targetLanguages?.firstOrNull())) + mutableStateOf(TranslationSideState(language = languages?.targetLanguages?.firstOrNull(), isTarget = true)) } BackHandler { @@ -132,6 +137,7 @@ fun TranslationScreen(selectedTaskType: TaskTypeData?, viewModel: AssistantViewM } } +@Suppress("LongMethod") @Composable private fun TranslationSection( labelId: Int, @@ -141,6 +147,8 @@ private fun TranslationSection( maxDp: Dp, onStateChange: (TranslationSideState) -> Unit ) { + val activity = LocalContext.current.getActivity() + Row( modifier = Modifier .padding(16.dp) @@ -179,11 +187,22 @@ private fun TranslationSection( ) } } + + if (state.isTarget && state.text.isNotBlank()) { + Spacer(modifier = Modifier.weight(1f)) + + IconButton(onClick = { + activity?.let { ClipboardUtil.copyToClipboard(it, state.text, true) } + }) { + Icon(painter = painterResource(R.drawable.ic_content_copy), contentDescription = "copy button") + } + } } TextField( value = state.text, onValueChange = { onStateChange(state.copy(text = it)) }, + readOnly = state.isTarget, modifier = Modifier .fillMaxWidth() .heightIn(min = 120.dp, max = maxDp), diff --git a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationSideState.kt b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationSideState.kt index 96a7e0cad782..b39e8dab7378 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationSideState.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationSideState.kt @@ -12,5 +12,6 @@ import com.owncloud.android.lib.resources.assistant.v2.model.TranslationLanguage data class TranslationSideState( val text: String = "", val language: TranslationLanguage? = null, - val isExpanded: Boolean = false + val isExpanded: Boolean = false, + val isTarget: Boolean ) From 9df7a363be3f520b24fad36eaef039e7583db6a3 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 22 Jan 2026 15:49:17 +0100 Subject: [PATCH 16/25] fix translation flow Signed-off-by: alperozturk96 --- .../com/nextcloud/client/assistant/AssistantViewModel.kt | 2 +- .../java/com/nextcloud/client/assistant/task/TaskView.kt | 2 +- gradle/verification-metadata.xml | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index e81e40255229..1757ebe56b8d 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -181,7 +181,7 @@ class AssistantViewModel( ) { selectedTask, selectedTaskType, chats, tasks -> val isChat = selectedTaskType?.isChat() == true val isTranslation = - selectedTaskType?.isTranslate() == true && selectedTask?.type == "core:text2text:translate" + selectedTaskType?.isTranslate() == true && selectedTask?.isTranslate() == true when { selectedTaskType == null -> AssistantScreenState.Loading diff --git a/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt b/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt index e94a3336c67b..508839ef10c0 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt @@ -64,7 +64,7 @@ fun TaskView(task: Task, viewModel: AssistantViewModel, capability: OCCapability .clickable { viewModel.selectTask(task) - if (task.type == "core:text2text:translate") { + if (task.isTranslate()) { viewModel.updateTranslationTaskState(true) viewModel.updateScreenState(AssistantScreenState.Translation(task)) } else { diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index d567b861b812..f0971229ee5d 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -20751,6 +20751,14 @@ + + + + + + + + From 922324271dc4264296f223ce4ee0954624014210 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 23 Jan 2026 16:08:42 +0100 Subject: [PATCH 17/25] better ui ux Signed-off-by: alperozturk96 --- .../client/assistant/AssistantScreen.kt | 3 +- .../client/assistant/AssistantViewModel.kt | 33 ++++-- .../assistant/translate/TranslationScreen.kt | 100 +++++++++++++----- gradle/verification-metadata.xml | 8 ++ 4 files changed, 108 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index 010d91625b75..62852e39399d 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -258,7 +258,8 @@ fun AssistantScreen( TranslationScreen( selectedTaskType, viewModel, - textToTranslate + textToTranslate, + isTaskExists = (task != null) ) } diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index 1757ebe56b8d..c8fb0fb2058e 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -16,7 +16,6 @@ import com.nextcloud.client.assistant.repository.remote.AssistantRemoteRepositor import com.nextcloud.utils.TimeConstants.MILLIS_PER_SECOND import com.nextcloud.utils.extensions.isHuman import com.owncloud.android.R -import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessage import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessageRequest @@ -66,8 +65,10 @@ class AssistantViewModel( private val _isTranslationTask = MutableStateFlow(false) val isTranslationTask: StateFlow = _isTranslationTask - private val _selectedTask = MutableStateFlow(null) - val selectedTask: StateFlow = _selectedTask + private val _isTranslationTaskCreated = MutableStateFlow(false) + val isTranslationTaskCreated: StateFlow = _isTranslationTaskCreated + + private val selectedTask = MutableStateFlow(null) private val _selectedTaskType = MutableStateFlow(null) val selectedTaskType: StateFlow = _selectedTaskType @@ -174,7 +175,7 @@ class AssistantViewModel( private fun observeScreenState() { viewModelScope.launch { combine( - _selectedTask, + selectedTask, _selectedTaskType, _chatMessages, _filteredTaskList @@ -231,7 +232,11 @@ class AssistantViewModel( ) val result = remoteRepository.translate(input, task) - handleTaskCreation(result) + if (result.isSuccess) { + _isTranslationTaskCreated.update { + true + } + } } } @@ -280,10 +285,6 @@ class AssistantViewModel( // region task fun createTask(input: String, taskType: TaskTypeData) = viewModelScope.launch(Dispatchers.IO) { val result = remoteRepository.createTask(input, taskType) - handleTaskCreation(result) - } - - private suspend fun handleTaskCreation(result: RemoteOperationResult<*>) { val message = if (result.isSuccess) { R.string.assistant_screen_task_create_success_message } else { @@ -379,7 +380,7 @@ class AssistantViewModel( } fun selectTask(task: Task?) { - _selectedTask.update { + selectedTask.update { task } } @@ -414,6 +415,18 @@ class AssistantViewModel( } } + fun updateTranslationTaskCreation(value: Boolean) { + _isTranslationTaskCreated.update { + value + } + } + + fun onTranslationScreenDismissed() { + updateTranslationTaskCreation(false) + updateTranslationTaskState(false) + selectTask(null) + } + private fun removeTaskFromList(id: Long) { _filteredTaskList.update { currentList -> currentList?.filter { it.id != id } diff --git a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt index 2c9db41d59af..6cfe7ea93696 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt @@ -8,7 +8,13 @@ package com.nextcloud.client.assistant.translate import androidx.activity.compose.BackHandler +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -32,6 +38,7 @@ import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -56,9 +63,14 @@ import com.owncloud.android.utils.ClipboardUtil @Suppress("LongMethod") @OptIn(ExperimentalMaterial3Api::class) @Composable -fun TranslationScreen(selectedTaskType: TaskTypeData?, viewModel: AssistantViewModel, textToTranslate: String) { +fun TranslationScreen( + selectedTaskType: TaskTypeData?, + viewModel: AssistantViewModel, + textToTranslate: String, + isTaskExists: Boolean +) { val languages = remember(selectedTaskType) { selectedTaskType?.toTranslationLanguages() } - + val isTranslationTaskCreated by viewModel.isTranslationTaskCreated.collectAsState() var sourceState by remember { mutableStateOf( TranslationSideState( @@ -73,16 +85,14 @@ fun TranslationScreen(selectedTaskType: TaskTypeData?, viewModel: AssistantViewM } BackHandler { - viewModel.updateTranslationTaskState(false) - viewModel.selectTask(null) + viewModel.onTranslationScreenDismissed() viewModel.updateScreenState(AssistantScreenState.TaskContent) } // task is unselected DisposableEffect(Unit) { onDispose { - viewModel.updateTranslationTaskState(false) - viewModel.selectTask(null) + viewModel.onTranslationScreenDismissed() } } @@ -111,6 +121,7 @@ fun TranslationScreen(selectedTaskType: TaskTypeData?, viewModel: AssistantViewM state = sourceState, availableLanguages = languages?.originLanguages ?: emptyList(), maxDp = 120.dp, + shimmer = false, onStateChange = { sourceState = it } ) } @@ -126,10 +137,15 @@ fun TranslationScreen(selectedTaskType: TaskTypeData?, viewModel: AssistantViewM item { TranslationSection( labelId = R.string.translation_screen_label_to, - hintId = R.string.translation_screen_hint_target, + hintId = if (isTaskExists) { + R.string.translation_screen_translating + } else { + R.string.translation_screen_start_to_translate_task + }, state = targetState, availableLanguages = languages?.targetLanguages ?: emptyList(), maxDp = Dp.Unspecified, + shimmer = isTaskExists || isTranslationTaskCreated, onStateChange = { targetState = it } ) } @@ -137,7 +153,7 @@ fun TranslationScreen(selectedTaskType: TaskTypeData?, viewModel: AssistantViewM } } -@Suppress("LongMethod") +@Suppress("LongMethod", "LongParameterList") @Composable private fun TranslationSection( labelId: Int, @@ -145,6 +161,7 @@ private fun TranslationSection( state: TranslationSideState, availableLanguages: List, maxDp: Dp, + shimmer: Boolean, onStateChange: (TranslationSideState) -> Unit ) { val activity = LocalContext.current.getActivity() @@ -199,23 +216,56 @@ private fun TranslationSection( } } - TextField( - value = state.text, - onValueChange = { onStateChange(state.copy(text = it)) }, - readOnly = state.isTarget, - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 120.dp, max = maxDp), - placeholder = { - Text(text = stringResource(hintId), style = MaterialTheme.typography.headlineSmall) - }, - textStyle = MaterialTheme.typography.headlineSmall, - colors = TextFieldDefaults.colors( - focusedContainerColor = Color.Transparent, - unfocusedContainerColor = Color.Transparent, - disabledContainerColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent + if (state.isTarget && shimmer) { + TranslatingShimmer( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 120.dp) + .padding(horizontal = 16.dp) + ) + } else { + TextField( + value = state.text, + onValueChange = { onStateChange(state.copy(text = it)) }, + readOnly = state.isTarget, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 120.dp, max = maxDp), + placeholder = { + Text(text = stringResource(hintId), style = MaterialTheme.typography.headlineSmall) + }, + textStyle = MaterialTheme.typography.headlineSmall, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ) ) + } +} + +@Suppress("MagicNumber") +@Composable +private fun TranslatingShimmer(modifier: Modifier = Modifier) { + val transition = rememberInfiniteTransition(label = "shimmer") + val alpha by transition.animateFloat( + initialValue = 0.3f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(900), + repeatMode = RepeatMode.Reverse + ), + label = "alpha" ) + + Column(modifier = modifier) { + Text( + text = stringResource(R.string.translation_screen_translating), + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = alpha), + modifier = Modifier.padding(vertical = 4.dp) + ) + } } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index f0971229ee5d..524f47ecfc4c 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -20639,6 +20639,14 @@ + + + + + + + + From 4f38e82cbb97a84567353a6d3034d90d49e00ed4 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Sat, 24 Jan 2026 02:42:40 +0100 Subject: [PATCH 18/25] better ui ux Signed-off-by: alperozturk96 --- .../client/assistant/AssistantViewModel.kt | 62 ++++++++++++++++--- .../assistant/translate/TranslationScreen.kt | 7 +++ 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index c8fb0fb2058e..7d1ff9b47cfc 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -62,12 +62,15 @@ class AssistantViewModel( private val _snackbarMessageId = MutableStateFlow(null) val snackbarMessageId: StateFlow = _snackbarMessageId - private val _isTranslationTask = MutableStateFlow(false) + private val _isTranslationTask = MutableStateFlow(false) val isTranslationTask: StateFlow = _isTranslationTask - private val _isTranslationTaskCreated = MutableStateFlow(false) + private val _isTranslationTaskCreated = MutableStateFlow(false) val isTranslationTaskCreated: StateFlow = _isTranslationTaskCreated + private val _translationTaskOutput = MutableStateFlow("") + val translationTaskOutput: StateFlow = _translationTaskOutput + private val selectedTask = MutableStateFlow(null) private val _selectedTaskType = MutableStateFlow(null) @@ -204,17 +207,18 @@ class AssistantViewModel( } } + // region translation fun translate(textToTranslate: String, originLanguage: TranslationLanguage, targetLanguage: TranslationLanguage) { viewModelScope.launch(Dispatchers.IO) { - val task = _selectedTaskType.value - if (task == null) { + val taskType = _selectedTaskType.value + if (taskType == null) { _snackbarMessageId.update { R.string.assistant_screen_select_task } return@launch } - val model = task.toTranslationModel() + val model = taskType.toTranslationModel() if (model == null) { _snackbarMessageId.update { @@ -231,15 +235,55 @@ class AssistantViewModel( model = model.model ) - val result = remoteRepository.translate(input, task) + val result = remoteRepository.translate(input, taskType) if (result.isSuccess) { - _isTranslationTaskCreated.update { - true - } + _isTranslationTaskCreated.update { true } + + val selectedTaskId = selectedTask.value?.id ?: return@launch + + pollTranslationResult( + taskType = taskType, + selectedTaskId = selectedTaskId + ) + + _isTranslationTaskCreated.update { false } } } } + private suspend fun pollTranslationResult( + taskType: TaskTypeData, + selectedTaskId: Long, + maxRetries: Int = 3, + ) { + val taskTypeId = taskType.id ?: return + + repeat(maxRetries) { attempt -> + val translationTasks = remoteRepository.getTaskList(taskTypeId) + val translationResult = translationTasks + ?.find { it.id == selectedTaskId } + ?.output + ?.output + + if (!translationResult.isNullOrBlank()) { + _translationTaskOutput.update { translationResult } + return + } + + Log_OC.d(TAG, "Translation not ready yet (attempt ${attempt + 1}/$maxRetries)") + + if (attempt < maxRetries - 1) { + delay(POLLING_INTERVAL_MS) + } + } + + Log_OC.w(TAG, "Translation polling finished but result is still empty") + updateSnackbarMessage(R.string.translation_screen_task_processing) + onTranslationScreenDismissed() + } + // endregion + + // region chat fun sendChatMessage(content: String, sessionId: Long) = viewModelScope.launch(Dispatchers.IO) { val request = ChatMessageRequest( diff --git a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt index 6cfe7ea93696..fd662b35ca7f 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt @@ -38,6 +38,7 @@ import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -71,6 +72,8 @@ fun TranslationScreen( ) { val languages = remember(selectedTaskType) { selectedTaskType?.toTranslationLanguages() } val isTranslationTaskCreated by viewModel.isTranslationTaskCreated.collectAsState() + val translationTaskOutput by viewModel.translationTaskOutput.collectAsState() + var sourceState by remember { mutableStateOf( TranslationSideState( @@ -84,6 +87,10 @@ fun TranslationScreen( mutableStateOf(TranslationSideState(language = languages?.targetLanguages?.firstOrNull(), isTarget = true)) } + LaunchedEffect(translationTaskOutput) { + targetState = targetState.copy(text = translationTaskOutput) + } + BackHandler { viewModel.onTranslationScreenDismissed() viewModel.updateScreenState(AssistantScreenState.TaskContent) From 41c89a70a77a098cb675753229f0d4296cf1dc46 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Sat, 24 Jan 2026 02:48:17 +0100 Subject: [PATCH 19/25] better ui ux Signed-off-by: alperozturk96 --- .../com/nextcloud/client/assistant/AssistantViewModel.kt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index 7d1ff9b47cfc..c30e847d8f04 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -251,11 +251,7 @@ class AssistantViewModel( } } - private suspend fun pollTranslationResult( - taskType: TaskTypeData, - selectedTaskId: Long, - maxRetries: Int = 3, - ) { + private suspend fun pollTranslationResult(taskType: TaskTypeData, selectedTaskId: Long, maxRetries: Int = 3) { val taskTypeId = taskType.id ?: return repeat(maxRetries) { attempt -> @@ -283,7 +279,6 @@ class AssistantViewModel( } // endregion - // region chat fun sendChatMessage(content: String, sessionId: Long) = viewModelScope.launch(Dispatchers.IO) { val request = ChatMessageRequest( From d3bd7b5fb39fed0621df8a8dafecb9ab4537baa6 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Sat, 24 Jan 2026 02:57:15 +0100 Subject: [PATCH 20/25] better ui ux Signed-off-by: alperozturk96 --- .../client/assistant/AssistantViewModel.kt | 1 + .../assistant/translate/TranslationScreen.kt | 21 +++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index c30e847d8f04..51d9f1e24cc7 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -238,6 +238,7 @@ class AssistantViewModel( val result = remoteRepository.translate(input, taskType) if (result.isSuccess) { _isTranslationTaskCreated.update { true } + // TODO: Select newly created translation task val selectedTaskId = selectedTask.value?.id ?: return@launch diff --git a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt index fd662b35ca7f..da57962cb552 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt @@ -109,15 +109,18 @@ fun TranslationScreen( .padding(16.dp) .padding(top = 32.dp), floatingActionButton = { - FloatingActionButton(onClick = { - val originLang = sourceState.language - val targetLang = targetState.language - if (originLang != null && targetLang != null) { - viewModel.translate(sourceState.text, originLang, targetLang) - } - }, content = { - Icon(painter = painterResource(R.drawable.ic_translate), contentDescription = "translate button") - }) + if (!isTaskExists) { + // TODO: After first task creation dont allow user to create another back to back + FloatingActionButton(onClick = { + val originLang = sourceState.language + val targetLang = targetState.language + if (originLang != null && targetLang != null) { + viewModel.translate(sourceState.text, originLang, targetLang) + } + }, content = { + Icon(painter = painterResource(R.drawable.ic_translate), contentDescription = "translate button") + }) + } } ) { paddingValues -> LazyColumn(modifier = Modifier.padding(paddingValues)) { From 3e3a0d8132f825ccf2718f593860dd39e99b914f Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 26 Jan 2026 12:25:59 +0100 Subject: [PATCH 21/25] add polling for task selection Signed-off-by: alperozturk96 --- .../com/nextcloud/client/assistant/AssistantViewModel.kt | 9 ++++++++- .../client/assistant/translate/TranslationScreen.kt | 3 +-- app/src/main/res/values/strings.xml | 6 +++++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index 51d9f1e24cc7..d615daf7e3bd 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -238,7 +238,6 @@ class AssistantViewModel( val result = remoteRepository.translate(input, taskType) if (result.isSuccess) { _isTranslationTaskCreated.update { true } - // TODO: Select newly created translation task val selectedTaskId = selectedTask.value?.id ?: return@launch @@ -420,6 +419,14 @@ class AssistantViewModel( } fun selectTask(task: Task?) { + viewModelScope.launch { + if (task?.isTranslate() == true) { + _selectedTaskType.value?.let { + pollTranslationResult(it, task.id) + } + } + } + selectedTask.update { task } diff --git a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt index da57962cb552..f0c5a66906da 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt @@ -109,8 +109,7 @@ fun TranslationScreen( .padding(16.dp) .padding(top = 32.dp), floatingActionButton = { - if (!isTaskExists) { - // TODO: After first task creation dont allow user to create another back to back + if (!isTaskExists && !isTranslationTaskCreated) { FloatingActionButton(onClick = { val originLang = sourceState.language val targetLang = targetState.language diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9ce215b888b7..0cc116977b6d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -81,10 +81,14 @@ failed + Please select task Translate from: Translate to: + Translating… + Press the button to translate Enter text to translate… - Translation will appear here… + Translation model not exists. + Translation is taking longer than expected. Conversations From d216afd1c5edb39333948d8e27d7484d81c47163 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 27 Jan 2026 15:03:14 +0100 Subject: [PATCH 22/25] fix ux Signed-off-by: alperozturk96 --- .../client/assistant/AssistantScreen.kt | 22 ++- .../client/assistant/AssistantViewModel.kt | 98 +---------- .../assistant/translate/TranslationScreen.kt | 86 ++++------ .../translate/TranslationScreenState.kt | 159 ++++++++++++++++++ .../translate/TranslationViewModel.kt | 134 +++++++++++++++ 5 files changed, 344 insertions(+), 155 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreenState.kt create mode 100644 app/src/main/java/com/nextcloud/client/assistant/translate/TranslationViewModel.kt diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index 62852e39399d..14a3a4b70c71 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -67,6 +67,7 @@ import com.nextcloud.client.assistant.repository.remote.MockAssistantRemoteRepos import com.nextcloud.client.assistant.task.TaskView import com.nextcloud.client.assistant.taskTypes.TaskTypesRow import com.nextcloud.client.assistant.translate.TranslationScreen +import com.nextcloud.client.assistant.translate.TranslationViewModel import com.nextcloud.ui.composeActivity.ComposeActivity import com.nextcloud.ui.composeActivity.ComposeViewModel import com.nextcloud.ui.composeComponents.alertDialog.SimpleAlertDialog @@ -252,15 +253,20 @@ fun AssistantScreen( } is AssistantScreenState.Translation -> { - val task = (screenState as AssistantScreenState.Translation).task - val textToTranslate = task?.input?.input ?: selectedText ?: "" + selectedTaskType?.let { + val task = (screenState as AssistantScreenState.Translation).task + val textToTranslate = task?.input?.input ?: selectedText ?: "" - TranslationScreen( - selectedTaskType, - viewModel, - textToTranslate, - isTaskExists = (task != null) - ) + val translationViewModel = + TranslationViewModel(remoteRepository = viewModel.getRemoteRepository()) + + translationViewModel.init(it, task, textToTranslate) + + TranslationScreen( + viewModel = translationViewModel, + assistantViewModel = viewModel + ) + } } else -> EmptyContent( diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index d615daf7e3bd..def6bf60882b 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -21,9 +21,6 @@ import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessage import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessageRequest import com.owncloud.android.lib.resources.assistant.v2.model.Task import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData -import com.owncloud.android.lib.resources.assistant.v2.model.TranslationLanguage -import com.owncloud.android.lib.resources.assistant.v2.model.TranslationRequest -import com.owncloud.android.lib.resources.assistant.v2.model.toTranslationModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -65,12 +62,6 @@ class AssistantViewModel( private val _isTranslationTask = MutableStateFlow(false) val isTranslationTask: StateFlow = _isTranslationTask - private val _isTranslationTaskCreated = MutableStateFlow(false) - val isTranslationTaskCreated: StateFlow = _isTranslationTaskCreated - - private val _translationTaskOutput = MutableStateFlow("") - val translationTaskOutput: StateFlow = _translationTaskOutput - private val selectedTask = MutableStateFlow(null) private val _selectedTaskType = MutableStateFlow(null) @@ -207,78 +198,6 @@ class AssistantViewModel( } } - // region translation - fun translate(textToTranslate: String, originLanguage: TranslationLanguage, targetLanguage: TranslationLanguage) { - viewModelScope.launch(Dispatchers.IO) { - val taskType = _selectedTaskType.value - if (taskType == null) { - _snackbarMessageId.update { - R.string.assistant_screen_select_task - } - return@launch - } - - val model = taskType.toTranslationModel() - - if (model == null) { - _snackbarMessageId.update { - R.string.translation_screen_error_message - } - return@launch - } - - val input = TranslationRequest( - input = textToTranslate, - originLanguage = originLanguage.code, - targetLanguage = targetLanguage.code, - maxTokens = model.maxTokens, - model = model.model - ) - - val result = remoteRepository.translate(input, taskType) - if (result.isSuccess) { - _isTranslationTaskCreated.update { true } - - val selectedTaskId = selectedTask.value?.id ?: return@launch - - pollTranslationResult( - taskType = taskType, - selectedTaskId = selectedTaskId - ) - - _isTranslationTaskCreated.update { false } - } - } - } - - private suspend fun pollTranslationResult(taskType: TaskTypeData, selectedTaskId: Long, maxRetries: Int = 3) { - val taskTypeId = taskType.id ?: return - - repeat(maxRetries) { attempt -> - val translationTasks = remoteRepository.getTaskList(taskTypeId) - val translationResult = translationTasks - ?.find { it.id == selectedTaskId } - ?.output - ?.output - - if (!translationResult.isNullOrBlank()) { - _translationTaskOutput.update { translationResult } - return - } - - Log_OC.d(TAG, "Translation not ready yet (attempt ${attempt + 1}/$maxRetries)") - - if (attempt < maxRetries - 1) { - delay(POLLING_INTERVAL_MS) - } - } - - Log_OC.w(TAG, "Translation polling finished but result is still empty") - updateSnackbarMessage(R.string.translation_screen_task_processing) - onTranslationScreenDismissed() - } - // endregion - // region chat fun sendChatMessage(content: String, sessionId: Long) = viewModelScope.launch(Dispatchers.IO) { val request = ChatMessageRequest( @@ -419,14 +338,6 @@ class AssistantViewModel( } fun selectTask(task: Task?) { - viewModelScope.launch { - if (task?.isTranslate() == true) { - _selectedTaskType.value?.let { - pollTranslationResult(it, task.id) - } - } - } - selectedTask.update { task } @@ -462,18 +373,13 @@ class AssistantViewModel( } } - fun updateTranslationTaskCreation(value: Boolean) { - _isTranslationTaskCreated.update { - value - } - } - fun onTranslationScreenDismissed() { - updateTranslationTaskCreation(false) updateTranslationTaskState(false) selectTask(null) } + fun getRemoteRepository(): AssistantRemoteRepository = remoteRepository + private fun removeTaskFromList(id: Long) { _filteredTaskList.update { currentList -> currentList?.filter { it.id != id } diff --git a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt index f0c5a66906da..9da714a95eec 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreen.kt @@ -33,6 +33,8 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults @@ -41,9 +43,7 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -56,7 +56,6 @@ import com.nextcloud.client.assistant.AssistantViewModel import com.nextcloud.client.assistant.model.AssistantScreenState import com.nextcloud.utils.extensions.getActivity import com.owncloud.android.R -import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData import com.owncloud.android.lib.resources.assistant.v2.model.TranslationLanguage import com.owncloud.android.lib.resources.assistant.v2.model.toTranslationLanguages import com.owncloud.android.utils.ClipboardUtil @@ -64,42 +63,28 @@ import com.owncloud.android.utils.ClipboardUtil @Suppress("LongMethod") @OptIn(ExperimentalMaterial3Api::class) @Composable -fun TranslationScreen( - selectedTaskType: TaskTypeData?, - viewModel: AssistantViewModel, - textToTranslate: String, - isTaskExists: Boolean -) { - val languages = remember(selectedTaskType) { selectedTaskType?.toTranslationLanguages() } - val isTranslationTaskCreated by viewModel.isTranslationTaskCreated.collectAsState() - val translationTaskOutput by viewModel.translationTaskOutput.collectAsState() - - var sourceState by remember { - mutableStateOf( - TranslationSideState( - text = textToTranslate, - language = languages?.originLanguages?.firstOrNull(), - isTarget = false - ) - ) - } - var targetState by remember { - mutableStateOf(TranslationSideState(language = languages?.targetLanguages?.firstOrNull(), isTarget = true)) - } +fun TranslationScreen(viewModel: TranslationViewModel, assistantViewModel: AssistantViewModel) { + val context = LocalContext.current + val state by viewModel.screenState.collectAsState() + val messageId by viewModel.snackbarMessageId.collectAsState() + val snackbarHostState = remember { SnackbarHostState() } - LaunchedEffect(translationTaskOutput) { - targetState = targetState.copy(text = translationTaskOutput) + BackHandler { + assistantViewModel.onTranslationScreenDismissed() + assistantViewModel.updateScreenState(AssistantScreenState.TaskContent) } - BackHandler { - viewModel.onTranslationScreenDismissed() - viewModel.updateScreenState(AssistantScreenState.TaskContent) + LaunchedEffect(messageId) { + messageId?.let { + snackbarHostState.showSnackbar(context.getString(it)) + viewModel.updateSnackbarMessage(null) + } } // task is unselected DisposableEffect(Unit) { onDispose { - viewModel.onTranslationScreenDismissed() + assistantViewModel.onTranslationScreenDismissed() } } @@ -109,17 +94,16 @@ fun TranslationScreen( .padding(16.dp) .padding(top = 32.dp), floatingActionButton = { - if (!isTaskExists && !isTranslationTaskCreated) { + if (state.fabVisibility) { FloatingActionButton(onClick = { - val originLang = sourceState.language - val targetLang = targetState.language - if (originLang != null && targetLang != null) { - viewModel.translate(sourceState.text, originLang, targetLang) - } + viewModel.translate() }, content = { Icon(painter = painterResource(R.drawable.ic_translate), contentDescription = "translate button") }) } + }, + snackbarHost = { + SnackbarHost(snackbarHostState) } ) { paddingValues -> LazyColumn(modifier = Modifier.padding(paddingValues)) { @@ -127,11 +111,13 @@ fun TranslationScreen( TranslationSection( labelId = R.string.translation_screen_label_from, hintId = R.string.translation_screen_hint_source, - state = sourceState, - availableLanguages = languages?.originLanguages ?: emptyList(), + state = state.source, + availableLanguages = state.taskTypeData.toTranslationLanguages().originLanguages, maxDp = 120.dp, shimmer = false, - onStateChange = { sourceState = it } + onStateChange = { + viewModel.updateSourceState(it) + } ) } @@ -146,16 +132,14 @@ fun TranslationScreen( item { TranslationSection( labelId = R.string.translation_screen_label_to, - hintId = if (isTaskExists) { - R.string.translation_screen_translating - } else { - R.string.translation_screen_start_to_translate_task - }, - state = targetState, - availableLanguages = languages?.targetLanguages ?: emptyList(), + hintId = state.targetHintMessageId, + state = state.target, + availableLanguages = state.taskTypeData.toTranslationLanguages().targetLanguages, maxDp = Dp.Unspecified, - shimmer = isTaskExists || isTranslationTaskCreated, - onStateChange = { targetState = it } + shimmer = state.shimmer, + onStateChange = { + viewModel.updateTargetState(it) + } ) } } @@ -166,7 +150,7 @@ fun TranslationScreen( @Composable private fun TranslationSection( labelId: Int, - hintId: Int, + hintId: Int?, state: TranslationSideState, availableLanguages: List, maxDp: Dp, @@ -241,7 +225,7 @@ private fun TranslationSection( .fillMaxWidth() .heightIn(min = 120.dp, max = maxDp), placeholder = { - Text(text = stringResource(hintId), style = MaterialTheme.typography.headlineSmall) + hintId?.let { Text(text = stringResource(it), style = MaterialTheme.typography.headlineSmall) } }, textStyle = MaterialTheme.typography.headlineSmall, colors = TextFieldDefaults.colors( diff --git a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreenState.kt b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreenState.kt new file mode 100644 index 000000000000..2e7be3d1e016 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreenState.kt @@ -0,0 +1,159 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.assistant.translate + +import com.owncloud.android.R +import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData +import com.owncloud.android.lib.resources.assistant.v2.model.toTranslationLanguages + +@Suppress("LongParameterList") +sealed class TranslationScreenState( + open val taskTypeData: TaskTypeData, + open val source: TranslationSideState, + open val target: TranslationSideState, + open val fabVisibility: Boolean, + open val shimmer: Boolean, + open val targetHintMessageId: Int? +) + +data class NewTranslation( + override val taskTypeData: TaskTypeData, + override val source: TranslationSideState, + override val target: TranslationSideState, + override val shimmer: Boolean = false +) : TranslationScreenState( + taskTypeData = taskTypeData, + source = source, + target = target, + fabVisibility = true, + shimmer = shimmer, + targetHintMessageId = R.string.translation_screen_start_to_translate_task +) { + companion object { + fun create(taskTypeData: TaskTypeData, textToTranslate: String): NewTranslation = NewTranslation( + taskTypeData = taskTypeData, + source = TranslationSideState( + text = textToTranslate, + language = taskTypeData.toTranslationLanguages().originLanguages.firstOrNull(), + isTarget = false + ), + target = TranslationSideState( + text = "", + language = taskTypeData.toTranslationLanguages().targetLanguages.firstOrNull(), + isTarget = true + ) + ) + } +} + +data class ExistingTranslation( + override val taskTypeData: TaskTypeData, + override val source: TranslationSideState, + override val target: TranslationSideState, + override val shimmer: Boolean = false +) : TranslationScreenState( + taskTypeData = taskTypeData, + source = source, + target = target, + fabVisibility = false, + shimmer = shimmer, + targetHintMessageId = null +) { + companion object { + fun create(taskTypeData: TaskTypeData, textToTranslate: String, translatedText: String): ExistingTranslation = + ExistingTranslation( + taskTypeData = taskTypeData, + source = TranslationSideState( + text = textToTranslate, + language = taskTypeData.toTranslationLanguages().originLanguages.firstOrNull(), + isTarget = false + ), + target = TranslationSideState( + text = translatedText, + language = taskTypeData.toTranslationLanguages().targetLanguages.firstOrNull(), + isTarget = true + ) + ) + } +} + +data class EditedTranslation( + override val taskTypeData: TaskTypeData, + override val source: TranslationSideState, + override val target: TranslationSideState, + override val shimmer: Boolean = false +) : TranslationScreenState( + taskTypeData = taskTypeData, + source = source, + target = target, + fabVisibility = true, + shimmer = shimmer, + targetHintMessageId = null +) { + companion object { + fun create(taskTypeData: TaskTypeData, textToTranslate: String, translatedText: String): EditedTranslation = + EditedTranslation( + taskTypeData = taskTypeData, + source = TranslationSideState( + text = textToTranslate, + language = taskTypeData.toTranslationLanguages().originLanguages.firstOrNull(), + isTarget = false + ), + target = TranslationSideState( + text = translatedText, + language = taskTypeData.toTranslationLanguages().targetLanguages.firstOrNull(), + isTarget = true + ) + ) + } +} + +fun TranslationScreenState.withShimmer(shimmer: Boolean): TranslationScreenState = when (this) { + is NewTranslation -> copy(shimmer = shimmer) + is ExistingTranslation -> copy(shimmer = shimmer) + is EditedTranslation -> copy(shimmer = shimmer) +} + +fun TranslationScreenState.withTargetText(text: String): TranslationScreenState = when (this) { + is NewTranslation -> EditedTranslation( + taskTypeData = taskTypeData, + source = source, + target = target.copy(text = text), + shimmer = shimmer + ) + is ExistingTranslation -> copy( + target = target.copy(text = text) + ) + is EditedTranslation -> copy( + target = target.copy(text = text) + ) +} + +fun TranslationScreenState.withSource(newSource: TranslationSideState): TranslationScreenState = when (this) { + is NewTranslation -> copy(source = newSource) + is ExistingTranslation -> EditedTranslation( + taskTypeData = taskTypeData, + source = newSource, + target = target, + shimmer = shimmer + ) + is EditedTranslation -> copy(source = newSource) +} + +fun TranslationScreenState.withTarget(newTarget: TranslationSideState): TranslationScreenState = when (this) { + is NewTranslation -> { + copy(target = newTarget) + } + is ExistingTranslation -> EditedTranslation( + taskTypeData = taskTypeData, + source = source, + target = newTarget, + shimmer = shimmer + ) + is EditedTranslation -> copy(target = newTarget) +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationViewModel.kt new file mode 100644 index 000000000000..606dc6ab839c --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationViewModel.kt @@ -0,0 +1,134 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.assistant.translate + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nextcloud.client.assistant.repository.remote.AssistantRemoteRepository +import com.owncloud.android.R +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.assistant.v2.model.Task +import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData +import com.owncloud.android.lib.resources.assistant.v2.model.TranslationRequest +import com.owncloud.android.lib.resources.assistant.v2.model.toTranslationModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class TranslationViewModel(private val remoteRepository: AssistantRemoteRepository) : ViewModel() { + + companion object { + private const val TAG = "TranslationViewModel" + private const val POLLING_INTERVAL_MS = 15_000L + private const val MAX_RETRY = 3 + } + + private lateinit var _screenState: MutableStateFlow + val screenState: StateFlow + get() = _screenState + + private val _snackbarMessageId = MutableStateFlow(null) + val snackbarMessageId: StateFlow = _snackbarMessageId + + private lateinit var taskTypeData: TaskTypeData + private var task: Task? = null + private var textToTranslate = "" + private var translatedText = "" + + fun init(taskTypeData: TaskTypeData, task: Task?, textToTranslate: String) { + this.task = task + this.textToTranslate = textToTranslate + this.taskTypeData = taskTypeData + + _screenState = if (task == null) { + MutableStateFlow(NewTranslation.create(taskTypeData, textToTranslate)) + } else { + val translatedText = task.output?.output ?: "" + this.translatedText = translatedText + + viewModelScope.launch { + pollTranslationResult() + } + MutableStateFlow(ExistingTranslation.create(taskTypeData, textToTranslate, translatedText)) + } + } + + fun translate() { + viewModelScope.launch(Dispatchers.IO) { + val stateValue = _screenState.value + val textToTranslate = stateValue.source.text + val originLanguage = stateValue.source.language ?: return@launch + val targetLanguage = stateValue.target.language ?: return@launch + + val model = taskTypeData.toTranslationModel() + + if (model == null) { + updateSnackbarMessage(R.string.translation_screen_error_message) + return@launch + } + + val input = TranslationRequest( + input = textToTranslate, + originLanguage = originLanguage.code, + targetLanguage = targetLanguage.code, + maxTokens = model.maxTokens, + model = model.model + ) + + val result = remoteRepository.translate(input, taskTypeData) + if (result.isSuccess) { + _screenState.update { it.withShimmer(true) } + pollTranslationResult() + _screenState.update { it.withShimmer(false) } + } + } + } + + private suspend fun pollTranslationResult() { + val taskTypeId = taskTypeData.id ?: return + + repeat(MAX_RETRY) { attempt -> + val translationTasks = remoteRepository.getTaskList(taskTypeId) + val translationResult = translationTasks + ?.find { it.id == task?.id } + ?.output + ?.output + + if (!translationResult.isNullOrBlank()) { + _screenState.update { it.withTargetText(translationResult) } + return + } + + Log_OC.d(TAG, "Translation not ready yet (attempt ${attempt + 1}/$MAX_RETRY)") + + if (attempt < MAX_RETRY - 1) { + delay(POLLING_INTERVAL_MS) + } + } + + Log_OC.w(TAG, "Translation polling finished but result is still empty") + updateSnackbarMessage(R.string.translation_screen_task_processing) + } + + fun updateSnackbarMessage(value: Int?) { + _snackbarMessageId.update { + value + } + } + + fun updateSourceState(newSourceState: TranslationSideState) { + _screenState.update { it.withSource(newSourceState) } + } + + fun updateTargetState(newTargetState: TranslationSideState) { + _screenState.update { it.withTarget(newTargetState) } + } +} From dd2b09a2fab151da5bc3d94891b3f3dec04f77ba Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 27 Jan 2026 15:26:01 +0100 Subject: [PATCH 23/25] fix ux Signed-off-by: alperozturk96 --- .../translate/TranslationScreenState.kt | 35 +++++++++++++++++++ .../translate/TranslationViewModel.kt | 25 +++++++------ app/src/main/res/values/strings.xml | 1 - 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreenState.kt b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreenState.kt index 2e7be3d1e016..fe4e8976610a 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreenState.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationScreenState.kt @@ -21,6 +21,23 @@ sealed class TranslationScreenState( open val targetHintMessageId: Int? ) +data object Uninitialized : TranslationScreenState( + taskTypeData = TaskTypeData(null, "", null, mapOf(), mapOf()), + source = TranslationSideState( + text = "", + language = null, + isTarget = false + ), + target = TranslationSideState( + text = "", + language = null, + isTarget = true + ), + fabVisibility = false, + shimmer = false, + targetHintMessageId = null +) + data class NewTranslation( override val taskTypeData: TaskTypeData, override val source: TranslationSideState, @@ -117,6 +134,9 @@ fun TranslationScreenState.withShimmer(shimmer: Boolean): TranslationScreenState is NewTranslation -> copy(shimmer = shimmer) is ExistingTranslation -> copy(shimmer = shimmer) is EditedTranslation -> copy(shimmer = shimmer) + Uninitialized -> { + Uninitialized + } } fun TranslationScreenState.withTargetText(text: String): TranslationScreenState = when (this) { @@ -126,12 +146,18 @@ fun TranslationScreenState.withTargetText(text: String): TranslationScreenState target = target.copy(text = text), shimmer = shimmer ) + is ExistingTranslation -> copy( target = target.copy(text = text) ) + is EditedTranslation -> copy( target = target.copy(text = text) ) + + Uninitialized -> { + Uninitialized + } } fun TranslationScreenState.withSource(newSource: TranslationSideState): TranslationScreenState = when (this) { @@ -142,18 +168,27 @@ fun TranslationScreenState.withSource(newSource: TranslationSideState): Translat target = target, shimmer = shimmer ) + is EditedTranslation -> copy(source = newSource) + Uninitialized -> { + Uninitialized + } } fun TranslationScreenState.withTarget(newTarget: TranslationSideState): TranslationScreenState = when (this) { is NewTranslation -> { copy(target = newTarget) } + is ExistingTranslation -> EditedTranslation( taskTypeData = taskTypeData, source = source, target = newTarget, shimmer = shimmer ) + is EditedTranslation -> copy(target = newTarget) + Uninitialized -> { + Uninitialized + } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationViewModel.kt index 606dc6ab839c..ed1f82fb5f2b 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationViewModel.kt @@ -31,7 +31,8 @@ class TranslationViewModel(private val remoteRepository: AssistantRemoteReposito private const val MAX_RETRY = 3 } - private lateinit var _screenState: MutableStateFlow + private var _screenState = + MutableStateFlow(Uninitialized) val screenState: StateFlow get() = _screenState @@ -53,10 +54,6 @@ class TranslationViewModel(private val remoteRepository: AssistantRemoteReposito } else { val translatedText = task.output?.output ?: "" this.translatedText = translatedText - - viewModelScope.launch { - pollTranslationResult() - } MutableStateFlow(ExistingTranslation.create(taskTypeData, textToTranslate, translatedText)) } } @@ -70,17 +67,12 @@ class TranslationViewModel(private val remoteRepository: AssistantRemoteReposito val model = taskTypeData.toTranslationModel() - if (model == null) { - updateSnackbarMessage(R.string.translation_screen_error_message) - return@launch - } - val input = TranslationRequest( input = textToTranslate, originLanguage = originLanguage.code, targetLanguage = targetLanguage.code, - maxTokens = model.maxTokens, - model = model.model + maxTokens = 0.0, + model = model?.model ?: "" ) val result = remoteRepository.translate(input, taskTypeData) @@ -92,13 +84,20 @@ class TranslationViewModel(private val remoteRepository: AssistantRemoteReposito } } + @Suppress("ReturnCount") private suspend fun pollTranslationResult() { + val screenStateValue = _screenState.value + if (screenStateValue is Uninitialized) { + return + } + val taskTypeId = taskTypeData.id ?: return + val input = screenStateValue.source.text repeat(MAX_RETRY) { attempt -> val translationTasks = remoteRepository.getTaskList(taskTypeId) val translationResult = translationTasks - ?.find { it.id == task?.id } + ?.find { it.input?.input == input } ?.output ?.output diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0cc116977b6d..97f04a8862af 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -87,7 +87,6 @@ Translating… Press the button to translate Enter text to translate… - Translation model not exists. Translation is taking longer than expected. From 23aec6ff1fe5913f2c2544691977c701c37816e5 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 27 Jan 2026 15:29:09 +0100 Subject: [PATCH 24/25] fix ux Signed-off-by: alperozturk96 --- .../client/assistant/translate/TranslationViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationViewModel.kt index ed1f82fb5f2b..df78ef73a35f 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/translate/TranslationViewModel.kt @@ -71,7 +71,7 @@ class TranslationViewModel(private val remoteRepository: AssistantRemoteReposito input = textToTranslate, originLanguage = originLanguage.code, targetLanguage = targetLanguage.code, - maxTokens = 0.0, + maxTokens = model?.maxTokens ?: 0.0, model = model?.model ?: "" ) From d1115bdc8c9f67cc2946e950db8b89c4595dae2a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 4 Feb 2026 09:26:05 +0100 Subject: [PATCH 25/25] update android library Signed-off-by: alperozturk96 --- gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 27 ++++++++++++++++----------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0d616a6e3279..8618391638e9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ androidCommonLibraryVersion = "4fc0f29981" androidGifDrawableVersion = "1.2.30" androidImageCropperVersion = "4.7.0" -androidLibraryVersion ="b14ecf0388338691b9442eb8535ebbf1b7b39495" +androidLibraryVersion ="38328f338c00870c6a062188974f8f30a61fb450" androidPluginVersion = "9.0.0" androidsvgVersion = "1.4" androidxMediaVersion = "1.5.1" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 524f47ecfc4c..37b90dc159d7 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -20879,6 +20879,14 @@ + + + + + + + + @@ -21343,17 +21351,14 @@ - - - - - - - - + + + + + + + +