A fork of Simple Time Tracker with WearOS support. (Now integrated upstream)
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: Support tag selection mode setting

On the phone, the user can request for activities to be started
immediately after picking one tag, or after picking multiple.

With this commit, that behavior is reflected in the WearOS app.

Additionally, the new Submit button on the TagList allows users
to start the activity from the TagsScreen, but without picking
any tags, so it further satisfies thehale/SimpleTimeTracker-WearOS#13

closes thehale/SimpleTimeTracker-WearOS#16
closes thehale/SimpleTimeTracker-WearOS#17

Joseph Hale (Feb 26, 2024, 10:27 PM -0700) 4c19b304 fa10574b

+222 -9
+35
wear/src/main/java/com/example/util/simpletimetracker/presentation/components/SubmitButton.kt
··· 1 + /* 2 + * This Source Code Form is subject to the terms of the Mozilla Public 3 + * License, v. 2.0. If a copy of the MPL was not distributed with this 4 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 + */ 6 + package com.example.util.simpletimetracker.presentation.components 7 + 8 + import androidx.compose.foundation.layout.padding 9 + import androidx.compose.material.icons.Icons 10 + import androidx.compose.material.icons.automirrored.rounded.Send 11 + import androidx.compose.runtime.Composable 12 + import androidx.compose.ui.Modifier 13 + import androidx.compose.ui.tooling.preview.Preview 14 + import androidx.compose.ui.unit.dp 15 + import androidx.wear.compose.material.Icon 16 + import androidx.wear.compose.material.OutlinedButton 17 + 18 + @Composable 19 + fun SubmitButton(onClick: () -> Unit, contentDescription: String = "Submit") { 20 + OutlinedButton( 21 + onClick = onClick, 22 + content = { 23 + Icon(Icons.AutoMirrored.Rounded.Send, contentDescription = contentDescription) 24 + }, 25 + modifier = Modifier.padding(all = 8.dp), 26 + ) 27 + } 28 + 29 + @Preview 30 + @Composable 31 + private fun Preview() { 32 + SubmitButton( 33 + onClick = { /* Log.i("Preview", "Refresh Button clicked!") */ }, 34 + ) 35 + }
+68 -1
wear/src/main/java/com/example/util/simpletimetracker/presentation/components/TagChip.kt
··· 5 5 */ 6 6 package com.example.util.simpletimetracker.presentation.components 7 7 8 + import androidx.compose.foundation.layout.fillMaxWidth 9 + import androidx.compose.foundation.layout.padding 8 10 import androidx.compose.runtime.Composable 11 + import androidx.compose.runtime.getValue 12 + import androidx.compose.runtime.mutableStateOf 13 + import androidx.compose.runtime.remember 14 + import androidx.compose.runtime.setValue 15 + import androidx.compose.ui.Modifier 9 16 import androidx.compose.ui.text.style.TextOverflow 17 + import androidx.compose.ui.tooling.preview.Preview 18 + import androidx.compose.ui.unit.dp 19 + import androidx.wear.compose.material.Checkbox 10 20 import androidx.wear.compose.material.Chip 11 21 import androidx.wear.compose.material.Text 22 + import androidx.wear.compose.material.ToggleChip 23 + import androidx.wear.tooling.preview.devices.WearDevices 12 24 import com.example.util.simpletimetracker.wearrpc.Tag 13 25 26 + enum class TagSelectionMode { 27 + SINGLE, MULTI, 28 + } 29 + 14 30 @Composable 15 - fun TagChip(tag: Tag, onClick: () -> Unit) { 31 + fun TagChip(tag: Tag, onClick: () -> Unit, mode: TagSelectionMode = TagSelectionMode.SINGLE) { 32 + when (mode) { 33 + TagSelectionMode.SINGLE -> { 34 + SingleSelectTagChip(tag = tag, onClick = onClick) 35 + } 36 + 37 + TagSelectionMode.MULTI -> { 38 + MultiSelectTagChip(tag = tag, onClick = onClick) 39 + } 40 + } 41 + 42 + } 43 + 44 + @Composable 45 + private fun SingleSelectTagChip(tag: Tag, onClick: () -> Unit) { 16 46 Chip( 17 47 onClick = onClick, 18 48 label = { Text(tag.name, maxLines = 1, overflow = TextOverflow.Ellipsis) }, 49 + modifier = Modifier 50 + .fillMaxWidth(0.9f) 51 + .padding(top = 10.dp), 52 + ) 53 + } 54 + 55 + @Composable 56 + private fun MultiSelectTagChip(tag: Tag, onClick: () -> Unit) { 57 + var checked by remember { mutableStateOf(false) } 58 + ToggleChip( 59 + checked = checked, 60 + onCheckedChange = { 61 + checked = !checked 62 + onClick() 63 + }, 64 + label = { Text(tag.name, maxLines = 1, overflow = TextOverflow.Ellipsis) }, 65 + toggleControl = { Checkbox(checked = checked) }, 66 + modifier = Modifier 67 + .fillMaxWidth(0.9f) 68 + .padding(top = 10.dp) 69 + ) 70 + } 71 + 72 + @Preview(device = WearDevices.LARGE_ROUND) 73 + @Composable 74 + private fun Default() { 75 + TagChip(tag = Tag(id = 123, name = "Sleep", isGeneral = false), onClick = {}) 76 + } 77 + 78 + 79 + @Preview(device = WearDevices.LARGE_ROUND) 80 + @Composable 81 + private fun MultiSelectMode() { 82 + TagChip( 83 + tag = Tag(id = 123, name = "Sleep", isGeneral = false), 84 + onClick = {}, 85 + mode = TagSelectionMode.MULTI, 19 86 ) 20 87 }
+64 -4
wear/src/main/java/com/example/util/simpletimetracker/presentation/components/TagList.kt
··· 5 5 */ 6 6 package com.example.util.simpletimetracker.presentation.components 7 7 8 + import androidx.compose.foundation.layout.padding 8 9 import androidx.compose.runtime.Composable 10 + import androidx.compose.runtime.getValue 11 + import androidx.compose.runtime.mutableStateOf 12 + import androidx.compose.runtime.remember 13 + import androidx.compose.runtime.setValue 14 + import androidx.compose.ui.Modifier 15 + import androidx.compose.ui.tooling.preview.Preview 16 + import androidx.compose.ui.unit.dp 17 + import androidx.wear.compose.material.Text 18 + import androidx.wear.tooling.preview.devices.WearDevices 9 19 import com.example.util.simpletimetracker.presentation.layout.ScaffoldedScrollingColumn 10 20 import com.example.util.simpletimetracker.wearrpc.Tag 11 21 12 22 @Composable 13 - fun TagList(tags: Array<Tag>, onSelectTag: (tag: Tag) -> Unit) { 23 + fun TagList( 24 + tags: Array<Tag>, 25 + mode: TagSelectionMode = TagSelectionMode.SINGLE, 26 + onSelectionComplete: (tags: Array<Tag>) -> Unit = {}, 27 + ) { 28 + var selectedTags: List<Tag> by remember { mutableStateOf(listOf()) } 14 29 ScaffoldedScrollingColumn { 15 - for (tag in tags) { 16 - item { 17 - TagChip(tag) { onSelectTag(tag) } 30 + if (tags.isEmpty()) { 31 + item { Text("No tags", modifier = Modifier.padding(8.dp)) } 32 + } else { 33 + for (tag in tags) { 34 + item { 35 + TagChip(tag = tag, mode = mode, onClick = { 36 + when (mode) { 37 + TagSelectionMode.SINGLE -> { onSelectionComplete(arrayOf(tag)) } 38 + TagSelectionMode.MULTI -> { 39 + if (selectedTags.contains(tag)) { 40 + selectedTags = selectedTags.minus(tag) 41 + } else { 42 + selectedTags = selectedTags.plus(tag) 43 + } 44 + } 45 + } 46 + }) 47 + } 18 48 } 19 49 } 50 + item { SubmitButton(onClick = { onSelectionComplete(selectedTags.toTypedArray()) }) } 20 51 } 52 + } 53 + 54 + @Preview(device = WearDevices.LARGE_ROUND) 55 + @Composable 56 + private fun NoTags() { 57 + TagList(tags = arrayOf(), onSelectionComplete = {}) 58 + } 59 + 60 + @Preview(device = WearDevices.LARGE_ROUND) 61 + @Composable 62 + private fun WithSomeTags() { 63 + TagList( 64 + tags = arrayOf( 65 + Tag(id = 123, name = "Sleep", isGeneral = false), 66 + Tag(id = 124, name = "Personal", isGeneral = true), 67 + ), 68 + ) 69 + } 70 + 71 + @Preview(device = WearDevices.LARGE_ROUND) 72 + @Composable 73 + private fun MultiSelectMode() { 74 + TagList( 75 + tags = arrayOf( 76 + Tag(id = 123, name = "Sleep", isGeneral = false), 77 + Tag(id = 124, name = "Personal", isGeneral = true), 78 + ), 79 + mode = TagSelectionMode.MULTI, 80 + ) 21 81 }
+45
wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/settings.kt
··· 1 + /* 2 + * This Source Code Form is subject to the terms of the Mozilla Public 3 + * License, v. 2.0. If a copy of the MPL was not distributed with this 4 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 + */ 6 + package com.example.util.simpletimetracker.presentation.remember 7 + 8 + import androidx.compose.runtime.Composable 9 + import androidx.compose.runtime.LaunchedEffect 10 + import androidx.compose.runtime.getValue 11 + import androidx.compose.runtime.mutableIntStateOf 12 + import androidx.compose.runtime.mutableStateOf 13 + import androidx.compose.runtime.remember 14 + import androidx.compose.runtime.setValue 15 + import com.example.util.simpletimetracker.wearrpc.Settings 16 + import com.example.util.simpletimetracker.wearrpc.Tag 17 + import kotlinx.coroutines.Dispatchers 18 + import kotlinx.coroutines.async 19 + 20 + /** 21 + * Handles asynchronous retrieval of the settings for Simple Time Tracker on the connected phone. 22 + * 23 + * Usage: 24 + * ``` 25 + * val (settings, refresh) = rememberSettings() 26 + * ``` 27 + * 28 + * Initially, `settings` will be `null`. Once the actual settings are received 29 + * from the phone, `settings` will *automatically* update to the object and the encapsulating 30 + * Composable will *automatically* re-render. 31 + * 32 + * `refresh` is a function you can call to forcibly re-request the settings from the phone. 33 + */ 34 + @Composable 35 + fun rememberSettings(): Pair<Settings?, () -> Unit> { 36 + var rpc = rememberRPCClient() 37 + var settings: Settings? by remember { mutableStateOf(null) } 38 + var settingsQueryCount by remember { mutableIntStateOf(0) } 39 + LaunchedEffect(settingsQueryCount) { 40 + async(Dispatchers.Default) { 41 + settings = rpc.querySettings() 42 + } 43 + } 44 + return Pair(settings) { settingsQueryCount++ } 45 + }
+10 -4
wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/TagsScreen.kt
··· 9 9 import androidx.compose.runtime.rememberCoroutineScope 10 10 import androidx.navigation.NavController 11 11 import com.example.util.simpletimetracker.presentation.components.TagList 12 + import com.example.util.simpletimetracker.presentation.components.TagSelectionMode 12 13 import com.example.util.simpletimetracker.presentation.mediators.CurrentActivitiesMediator 13 14 import com.example.util.simpletimetracker.presentation.remember.rememberCurrentActivities 14 15 import com.example.util.simpletimetracker.presentation.remember.rememberRPCClient 16 + import com.example.util.simpletimetracker.presentation.remember.rememberSettings 15 17 import com.example.util.simpletimetracker.presentation.remember.rememberTags 16 - import com.example.util.simpletimetracker.wearrpc.CurrentActivity 17 18 import kotlinx.coroutines.Dispatchers 18 19 import kotlinx.coroutines.launch 19 - import java.time.Instant 20 20 21 21 @Composable 22 22 fun TagsScreen(activityId: Long, navigation: NavController) { 23 23 val coroutineScope = rememberCoroutineScope() 24 24 val rpc = rememberRPCClient() 25 + val (settings) = rememberSettings() 25 26 val (tags) = rememberTags(activityId) 26 27 val (currentActivities) = rememberCurrentActivities() 27 28 val currentActivitiesMediator = CurrentActivitiesMediator(rpc, currentActivities) 28 29 29 30 TagList( 30 31 tags, 31 - onSelectTag = { 32 + mode = if (settings?.recordTagSelectionCloseAfterOne != false) { 33 + TagSelectionMode.SINGLE 34 + } else { 35 + TagSelectionMode.MULTI 36 + }, 37 + onSelectionComplete = { 32 38 coroutineScope.launch(Dispatchers.Default) { 33 - currentActivitiesMediator.start(activityId, arrayOf(it)) 39 + currentActivitiesMediator.start(activityId, it) 34 40 coroutineScope.launch(Dispatchers.Main) { 35 41 navigation.navigate("activities") 36 42 }