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: Show current activities and duration on Wear

The chip for each activity will now render differently if the
activity is currently running.

closes thehale/SimpleTimeTracker-WearOS#6

Joseph Hale (Feb 26, 2024, 10:23 PM -0700) a89bfb47 4673dd17

+133 -51
+108
wear/src/main/java/com/example/util/simpletimetracker/presentation/ActivityChip.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 7 + 8 + import android.widget.Toast 9 + import androidx.compose.foundation.BorderStroke 10 + import androidx.compose.foundation.layout.fillMaxWidth 11 + import androidx.compose.foundation.layout.padding 12 + import androidx.compose.runtime.Composable 13 + import androidx.compose.ui.Modifier 14 + import androidx.compose.ui.graphics.Color 15 + import androidx.compose.ui.platform.LocalContext 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.Chip 20 + import androidx.wear.compose.material.ChipDefaults 21 + import androidx.wear.compose.material.Text 22 + import com.example.util.simpletimetracker.presentation.theme.hexCodeToColor 23 + import com.example.util.simpletimetracker.wearrpc.Activity 24 + import java.time.Instant 25 + import java.time.LocalDateTime 26 + import java.time.ZoneId 27 + import java.time.format.DateTimeFormatter 28 + 29 + @Composable 30 + fun ActivityChip(activity: Activity, startedAt: Long? = null) { 31 + val context = LocalContext.current 32 + val briefIcon = if (activity.icon.startsWith("ic_")) { 33 + "?" 34 + } else { 35 + activity.icon.substring(0, activity.icon.length.coerceAtMost(2)) 36 + } 37 + val color = hexCodeToColor(activity.color) 38 + var modifier = Modifier 39 + .fillMaxWidth(0.9f) 40 + .padding(top = 10.dp) 41 + Chip( 42 + modifier = modifier, 43 + label = { 44 + Text( 45 + text = "$briefIcon : ${activity.name}", 46 + maxLines = 1, 47 + overflow = TextOverflow.Ellipsis, 48 + ) 49 + }, 50 + border = ChipDefaults.chipBorder( 51 + borderStroke = if (startedAt != null) { 52 + BorderStroke(2.dp, Color.White) 53 + } else { 54 + null 55 + }, 56 + ), 57 + secondaryLabel = { 58 + if (startedAt != null) { 59 + Text("Since ${recentTimestampToString(startedAt)}") 60 + } else { 61 + null 62 + } 63 + }, 64 + colors = ChipDefaults.chipColors( 65 + backgroundColor = color, 66 + ), 67 + onClick = { 68 + Toast.makeText( 69 + context, 70 + "Starting `${activity.name}` (id: ${activity.id})\n[Not Yet Implemented]", 71 + Toast.LENGTH_SHORT, 72 + ).show() 73 + }, 74 + ) 75 + } 76 + 77 + fun recentTimestampToString(epochMillis: Long): String { 78 + // Someday, it would be nice for this to show nicer time strings 79 + // e.g. "a few minutes ago", "yesterday", etc. 80 + val time = LocalDateTime.ofInstant( 81 + Instant.ofEpochMilli(epochMillis), 82 + ZoneId.systemDefault(), 83 + ) 84 + if (time > LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()).minusDays(1)) { 85 + return time.format(DateTimeFormatter.ISO_LOCAL_TIME) 86 + } else { 87 + return time.format(DateTimeFormatter.ISO_DATE_TIME).replace("T", " ") 88 + } 89 + 90 + } 91 + 92 + @Preview() 93 + @Composable 94 + fun SampleCooking() { 95 + ActivityChip(Activity(123, "Cooking", "🎉", "#123456")) 96 + } 97 + 98 + @Preview() 99 + @Composable 100 + fun SampleSleep() { 101 + ActivityChip(Activity(456, "Sleeping", "🛏️", "#ABCDEF")) 102 + } 103 + 104 + @Preview() 105 + @Composable 106 + fun CurrentlyRunning() { 107 + ActivityChip(Activity(456, "Sleeping", "🛏️", "#ABCDEF"), startedAt = 1706751601000L) 108 + }
+21 -51
wear/src/main/java/com/example/util/simpletimetracker/presentation/MainActivity.kt
··· 13 13 import androidx.compose.foundation.layout.Arrangement 14 14 import androidx.compose.foundation.layout.PaddingValues 15 15 import androidx.compose.foundation.layout.fillMaxSize 16 - import androidx.compose.foundation.layout.fillMaxWidth 17 16 import androidx.compose.foundation.layout.padding 18 17 import androidx.compose.foundation.selection.selectableGroup 19 18 import androidx.compose.material.icons.Icons ··· 26 25 import androidx.compose.runtime.remember 27 26 import androidx.compose.runtime.setValue 28 27 import androidx.compose.ui.Modifier 29 - import androidx.compose.ui.graphics.Color 30 28 import androidx.compose.ui.platform.LocalContext 31 - import androidx.compose.ui.text.style.TextOverflow 32 29 import androidx.compose.ui.unit.dp 33 - import androidx.wear.compose.material.Button 34 - import androidx.wear.compose.material.Chip 35 - import androidx.wear.compose.material.ChipDefaults 36 30 import androidx.wear.compose.material.Icon 37 31 import androidx.wear.compose.material.MaterialTheme 38 32 import androidx.wear.compose.material.OutlinedButton ··· 40 34 import androidx.wear.compose.material.Scaffold 41 35 import androidx.wear.compose.material.ScalingLazyColumn 42 36 import androidx.wear.compose.material.ScalingLazyListState 43 - import androidx.wear.compose.material.Text 44 37 import androidx.wear.compose.material.TimeText 45 38 import androidx.wear.compose.material.Vignette 46 39 import androidx.wear.compose.material.VignettePosition ··· 49 42 import com.example.util.simpletimetracker.presentation.theme.SimpleTimeTrackerForWearOSTheme 50 43 import com.example.util.simpletimetracker.wearrpc.Activity 51 44 import com.example.util.simpletimetracker.wearrpc.ContextMessenger 45 + import com.example.util.simpletimetracker.wearrpc.CurrentActivity 52 46 import com.example.util.simpletimetracker.wearrpc.WearRPCClient 53 47 import com.google.android.horologist.compose.focus.rememberActiveFocusRequester 54 48 import com.google.android.horologist.compose.navscaffold.ExperimentalHorologistComposeLayoutApi ··· 91 85 @Composable 92 86 fun ActivityList(scrollState: ScalingLazyListState = rememberScalingLazyListState()) { 93 87 val context = LocalContext.current 88 + 94 89 var activities: Array<Activity> by remember { mutableStateOf(arrayOf()) } 95 - var queryCount by remember { mutableIntStateOf(0) } 90 + var activitiesQueryCount by remember { mutableIntStateOf(0) } 96 91 LaunchedEffect( 97 - key1 = queryCount, 92 + key1 = activitiesQueryCount, 98 93 block = { 99 94 async(Dispatchers.Default) { 100 95 activities = WearRPCClient(ContextMessenger(context)).queryActivities() 96 + } 97 + }, 98 + ) 99 + 100 + var currentActivities: Array<CurrentActivity> by remember { mutableStateOf(arrayOf()) } 101 + var currentActivitiesQueryCount by remember { mutableIntStateOf(0) } 102 + LaunchedEffect( 103 + key1 = activitiesQueryCount, 104 + block = { 105 + async(Dispatchers.Default) { 106 + currentActivities = 107 + WearRPCClient(ContextMessenger(context)).queryCurrentActivities() 101 108 } 102 109 }, 103 110 ) ··· 115 122 state = scrollState, 116 123 ) { 117 124 for (activity in activities) { 125 + val startedAt = 126 + currentActivities.filter { it.id == activity.id }.getOrNull(0)?.startedAt 118 127 item { 119 - Activity( 120 - id = activity.id, 121 - name = activity.name, 122 - color = Color(android.graphics.Color.parseColor(activity.color)), 123 - icon = activity.icon, 124 - ) 128 + ActivityChip(activity, startedAt) 125 129 } 126 130 } 127 131 item { 128 132 OutlinedButton( 129 133 onClick = { 130 - queryCount++ 134 + activitiesQueryCount++ 135 + currentActivitiesQueryCount++ 131 136 Toast.makeText(context, "Refreshing activities", Toast.LENGTH_SHORT).show() 132 137 }, 133 138 content = { ··· 139 144 } 140 145 } 141 146 142 - @Composable 143 - fun Activity( 144 - id: Long, 145 - name: String, 146 - color: Color = Color(96, 125, 139, 255), 147 - icon: String = "?", 148 - ) { 149 - val context = LocalContext.current 150 - val briefIcon = if (icon.startsWith("ic_")) { 151 - "?" 152 - } else { 153 - icon.substring(0, icon.length.coerceAtMost(2)) 154 - } 155 - Chip( 156 - modifier = Modifier 157 - .fillMaxWidth(0.9f) 158 - .padding(top = 10.dp), 159 - label = { 160 - Text( 161 - text = "$briefIcon : $name", 162 - maxLines = 1, 163 - overflow = TextOverflow.Ellipsis, 164 - ) 165 - }, 166 - colors = ChipDefaults.chipColors( 167 - backgroundColor = color, 168 - ), 169 - onClick = { 170 - Toast.makeText( 171 - context, 172 - "Starting `$name` (id: $id)\n[Not Yet Implemented]", Toast.LENGTH_SHORT, 173 - ).show() 174 - }, 175 - ) 176 - } 177 147
+4
wear/src/main/java/com/example/util/simpletimetracker/presentation/theme/Color.kt
··· 24 24 onSecondary = Color.Black, 25 25 onError = Color.Black, 26 26 ) 27 + 28 + fun hexCodeToColor(hex: String): Color { 29 + return Color(android.graphics.Color.parseColor(hex)) 30 + }