Group separators on Android Wear

main
Alex Baker 1 day ago
parent d5f9c24da4
commit d2ff0519be

@ -9,6 +9,7 @@ import com.todoroo.astrid.service.TaskCompleter
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.tasks.WearServiceGrpcKt import org.tasks.WearServiceGrpcKt
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.tasklist.HeaderFormatter
import javax.inject.Inject import javax.inject.Inject
@OptIn(ExperimentalHorologistApi::class) @OptIn(ExperimentalHorologistApi::class)
@ -18,6 +19,7 @@ class WearDataService : BaseGrpcDataService<WearServiceGrpcKt.WearServiceCorouti
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var taskCompleter: TaskCompleter @Inject lateinit var taskCompleter: TaskCompleter
@Inject lateinit var headerFormatter: HeaderFormatter
override val registry: WearDataLayerRegistry by lazy { override val registry: WearDataLayerRegistry by lazy {
WearDataLayerRegistry.fromContext( WearDataLayerRegistry.fromContext(
@ -33,6 +35,7 @@ class WearDataService : BaseGrpcDataService<WearServiceGrpcKt.WearServiceCorouti
taskDao = taskDao, taskDao = taskDao,
preferences = preferences, preferences = preferences,
taskCompleter = taskCompleter, taskCompleter = taskCompleter,
headerFormatter = headerFormatter,
) )
} }
} }

@ -2,26 +2,61 @@ package org.tasks.wear
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskCompleter import com.todoroo.astrid.service.TaskCompleter
import kotlinx.coroutines.Dispatchers import org.tasks.GrpcProto
import kotlinx.coroutines.withContext
import org.tasks.GrpcProto.CompleteTaskRequest import org.tasks.GrpcProto.CompleteTaskRequest
import org.tasks.GrpcProto.CompleteTaskResponse import org.tasks.GrpcProto.CompleteTaskResponse
import org.tasks.GrpcProto.GetTasksRequest import org.tasks.GrpcProto.GetTasksRequest
import org.tasks.GrpcProto.Task
import org.tasks.GrpcProto.Tasks import org.tasks.GrpcProto.Tasks
import org.tasks.WearServiceGrpcKt import org.tasks.WearServiceGrpcKt
import org.tasks.filters.AstridOrderingFilter
import org.tasks.filters.MyTasksFilter import org.tasks.filters.MyTasksFilter
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.tasklist.HeaderFormatter
import org.tasks.tasklist.SectionedDataSource
import org.tasks.tasklist.UiItem
class WearService( class WearService(
private val taskDao: TaskDao, private val taskDao: TaskDao,
private val preferences: Preferences, private val preferences: Preferences,
private val taskCompleter: TaskCompleter, private val taskCompleter: TaskCompleter,
private val headerFormatter: HeaderFormatter,
) : WearServiceGrpcKt.WearServiceCoroutineImplBase() { ) : WearServiceGrpcKt.WearServiceCoroutineImplBase() {
override suspend fun getTasks(request: GetTasksRequest): Tasks { override suspend fun getTasks(request: GetTasksRequest): Tasks {
val filter = MyTasksFilter.create()
val payload = SectionedDataSource(
tasks = taskDao.fetchTasks(preferences, filter),
disableHeaders = filter.disableHeaders()
|| (filter.supportsManualSort() && preferences.isManualSort)
|| (filter is AstridOrderingFilter && preferences.isAstridSort),
groupMode = preferences.groupMode,
subtaskMode = preferences.subtaskMode,
completedAtBottom = preferences.completedTasksAtBottom,
)
return Tasks.newBuilder() return Tasks.newBuilder()
.addAllTasks(getTasks()) .addAllItems(
payload.map { item ->
when (item) {
is UiItem.Header ->
GrpcProto.UiItem.newBuilder()
.setType(GrpcProto.UiItemType.Header)
.setTitle(headerFormatter.headerString(item.value))
.build()
is UiItem.Task ->
GrpcProto.UiItem.newBuilder()
.setType(GrpcProto.UiItemType.Task)
.setId(item.task.id)
.setPriority(item.task.priority)
.setCompleted(item.task.isCompleted)
.apply {
if (item.task.title != null) {
setTitle(item.task.title)
}
}
.setRepeating(item.task.task.isRecurring)
.build()
}
}
)
.build() .build()
} }
@ -29,21 +64,4 @@ class WearService(
taskCompleter.setComplete(request.id, request.completed) taskCompleter.setComplete(request.id, request.completed)
return CompleteTaskResponse.newBuilder().setSuccess(true).build() return CompleteTaskResponse.newBuilder().setSuccess(true).build()
} }
private suspend fun getTasks(): List<Task> = withContext(Dispatchers.IO) {
val tasks = taskDao.fetchTasks(preferences, MyTasksFilter.create())
return@withContext tasks.map {
Task.newBuilder()
.setId(it.task.id)
.setPriority(it.task.priority)
.setCompleted(it.task.isCompleted)
.apply {
if (it.task.title != null) {
setTitle(it.task.title)
}
}
.setRepeating(it.task.isRecurring)
.build()
}
}
} }

@ -29,7 +29,7 @@ class HeaderFormatter @Inject constructor(
headerString(value, groupMode, alwaysDisplayFullDate, style, compact) headerString(value, groupMode, alwaysDisplayFullDate, style, compact)
} }
private suspend fun headerString( suspend fun headerString(
value: Long, value: Long,
groupMode: Int = preferences.groupMode, groupMode: Int = preferences.groupMode,
alwaysDisplayFullDate: Boolean = preferences.alwaysDisplayFullDate, alwaysDisplayFullDate: Boolean = preferences.alwaysDisplayFullDate,

@ -5,16 +5,22 @@ package org.tasks.grpc;
option java_package = "org.tasks"; option java_package = "org.tasks";
option java_outer_classname = "GrpcProto"; option java_outer_classname = "GrpcProto";
message Task { enum UiItemType {
uint64 id = 1; Header = 0;
string title = 2; Task = 1;
bool completed = 3; }
uint32 priority = 4;
bool repeating = 5; message UiItem {
UiItemType type = 1;
uint64 id = 2;
string title = 3;
bool completed = 4;
uint32 priority = 5;
bool repeating = 6;
} }
message Tasks { message Tasks {
repeated Task tasks = 1; repeated UiItem items = 1;
} }
message LastUpdate { message LastUpdate {

@ -59,7 +59,7 @@ class MainActivity : ComponentActivity() {
val viewModel: TaskListViewModel = viewModel(navBackStackEntry) val viewModel: TaskListViewModel = viewModel(navBackStackEntry)
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
TaskListScreen( TaskListScreen(
tasks = uiState.tasks.tasksList, uiItems = uiState.tasks.itemsList,
onComplete = { viewModel.completeTask(it) }, onComplete = { viewModel.completeTask(it) },
onClick = { navController.navigate("task_edit/$it") }, onClick = { navController.navigate("task_edit/$it") },
) )

@ -3,6 +3,7 @@ package org.tasks.presentation.screens
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CheckBox import androidx.compose.material.icons.outlined.CheckBox
@ -14,6 +15,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.ColorPainter import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.Button import androidx.wear.compose.material.Button
import androidx.wear.compose.material.ButtonDefaults import androidx.wear.compose.material.ButtonDefaults
@ -31,7 +33,7 @@ import org.tasks.kmp.org.tasks.themes.ColorProvider
@OptIn(ExperimentalHorologistApi::class) @OptIn(ExperimentalHorologistApi::class)
@Composable @Composable
fun TaskListScreen( fun TaskListScreen(
tasks: MutableList<GrpcProto.Task>, uiItems: List<GrpcProto.UiItem>,
onComplete: (Long) -> Unit, onComplete: (Long) -> Unit,
onClick: (Long) -> Unit, onClick: (Long) -> Unit,
) { ) {
@ -41,23 +43,46 @@ fun TaskListScreen(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
columnState = columnState, columnState = columnState,
) { ) {
items(tasks.size) { index -> items(uiItems.size) { index ->
val task = tasks[index] val item = uiItems[index]
key(task.id) { key(item.id) {
TaskCard( when (item.type) {
task = task, GrpcProto.UiItemType.Task ->
onComplete = { onComplete(task.id) }, TaskCard(
onClick = { onClick(task.id) }, task = item,
) onComplete = { onComplete(item.id) },
onClick = { onClick(item.id) },
)
GrpcProto.UiItemType.Header ->
GroupSeparator(header = item)
else -> {
throw IllegalStateException("Unknown item type: ${item.type}")
}
}
} }
} }
} }
} }
} }
@Composable
fun GroupSeparator(
header: GrpcProto.UiItem,
) {
Text(
text = header.title,
modifier = Modifier
.padding(vertical = 12.dp)
.fillMaxWidth(),
textAlign = TextAlign.Center,
)
}
@Composable @Composable
fun TaskCard( fun TaskCard(
task: GrpcProto.Task, task: GrpcProto.UiItem,
onComplete: () -> Unit, onComplete: () -> Unit,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
@ -80,7 +105,11 @@ fun TaskCard(
else -> Icons.Outlined.CheckBoxOutlineBlank else -> Icons.Outlined.CheckBoxOutlineBlank
}, },
tint = Color( tint = Color(
ColorProvider.priorityColor(task.priority, isDarkMode = true, desaturate = true) ColorProvider.priorityColor(
task.priority,
isDarkMode = true,
desaturate = true
)
), ),
contentDescription = null, contentDescription = null,
) )

Loading…
Cancel
Save