diff --git a/app/src/main/java/com/example/util/simpletimetracker/wear/DomainAPI.kt b/app/src/main/java/com/example/util/simpletimetracker/wear/DomainAPI.kt index bbe3f885..710a89e8 100644 --- a/app/src/main/java/com/example/util/simpletimetracker/wear/DomainAPI.kt +++ b/app/src/main/java/com/example/util/simpletimetracker/wear/DomainAPI.kt @@ -11,6 +11,7 @@ import com.example.util.simpletimetracker.domain.interactor.RecordTypeInteractor import com.example.util.simpletimetracker.domain.interactor.RemoveRunningRecordMediator import com.example.util.simpletimetracker.domain.interactor.RunningRecordInteractor import com.example.util.simpletimetracker.domain.mapper.AppColorMapper +import com.example.util.simpletimetracker.domain.model.AppColor import com.example.util.simpletimetracker.domain.model.RecordTag import com.example.util.simpletimetracker.domain.model.RunningRecord import com.example.util.simpletimetracker.wearrpc.Activity @@ -31,12 +32,11 @@ class DomainAPI( override suspend fun queryActivities(): Array { return recordTypeInteractor.getAll().filter { recordType -> !recordType.hidden } .map { recordType -> - val color = appColorMapper.mapToColorInt(recordType.color) Activity( id = recordType.id, name = recordType.name, icon = recordType.icon, - color = color.toLong(), + color = asColor(recordType.color), ) }.toTypedArray() } @@ -72,16 +72,37 @@ class DomainAPI( } override suspend fun queryTagsForActivity(activityId: Long): Array { + val activityColor = recordTypeInteractor.get(activityId)?.color return recordTagInteractor.getByTypeOrUntyped(activityId).filter { !it.archived } - .map { asTag(it) }.toTypedArray() + .map { asTag(it, asColor(activityColor)) }.sortedBy { it.name } + .sortedBy { it.isGeneral }.toTypedArray() } - private fun asTag(recordTag: RecordTag?): Tag { - return Tag( - id = recordTag?.id ?: -1, - name = recordTag?.name ?: "", - isGeneral = recordTag?.typeId == 0L, - ) + private fun asTag(recordTag: RecordTag?, activityColor: Long = 0x00000000): Tag { + return if (recordTag != null) { + val isGeneral = recordTag.typeId == 0L + val tagColor = if (isGeneral) { + asColor(recordTag.color) + } else { + activityColor + } + Tag( + id = recordTag.id, + name = recordTag.name, + isGeneral = isGeneral, + color = tagColor, + ) + } else { + Tag(id = -1, name = "", isGeneral = true, color = 0xFF555555) + } + } + + private fun asColor(appColor: AppColor?): Long { + return if (appColor == null) { + 0x00000000 + } else { + appColorMapper.mapToColorInt(appColor).toLong() + } } override suspend fun querySettings(): Settings { diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/ActivitiesList.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/ActivitiesList.kt index c297c3bf..f0ff5f26 100644 --- a/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/ActivitiesList.kt +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/ActivitiesList.kt @@ -43,7 +43,7 @@ fun ActivitiesList( activity, startedAt = currentActivity?.startedAt, tags = currentActivity?.tags ?: arrayOf(), - onPress = { onSelectActivity(activity) }, + onClick = { onSelectActivity(activity) }, onToggleOn = { onEnableActivity(activity) }, onToggleOff = { onDisableActivity(activity) }, ) diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/ActivityChip.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/ActivityChip.kt index 39264c59..451ab112 100644 --- a/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/ActivityChip.kt +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/ActivityChip.kt @@ -36,7 +36,7 @@ fun ActivityChip( activity: Activity, startedAt: Long? = null, tags: Array = arrayOf(), - onPress: () -> Unit = {}, + onClick: () -> Unit = {}, onToggleOn: () -> Unit = {}, onToggleOff: () -> Unit = {}, ) { @@ -89,7 +89,7 @@ fun ActivityChip( } }, checked = switchChecked, - onClick = onPress, + onClick = onClick, toggleControl = { Switch( checked = switchChecked, @@ -173,8 +173,8 @@ fun CurrentlyRunningWithTags() { ActivityChip( Activity(456, "Sleeping", "🛏️", 0xFFABCDEF), startedAt = 1706751601000L, tags = arrayOf( - Tag(id = 2, name = "Work", isGeneral = false), - Tag(id = 4, name = "Hotel", isGeneral = false), + Tag(id = 2, name = "Work", isGeneral = true, color = 0xFFFFAA22), + Tag(id = 4, name = "Hotel", isGeneral = false, color = 0xFFABCDEF), ), ) } \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/TagChip.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/TagChip.kt index a2b6c850..4e496b99 100644 --- a/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/TagChip.kt +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/TagChip.kt @@ -13,13 +13,17 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.wear.compose.material.Checkbox +import androidx.wear.compose.material.CheckboxDefaults import androidx.wear.compose.material.Chip +import androidx.wear.compose.material.ChipDefaults +import androidx.wear.compose.material.SplitToggleChip import androidx.wear.compose.material.Text -import androidx.wear.compose.material.ToggleChip +import androidx.wear.compose.material.ToggleChipDefaults import androidx.wear.tooling.preview.devices.WearDevices import com.example.util.simpletimetracker.wearrpc.Tag @@ -28,14 +32,25 @@ enum class TagSelectionMode { } @Composable -fun TagChip(tag: Tag, onClick: () -> Unit, mode: TagSelectionMode = TagSelectionMode.SINGLE) { +fun TagChip( + tag: Tag, + onClick: () -> Unit = {}, + onToggleOn: () -> Unit = {}, + onToggleOff: () -> Unit = {}, + mode: TagSelectionMode = TagSelectionMode.SINGLE, +) { when (mode) { TagSelectionMode.SINGLE -> { SingleSelectTagChip(tag = tag, onClick = onClick) } TagSelectionMode.MULTI -> { - MultiSelectTagChip(tag = tag, onClick = onClick) + MultiSelectTagChip( + tag = tag, + onClick = onClick, + onToggleOn = onToggleOn, + onToggleOff = onToggleOff, + ) } } @@ -46,6 +61,7 @@ private fun SingleSelectTagChip(tag: Tag, onClick: () -> Unit) { Chip( onClick = onClick, label = { Text(tag.name, maxLines = 1, overflow = TextOverflow.Ellipsis) }, + colors = ChipDefaults.chipColors(backgroundColor = Color(tag.color)), modifier = Modifier .fillMaxWidth(0.9f) .padding(top = 10.dp), @@ -53,26 +69,58 @@ private fun SingleSelectTagChip(tag: Tag, onClick: () -> Unit) { } @Composable -private fun MultiSelectTagChip(tag: Tag, onClick: () -> Unit) { - var checked by remember { mutableStateOf(false) } - ToggleChip( - checked = checked, +private fun MultiSelectTagChip( + tag: Tag, + onClick: () -> Unit = {}, + onToggleOn: () -> Unit = {}, + onToggleOff: () -> Unit = {}, + checked: Boolean = false, +) { + var _checked by remember { mutableStateOf(checked) } + SplitToggleChip( + checked = _checked, onCheckedChange = { - checked = !checked - onClick() + _checked = !_checked + if (_checked) { + onToggleOn() + } else { + onToggleOff() + } }, + onClick = onClick, label = { Text(tag.name, maxLines = 1, overflow = TextOverflow.Ellipsis) }, - toggleControl = { Checkbox(checked = checked) }, + toggleControl = { + Checkbox( + checked = _checked, + colors = CheckboxDefaults.colors( + checkedBoxColor = Color.White, + checkedCheckmarkColor = Color.White, + uncheckedBoxColor = Color.White, + uncheckedCheckmarkColor = Color.White, + ), + ) + }, + colors = ToggleChipDefaults.splitToggleChipColors( + backgroundColor = Color(tag.color), + splitBackgroundOverlayColor = if (_checked) { + Color.White.copy(alpha = .1F) + } else { + Color.Black.copy(alpha = .3F) + }, + ), modifier = Modifier .fillMaxWidth(0.9f) - .padding(top = 10.dp) + .padding(top = 10.dp), ) } @Preview(device = WearDevices.LARGE_ROUND) @Composable private fun Default() { - TagChip(tag = Tag(id = 123, name = "Sleep", isGeneral = false), onClick = {}) + TagChip( + tag = Tag(id = 123, name = "Sleep", isGeneral = false, color = 0xFF123456), + onClick = {}, + ) } @@ -80,8 +128,18 @@ private fun Default() { @Composable private fun MultiSelectMode() { TagChip( - tag = Tag(id = 123, name = "Sleep", isGeneral = false), + tag = Tag(id = 123, name = "Sleep", isGeneral = false, color = 0xFF654321), onClick = {}, mode = TagSelectionMode.MULTI, ) +} + +@Preview(device = WearDevices.LARGE_ROUND) +@Composable +private fun MultiSelectChecked() { + MultiSelectTagChip( + tag = Tag(id = 123, name = "Sleep", isGeneral = false, color = 0xFF654321), + onClick = {}, + checked = true, + ) } \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/TagList.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/TagList.kt index 6ad33b90..51d4833d 100644 --- a/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/TagList.kt +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/TagList.kt @@ -42,20 +42,11 @@ fun TagList( TagChip( tag = tag, mode = mode, onClick = { - when (mode) { - TagSelectionMode.SINGLE -> { - onSelectionComplete(arrayOf(tag)) - } - - TagSelectionMode.MULTI -> { - if (selectedTags.contains(tag)) { - selectedTags = selectedTags.minus(tag) - } else { - selectedTags = selectedTags.plus(tag) - } - } - } + onSelectionComplete(selectedTags.minus(tag).plus(tag).toTypedArray()) + // ^No duplicate tags^ }, + onToggleOn = { selectedTags = selectedTags.plus(tag) }, + onToggleOff = { selectedTags = selectedTags.minus(tag) }, ) } } @@ -75,8 +66,8 @@ private fun NoTags() { private fun WithSomeTags() { TagList( tags = arrayOf( - Tag(id = 123, name = "Sleep", isGeneral = false), - Tag(id = 124, name = "Personal", isGeneral = true), + Tag(id = 123, name = "Sleep", isGeneral = false, color = 0xFF123456), + Tag(id = 124, name = "Personal", isGeneral = true, color = 0xFF123456), ), ) } @@ -86,8 +77,8 @@ private fun WithSomeTags() { private fun MultiSelectMode() { TagList( tags = arrayOf( - Tag(id = 123, name = "Sleep", isGeneral = false), - Tag(id = 124, name = "Personal", isGeneral = true), + Tag(id = 123, name = "Sleep", isGeneral = false, color = 0xFF123456), + Tag(id = 124, name = "Personal", isGeneral = true, color = 0xFF123456), ), mode = TagSelectionMode.MULTI, ) diff --git a/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/DTO.kt b/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/DTO.kt index 34bbf9f1..977b8014 100644 --- a/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/DTO.kt +++ b/wearrpc/src/main/java/com/example/util/simpletimetracker/wearrpc/DTO.kt @@ -35,7 +35,7 @@ data class CurrentActivity(val id: Long, val startedAt: Long, val tags: Array Unit { +class MockMediatorCallback : (Activity) -> Unit { private var calledWith: Activity? = null private var callCount: Int = 0 override fun invoke(activity: Activity) { diff --git a/wearrpc/src/test/java/com/example/util/simpletimetracker/wearrpc/WearRPCTest.kt b/wearrpc/src/test/java/com/example/util/simpletimetracker/wearrpc/WearRPCTest.kt index 0630f17e..f876c6d8 100644 --- a/wearrpc/src/test/java/com/example/util/simpletimetracker/wearrpc/WearRPCTest.kt +++ b/wearrpc/src/test/java/com/example/util/simpletimetracker/wearrpc/WearRPCTest.kt @@ -21,6 +21,11 @@ open class WearRPCServerTestBase { lateinit var messenger: Messenger lateinit var client: WearRPCClient + val tagFriends = Tag(1, "Friends", isGeneral = false, 0xFFFD3251) + val tagFamily = Tag(2, "Family", isGeneral = true, 0xFFFD3251) + val tagShopping = Tag(3, "Shopping", isGeneral = false, 0xFFFF0000) + val tagWork = Tag(14, "Work", isGeneral = false, 0xFF00FF00) + @Before fun setup() { api = MockSimpleTimeTrackerAPI() @@ -101,7 +106,7 @@ class QueryCurrentActivitiesTest : WearRPCServerTestBase() { CurrentActivity( 42, jan_31_2024_afternoon, - arrayOf(Tag(1, "Friends", isGeneral = false), Tag(2, "Family", isGeneral = false)), + arrayOf(tagFriends, tagFamily), ), ) api.mock_queryCurrentActivities(activities) @@ -117,12 +122,12 @@ class QueryCurrentActivitiesTest : WearRPCServerTestBase() { CurrentActivity( 42, jan_31_2024_afternoon, - arrayOf(Tag(1, "Friends", isGeneral = false), Tag(2, "Family", isGeneral = false)), + arrayOf(tagFriends, tagFamily), ), CurrentActivity( 42, jan_31_2024_evening, - arrayOf(Tag(5, "Shopping", isGeneral = false)), + arrayOf(tagShopping), ), ) api.mock_queryCurrentActivities(activities) @@ -147,7 +152,7 @@ class QueryTagsForActivityTest : WearRPCServerTestBase() { @Test fun returns_one_tag_associated_with_activity() = runTest { - val tags = arrayOf(Tag(5, "Shopping", isGeneral = false)) + val tags = arrayOf(tagShopping) api.mock_queryTagsForActivity(mapOf(13L to tags)) val response = client.queryTagsForActivity(13) assertArrayEquals(tags, response) @@ -155,7 +160,7 @@ class QueryTagsForActivityTest : WearRPCServerTestBase() { @Test fun returns_all_tags_associated_with_activity() = runTest { - val tags = arrayOf(Tag(5, "Shopping", isGeneral = false), Tag(14, "Work", isGeneral = false)) + val tags = arrayOf(tagShopping, tagWork) api.mock_queryTagsForActivity(mapOf(13L to tags)) val response = client.queryTagsForActivity(13L) assertArrayEquals(tags, response) @@ -163,8 +168,8 @@ class QueryTagsForActivityTest : WearRPCServerTestBase() { @Test fun returns_only_tags_associated_with_requested_activity() = runTest { - val tags = arrayOf(Tag(5, "Shopping", isGeneral = false), Tag(14, "Work", isGeneral = false)) - val otherTags = arrayOf(Tag(7, "Chores", isGeneral = false), Tag(13, "Sleep", isGeneral = false)) + val tags = arrayOf(tagShopping, tagWork) + val otherTags = arrayOf(tagFriends, tagFamily) api.mock_queryTagsForActivity(mapOf(10L to tags, 17L to otherTags)) val response = client.queryTagsForActivity(10L) assertArrayEquals(tags, response)