diff --git a/app/src/androidTest/java/com/nextcloud/client/assistant/AssistantRepositoryTests.kt b/app/src/androidTest/java/com/nextcloud/client/assistant/AssistantRepositoryTests.kt index 6d1c0475a46e..d2d333f4797f 100644 --- a/app/src/androidTest/java/com/nextcloud/client/assistant/AssistantRepositoryTests.kt +++ b/app/src/androidTest/java/com/nextcloud/client/assistant/AssistantRepositoryTests.kt @@ -35,7 +35,7 @@ class AssistantRepositoryTests : AbstractOnServerIT() { } runBlocking { - val result = sut?.getTaskTypes() + val result = sut?.fetchTaskTypes() assertTrue(result?.isNotEmpty() == true) } } 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 14a3a4b70c71..863a6346b687 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -71,14 +71,17 @@ 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 +import com.nextcloud.ui.composeComponents.alertDialog.TaskSelectionAlertDialog import com.nextcloud.ui.composeComponents.bottomSheet.MoreActionsBottomSheet import com.nextcloud.utils.extensions.getChat import com.owncloud.android.R 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.status.OCCapability +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext private const val CHAT_INPUT_DELAY = 100L private const val PULL_TO_REFRESH_DELAY = 1500L @@ -116,8 +119,8 @@ fun AssistantScreen( } LaunchedEffect(selectedText) { - selectedText?.let { - if (it.isBlank()) { + selectedText?.let { copiedText -> + if (copiedText.isBlank()) { return@LaunchedEffect } @@ -125,8 +128,15 @@ fun AssistantScreen( pagerState.scrollToPage(AssistantPage.Content.id) } - viewModel.updateInputBarText(it) - snackbarHostState.showSnackbar(activity.getString(R.string.assistant_screen_text_selected)) + scope.launch(Dispatchers.IO) { + val types = viewModel.getRemoteRepository().fetchTaskTypes() + if (!types.isNullOrEmpty()) { + withContext(Dispatchers.Main) { + viewModel.updateScreenOverlayState(ScreenOverlayState.TaskTypes(copiedText, types)) + snackbarHostState.showSnackbar(activity.getString(R.string.assistant_screen_text_selected)) + } + } + } } } @@ -367,29 +377,43 @@ private fun InputBar(sessionId: Long?, selectedTaskType: TaskTypeData?, viewMode @Suppress("LongMethod") @Composable private fun OverlayState(state: ScreenOverlayState?, activity: Activity, viewModel: AssistantViewModel) { - when (state) { - is ScreenOverlayState.DeleteTask -> { - SimpleAlertDialog( - title = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_title), - description = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_description), - dismiss = { viewModel.updateScreenOverlayState(null) }, - onComplete = { viewModel.deleteTask(state.id) } - ) - } + state?.let { + when (state) { + is ScreenOverlayState.DeleteTask -> { + SimpleAlertDialog( + title = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_title), + description = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_description), + onDismiss = { viewModel.updateScreenOverlayState(null) }, + onComplete = { viewModel.deleteTask(state.id) } + ) + } - is ScreenOverlayState.TaskActions -> { - val actions = state.getActions(activity, onDeleteCompleted = { deleteTask -> - viewModel.updateScreenOverlayState(deleteTask) - }) + is ScreenOverlayState.TaskActions -> { + val actions = state.getActions(activity, onDeleteCompleted = { deleteTask -> + viewModel.updateScreenOverlayState(deleteTask) + }) - MoreActionsBottomSheet( - title = state.task.getInputTitle(), - actions = actions, - dismiss = { viewModel.updateScreenOverlayState(null) } - ) - } + MoreActionsBottomSheet( + title = state.task.getInputTitle(), + actions = actions, + onDismiss = { viewModel.updateScreenOverlayState(null) } + ) + } + + is ScreenOverlayState.TaskTypes -> { + TaskSelectionAlertDialog(state.taskTypes, onDismiss = { + viewModel.updateScreenOverlayState(null) + }, onConfirm = { + viewModel.selectTaskType(it) + viewModel.updateInputBarText(state.copiedText) - else -> Unit + if (it.isTranslate()) { + viewModel.updateTranslationTaskState(true) + viewModel.updateScreenState(AssistantScreenState.Translation(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 7bc212667745..8134df9fbe58 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -286,7 +286,7 @@ class AssistantViewModel( } private fun fetchTaskTypes() = viewModelScope.launch(Dispatchers.IO) { - val result = remoteRepository.getTaskTypes() + val result = remoteRepository.fetchTaskTypes() if (result.isNullOrEmpty()) { _screenState.value = AssistantScreenState.emptyTaskTypes() return@launch @@ -379,6 +379,7 @@ class AssistantViewModel( } fun onTranslationScreenDismissed() { + updateInputBarText("") updateTranslationTaskState(false) selectTask(null) } diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt index 36ae8c1ee8f6..29e37fabade3 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt @@ -200,7 +200,7 @@ private fun ConversationList( MoreActionsBottomSheet( actions = bottomSheetAction, - dismiss = { selectedConversationId = -1L } + onDismiss = { selectedConversationId = -1L } ) } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/model/ScreenOverlayState.kt b/app/src/main/java/com/nextcloud/client/assistant/model/ScreenOverlayState.kt index 76a100b60d96..5d63e66dab44 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/model/ScreenOverlayState.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/model/ScreenOverlayState.kt @@ -12,6 +12,7 @@ import com.nextcloud.client.assistant.extensions.getInputAndOutput import com.nextcloud.utils.extensions.showShareIntent import com.owncloud.android.R import com.owncloud.android.lib.resources.assistant.v2.model.Task +import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData import com.owncloud.android.utils.ClipboardUtil sealed class ScreenOverlayState { @@ -52,4 +53,5 @@ sealed class ScreenOverlayState { }) ) } + data class TaskTypes(val copiedText: String, val taskTypes: List) : ScreenOverlayState() } 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 8b1f2397d421..4a578f6b512e 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 @@ -18,7 +18,7 @@ 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? + suspend fun fetchTaskTypes(): List? suspend fun createTask(input: String, 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 923f689bd0e9..596e5353b3b9 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 @@ -44,7 +44,7 @@ class AssistantRemoteRepositoryImpl(private val client: NextcloudClient, capabil private val supportsV2 = capability.version.isNewerOrEqual(NextcloudVersion.nextcloud_30) - override suspend fun getTaskTypes(): List? = withContext(Dispatchers.IO) { + override suspend fun fetchTaskTypes(): List? = withContext(Dispatchers.IO) { if (supportsV2) { val result = GetTaskTypesRemoteOperationV2().execute(client) if (result.isSuccess) { 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 e11ec5d1a873..0788cc4f8535 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 @@ -22,7 +22,7 @@ import com.owncloud.android.lib.resources.assistant.v2.model.TranslationRequest @Suppress("MagicNumber") class MockAssistantRemoteRepository(private val giveEmptyTasks: Boolean = false) : AssistantRemoteRepository { - override suspend fun getTaskTypes(): List = listOf( + override suspend fun fetchTaskTypes(): List = listOf( TaskTypeData( id = "core:text2text", name = "Free text to text prompt", diff --git a/app/src/main/java/com/nextcloud/ui/composeComponents/alertDialog/SimpleAlertDialog.kt b/app/src/main/java/com/nextcloud/ui/composeComponents/alertDialog/SimpleAlertDialog.kt index bcf724e3dbd1..738c9ddc1bab 100644 --- a/app/src/main/java/com/nextcloud/ui/composeComponents/alertDialog/SimpleAlertDialog.kt +++ b/app/src/main/java/com/nextcloud/ui/composeComponents/alertDialog/SimpleAlertDialog.kt @@ -31,7 +31,7 @@ fun SimpleAlertDialog( heightFraction: Float? = null, content: @Composable (() -> Unit)? = null, onComplete: () -> Unit, - dismiss: () -> Unit + onDismiss: () -> Unit ) { val modifier = if (heightFraction != null) { Modifier @@ -46,7 +46,7 @@ fun SimpleAlertDialog( iconContentColor = MaterialTheme.colorScheme.onPrimaryContainer, titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer, textContentColor = MaterialTheme.colorScheme.onPrimaryContainer, - onDismissRequest = { dismiss() }, + onDismissRequest = { onDismiss() }, title = { Text(text = title) }, @@ -66,7 +66,7 @@ fun SimpleAlertDialog( confirmButton = { FilledTonalButton(onClick = { onComplete() - dismiss() + onDismiss() }) { Text( stringResource(id = R.string.common_ok) @@ -74,7 +74,7 @@ fun SimpleAlertDialog( } }, dismissButton = { - TextButton(onClick = { dismiss() }) { + TextButton(onClick = { onDismiss() }) { Text( stringResource(id = R.string.common_cancel) ) diff --git a/app/src/main/java/com/nextcloud/ui/composeComponents/alertDialog/TaskSelectionAlertDialog.kt b/app/src/main/java/com/nextcloud/ui/composeComponents/alertDialog/TaskSelectionAlertDialog.kt new file mode 100644 index 000000000000..4e78c8e72876 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/composeComponents/alertDialog/TaskSelectionAlertDialog.kt @@ -0,0 +1,112 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.nextcloud.ui.composeComponents.alertDialog + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.res.stringResource +import androidx.compose.ui.unit.dp +import com.owncloud.android.R +import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData + +@Suppress("LongMethod") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TaskSelectionAlertDialog(taskTypes: List, onDismiss: () -> Unit, onConfirm: (TaskTypeData) -> Unit) { + var expanded by remember { mutableStateOf(false) } + var tempSelectedTask by remember { mutableStateOf(taskTypes.firstOrNull()) } + + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text( + text = stringResource(R.string.assistant_screen_select_task_type_title), + style = MaterialTheme.typography.titleLarge + ) + }, + text = { + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp) + ) { + OutlinedTextField( + value = tempSelectedTask?.name ?: "", + onValueChange = {}, + readOnly = true, + label = { Text(stringResource(R.string.assistant_screen_select_task_type_label)) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), + modifier = Modifier + .menuAnchor( + type = ExposedDropdownMenuAnchorType.PrimaryNotEditable, + enabled = true + ) + .fillMaxWidth() + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + taskTypes.forEach { task -> + DropdownMenuItem( + text = { + Text( + text = task.name, + style = MaterialTheme.typography.bodyLarge + ) + }, + onClick = { + tempSelectedTask = task + expanded = false + }, + contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding + ) + } + } + } + }, + confirmButton = { + FilledTonalButton( + onClick = { + tempSelectedTask?.let { + onDismiss() + onConfirm(it) + } + }, + enabled = tempSelectedTask != null + ) { + Text(text = stringResource(android.R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(text = stringResource(android.R.string.cancel)) + } + } + ) +} diff --git a/app/src/main/java/com/nextcloud/ui/composeComponents/bottomSheet/MoreActionsBottomSheet.kt b/app/src/main/java/com/nextcloud/ui/composeComponents/bottomSheet/MoreActionsBottomSheet.kt index b3d409aee0ca..94dd7c39b9b6 100644 --- a/app/src/main/java/com/nextcloud/ui/composeComponents/bottomSheet/MoreActionsBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/ui/composeComponents/bottomSheet/MoreActionsBottomSheet.kt @@ -37,7 +37,7 @@ import kotlinx.coroutines.launch @SuppressLint("ResourceAsColor") @OptIn(ExperimentalMaterial3Api::class) @Composable -fun MoreActionsBottomSheet(title: String? = null, actions: List Unit>>, dismiss: () -> Unit) { +fun MoreActionsBottomSheet(title: String? = null, actions: List Unit>>, onDismiss: () -> Unit) { val sheetState = rememberModalBottomSheetState() val scope = rememberCoroutineScope() @@ -45,7 +45,7 @@ fun MoreActionsBottomSheet(title: String? = null, actions: ListDelete task Are you sure you want to delete this task? Delete Task + Select task + Task Task created An error occurred while creating the task Task deleted