mirror of https://github.com/tasks/tasks
WearOS task edit screen - WIP
parent
b6f1722350
commit
4e7e05c8af
@ -0,0 +1,123 @@
|
|||||||
|
package org.tasks.presentation.screens
|
||||||
|
|
||||||
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.app.RemoteInput
|
||||||
|
import android.content.Intent
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.wear.compose.material.Button
|
||||||
|
import androidx.wear.compose.material.ButtonDefaults
|
||||||
|
import androidx.wear.compose.material.CircularProgressIndicator
|
||||||
|
import androidx.wear.compose.material.Text
|
||||||
|
import androidx.wear.input.RemoteInputIntentHelper
|
||||||
|
import androidx.wear.input.wearableExtender
|
||||||
|
import com.google.android.horologist.annotations.ExperimentalHorologistApi
|
||||||
|
import com.google.android.horologist.compose.layout.ScalingLazyColumn
|
||||||
|
import com.google.android.horologist.compose.layout.ScreenScaffold
|
||||||
|
import com.google.android.horologist.compose.layout.fillMaxRectangle
|
||||||
|
import com.google.android.horologist.compose.layout.rememberResponsiveColumnState
|
||||||
|
import org.tasks.presentation.components.Card
|
||||||
|
import org.tasks.presentation.components.Checkbox
|
||||||
|
|
||||||
|
@OptIn(ExperimentalHorologistApi::class)
|
||||||
|
@Composable
|
||||||
|
fun TaskEditScreen(
|
||||||
|
uiState: UiState,
|
||||||
|
setTitle: (String) -> Unit,
|
||||||
|
save: () -> Unit,
|
||||||
|
) {
|
||||||
|
if (uiState.loading) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxRectangle(),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val columnState = rememberResponsiveColumnState(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
)
|
||||||
|
val keyboardInputRequest = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result: ActivityResult ->
|
||||||
|
if (result.resultCode == RESULT_OK) {
|
||||||
|
val text =
|
||||||
|
result
|
||||||
|
.data
|
||||||
|
?.let { RemoteInput.getResultsFromIntent(it) }
|
||||||
|
?.getCharSequence("input")
|
||||||
|
?: return@rememberLauncherForActivityResult
|
||||||
|
setTitle(text.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ScreenScaffold(
|
||||||
|
scrollState = columnState,
|
||||||
|
) {
|
||||||
|
ScalingLazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
columnState = columnState,
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Text("New task")
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
icon = {
|
||||||
|
Checkbox(
|
||||||
|
completed = uiState.completed,
|
||||||
|
repeating = uiState.repeating,
|
||||||
|
priority = uiState.priority,
|
||||||
|
toggleComplete = {},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
content = {
|
||||||
|
Text(
|
||||||
|
text = uiState.title,
|
||||||
|
modifier = Modifier.padding(vertical = 4.dp),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Button(
|
||||||
|
onClick = { save() },
|
||||||
|
colors = ButtonDefaults.buttonColors(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Text("Save")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (uiState.isNew) {
|
||||||
|
val intent: Intent = RemoteInputIntentHelper.createActionRemoteInputIntent()
|
||||||
|
val remoteInputs: List<RemoteInput> = listOf(
|
||||||
|
RemoteInput
|
||||||
|
.Builder("input")
|
||||||
|
.setLabel("Enter title")
|
||||||
|
.setAllowFreeFormInput(true)
|
||||||
|
.wearableExtender {
|
||||||
|
setInputActionType(EditorInfo.IME_ACTION_DONE)
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
RemoteInputIntentHelper.putRemoteInputsExtra(intent, remoteInputs)
|
||||||
|
keyboardInputRequest.launch(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
package org.tasks.presentation.screens
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.google.android.horologist.annotations.ExperimentalHorologistApi
|
||||||
|
import com.google.android.horologist.data.TargetNodeId
|
||||||
|
import com.google.android.horologist.datalayer.grpc.GrpcExtensions.grpcClient
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.tasks.GrpcProto
|
||||||
|
import org.tasks.WearServiceGrpcKt
|
||||||
|
import org.tasks.extensions.wearDataLayerRegistry
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
data class UiState(
|
||||||
|
val isNew: Boolean,
|
||||||
|
val loading: Boolean = !isNew,
|
||||||
|
val completed: Boolean = false,
|
||||||
|
val repeating: Boolean = false,
|
||||||
|
val priority: Int = 0,
|
||||||
|
val title: String = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalHorologistApi::class)
|
||||||
|
class TaskEditViewModel(
|
||||||
|
applicationContext: Context,
|
||||||
|
private val taskId: Long,
|
||||||
|
) : ViewModel() {
|
||||||
|
private val _uiState = MutableStateFlow(UiState(isNew = taskId == 0L))
|
||||||
|
val uiState = _uiState.asStateFlow()
|
||||||
|
private val registry = applicationContext.wearDataLayerRegistry(viewModelScope)
|
||||||
|
|
||||||
|
private val wearService : WearServiceGrpcKt.WearServiceCoroutineStub = registry.grpcClient(
|
||||||
|
nodeId = TargetNodeId.PairedPhone,
|
||||||
|
coroutineScope = viewModelScope,
|
||||||
|
) {
|
||||||
|
WearServiceGrpcKt.WearServiceCoroutineStub(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (taskId > 0) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val task = wearService
|
||||||
|
.getTask(GrpcProto.GetTaskRequest.newBuilder().setTaskId(taskId).build())
|
||||||
|
Timber.d("Received $task")
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
loading = false,
|
||||||
|
completed = task.completed,
|
||||||
|
title = task.title,
|
||||||
|
repeating = task.repeating,
|
||||||
|
priority = task.priority,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(onComplete: () -> Unit) = viewModelScope.launch {
|
||||||
|
val state = uiState.value
|
||||||
|
wearService.saveTask(
|
||||||
|
GrpcProto.SaveTaskRequest.newBuilder()
|
||||||
|
.setTitle(state.title)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
onComplete()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTitle(title: String) {
|
||||||
|
_uiState.update { it.copy(title = title) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TaskEditViewModelFactory(
|
||||||
|
private val applicationContext: Context,
|
||||||
|
private val taskId: Long,
|
||||||
|
) : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
if (modelClass.isAssignableFrom(TaskEditViewModel::class.java)) {
|
||||||
|
return TaskEditViewModel(
|
||||||
|
applicationContext = applicationContext,
|
||||||
|
taskId = taskId,
|
||||||
|
) as T
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("Unknown ViewModel class")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue