diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-07-12 09:41:26 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-07-12 09:41:26 +0000 |
| commit | e1d93d9de7a8af3d47fefdb797ae36c7bde555aa (patch) | |
| tree | 436a7b5ea15b0146d624346f720990358d1b73b9 /android | |
| parent | 35a84d319d926b57bae82578037ce5e68442cdb1 (diff) | |
feat(widget): add manual refresh button with loading-state icon
Diffstat (limited to 'android')
6 files changed, 99 insertions, 8 deletions
diff --git a/android/app/src/main/java/org/terst/doot/widget/data/DataStore.kt b/android/app/src/main/java/org/terst/doot/widget/data/DataStore.kt index 164e3a2..d2f2cbb 100644 --- a/android/app/src/main/java/org/terst/doot/widget/data/DataStore.kt +++ b/android/app/src/main/java/org/terst/doot/widget/data/DataStore.kt @@ -3,6 +3,7 @@ package org.terst.doot.widget.data import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore @@ -15,4 +16,5 @@ object Keys { val ITEMS_JSON = stringPreferencesKey("items_json") val NOW = stringPreferencesKey("now") val LAST_UPDATED = longPreferencesKey("last_updated") + val IS_REFRESHING = booleanPreferencesKey("is_refreshing") } diff --git a/android/app/src/main/java/org/terst/doot/widget/ui/Actions.kt b/android/app/src/main/java/org/terst/doot/widget/ui/Actions.kt index 27a49e6..3a813fd 100644 --- a/android/app/src/main/java/org/terst/doot/widget/ui/Actions.kt +++ b/android/app/src/main/java/org/terst/doot/widget/ui/Actions.kt @@ -1,10 +1,15 @@ package org.terst.doot.widget.ui import android.content.Context +import androidx.datastore.preferences.core.edit import androidx.glance.GlanceId import androidx.glance.action.ActionParameters import androidx.glance.appwidget.action.ActionCallback +import androidx.glance.appwidget.updateAll +import org.terst.doot.widget.data.Keys +import org.terst.doot.widget.data.dataStore import org.terst.doot.widget.work.CompleteWorker +import org.terst.doot.widget.work.RefreshWorker class CompleteTaskAction : ActionCallback { override suspend fun onAction( @@ -22,3 +27,19 @@ class CompleteTaskAction : ActionCallback { val sourceKey = ActionParameters.Key<String>("item_source") } } + +// Sets IS_REFRESHING synchronously (before the network round trip) so the +// widget's icon flips to the loading state on the spot -- RefreshWorker +// clears the flag when it finishes, regardless of outcome, so the icon +// never gets stuck. +class RefreshTaskAction : ActionCallback { + override suspend fun onAction( + context: Context, + glanceId: GlanceId, + parameters: ActionParameters + ) { + context.dataStore.edit { it[Keys.IS_REFRESHING] = true } + DootWidget().updateAll(context) + RefreshWorker.runOnce(context) + } +} diff --git a/android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt b/android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt index 7674ddb..7c84168 100644 --- a/android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt +++ b/android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt @@ -47,9 +47,10 @@ class DootWidget : GlanceAppWidget() { val items = parseItems(prefs) val now = prefs[Keys.NOW]?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: Instant.now() + val isRefreshing = prefs[Keys.IS_REFRESHING] ?: false provideContent { - WidgetRoot(items, now) + WidgetRoot(items, now, isRefreshing) } } @@ -60,7 +61,7 @@ class DootWidget : GlanceAppWidget() { } @Composable -fun WidgetRoot(items: List<WidgetItem>, now: Instant) { +fun WidgetRoot(items: List<WidgetItem>, now: Instant, isRefreshing: Boolean) { val zone = ZoneId.systemDefault() val nowZoned: ZonedDateTime = now.atZone(zone) val todayStart = nowZoned.toLocalDate().atStartOfDay(zone).toInstant() @@ -107,15 +108,20 @@ fun WidgetRoot(items: List<WidgetItem>, now: Instant) { .background(Color.Transparent) .padding(horizontal = 8.dp, vertical = 4.dp) ) { - Row(modifier = GlanceModifier.fillMaxWidth().padding(bottom = 4.dp)) { + Row( + modifier = GlanceModifier.fillMaxWidth().padding(bottom = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { Text( "TODAY", style = TextStyle( color = ColorProvider(Color(0x66FFFFFF)), fontSize = 11.sp, fontWeight = FontWeight.Bold - ) + ), + modifier = GlanceModifier.defaultWeight() ) + RefreshButton(isRefreshing) } todayAllDay.forEach { AllDayRow(it) } @@ -151,6 +157,26 @@ fun AllDayRow(event: WidgetItem) { } @Composable +fun RefreshButton(isRefreshing: Boolean) { + Box( + modifier = GlanceModifier + .size(24.dp) + .clickable(actionRunCallback<RefreshTaskAction>()), + contentAlignment = Alignment.Center + ) { + Image( + provider = ImageProvider( + if (isRefreshing) org.terst.doot.widget.R.drawable.ic_refresh_loading + else org.terst.doot.widget.R.drawable.ic_refresh + ), + contentDescription = if (isRefreshing) "Refreshing" else "Refresh", + colorFilter = ColorFilter.tint(ColorProvider(Color(0x99FFFFFF))), + modifier = GlanceModifier.size(14.dp) + ) + } +} + +@Composable fun HourRow( hour: Int, nowZoned: ZonedDateTime, diff --git a/android/app/src/main/java/org/terst/doot/widget/work/RefreshWorker.kt b/android/app/src/main/java/org/terst/doot/widget/work/RefreshWorker.kt index 5c0e89c..3bb8bdd 100644 --- a/android/app/src/main/java/org/terst/doot/widget/work/RefreshWorker.kt +++ b/android/app/src/main/java/org/terst/doot/widget/work/RefreshWorker.kt @@ -1,6 +1,7 @@ package org.terst.doot.widget.work import android.content.Context +import androidx.datastore.preferences.core.edit import androidx.glance.appwidget.updateAll import androidx.work.* import kotlinx.coroutines.flow.first @@ -14,16 +15,20 @@ class RefreshWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { + val result = performRefresh() + applicationContext.dataStore.edit { it[Keys.IS_REFRESHING] = false } + DootWidget().updateAll(applicationContext) + return result + } + + private suspend fun performRefresh(): Result { val prefs = applicationContext.dataStore.data.first() val url = prefs[Keys.SERVER_URL]?.takeIf { it.isNotBlank() } ?: return Result.failure() val token = prefs[Keys.TOKEN]?.takeIf { it.isNotBlank() } ?: return Result.failure() val repo = WidgetRepository(okhttp3.OkHttpClient(), url, token) return repo.fetchAndPersist(applicationContext).fold( - onSuccess = { - DootWidget().updateAll(applicationContext) - Result.success() - }, + onSuccess = { Result.success() }, onFailure = { Result.retry() } ) } diff --git a/android/app/src/main/res/drawable/ic_refresh.xml b/android/app/src/main/res/drawable/ic_refresh.xml new file mode 100644 index 0000000..4b67528 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_refresh.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M4,12 A8,8 0 1,1 7,18" + android:strokeWidth="2" + android:strokeColor="#FFFFFF" + android:fillColor="@android:color/transparent" + android:strokeLineCap="round" + android:strokeLineJoin="round" /> + <path + android:pathData="M4,12 L4,7 L9,7" + android:strokeWidth="2" + android:strokeColor="#FFFFFF" + android:fillColor="@android:color/transparent" + android:strokeLineCap="round" + android:strokeLineJoin="round" /> +</vector> diff --git a/android/app/src/main/res/drawable/ic_refresh_loading.xml b/android/app/src/main/res/drawable/ic_refresh_loading.xml new file mode 100644 index 0000000..7ad3b2d --- /dev/null +++ b/android/app/src/main/res/drawable/ic_refresh_loading.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M5,12 m-2,0 a2,2 0 1,1 4,0 a2,2 0 1,1 -4,0" + android:fillColor="#FFFFFF" /> + <path + android:pathData="M12,12 m-2,0 a2,2 0 1,1 4,0 a2,2 0 1,1 -4,0" + android:fillColor="#FFFFFF" /> + <path + android:pathData="M19,12 m-2,0 a2,2 0 1,1 4,0 a2,2 0 1,1 -4,0" + android:fillColor="#FFFFFF" /> +</vector> |
