Implement drawer for Android Wear

pull/3074/head
Alex Baker 1 year ago
parent 689cd20a88
commit 105757af53

@ -11,9 +11,14 @@ import com.todoroo.astrid.service.TaskCompleter
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.tasks.GrpcProto.Settings import org.tasks.GrpcProto.Settings
import org.tasks.WearServiceGrpcKt import org.tasks.WearServiceGrpcKt
import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory
import org.tasks.extensions.wearDataLayerRegistry import org.tasks.extensions.wearDataLayerRegistry
import org.tasks.filters.FilterProvider
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.tasklist.HeaderFormatter import org.tasks.tasklist.HeaderFormatter
import org.tasks.themes.ColorProvider
import javax.inject.Inject import javax.inject.Inject
@OptIn(ExperimentalHorologistApi::class) @OptIn(ExperimentalHorologistApi::class)
@ -24,6 +29,11 @@ class WearDataService : BaseGrpcDataService<WearServiceGrpcKt.WearServiceCorouti
@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 @Inject lateinit var headerFormatter: HeaderFormatter
@Inject lateinit var firebase: Firebase
@Inject lateinit var filterProvider: FilterProvider
@Inject lateinit var inventory: Inventory
@Inject lateinit var colorProvider: ColorProvider
@Inject lateinit var defaultFilterProvider: DefaultFilterProvider
override val registry: WearDataLayerRegistry by lazy { override val registry: WearDataLayerRegistry by lazy {
applicationContext.wearDataLayerRegistry(lifecycleScope) applicationContext.wearDataLayerRegistry(lifecycleScope)
@ -40,6 +50,11 @@ class WearDataService : BaseGrpcDataService<WearServiceGrpcKt.WearServiceCorouti
taskCompleter = taskCompleter, taskCompleter = taskCompleter,
headerFormatter = headerFormatter, headerFormatter = headerFormatter,
settings = settings, settings = settings,
firebase = firebase,
filterProvider = filterProvider,
inventory = inventory,
colorProvider = colorProvider,
defaultFilterProvider = defaultFilterProvider,
) )
} }
} }

@ -7,20 +7,32 @@ import kotlinx.coroutines.flow.firstOrNull
import org.tasks.GrpcProto import org.tasks.GrpcProto
import org.tasks.GrpcProto.CompleteTaskRequest import org.tasks.GrpcProto.CompleteTaskRequest
import org.tasks.GrpcProto.CompleteTaskResponse import org.tasks.GrpcProto.CompleteTaskResponse
import org.tasks.GrpcProto.GetListsResponse
import org.tasks.GrpcProto.GetTasksRequest import org.tasks.GrpcProto.GetTasksRequest
import org.tasks.GrpcProto.ListItem
import org.tasks.GrpcProto.ListItemType
import org.tasks.GrpcProto.Tasks import org.tasks.GrpcProto.Tasks
import org.tasks.GrpcProto.ToggleGroupRequest import org.tasks.GrpcProto.ToggleGroupRequest
import org.tasks.GrpcProto.ToggleGroupResponse import org.tasks.GrpcProto.ToggleGroupResponse
import org.tasks.WearServiceGrpcKt import org.tasks.WearServiceGrpcKt
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory
import org.tasks.copy import org.tasks.copy
import org.tasks.data.NO_COUNT
import org.tasks.data.isHidden import org.tasks.data.isHidden
import org.tasks.filters.AstridOrderingFilter import org.tasks.filters.AstridOrderingFilter
import org.tasks.filters.Filter
import org.tasks.filters.FilterProvider
import org.tasks.filters.MyTasksFilter import org.tasks.filters.MyTasksFilter
import org.tasks.filters.NavigationDrawerSubheader
import org.tasks.filters.getIcon
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.tasklist.HeaderFormatter import org.tasks.tasklist.HeaderFormatter
import org.tasks.tasklist.SectionedDataSource import org.tasks.tasklist.SectionedDataSource
import org.tasks.tasklist.UiItem import org.tasks.tasklist.UiItem
import org.tasks.themes.ColorProvider
import timber.log.Timber
class WearService( class WearService(
private val taskDao: TaskDao, private val taskDao: TaskDao,
@ -29,12 +41,17 @@ class WearService(
private val headerFormatter: HeaderFormatter, private val headerFormatter: HeaderFormatter,
private val settings: DataStore<GrpcProto.Settings>, private val settings: DataStore<GrpcProto.Settings>,
private val firebase: Firebase, private val firebase: Firebase,
private val filterProvider: FilterProvider,
private val inventory: Inventory,
private val colorProvider: ColorProvider,
private val defaultFilterProvider: DefaultFilterProvider,
) : WearServiceGrpcKt.WearServiceCoroutineImplBase() { ) : WearServiceGrpcKt.WearServiceCoroutineImplBase() {
override suspend fun getTasks(request: GetTasksRequest): Tasks { override suspend fun getTasks(request: GetTasksRequest): Tasks {
val position = request.position val position = request.position
val limit = request.limit.takeIf { it > 0 } ?: Int.MAX_VALUE val limit = request.limit.takeIf { it > 0 } ?: Int.MAX_VALUE
val filter = MyTasksFilter.create()
val settingsData = settings.data.firstOrNull() ?: GrpcProto.Settings.getDefaultInstance() val settingsData = settings.data.firstOrNull() ?: GrpcProto.Settings.getDefaultInstance()
val filter =
defaultFilterProvider.getFilterFromPreference(settingsData.filter.takeIf { it.isNotBlank() })
val preferences = WearPreferences(appPreferences, settingsData) val preferences = WearPreferences(appPreferences, settingsData)
val collapsed = settingsData?.collapsedList?.toSet() ?: emptySet() val collapsed = settingsData?.collapsedList?.toSet() ?: emptySet()
val payload = SectionedDataSource( val payload = SectionedDataSource(
@ -57,14 +74,14 @@ class WearService(
is UiItem.Header -> is UiItem.Header ->
GrpcProto.UiItem.newBuilder() GrpcProto.UiItem.newBuilder()
.setId(item.value) .setId(item.value)
.setType(GrpcProto.UiItemType.Header) .setType(ListItemType.Header)
.setTitle(headerFormatter.headerString(item.value)) .setTitle(headerFormatter.headerString(item.value))
.setCollapsed(collapsed.contains(item.value)) .setCollapsed(collapsed.contains(item.value))
.build() .build()
is UiItem.Task -> is UiItem.Task ->
GrpcProto.UiItem.newBuilder() GrpcProto.UiItem.newBuilder()
.setType(GrpcProto.UiItemType.Task) .setType(ListItemType.Item)
.setId(item.task.id) .setId(item.task.id)
.setPriority(item.task.priority) .setPriority(item.task.priority)
.setCompleted(item.task.isCompleted) .setCompleted(item.task.isCompleted)
@ -80,7 +97,7 @@ class WearService(
.setRepeating(item.task.task.isRecurring) .setRepeating(item.task.task.isRecurring)
.build() .build()
} }
} }
) )
.build() .build()
} }
@ -101,7 +118,8 @@ class WearService(
} else { } else {
if (collapsed.contains(request.value)) { if (collapsed.contains(request.value)) {
collapsed.clear() collapsed.clear()
collapsed.addAll(it.collapsedList.toMutableList().apply { remove(request.value) }) collapsed.addAll(
it.collapsedList.toMutableList().apply { remove(request.value) })
} }
} }
} }
@ -118,4 +136,57 @@ class WearService(
taskDao.setCollapsed(request.value, request.collapsed) taskDao.setCollapsed(request.value, request.collapsed)
return ToggleGroupResponse.newBuilder().build() return ToggleGroupResponse.newBuilder().build()
} }
override suspend fun getLists(request: GrpcProto.GetListsRequest): GetListsResponse {
val position = request.position
val limit = request.limit.takeIf { it > 0 } ?: Int.MAX_VALUE
val selected = settings.data.firstOrNull()?.filter?.takeIf { it.isNotBlank() }
?: defaultFilterProvider.getFilterPreferenceValue(MyTasksFilter.create())
val filters = filterProvider.wearableFilters()
return GetListsResponse.newBuilder()
.setTotalItems(filters.size)
.addAllItems(
filters
.subList(position, (position + limit).coerceAtMost(filters.size))
.map { item ->
when (item) {
is Filter -> {
ListItem.newBuilder()
.setId(defaultFilterProvider.getFilterPreferenceValue(item))
.setType(ListItemType.Item)
.setTitle(item.title ?: "")
.setIcon(item.getIcon(inventory))
.setColor(getColor(item))
.setTaskCount(item.count.takeIf { it != NO_COUNT } ?: try {
taskDao.count(item)
} catch (e: Exception) {
Timber.e(e)
0
})
.build()
}
is NavigationDrawerSubheader ->
ListItem.newBuilder()
.setType(ListItemType.Header)
.setTitle(item.title ?: "")
.setId("${item.subheaderType}_${item.id}")
.build()
else -> throw IllegalArgumentException()
}
}
)
.build()
}
private fun getColor(filter: Filter): Int {
if (filter.tint != 0) {
val color = colorProvider.getThemeColor(filter.tint, true)
if (color.isFree || inventory.purchasedThemes()) {
return color.primaryColor
}
}
return 0
}
} }

@ -8,6 +8,7 @@ package com.todoroo.astrid.dao
import com.todoroo.astrid.timers.TimerPlugin import com.todoroo.astrid.timers.TimerPlugin
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.data.TaskContainer import org.tasks.data.TaskContainer
import org.tasks.data.count
import org.tasks.data.dao.TaskDao import org.tasks.data.dao.TaskDao
import org.tasks.data.db.SuspendDbUtils.eachChunk import org.tasks.data.db.SuspendDbUtils.eachChunk
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
@ -40,6 +41,8 @@ class TaskDao @Inject constructor(
suspend fun fetch(remoteId: String): Task? = taskDao.fetch(remoteId) suspend fun fetch(remoteId: String): Task? = taskDao.fetch(remoteId)
suspend fun count(item: Filter): Int = taskDao.count(item)
suspend fun getRecurringTasks(remoteIds: List<String>): List<Task> = suspend fun getRecurringTasks(remoteIds: List<String>): List<Task> =
taskDao.getRecurringTasks(remoteIds) taskDao.getRecurringTasks(remoteIds)

@ -1,6 +1,7 @@
package org.tasks.filters package org.tasks.filters
import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.getString
import org.tasks.compose.drawer.DrawerConfiguration
import org.tasks.data.GoogleTaskFilters import org.tasks.data.GoogleTaskFilters
import org.tasks.data.LocationFilters import org.tasks.data.LocationFilters
import org.tasks.data.NO_ORDER import org.tasks.data.NO_ORDER
@ -16,11 +17,10 @@ import org.tasks.data.entity.CaldavAccount.Companion.TYPE_LOCAL
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_OPENTASKS import org.tasks.data.entity.CaldavAccount.Companion.TYPE_OPENTASKS
import org.tasks.data.setupLocalAccount import org.tasks.data.setupLocalAccount
import org.tasks.data.toGtasksFilter import org.tasks.data.toGtasksFilter
import org.tasks.filters.NavigationDrawerSubheader.SubheaderType
import org.tasks.kmp.IS_DEBUG
import org.tasks.compose.drawer.DrawerConfiguration
import org.tasks.data.toLocationFilter import org.tasks.data.toLocationFilter
import org.tasks.data.toTagFilter import org.tasks.data.toTagFilter
import org.tasks.filters.NavigationDrawerSubheader.SubheaderType
import org.tasks.kmp.IS_DEBUG
import org.tasks.preferences.TasksPreferences import org.tasks.preferences.TasksPreferences
import org.tasks.preferences.TasksPreferences.Companion.collapseDebug import org.tasks.preferences.TasksPreferences.Companion.collapseDebug
import org.tasks.preferences.TasksPreferences.Companion.collapseFilters import org.tasks.preferences.TasksPreferences.Companion.collapseFilters
@ -59,6 +59,9 @@ class FilterProvider(
suspend fun filterPickerItems(): List<FilterListItem> = suspend fun filterPickerItems(): List<FilterListItem> =
getAllFilters(showCreate = false) getAllFilters(showCreate = false)
suspend fun wearableFilters(): List<FilterListItem> =
getAllFilters(showCreate = false, forceExpand = true, hideUnused = true)
suspend fun drawerCustomizationItems(): List<FilterListItem> = suspend fun drawerCustomizationItems(): List<FilterListItem> =
getAllFilters(showBuiltIn = false, showCreate = true) getAllFilters(showBuiltIn = false, showCreate = true)

@ -5,13 +5,13 @@ package org.tasks.grpc;
option java_package = "org.tasks"; option java_package = "org.tasks";
option java_outer_classname = "GrpcProto"; option java_outer_classname = "GrpcProto";
enum UiItemType { enum ListItemType {
Header = 0; Header = 0;
Task = 1; Item = 1;
} }
message UiItem { message UiItem {
UiItemType type = 1; ListItemType type = 1;
uint64 id = 2; uint64 id = 2;
string title = 3; string title = 3;
bool completed = 4; bool completed = 4;
@ -34,7 +34,7 @@ message LastUpdate {
message Settings { message Settings {
repeated uint64 collapsed = 1; repeated uint64 collapsed = 1;
string filter = 2; optional string filter = 2;
bool showHidden = 3; bool showHidden = 3;
bool showCompleted = 4; bool showCompleted = 4;
} }
@ -59,10 +59,29 @@ message ToggleGroupRequest {
} }
message ToggleGroupResponse {} message ToggleGroupResponse {}
message ListItem {
ListItemType type = 1;
string id = 2;
string title = 3;
string icon = 4;
uint32 color = 5;
uint32 taskCount = 6;
}
message GetListsRequest {
uint32 position = 1;
uint32 limit = 2;
}
message GetListsResponse {
uint32 totalItems = 1;
repeated ListItem items = 2;
}
service WearService { service WearService {
rpc getTasks(GetTasksRequest) returns (Tasks); rpc getTasks(GetTasksRequest) returns (Tasks);
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); rpc toggleSubtasks(ToggleGroupRequest) returns (ToggleGroupResponse);
rpc getLists(GetListsRequest) returns (GetListsResponse);
} }

@ -31,6 +31,8 @@ import androidx.wear.compose.navigation.composable
import androidx.wear.compose.navigation.rememberSwipeDismissableNavController import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
import androidx.wear.tooling.preview.devices.WearDevices import androidx.wear.tooling.preview.devices.WearDevices
import com.google.android.horologist.compose.layout.AppScaffold import com.google.android.horologist.compose.layout.AppScaffold
import org.tasks.presentation.screens.MenuScreen
import org.tasks.presentation.screens.MenuViewModel
import org.tasks.presentation.screens.SettingsScreen import org.tasks.presentation.screens.SettingsScreen
import org.tasks.presentation.screens.SettingsViewModel import org.tasks.presentation.screens.SettingsViewModel
import org.tasks.presentation.screens.TaskListScreen import org.tasks.presentation.screens.TaskListScreen
@ -54,6 +56,7 @@ class MainActivity : ComponentActivity() {
val navController = rememberSwipeDismissableNavController() val navController = rememberSwipeDismissableNavController()
val taskListViewModel: TaskListViewModel = viewModel() val taskListViewModel: TaskListViewModel = viewModel()
val taskListItems = taskListViewModel.uiItems.collectAsLazyPagingItems() val taskListItems = taskListViewModel.uiItems.collectAsLazyPagingItems()
val settingsViewModel: SettingsViewModel = viewModel()
SwipeDismissableNavHost( SwipeDismissableNavHost(
startDestination = "task_list", startDestination = "task_list",
navController = navController, navController = navController,
@ -87,14 +90,21 @@ class MainActivity : ComponentActivity() {
} }
composable( composable(
route = "menu", route = "menu",
) { ) { navBackStackEntry ->
val menuViewModel: MenuViewModel = viewModel(navBackStackEntry)
MenuScreen(
items = menuViewModel.uiItems.collectAsLazyPagingItems(),
selectFilter = {
settingsViewModel.setFilter(it.id)
navController.popBackStack()
},
)
} }
composable( composable(
route = "settings", route = "settings",
) { navBackStackEntry -> ) {
val settingsViewModel: SettingsViewModel = viewModel(navBackStackEntry) val viewState =
val viewState = settingsViewModel.viewState.collectAsStateWithLifecycle().value settingsViewModel.viewState.collectAsStateWithLifecycle().value
if (viewState.initialized) { if (viewState.initialized) {
SettingsScreen( SettingsScreen(
showHidden = viewState.settings.showHidden, showHidden = viewState.settings.showHidden,

@ -0,0 +1,33 @@
package org.tasks.presentation.components
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.Card
import androidx.wear.compose.material.MaterialTheme
@Composable
fun Card(
backgroundColor: Color = MaterialTheme.colors.surface,
icon: @Composable () -> Unit = {},
onClick: () -> Unit,
content: @Composable RowScope.() -> Unit,
) {
Card(
onClick = onClick,
backgroundPainter = ColorPainter(backgroundColor),
contentPadding = PaddingValues(0.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
icon()
content()
}
}
}

@ -0,0 +1,25 @@
package org.tasks.presentation.components
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.Text
@Composable
fun CollapsibleHeader(
title: String,
collapsed: Boolean,
onClick: () -> Unit,
) {
Header(
onClick = onClick,
) {
Text(
text = title,
)
Spacer(modifier = Modifier.width(4.dp))
Chevron(collapsed = collapsed)
}
}

@ -0,0 +1,6 @@
package org.tasks.presentation.components
import androidx.compose.runtime.Composable
@Composable
fun EmptyCard() = Card(onClick = {}) { }

@ -3,10 +3,8 @@ package org.tasks.presentation.components
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -16,10 +14,17 @@ import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Text import androidx.wear.compose.material.Text
@Composable @Composable
fun GroupSeparator( fun Header(text: String) {
title: String, Header(clickable = false) {
collapsed: Boolean, Text(text)
onClick: () -> Unit, }
}
@Composable
internal fun Header(
clickable: Boolean = true,
onClick: () -> Unit = {},
content: @Composable () -> Unit,
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -27,13 +32,9 @@ fun GroupSeparator(
modifier = Modifier modifier = Modifier
.clip(MaterialTheme.shapes.large) .clip(MaterialTheme.shapes.large)
.fillMaxWidth() .fillMaxWidth()
.clickable(onClick = onClick) .clickable(enabled = clickable, onClick = onClick)
.padding(12.dp) .padding(12.dp)
) { ) {
Text( content()
text = title,
)
Spacer(modifier = Modifier.width(4.dp))
Chevron(collapsed = collapsed)
} }
} }

@ -0,0 +1,97 @@
package org.tasks.presentation.screens
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.paging.compose.LazyPagingItems
import androidx.wear.compose.material.Icon
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Text
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.rememberResponsiveColumnState
import com.google.android.horologist.compose.paging.items
import org.tasks.GrpcProto.ListItem
import org.tasks.GrpcProto.ListItemType
import org.tasks.compose.components.imageVectorByName
import org.tasks.presentation.components.Card
import org.tasks.presentation.components.EmptyCard
import org.tasks.presentation.components.Header
@OptIn(ExperimentalHorologistApi::class)
@Composable
fun MenuScreen(
items: LazyPagingItems<ListItem>,
selectFilter: (ListItem) -> Unit,
) {
val columnState = rememberResponsiveColumnState()
ScreenScaffold(
scrollState = columnState,
) {
ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
columnState = columnState,
) {
items(
items = items,
key = { item -> "${item.type}_${item.id}" },
) { item ->
if (item == null) {
EmptyCard()
} else {
when (item.type) {
ListItemType.Header ->
Header(text = item.title)
ListItemType.Item -> {
Card(
icon = {
val icon = imageVectorByName(item.icon)
Box(
modifier = Modifier.size(48.dp),
contentAlignment = Alignment.Center,
) {
if (icon != null) {
Icon(
imageVector = icon,
contentDescription = null,
tint = when (item.color) {
0 -> MaterialTheme.colors.onSurface
else -> Color(color = item.color)
},
)
}
}
},
onClick = { selectFilter(item) },
) {
Text(
item.title,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(end = 12.dp).weight(1f),
)
if (item.taskCount > 0) {
Text(
item.taskCount.toString(), // TODO: localize
modifier = Modifier.padding(end = 16.dp)
)
}
}
}
else -> throw IllegalArgumentException()
}
}
}
}
}
}

@ -0,0 +1,74 @@
package org.tasks.presentation.screens
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.data.ProtoDataStoreHelper.protoFlow
import com.google.android.horologist.data.TargetNodeId
import com.google.android.horologist.datalayer.grpc.GrpcExtensions.grpcClient
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.tasks.GrpcProto
import org.tasks.GrpcProto.LastUpdate
import org.tasks.GrpcProto.ListItem
import org.tasks.GrpcProto.Settings
import org.tasks.WearServiceGrpcKt
import org.tasks.extensions.wearDataLayerRegistry
import org.tasks.presentation.MyPagingSource
@OptIn(ExperimentalHorologistApi::class)
class MenuViewModel(
application: Application
): AndroidViewModel(application) {
private var pagingSource: MyPagingSource<ListItem>? = null
val uiItems: Flow<PagingData<ListItem>> = Pager(
config = PagingConfig(pageSize = 20),
pagingSourceFactory = {
MyPagingSource { position, limit ->
wearService
.getLists(
GrpcProto
.GetListsRequest
.newBuilder()
.setPosition(position)
.setLimit(limit)
.build()
)
.let { Pair(it.totalItems, it.itemsList) }
}
.also { pagingSource = it }
}
)
.flow
.cachedIn(viewModelScope)
private val registry = application.wearDataLayerRegistry(viewModelScope)
private val wearService : WearServiceGrpcKt.WearServiceCoroutineStub = registry.grpcClient(
nodeId = TargetNodeId.PairedPhone,
coroutineScope = viewModelScope,
) {
WearServiceGrpcKt.WearServiceCoroutineStub(it)
}
init {
registry
.protoFlow<LastUpdate>(TargetNodeId.PairedPhone)
.onEach { invalidate() }
.launchIn(viewModelScope)
registry
.protoFlow<Settings>(TargetNodeId.PairedPhone)
.onEach { invalidate() }
.launchIn(viewModelScope)
}
private fun invalidate() {
pagingSource?.invalidate()
}
}

@ -19,6 +19,7 @@ import org.tasks.SettingsKt
import org.tasks.WearServiceGrpcKt import org.tasks.WearServiceGrpcKt
import org.tasks.copy import org.tasks.copy
import org.tasks.extensions.wearDataLayerRegistry import org.tasks.extensions.wearDataLayerRegistry
import org.tasks.tasklist.SectionedDataSource.Companion.HEADER_COMPLETED
data class ViewState( data class ViewState(
val initialized: Boolean = false, val initialized: Boolean = false,
@ -65,6 +66,14 @@ class SettingsViewModel(
} }
} }
fun setFilter(filter: String) {
updateSettings {
this.filter = filter
collapsed.clear()
collapsed.add(HEADER_COMPLETED)
}
}
private fun updateSettings(block: SettingsKt.Dsl.() -> Unit) = viewModelScope.launch { private fun updateSettings(block: SettingsKt.Dsl.() -> Unit) = viewModelScope.launch {
wearService.updateSettings( wearService.updateSettings(
GrpcProto.UpdateSettingsRequest.newBuilder() GrpcProto.UpdateSettingsRequest.newBuilder()

@ -33,7 +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.CollapsibleHeader
import org.tasks.presentation.components.EmptyCard
import org.tasks.presentation.components.TaskCard 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
@ -75,14 +76,10 @@ fun TaskListScreen(
key = { item -> "${item.type}_${item.id}" }, key = { item -> "${item.type}_${item.id}" },
) { item -> ) { item ->
if (item == null) { if (item == null) {
TaskCard( EmptyCard()
text = "",
icon = {},
onClick = {},
)
} else { } else {
when (item.type) { when (item.type) {
GrpcProto.UiItemType.Task -> GrpcProto.ListItemType.Item ->
Row { Row {
if (item.indent > 0) { if (item.indent > 0) {
Spacer(modifier = Modifier.width(20.dp * item.indent)) Spacer(modifier = Modifier.width(20.dp * item.indent))
@ -119,8 +116,8 @@ fun TaskListScreen(
) )
} }
GrpcProto.UiItemType.Header -> GrpcProto.ListItemType.Header ->
GroupSeparator( CollapsibleHeader(
title = item.title, title = item.title,
collapsed = item.collapsed, collapsed = item.collapsed,
onClick = { toggleGroup(item.id, !item.collapsed) }, onClick = { toggleGroup(item.id, !item.collapsed) },

@ -63,11 +63,11 @@ class TaskListViewModel(
init { init {
registry registry
.protoFlow<LastUpdate>(TargetNodeId.PairedPhone) .protoFlow<LastUpdate>(TargetNodeId.PairedPhone)
.onEach { pagingSource?.invalidate() } .onEach { invalidate() }
.launchIn(viewModelScope) .launchIn(viewModelScope)
registry registry
.protoFlow<Settings>(TargetNodeId.PairedPhone) .protoFlow<Settings>(TargetNodeId.PairedPhone)
.onEach { pagingSource?.invalidate() } .onEach { invalidate() }
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }
@ -78,17 +78,24 @@ class TaskListViewModel(
.setCollapsed(setCollapsed) .setCollapsed(setCollapsed)
.build() .build()
) )
invalidate()
} }
fun completeTask(id: Long, completed: Boolean) = viewModelScope.launch { fun completeTask(id: Long, completed: Boolean) = viewModelScope.launch {
wearService.completeTask( wearService.completeTask(
CompleteTaskRequest.newBuilder().setId(id).setCompleted(completed).build() CompleteTaskRequest.newBuilder().setId(id).setCompleted(completed).build()
) )
invalidate()
} }
fun toggleSubtasks(id: Long, collapsed: Boolean) = viewModelScope.launch { fun toggleSubtasks(id: Long, collapsed: Boolean) = viewModelScope.launch {
wearService.toggleSubtasks( wearService.toggleSubtasks(
ToggleGroupRequest.newBuilder().setValue(id).setCollapsed(collapsed).build() ToggleGroupRequest.newBuilder().setValue(id).setCollapsed(collapsed).build()
) )
invalidate()
}
private fun invalidate() {
pagingSource?.invalidate()
} }
} }

Loading…
Cancel
Save