Skip to content

Commit 13a015f

Browse files
authored
Merge pull request #165 from thehale/wearos
[WearOS] Changed activity 'Since' labels to live time tracking
2 parents dfb3248 + 6c602bd commit 13a015f

File tree

11 files changed

+178
-25
lines changed

11 files changed

+178
-25
lines changed

README.md

+21
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,18 @@ Simple app that helps track how much time you spend on all the useless activitie
6767
[![darkmode3_thumb]][darkmode3]
6868
<br>
6969

70+
## WearOS
71+
72+
[![wearos_demo]][wearos_demo]
73+
<br>
74+
7075
## Technology stack
7176
- Kotlin
7277
- Multi module
7378
- Single Activity
7479
- MVVM (Jetpack ViewModel + LiveData)
7580
- Jetpack Navigation
81+
- Jetpack Compose (WearOS)
7682
- Hilt
7783
- Room, migrations
7884
- Coroutines
@@ -98,6 +104,8 @@ Simple app that helps track how much time you spend on all the useless activitie
98104
├── data_local # Database.
99105
├── domain # Business logic.
100106
├── navigation # Navigation interfaces and screen params.
107+
├── wear # WearOS app.
108+
├── wearrpc # Mobile <--> WearOS communication
101109
├── features
102110
│ ├── feature_archive # Screen for archived data.
103111
│ ├── feature_base_adapter # Shared recycler adapters.
@@ -125,6 +133,9 @@ Simple app that helps track how much time you spend on all the useless activitie
125133
│ └── feature_widget # Widgets.
126134

127135
## License
136+
137+
**Android App**
138+
128139
Copyright (C) 2020-2024 Anton Razinkov devrazeeman@gmail.com
129140

130141
This program is free software: you can redistribute it and/or modify
@@ -140,6 +151,14 @@ GNU General Public License for more details.
140151
You should have received a copy of the GNU General Public License
141152
along with this program. If not, see <https://www.gnu.org/licenses/>.
142153

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/.
143162

144163

145164
[change_record_thumb]: dev_files/screens/change_record_thumb.png
@@ -185,3 +204,5 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
185204
[darkmode2]: dev_files/screens/darkmode2.png
186205
[darkmode3_thumb]: dev_files/screens/darkmode3_thumb.png
187206
[darkmode3]: dev_files/screens/darkmode3.png
207+
208+
[wearos_demo]: dev_files/wearos_demo.gif

dev_files/wearos_demo.gif

387 KB
Loading

wear/src/main/java/com/example/util/simpletimetracker/presentation/components/ActivitiesList.kt

+5
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ fun ActivitiesList(
2626
onEnableActivity: (activity: Activity) -> Unit,
2727
onDisableActivity: (activity: Activity) -> Unit,
2828
onRefresh: () -> Unit,
29+
footer: @Composable () -> Unit = {}
2930
) {
3031
ScaffoldedScrollingColumn {
3132
if (activities.isEmpty()) {
@@ -52,6 +53,7 @@ fun ActivitiesList(
5253
}
5354

5455
item { RefreshButton(onClick = onRefresh) }
56+
item { footer() }
5557
}
5658
}
5759

@@ -85,5 +87,8 @@ private fun Preview() {
8587
onEnableActivity = { /* `it` is the enabled activity */ },
8688
onDisableActivity = { /* `it` is the disabled activity */ },
8789
onRefresh = { /* What to do when requesting a refresh */ },
90+
footer = {
91+
Text("Sample Footer")
92+
}
8893
)
8994
}

wear/src/main/java/com/example/util/simpletimetracker/presentation/components/ActivityChip.kt

+37-20
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,22 @@ import androidx.compose.ui.semantics.semantics
1818
import androidx.compose.ui.text.style.TextOverflow
1919
import androidx.compose.ui.tooling.preview.Preview
2020
import androidx.compose.ui.unit.dp
21+
import androidx.compose.ui.unit.sp
2122
import androidx.wear.compose.material.SplitToggleChip
2223
import androidx.wear.compose.material.Switch
2324
import androidx.wear.compose.material.SwitchDefaults
2425
import androidx.wear.compose.material.Text
2526
import androidx.wear.compose.material.ToggleChipDefaults
2627
import androidx.wear.tooling.preview.devices.WearDevices
28+
import com.example.util.simpletimetracker.presentation.remember.rememberDurationSince
2729
import com.example.util.simpletimetracker.wearrpc.Activity
2830
import com.example.util.simpletimetracker.wearrpc.Tag
31+
import java.time.Duration
2932
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)"
3337

3438
@Composable
3539
fun ActivityChip(
@@ -46,7 +50,7 @@ fun ActivityChip(
4650
""
4751
}
4852
val tagString = if (tagsList.isNotEmpty()) {
49-
" ($tagsList)"
53+
" - $tagsList"
5054
} else {
5155
""
5256
}
@@ -59,7 +63,7 @@ fun ActivityChip(
5963
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
6064
ActivityIcon(activityIcon = activity.icon)
6165
Text(
62-
text = activity.name + tagString,
66+
text = activity.name,
6367
maxLines = 1,
6468
overflow = TextOverflow.Ellipsis,
6569
modifier = Modifier.padding(start = 4.dp),
@@ -68,7 +72,22 @@ fun ActivityChip(
6872
},
6973
secondaryLabel = {
7074
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+
)
7291
} else {
7392
null
7493
}
@@ -108,18 +127,12 @@ fun ActivityChip(
108127
)
109128
}
110129

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()
123136
}
124137

125138
@Preview(device = WearDevices.LARGE_ROUND)
@@ -164,14 +177,18 @@ fun White() {
164177
@Preview(device = WearDevices.LARGE_ROUND)
165178
@Composable
166179
fun CurrentlyRunning() {
167-
ActivityChip(Activity(456, "Sleeping", "🛏️", 0xFFABCDEF), startedAt = 1706751601000L)
180+
ActivityChip(
181+
Activity(456, "Sleeping", "🛏️", 0xFFABCDEF),
182+
startedAt = Instant.now().toEpochMilli() - 360000,
183+
)
168184
}
169185

170186
@Preview(device = WearDevices.LARGE_ROUND)
171187
@Composable
172188
fun CurrentlyRunningWithTags() {
173189
ActivityChip(
174-
Activity(456, "Sleeping", "🛏️", 0xFFABCDEF), startedAt = 1706751601000L,
190+
Activity(456, "Sleeping", "🛏️", 0xFFABCDEF),
191+
startedAt = Instant.now().toEpochMilli() - 360000,
175192
tags = arrayOf(
176193
Tag(id = 2, name = "Work", isGeneral = true, color = 0xFFFFAA22),
177194
Tag(id = 4, name = "Hotel", isGeneral = false, color = 0xFFABCDEF),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
}

wear/src/main/java/com/example/util/simpletimetracker/presentation/navigation/StartActivityNavigator.kt

+9-3
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import androidx.wear.compose.navigation.SwipeDismissableNavHost
1010
import androidx.wear.compose.navigation.composable
1111
import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
1212
import com.example.util.simpletimetracker.presentation.screens.ActivitiesScreen
13+
import com.example.util.simpletimetracker.presentation.screens.CreditsScreen
1314
import com.example.util.simpletimetracker.presentation.screens.TagsScreen
1415

1516
object Route {
1617
const val Activities = "activities"
1718
const val Tags = "activities/{id}/tags"
19+
const val Credits = "credits"
1820
}
1921

2022
@Composable
@@ -25,9 +27,12 @@ fun StartActivityNavigator() {
2527
startDestination = Route.Activities,
2628
) {
2729
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+
)
3136
}
3237
composable(Route.Tags) {
3338
TagsScreen(
@@ -41,5 +46,6 @@ fun StartActivityNavigator() {
4146
},
4247
)
4348
}
49+
composable(Route.Credits) { CreditsScreen() }
4450
}
4551
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
}

wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/ActivitiesScreen.kt

+6-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ package com.example.util.simpletimetracker.presentation.screens
88
import android.util.Log
99
import androidx.compose.runtime.Composable
1010
import androidx.compose.runtime.rememberCoroutineScope
11-
import androidx.navigation.NavController
1211
import com.example.util.simpletimetracker.presentation.components.ActivitiesList
12+
import com.example.util.simpletimetracker.presentation.components.CreditsButton
1313
import com.example.util.simpletimetracker.presentation.mediators.CurrentActivitiesMediator
1414
import com.example.util.simpletimetracker.presentation.remember.rememberActivities
1515
import com.example.util.simpletimetracker.presentation.remember.rememberCurrentActivities
@@ -20,7 +20,10 @@ import kotlinx.coroutines.Dispatchers
2020
import kotlinx.coroutines.launch
2121

2222
@Composable
23-
fun ActivitiesScreen(onRequestTagSelection: (activityId: Long) -> Unit) {
23+
fun ActivitiesScreen(
24+
onRequestTagSelection: (activityId: Long) -> Unit,
25+
onRequestCredits: () -> Unit,
26+
) {
2427
val coroutineScope = rememberCoroutineScope()
2528
val rpc = rememberRPCClient()
2629
val (activities, refreshActivities) = rememberActivities()
@@ -69,5 +72,6 @@ fun ActivitiesScreen(onRequestTagSelection: (activityId: Long) -> Unit) {
6972
onEnableActivity = startActivityWithoutTags,
7073
onDisableActivity = stopActivity,
7174
onRefresh = refresh,
75+
footer = { CreditsButton(onClick = onRequestCredits) },
7276
)
7377
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
}

wear/src/main/res/values-es/strings.xml

+2
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@
44
<string name="refresh_button_default_content_description">Actualizar</string>
55
<string name="no_tags">Sin etiquetas</string>
66
<string name="no_activities">Sin actividades</string>
7+
<string name="credits_button">Reconocimientos</string>
8+
<string name="credits_by">por</string>
79
</resources>

wear/src/main/res/values/strings.xml

+2
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@
88
<string name="refresh_button_default_content_description">Refresh</string>
99
<string name="no_tags">No tags</string>
1010
<string name="no_activities">No activities</string>
11+
<string name="credits_button">Credits</string>
12+
<string name="credits_by">by</string>
1113
</resources>

0 commit comments

Comments
 (0)