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.

Merge pull request #165 from thehale/wearos

[WearOS] Changed activity 'Since' labels to live time tracking

authored by

Anton Razinkov and committed by
GitHub
(Feb 29, 2024, 9:05 AM +0300) 13a015f6 dfb32485

+178 -25
+21
README.md
··· 67 67 [![darkmode3_thumb]][darkmode3] 68 68 <br> 69 69 70 + ## WearOS 71 + 72 + [![wearos_demo]][wearos_demo] 73 + <br> 74 + 70 75 ## Technology stack 71 76 - Kotlin 72 77 - Multi module 73 78 - Single Activity 74 79 - MVVM (Jetpack ViewModel + LiveData) 75 80 - Jetpack Navigation 81 + - Jetpack Compose (WearOS) 76 82 - Hilt 77 83 - Room, migrations 78 84 - Coroutines ··· 98 104 ├── data_local # Database. 99 105 ├── domain # Business logic. 100 106 ├── navigation # Navigation interfaces and screen params. 107 + ├── wear # WearOS app. 108 + ├── wearrpc # Mobile <--> WearOS communication 101 109 ├── features 102 110 │ ├── feature_archive # Screen for archived data. 103 111 │ ├── feature_base_adapter # Shared recycler adapters. ··· 125 133 │ └── feature_widget # Widgets. 126 134 127 135 ## License 136 + 137 + **Android App** 138 + 128 139 Copyright (C) 2020-2024 Anton Razinkov devrazeeman@gmail.com 129 140 130 141 This program is free software: you can redistribute it and/or modify ··· 140 151 You should have received a copy of the GNU General Public License 141 152 along with this program. If not, see <https://www.gnu.org/licenses/>. 142 153 154 + **WearOS App** 155 + 156 + Copyright (C) 2023-2024 Joseph Hale https://jhale.dev, 157 + [@kantahrek](https://github.com/kantahrek), Anton Razinkov devrazeeman@gmail.com 158 + 159 + This Source Code Form is subject to the terms of the Mozilla Public 160 + License, v. 2.0. If a copy of the MPL was not distributed with this 161 + file, You can obtain one at https://mozilla.org/MPL/2.0/. 143 162 144 163 145 164 [change_record_thumb]: dev_files/screens/change_record_thumb.png ··· 185 204 [darkmode2]: dev_files/screens/darkmode2.png 186 205 [darkmode3_thumb]: dev_files/screens/darkmode3_thumb.png 187 206 [darkmode3]: dev_files/screens/darkmode3.png 207 + 208 + [wearos_demo]: dev_files/wearos_demo.gif
dev_files/wearos_demo.gif

This is a binary file and will not be displayed.

+5
wear/src/main/java/com/example/util/simpletimetracker/presentation/components/ActivitiesList.kt
··· 26 26 onEnableActivity: (activity: Activity) -> Unit, 27 27 onDisableActivity: (activity: Activity) -> Unit, 28 28 onRefresh: () -> Unit, 29 + footer: @Composable () -> Unit = {} 29 30 ) { 30 31 ScaffoldedScrollingColumn { 31 32 if (activities.isEmpty()) { ··· 52 53 } 53 54 54 55 item { RefreshButton(onClick = onRefresh) } 56 + item { footer() } 55 57 } 56 58 } 57 59 ··· 85 87 onEnableActivity = { /* `it` is the enabled activity */ }, 86 88 onDisableActivity = { /* `it` is the disabled activity */ }, 87 89 onRefresh = { /* What to do when requesting a refresh */ }, 90 + footer = { 91 + Text("Sample Footer") 92 + } 88 93 ) 89 94 }
+37 -20
wear/src/main/java/com/example/util/simpletimetracker/presentation/components/ActivityChip.kt
··· 18 18 import androidx.compose.ui.text.style.TextOverflow 19 19 import androidx.compose.ui.tooling.preview.Preview 20 20 import androidx.compose.ui.unit.dp 21 + import androidx.compose.ui.unit.sp 21 22 import androidx.wear.compose.material.SplitToggleChip 22 23 import androidx.wear.compose.material.Switch 23 24 import androidx.wear.compose.material.SwitchDefaults 24 25 import androidx.wear.compose.material.Text 25 26 import androidx.wear.compose.material.ToggleChipDefaults 26 27 import androidx.wear.tooling.preview.devices.WearDevices 28 + import com.example.util.simpletimetracker.presentation.remember.rememberDurationSince 27 29 import com.example.util.simpletimetracker.wearrpc.Activity 28 30 import com.example.util.simpletimetracker.wearrpc.Tag 31 + import java.time.Duration 29 32 import java.time.Instant 30 - import java.time.LocalDateTime 31 - import java.time.ZoneId 32 - import java.time.format.DateTimeFormatter 33 + 34 + private const val ISO_HOURS_MINUTES_PARTS_REGEX = "(\\d[HM])(?!$)" 35 + private const val DECIMAL_SEPARATOR_AND_FRACTIONAL_PART_REGEX = "\\.\\d+" 36 + private const val ISO_MISSING_MINUTES_REGEX = "(\\d+H) (\\d+S)" 33 37 34 38 @Composable 35 39 fun ActivityChip( ··· 46 50 "" 47 51 } 48 52 val tagString = if (tagsList.isNotEmpty()) { 49 - " ($tagsList)" 53 + " - $tagsList" 50 54 } else { 51 55 "" 52 56 } ··· 59 63 Row(modifier = Modifier.height(IntrinsicSize.Min)) { 60 64 ActivityIcon(activityIcon = activity.icon) 61 65 Text( 62 - text = activity.name + tagString, 66 + text = activity.name, 63 67 maxLines = 1, 64 68 overflow = TextOverflow.Ellipsis, 65 69 modifier = Modifier.padding(start = 4.dp), ··· 68 72 }, 69 73 secondaryLabel = { 70 74 if (startedAt != null) { 71 - Text("Since ${recentTimestampToString(startedAt)}") 75 + var startedDiff = rememberDurationSince(epochMillis = startedAt) 76 + 77 + Text( 78 + text = durationToLabel(startedDiff) + tagString, 79 + maxLines = 1, 80 + overflow = TextOverflow.Ellipsis, 81 + color = Color.White, 82 + fontSize = 10.sp, 83 + modifier = Modifier.padding( 84 + start = if (tagString.isNotEmpty()) { 85 + 2.dp 86 + } else { 87 + 22.dp 88 + }, 89 + ), 90 + ) 72 91 } else { 73 92 null 74 93 } ··· 108 127 ) 109 128 } 110 129 111 - fun recentTimestampToString(epochMillis: Long): String { 112 - // Someday, it would be nice for this to show nicer time strings 113 - // e.g. "a few minutes ago", "yesterday", etc. 114 - val time = LocalDateTime.ofInstant( 115 - Instant.ofEpochMilli(epochMillis), 116 - ZoneId.systemDefault(), 117 - ) 118 - return if (time > LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()).minusDays(1)) { 119 - time.format(DateTimeFormatter.ISO_LOCAL_TIME) 120 - } else { 121 - time.format(DateTimeFormatter.ISO_DATE_TIME).replace("T", " ") 122 - } 130 + fun durationToLabel(duration: Duration): String { 131 + return duration.toString() 132 + .substring(2) // remove "PT" at the beginning of the string representation 133 + .replace(ISO_HOURS_MINUTES_PARTS_REGEX.toRegex(), "$1 ") 134 + .replace(DECIMAL_SEPARATOR_AND_FRACTIONAL_PART_REGEX.toRegex(), "") 135 + .replace(ISO_MISSING_MINUTES_REGEX.toRegex(), "$1 0M $2").lowercase() 123 136 } 124 137 125 138 @Preview(device = WearDevices.LARGE_ROUND) ··· 164 177 @Preview(device = WearDevices.LARGE_ROUND) 165 178 @Composable 166 179 fun CurrentlyRunning() { 167 - ActivityChip(Activity(456, "Sleeping", "🛏️", 0xFFABCDEF), startedAt = 1706751601000L) 180 + ActivityChip( 181 + Activity(456, "Sleeping", "🛏️", 0xFFABCDEF), 182 + startedAt = Instant.now().toEpochMilli() - 360000, 183 + ) 168 184 } 169 185 170 186 @Preview(device = WearDevices.LARGE_ROUND) 171 187 @Composable 172 188 fun CurrentlyRunningWithTags() { 173 189 ActivityChip( 174 - Activity(456, "Sleeping", "🛏️", 0xFFABCDEF), startedAt = 1706751601000L, 190 + Activity(456, "Sleeping", "🛏️", 0xFFABCDEF), 191 + startedAt = Instant.now().toEpochMilli() - 360000, 175 192 tags = arrayOf( 176 193 Tag(id = 2, name = "Work", isGeneral = true, color = 0xFFFFAA22), 177 194 Tag(id = 4, name = "Hotel", isGeneral = false, color = 0xFFABCDEF),
+25
wear/src/main/java/com/example/util/simpletimetracker/presentation/components/CreditsButton.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.runtime.Composable 9 + import androidx.compose.ui.graphics.Color 10 + import androidx.compose.ui.platform.LocalContext 11 + import androidx.wear.compose.material.ChipDefaults 12 + import androidx.wear.compose.material.CompactChip 13 + import androidx.wear.compose.material.Text 14 + import com.example.util.simpletimetracker.R 15 + 16 + @Composable 17 + fun CreditsButton(onClick: () -> Unit) { 18 + CompactChip( 19 + onClick = onClick, 20 + label = { Text(LocalContext.current.getString(R.string.credits_button)) }, 21 + colors = ChipDefaults.chipColors( 22 + backgroundColor = Color.Transparent 23 + ) 24 + ) 25 + }
+9 -3
wear/src/main/java/com/example/util/simpletimetracker/presentation/navigation/StartActivityNavigator.kt
··· 10 10 import androidx.wear.compose.navigation.composable 11 11 import androidx.wear.compose.navigation.rememberSwipeDismissableNavController 12 12 import com.example.util.simpletimetracker.presentation.screens.ActivitiesScreen 13 + import com.example.util.simpletimetracker.presentation.screens.CreditsScreen 13 14 import com.example.util.simpletimetracker.presentation.screens.TagsScreen 14 15 15 16 object Route { 16 17 const val Activities = "activities" 17 18 const val Tags = "activities/{id}/tags" 19 + const val Credits = "credits" 18 20 } 19 21 20 22 @Composable ··· 25 27 startDestination = Route.Activities, 26 28 ) { 27 29 composable(Route.Activities) { 28 - ActivitiesScreen(onRequestTagSelection = { 29 - navigation.navigate(Route.Tags.replace("{id}", it.toString())) 30 - }) 30 + ActivitiesScreen( 31 + onRequestTagSelection = { 32 + navigation.navigate(Route.Tags.replace("{id}", it.toString())) 33 + }, 34 + onRequestCredits = { navigation.navigate(Route.Credits) }, 35 + ) 31 36 } 32 37 composable(Route.Tags) { 33 38 TagsScreen( ··· 41 46 }, 42 47 ) 43 48 } 49 + composable(Route.Credits) { CreditsScreen() } 44 50 } 45 51 }
+30
wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/durationSince.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.mutableStateOf 12 + import androidx.compose.runtime.remember 13 + import androidx.compose.runtime.setValue 14 + import kotlinx.coroutines.time.delay 15 + import java.time.Duration 16 + import java.time.Instant 17 + 18 + @Composable 19 + fun rememberDurationSince(epochMillis: Long): Duration { 20 + var duration by remember { mutableStateOf(durationSince(epochMillis)) } 21 + LaunchedEffect(duration) { 22 + delay(Duration.ofSeconds(1L)) 23 + duration = durationSince(epochMillis) 24 + } 25 + return duration 26 + } 27 + 28 + private fun durationSince(epochMillis: Long): Duration { 29 + return Duration.between(Instant.ofEpochMilli(epochMillis), Instant.now()) 30 + }
+6 -2
wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/ActivitiesScreen.kt
··· 8 8 import android.util.Log 9 9 import androidx.compose.runtime.Composable 10 10 import androidx.compose.runtime.rememberCoroutineScope 11 - import androidx.navigation.NavController 12 11 import com.example.util.simpletimetracker.presentation.components.ActivitiesList 12 + import com.example.util.simpletimetracker.presentation.components.CreditsButton 13 13 import com.example.util.simpletimetracker.presentation.mediators.CurrentActivitiesMediator 14 14 import com.example.util.simpletimetracker.presentation.remember.rememberActivities 15 15 import com.example.util.simpletimetracker.presentation.remember.rememberCurrentActivities ··· 20 20 import kotlinx.coroutines.launch 21 21 22 22 @Composable 23 - fun ActivitiesScreen(onRequestTagSelection: (activityId: Long) -> Unit) { 23 + fun ActivitiesScreen( 24 + onRequestTagSelection: (activityId: Long) -> Unit, 25 + onRequestCredits: () -> Unit, 26 + ) { 24 27 val coroutineScope = rememberCoroutineScope() 25 28 val rpc = rememberRPCClient() 26 29 val (activities, refreshActivities) = rememberActivities() ··· 69 72 onEnableActivity = startActivityWithoutTags, 70 73 onDisableActivity = stopActivity, 71 74 onRefresh = refresh, 75 + footer = { CreditsButton(onClick = onRequestCredits) }, 72 76 ) 73 77 }
+41
wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/CreditsScreen.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.screens 7 + 8 + import androidx.compose.runtime.Composable 9 + import androidx.compose.ui.platform.LocalContext 10 + import androidx.compose.ui.text.font.FontWeight 11 + import androidx.compose.ui.tooling.preview.Preview 12 + import androidx.wear.compose.material.Text 13 + import androidx.wear.tooling.preview.devices.WearDevices 14 + import com.example.util.simpletimetracker.presentation.layout.ScaffoldedScrollingColumn 15 + import com.example.util.simpletimetracker.R 16 + 17 + @Composable 18 + fun CreditsScreen() { 19 + ScaffoldedScrollingColumn{ 20 + item { Text(text = "Simple Time Tracker", fontWeight = FontWeight.Bold) } 21 + item { Text(text = "for WearOS") } 22 + item { Text(text = "") } 23 + item { Text(text = LocalContext.current.getString(R.string.credits_by)) } 24 + item { Text(text = "") } 25 + item { Text(text = "Joseph Hale") } 26 + item { Text(text = "https://jhale.dev") } 27 + item { Text(text = "") } 28 + item { Text(text = "@kantahrek") } 29 + item { Text(text = "") } 30 + item { Text(text = "Anton Razinkov") } 31 + item { Text(text = "@Razeeman") } 32 + 33 + } 34 + } 35 + 36 + 37 + @Preview(device = WearDevices.LARGE_ROUND) 38 + @Composable 39 + fun CreditsScreenPreview() { 40 + CreditsScreen() 41 + }
+2
wear/src/main/res/values-es/strings.xml
··· 4 4 <string name="refresh_button_default_content_description">Actualizar</string> 5 5 <string name="no_tags">Sin etiquetas</string> 6 6 <string name="no_activities">Sin actividades</string> 7 + <string name="credits_button">Reconocimientos</string> 8 + <string name="credits_by">por</string> 7 9 </resources>
+2
wear/src/main/res/values/strings.xml
··· 8 8 <string name="refresh_button_default_content_description">Refresh</string> 9 9 <string name="no_tags">No tags</string> 10 10 <string name="no_activities">No activities</string> 11 + <string name="credits_button">Credits</string> 12 + <string name="credits_by">by</string> 11 13 </resources>