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) .setPriority(item.task.priority)
.setCompleted(item.task.isCompleted) .setCompleted(item.task.isCompleted)
.setHidden(item.task.task.isHidden) .setHidden(item.task.task.isHidden)
.setIndent(item.task.indent)
.setCollapsed(item.task.isCollapsed)
.setNumSubtasks(item.task.children)
.apply { .apply {
if (item.task.title != null) { if (item.task.title != null) {
setTitle(item.task.title) setTitle(item.task.title)
@ -107,4 +110,9 @@ class WearService(
override suspend fun updateSettings(request: GrpcProto.UpdateSettingsRequest): GrpcProto.Settings { override suspend fun updateSettings(request: GrpcProto.UpdateSettingsRequest): GrpcProto.Settings {
return settings.updateData { request.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 repeating = 6;
bool collapsed = 7; bool collapsed = 7;
bool hidden = 8; bool hidden = 8;
uint32 indent = 9;
uint32 numSubtasks = 10;
} }
message Tasks { message Tasks {
@ -61,4 +63,5 @@ service WearService {
rpc completeTask(CompleteTaskRequest) returns (CompleteTaskResponse); rpc completeTask(CompleteTaskRequest) returns (CompleteTaskResponse);
rpc toggleGroup(ToggleGroupRequest) returns (ToggleGroupResponse); rpc toggleGroup(ToggleGroupRequest) returns (ToggleGroupResponse);
rpc updateSettings(UpdateSettingsRequest) returns (Settings); rpc updateSettings(UpdateSettingsRequest) returns (Settings);
rpc toggleSubtasks(ToggleGroupRequest) returns (ToggleGroupResponse);
} }

@ -73,6 +73,9 @@ class MainActivity : ComponentActivity() {
addTask = {}, addTask = {},
openMenu = { navController.navigate("menu") }, openMenu = { navController.navigate("menu") },
openSettings = { navController.navigate("settings") }, openSettings = { navController.navigate("settings") },
toggleSubtasks = { id, collapsed ->
taskListViewModel.toggleSubtasks(id, collapsed)
}
) )
} }
composable( 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 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.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.CheckBox import androidx.compose.material.icons.outlined.CheckBox
import androidx.compose.material.icons.outlined.CheckBoxOutlineBlank 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.Menu
import androidx.compose.material.icons.outlined.Repeat import androidx.compose.material.icons.outlined.Repeat
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.Color
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.LazyPagingItems
import androidx.wear.compose.material.Button import androidx.wear.compose.material.Button
import androidx.wear.compose.material.ButtonDefaults import androidx.wear.compose.material.ButtonDefaults
import androidx.wear.compose.material.Card
import androidx.wear.compose.material.Icon import androidx.wear.compose.material.Icon
import androidx.wear.compose.material.MaterialTheme import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Text 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.jetbrains.compose.resources.stringResource
import org.tasks.GrpcProto import org.tasks.GrpcProto
import org.tasks.kmp.org.tasks.themes.ColorProvider 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.Res
import tasks.kmp.generated.resources.add_task import tasks.kmp.generated.resources.add_task
@ -59,6 +48,7 @@ fun TaskListScreen(
addTask: () -> Unit, addTask: () -> Unit,
openMenu: () -> Unit, openMenu: () -> Unit,
openSettings: () -> Unit, openSettings: () -> Unit,
toggleSubtasks: (Long, Boolean) -> Unit,
) { ) {
val columnState = rememberResponsiveColumnState() val columnState = rememberResponsiveColumnState()
ScreenScaffold( ScreenScaffold(
@ -93,33 +83,41 @@ fun TaskListScreen(
} else { } else {
when (item.type) { when (item.type) {
GrpcProto.UiItemType.Task -> GrpcProto.UiItemType.Task ->
TaskCard( Row {
text = item.title, if (item.indent > 0) {
hidden = item.hidden, Spacer(modifier = Modifier.width(20.dp * item.indent))
icon = { }
Button( TaskCard(
onClick = { onComplete(item.id, !item.completed) }, text = item.title,
colors = ButtonDefaults.iconButtonColors(), hidden = item.hidden,
) { subtasksCollapsed = item.collapsed,
Icon( numSubtasks = item.numSubtasks,
imageVector = when { icon = {
item.completed -> Icons.Outlined.CheckBox Button(
item.repeating -> Icons.Outlined.Repeat onClick = { onComplete(item.id, !item.completed) },
else -> Icons.Outlined.CheckBoxOutlineBlank colors = ButtonDefaults.iconButtonColors(),
}, ) {
tint = Color( Icon(
ColorProvider.priorityColor( imageVector = when {
item.priority, item.completed -> Icons.Outlined.CheckBox
isDarkMode = true, item.repeating -> Icons.Outlined.Repeat
desaturate = true else -> Icons.Outlined.CheckBoxOutlineBlank
) },
), tint = Color(
contentDescription = null, ColorProvider.priorityColor(
) item.priority,
} isDarkMode = true,
}, desaturate = true
onClick = { openTask(item.id) }, )
) ),
contentDescription = null,
)
}
},
onClick = { openTask(item.id) },
toggleSubtasks = { toggleSubtasks(item.id, !item.collapsed) },
)
}
GrpcProto.UiItemType.Header -> GrpcProto.UiItemType.Header ->
GroupSeparator( 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 @Composable
fun TitleHeader( fun TitleHeader(
title: String, title: String,

@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tasks.GrpcProto import org.tasks.GrpcProto
import org.tasks.GrpcProto.CompleteTaskRequest
import org.tasks.GrpcProto.LastUpdate import org.tasks.GrpcProto.LastUpdate
import org.tasks.GrpcProto.Settings import org.tasks.GrpcProto.Settings
import org.tasks.GrpcProto.ToggleGroupRequest 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( 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