Subtask support on Android Wear

pull/3074/head
Alex Baker 1 year ago
parent 751c8aabc1
commit 45fbb2794d

@ -67,6 +67,9 @@ class WearService(
.setPriority(item.task.priority)
.setCompleted(item.task.isCompleted)
.setHidden(item.task.task.isHidden)
.setIndent(item.task.indent)
.setCollapsed(item.task.isCollapsed)
.setNumSubtasks(item.task.children)
.apply {
if (item.task.title != null) {
setTitle(item.task.title)
@ -107,4 +110,9 @@ class WearService(
override suspend fun updateSettings(request: GrpcProto.UpdateSettingsRequest): GrpcProto.Settings {
return settings.updateData { request.settings }
}
override suspend fun toggleSubtasks(request: ToggleGroupRequest): ToggleGroupResponse {
taskDao.setCollapsed(request.value, request.collapsed)
return ToggleGroupResponse.newBuilder().build()
}
}

@ -19,6 +19,8 @@ message UiItem {
bool repeating = 6;
bool collapsed = 7;
bool hidden = 8;
uint32 indent = 9;
uint32 numSubtasks = 10;
}
message Tasks {
@ -61,4 +63,5 @@ service WearService {
rpc completeTask(CompleteTaskRequest) returns (CompleteTaskResponse);
rpc toggleGroup(ToggleGroupRequest) returns (ToggleGroupResponse);
rpc updateSettings(UpdateSettingsRequest) returns (Settings);
rpc toggleSubtasks(ToggleGroupRequest) returns (ToggleGroupResponse);
}

@ -73,6 +73,9 @@ class MainActivity : ComponentActivity() {
addTask = {},
openMenu = { navController.navigate("menu") },
openSettings = { navController.navigate("settings") },
toggleSubtasks = { id, collapsed ->
taskListViewModel.toggleSubtasks(id, collapsed)
}
)
}
composable(

@ -0,0 +1,27 @@
package org.tasks.presentation.components
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ExpandMore
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.wear.compose.material.Icon
import androidx.wear.compose.material.MaterialTheme
@Composable
fun Chevron(collapsed: Boolean) {
val rotation by animateFloatAsState(
targetValue = if (collapsed) 0f else 180f,
animationSpec = tween(250),
label = "arrow rotation",
)
Icon(
modifier = Modifier.rotate(rotation),
imageVector = Icons.Outlined.ExpandMore,
contentDescription = null,
tint = MaterialTheme.colors.onSurface,
)
}

@ -0,0 +1,39 @@
package org.tasks.presentation.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Text
@Composable
fun GroupSeparator(
title: String,
collapsed: Boolean,
onClick: () -> Unit,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.clip(MaterialTheme.shapes.large)
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(12.dp)
) {
Text(
text = title,
)
Spacer(modifier = Modifier.width(4.dp))
Chevron(collapsed = collapsed)
}
}

@ -0,0 +1,69 @@
package org.tasks.presentation.components
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.Button
import androidx.wear.compose.material.ButtonDefaults
import androidx.wear.compose.material.Card
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Text
@Composable
fun TaskCard(
text: String,
hidden: Boolean = false,
numSubtasks: Int = 0,
subtasksCollapsed: Boolean = false,
toggleSubtasks: () -> Unit = {},
icon: @Composable () -> Unit = {},
backgroundColor: Color = MaterialTheme.colors.surface,
contentColor: Color = MaterialTheme.colors.onSurface,
onClick: () -> Unit,
) {
Card(
onClick = onClick,
backgroundPainter = ColorPainter(backgroundColor),
contentPadding = PaddingValues(0.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
icon()
Text(
text = text,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
color = contentColor,
modifier = Modifier.alpha(if (hidden) .6f else 1f).weight(1f),
)
if (numSubtasks > 0) {
Button(
onClick = toggleSubtasks,
colors = ButtonDefaults.outlinedButtonColors()
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = numSubtasks.toString(), // TODO: use number formatter
color = contentColor,
)
Chevron(subtasksCollapsed)
}
}
} else {
Spacer(modifier = Modifier.width(12.dp))
}
}
}
}

@ -1,40 +1,27 @@
package org.tasks.presentation.screens
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
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.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.CheckBox
import androidx.compose.material.icons.outlined.CheckBoxOutlineBlank
import androidx.compose.material.icons.outlined.ExpandMore
import androidx.compose.material.icons.outlined.Menu
import androidx.compose.material.icons.outlined.Repeat
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.paging.compose.LazyPagingItems
import androidx.wear.compose.material.Button
import androidx.wear.compose.material.ButtonDefaults
import androidx.wear.compose.material.Card
import androidx.wear.compose.material.Icon
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Text
@ -46,6 +33,8 @@ import com.google.android.horologist.compose.paging.items
import org.jetbrains.compose.resources.stringResource
import org.tasks.GrpcProto
import org.tasks.kmp.org.tasks.themes.ColorProvider
import org.tasks.presentation.components.GroupSeparator
import org.tasks.presentation.components.TaskCard
import tasks.kmp.generated.resources.Res
import tasks.kmp.generated.resources.add_task
@ -59,6 +48,7 @@ fun TaskListScreen(
addTask: () -> Unit,
openMenu: () -> Unit,
openSettings: () -> Unit,
toggleSubtasks: (Long, Boolean) -> Unit,
) {
val columnState = rememberResponsiveColumnState()
ScreenScaffold(
@ -93,33 +83,41 @@ fun TaskListScreen(
} else {
when (item.type) {
GrpcProto.UiItemType.Task ->
TaskCard(
text = item.title,
hidden = item.hidden,
icon = {
Button(
onClick = { onComplete(item.id, !item.completed) },
colors = ButtonDefaults.iconButtonColors(),
) {
Icon(
imageVector = when {
item.completed -> Icons.Outlined.CheckBox
item.repeating -> Icons.Outlined.Repeat
else -> Icons.Outlined.CheckBoxOutlineBlank
},
tint = Color(
ColorProvider.priorityColor(
item.priority,
isDarkMode = true,
desaturate = true
)
),
contentDescription = null,
)
}
},
onClick = { openTask(item.id) },
)
Row {
if (item.indent > 0) {
Spacer(modifier = Modifier.width(20.dp * item.indent))
}
TaskCard(
text = item.title,
hidden = item.hidden,
subtasksCollapsed = item.collapsed,
numSubtasks = item.numSubtasks,
icon = {
Button(
onClick = { onComplete(item.id, !item.completed) },
colors = ButtonDefaults.iconButtonColors(),
) {
Icon(
imageVector = when {
item.completed -> Icons.Outlined.CheckBox
item.repeating -> Icons.Outlined.Repeat
else -> Icons.Outlined.CheckBoxOutlineBlank
},
tint = Color(
ColorProvider.priorityColor(
item.priority,
isDarkMode = true,
desaturate = true
)
),
contentDescription = null,
)
}
},
onClick = { openTask(item.id) },
toggleSubtasks = { toggleSubtasks(item.id, !item.collapsed) },
)
}
GrpcProto.UiItemType.Header ->
GroupSeparator(
@ -138,73 +136,6 @@ fun TaskListScreen(
}
}
@Composable
fun GroupSeparator(
title: String,
collapsed: Boolean,
onClick: () -> Unit,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.clip(MaterialTheme.shapes.large)
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(12.dp)
) {
Text(
text = title,
)
Spacer(modifier = Modifier.width(4.dp))
Chevron(collapsed = collapsed)
}
}
@Composable
private fun Chevron(collapsed: Boolean) {
val rotation by animateFloatAsState(
targetValue = if (collapsed) 0f else 180f,
animationSpec = tween(250),
label = "arrow rotation",
)
Icon(
modifier = Modifier.rotate(rotation),
imageVector = Icons.Outlined.ExpandMore,
contentDescription = null,
tint = MaterialTheme.colors.onSurface,
)
}
@Composable
fun TaskCard(
text: String,
hidden: Boolean = false,
icon: @Composable () -> Unit = {},
backgroundColor: Color = MaterialTheme.colors.surface,
contentColor: Color = MaterialTheme.colors.onSurface,
onClick: () -> Unit,
) {
Card(
onClick = onClick,
backgroundPainter = ColorPainter(backgroundColor),
contentPadding = PaddingValues(start = 0.dp, top = 0.dp, end = 12.dp, bottom = 0.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
icon()
Text(
text = text,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
color = contentColor,
modifier = Modifier.alpha(if (hidden) .6f else 1f)
)
}
}
}
@Composable
fun TitleHeader(
title: String,

@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.tasks.GrpcProto
import org.tasks.GrpcProto.CompleteTaskRequest
import org.tasks.GrpcProto.LastUpdate
import org.tasks.GrpcProto.Settings
import org.tasks.GrpcProto.ToggleGroupRequest
@ -79,9 +80,15 @@ class TaskListViewModel(
)
}
fun completeTask(it: Long, completed: Boolean) = viewModelScope.launch {
fun completeTask(id: Long, completed: Boolean) = viewModelScope.launch {
wearService.completeTask(
GrpcProto.CompleteTaskRequest.newBuilder().setId(it).setCompleted(completed).build()
CompleteTaskRequest.newBuilder().setId(id).setCompleted(completed).build()
)
}
fun toggleSubtasks(id: Long, collapsed: Boolean) = viewModelScope.launch {
wearService.toggleSubtasks(
ToggleGroupRequest.newBuilder().setValue(id).setCollapsed(collapsed).build()
)
}
}

Loading…
Cancel
Save