From 99dca0bd88eb62a0db7e0641bab686a3647e17ca Mon Sep 17 00:00:00 2001 From: whyKVD Date: Thu, 26 Feb 2026 12:27:01 +0100 Subject: [PATCH 01/43] feat: Started implementing Widgets --- app/build.gradle | 2 + app/src/main/AndroidManifest.xml | 9 +++ .../org/stypox/tridenta/widget/MyAppWidget.kt | 67 +++++++++++++++++++ .../tridenta/widget/MyAppWidgetReceiver.kt | 18 +++++ app/src/main/res/xml/my_app_widget_info.xml | 4 ++ gradle/libs.versions.toml | 4 ++ 6 files changed, 104 insertions(+) create mode 100644 app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt create mode 100644 app/src/main/java/org/stypox/tridenta/widget/MyAppWidgetReceiver.kt create mode 100644 app/src/main/res/xml/my_app_widget_info.xml diff --git a/app/build.gradle b/app/build.gradle index d0eeca7..bd3d9a7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -84,6 +84,8 @@ dependencies { debugImplementation libs.compose.androidx.ui.test.manifest implementation libs.compose.androidx.runtime.livedata implementation libs.androidx.material3 + implementation libs.glance.appwidget + implementation libs.glance.material3 // Navigation implementation libs.compose.destinations.core diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 67c9c3c..48232f7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -37,6 +37,15 @@ android:name="android.app.shortcuts" android:resource="@xml/shortcuts" /> + + + + + + \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt new file mode 100644 index 0000000..5316cfa --- /dev/null +++ b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt @@ -0,0 +1,67 @@ +package org.stypox.tridenta.widget + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.glance.Button +import androidx.glance.GlanceId +import androidx.glance.GlanceModifier +import androidx.glance.action.actionStartActivity +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.provideContent +import androidx.glance.layout.Alignment +import androidx.glance.layout.Column +import androidx.glance.layout.Row +import androidx.glance.layout.fillMaxSize +import androidx.glance.layout.padding +import androidx.glance.text.Text +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.stypox.tridenta.db.LineDao +import org.stypox.tridenta.db.StopDao +import org.stypox.tridenta.log.logError +import org.stypox.tridenta.log.logInfo +import org.stypox.tridenta.ui.MainActivity + +class MyAppWidget(private val stopDao: StopDao, private val lineDao: LineDao) : GlanceAppWidget() { + override suspend fun provideGlance( + context: Context, + id: GlanceId + ) { + // In this method, load data needed to render the AppWidget. + // Use `withContext` to switch to another thread for long running + // operations. + try { + withContext(Dispatchers.IO) { + logInfo(lineDao.getAllLines().toString()) + } + } catch(e: Exception) { + logError(e.message!!,e.cause) + } + + provideContent { + MyContent() + } + } + + @Composable + private fun MyContent() { + Column( + modifier = GlanceModifier.fillMaxSize(), + verticalAlignment = Alignment.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(text = "Where to?", modifier = GlanceModifier.padding(12.dp)) + Row(horizontalAlignment = Alignment.CenterHorizontally) { + Button( + text = "Home", + onClick = actionStartActivity() + ) + Button( + text = "Work", + onClick = actionStartActivity() + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidgetReceiver.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidgetReceiver.kt new file mode 100644 index 0000000..ab781f1 --- /dev/null +++ b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidgetReceiver.kt @@ -0,0 +1,18 @@ +package org.stypox.tridenta.widget + +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetReceiver +import dagger.hilt.android.AndroidEntryPoint +import org.stypox.tridenta.db.LineDao +import org.stypox.tridenta.db.StopDao +import javax.inject.Inject + +@AndroidEntryPoint +class MyAppWidgetReceiver : GlanceAppWidgetReceiver() { + @Inject + lateinit var stopDao: StopDao + @Inject + lateinit var lineDao: LineDao + + override val glanceAppWidget: GlanceAppWidget get() = MyAppWidget(stopDao,lineDao) +} \ No newline at end of file diff --git a/app/src/main/res/xml/my_app_widget_info.xml b/app/src/main/res/xml/my_app_widget_info.xml new file mode 100644 index 0000000..cd2fed8 --- /dev/null +++ b/app/src/main/res/xml/my_app_widget_info.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 76d835b..b7e05f1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,6 +19,8 @@ compose = "1.7.8" kotlin = "2.1.10" ksp = "2.1.10-1.0.30" java = "11" +glanceAppWidget = "1.1.1" +glanceMaterial3 = "1.1.1" [libraries] activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } @@ -49,6 +51,8 @@ compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } +glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = "glanceAppWidget" } +glance-material3 = { module = "androidx.glance:glance-material3", version.ref = "glanceMaterial3" } [plugins] com-android-application = { id = "com.android.application", version.ref = "agp" } From b01941f1f08eb5d711af8e2a552563975de0a7d9 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Fri, 27 Feb 2026 17:26:26 +0100 Subject: [PATCH 02/43] feat: Started working on how to display the trips in the widget --- app/build.gradle | 3 + .../org/stypox/tridenta/widget/MyAppWidget.kt | 426 +++++++++++++++++- .../tridenta/widget/MyAppWidgetReceiver.kt | 11 +- app/src/main/res/drawable/arrow_right.xml | 5 + app/src/main/res/drawable/check_circle.xml | 5 + app/src/main/res/drawable/double_arrow.xml | 5 + app/src/main/res/drawable/favorite.xml | 5 + .../res/drawable/radio_button_unchecked.xml | 5 + gradle/libs.versions.toml | 10 +- 9 files changed, 447 insertions(+), 28 deletions(-) create mode 100644 app/src/main/res/drawable/arrow_right.xml create mode 100644 app/src/main/res/drawable/check_circle.xml create mode 100644 app/src/main/res/drawable/double_arrow.xml create mode 100644 app/src/main/res/drawable/favorite.xml create mode 100644 app/src/main/res/drawable/radio_button_unchecked.xml diff --git a/app/build.gradle b/app/build.gradle index bd3d9a7..edf0f8a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -84,8 +84,11 @@ dependencies { debugImplementation libs.compose.androidx.ui.test.manifest implementation libs.compose.androidx.runtime.livedata implementation libs.androidx.material3 + implementation libs.glance implementation libs.glance.appwidget implementation libs.glance.material3 + implementation libs.glance.preview + implementation libs.glance.appwidget.preview // Navigation implementation libs.compose.destinations.core diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt index 5316cfa..8688812 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt @@ -1,29 +1,71 @@ package org.stypox.tridenta.widget import android.content.Context +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.glance.Button import androidx.glance.GlanceId import androidx.glance.GlanceModifier -import androidx.glance.action.actionStartActivity +import androidx.glance.Image +import androidx.glance.ImageProvider +import androidx.glance.action.clickable import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.lazy.LazyColumn +import androidx.glance.appwidget.lazy.itemsIndexed import androidx.glance.appwidget.provideContent import androidx.glance.layout.Alignment import androidx.glance.layout.Column import androidx.glance.layout.Row +import androidx.glance.layout.Spacer import androidx.glance.layout.fillMaxSize +import androidx.glance.layout.fillMaxWidth +import androidx.glance.layout.height import androidx.glance.layout.padding +import androidx.glance.layout.size +import androidx.glance.preview.ExperimentalGlancePreviewApi +import androidx.glance.preview.Preview +import androidx.glance.text.FontWeight import androidx.glance.text.Text +import androidx.glance.text.TextStyle import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.stypox.tridenta.R import org.stypox.tridenta.db.LineDao import org.stypox.tridenta.db.StopDao +import org.stypox.tridenta.db.data.DbLine +import org.stypox.tridenta.db.data.DbStop +import org.stypox.tridenta.enums.Area +import org.stypox.tridenta.enums.CardinalPoint +import org.stypox.tridenta.enums.Direction +import org.stypox.tridenta.enums.StopLineType +import org.stypox.tridenta.extractor.data.ExTrip import org.stypox.tridenta.log.logError -import org.stypox.tridenta.log.logInfo -import org.stypox.tridenta.ui.MainActivity +import org.stypox.tridenta.repo.LineTripsRepository +import org.stypox.tridenta.repo.LinesRepository +import org.stypox.tridenta.repo.data.UiStopTime +import org.stypox.tridenta.repo.data.UiTrip +import org.stypox.tridenta.sample.SampleUiTripProvider +import org.stypox.tridenta.ui.theme.LabelText +import org.stypox.tridenta.ui.trip.TripViewStops +import org.stypox.tridenta.util.formatConcatStrings +import org.stypox.tridenta.util.formatTime +import java.time.OffsetDateTime +import java.time.ZoneOffset +import java.time.ZonedDateTime -class MyAppWidget(private val stopDao: StopDao, private val lineDao: LineDao) : GlanceAppWidget() { +class MyAppWidget( + private val stopDao: StopDao, + private val lineDao: LineDao, + private val linesRepository: LinesRepository, + private val tripsRepository: LineTripsRepository +) : GlanceAppWidget() { override suspend fun provideGlance( context: Context, id: GlanceId @@ -31,37 +73,375 @@ class MyAppWidget(private val stopDao: StopDao, private val lineDao: LineDao) : // In this method, load data needed to render the AppWidget. // Use `withContext` to switch to another thread for long running // operations. + lateinit var favoriteLines: List try { withContext(Dispatchers.IO) { - logInfo(lineDao.getAllLines().toString()) + val lines = lineDao.getAllLines(); + favoriteLines = lines.filter { l -> l.isFavorite } + val line = favoriteLines[0] + tripsRepository.getUiTrip( + line.lineId, + line.type, + ZonedDateTime.now(), + Direction.ForwardAndBackward + ) + } - } catch(e: Exception) { - logError(e.message!!,e.cause) + } catch (e: Exception) { + logError(e.message!!, e.cause) } provideContent { MyContent() } } +} + +@Composable +fun MyContent() { + Column( + modifier = GlanceModifier.fillMaxSize(), + verticalAlignment = Alignment.Top, + horizontalAlignment = Alignment.Start + ) { + Row(GlanceModifier.fillMaxWidth()) { + Text("Trips -") + Spacer() + Text("> ") + } + Image( + provider = ImageProvider(R.drawable.radio_button_unchecked), + contentDescription = null, + ) + } +} + +@Composable +fun TripViewStops( + trip: UiTrip, + stopIdToHighlight: Int?, + stopTypeToHighlight: StopLineType?, + modifier: GlanceModifier = GlanceModifier, + onStopClick: ((DbStop) -> Unit)? = null, +) { + LazyColumn( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + itemsIndexed(trip.stopTimes) { index, stopTime -> + TripViewStopItem( + trip = trip, + highlight = stopTime.stop != null && + stopTime.stop.stopId == stopIdToHighlight && + stopTime.stop.type == stopTypeToHighlight, + completed = index < trip.completedStops, + stopTime = stopTime, + modifier = if (stopTime.stop == null || onStopClick == null) { + GlanceModifier // not clickable, since there is no stop + } else { + GlanceModifier.clickable { onStopClick(stopTime.stop) } + } + ) + } + + item { + LabelText( + text = formatConcatStrings( + if (trip.lastEventReceivedAt == null) { + stringResource(R.string.no_update) + } else { + stringResource(R.string.last_update, formatTime(trip.lastEventReceivedAt)) + }, + if (trip.busId == ExTrip.BUS_ID_UNKNOWN) { + null + } else { + stringResource(R.string.bus_id, trip.busId) + } + ), + modifier = Modifier.padding(8.dp) + ) + } + + item { + // space for FABs + Spacer(modifier = GlanceModifier.size(height = 84.dp, width = 0.dp)) + } + } +} - @Composable - private fun MyContent() { - Column( - modifier = GlanceModifier.fillMaxSize(), - verticalAlignment = Alignment.Top, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text(text = "Where to?", modifier = GlanceModifier.padding(12.dp)) - Row(horizontalAlignment = Alignment.CenterHorizontally) { - Button( - text = "Home", - onClick = actionStartActivity() +@Composable +private fun TripViewStopItem( + trip: UiTrip, + highlight: Boolean, + completed: Boolean, + stopTime: UiStopTime, + modifier: GlanceModifier = GlanceModifier, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.padding(horizontal = 16.dp, vertical = 0.dp) + ) { + Image( + provider = ImageProvider( + if (trip.lastEventReceivedAt == null) { + R.drawable.arrow_right + } else if (completed) { + R.drawable.check_circle + } else { + R.drawable.radio_button_unchecked + } + ), + contentDescription = null, + //tint = MaterialTheme.colorScheme.primary, TODO: Correct the color + modifier = GlanceModifier.padding( + end = 6.dp, + ), + ) + + if (stopTime.stop?.isFavorite == true) { + Image( + provider = ImageProvider(R.drawable.favorite), + contentDescription = stringResource(R.string.favorite), + //tint = MaterialTheme.colorScheme.primary, TODO: Correct the color + modifier = GlanceModifier.padding(end = 3.dp) + .height(16.dp), + ) + } + + stopTime.stop?.cardinalPoint?.let { + Text( + text = stringResource(it.shortName), + //color = MaterialTheme.colorScheme.primary, TODO: Correct the color + modifier = GlanceModifier.padding(end = 3.dp), + ) + } + + Text( + text = stopTime.stop?.name ?: stringResource(R.string.error), + maxLines = 1, + style = TextStyle( + fontWeight = if (highlight) FontWeight.Bold else null + ), + // overflow = TextOverflow.Ellipsis, TODO: overflow text to ellipsis + modifier = GlanceModifier + .run { + if (stopTime.arrivalTime == null && stopTime.departureTime == null) { + this // do not apply end padding if there is nothing after + } else { + padding(end = 6.dp) + } + }, + /*color = if (stopTime.stop == null) { TODO: Correct the color + MaterialTheme.colorScheme.error + } else if (highlight) { + MaterialTheme.colorScheme.primary + } else { + Color.Unspecified + }*/ + ) + + val isLate = trip.lastEventReceivedAt != null + && !completed + && trip.delay > 0 + //val lateDecoration = if (isLate) TextDecoration.LineThrough else null + val highlightWeight = if (highlight) FontWeight.Bold else null + + if (stopTime.arrivalTime != null) { + // TODO `key` forces recompositions when `lateDecoration` changes, needed because of + // probably a bug in Compose (also see below) + Text( + text = formatTime(stopTime.arrivalTime), + maxLines = 1, + //textDecoration = lateDecoration, + style = TextStyle( + fontWeight = highlightWeight, + ), + ) + } + if (stopTime.arrivalTime != stopTime.departureTime) { + if (stopTime.arrivalTime != null) { + Image( + provider = ImageProvider(R.drawable.double_arrow), + contentDescription = null, + modifier = GlanceModifier.size(8.dp), ) - Button( - text = "Work", - onClick = actionStartActivity() + } + if (stopTime.departureTime != null) { + Text( + text = formatTime(stopTime.departureTime), + maxLines = 1, + style = TextStyle( + fontWeight = highlightWeight, + ) + //textDecoration = lateDecoration, ) } } + if (isLate) { + sequenceOf(stopTime.arrivalTime, stopTime.departureTime).firstOrNull { it != null } + ?.let { time -> + Text( + text = formatTime( + time.plusMinutes(trip.delay.toLong()) + ), + modifier = GlanceModifier.padding(start = 5.dp), + // color = MaterialTheme.colorScheme.error, TODO: Correct the color + maxLines = 1, + style = TextStyle( + fontWeight = highlightWeight, + ), + ) + } + } } +} + +@OptIn(ExperimentalGlancePreviewApi::class) +@Preview(widthDp = 203, heightDp = 130) +@Composable +fun TripViewStopsPreview() { + val sampleDbStops: List = listOf( + DbStop( + stopId = 0, + latitude = 0.0, + longitude = 0.0, + name = "Sorni", + street = "Sorni", + town = "", + type = StopLineType.Urban, + wheelchairAccessible = false, + cardinalPoint = CardinalPoint.North, + isFavorite = false, + ), + DbStop( + stopId = 1, + latitude = 0.0, + longitude = 0.0, + name = "Funivia-Staz. di Monte-Sardagna lorem ipsum dolor sit amet", + street = "Cembra - Via 4 Novembre - Dir.Cavalese lorem ipsum dolor sit", + town = "Appiano sulla strada del vino", + type = StopLineType.Suburban, + wheelchairAccessible = true, + cardinalPoint = null, + isFavorite = true, + ), + DbStop( + stopId = 2, + latitude = 0.0, + longitude = 0.0, + name = "Pinè Bivio", + street = "Civezzano-Loc.La Mochena", + town = "Civezzano", + type = StopLineType.Suburban, + wheelchairAccessible = false, + cardinalPoint = CardinalPoint.NorthWest, + isFavorite = true, + ), + DbStop( + stopId = 3, + latitude = 0.0, + longitude = 0.0, + name = "Verona Big Center", + street = "Verona \"Big Center\"", + town = "Trento", + type = StopLineType.Suburban, + wheelchairAccessible = true, + cardinalPoint = null, + isFavorite = false, + ) + ) + val sampleDbLines: Sequence = sequenceOf( + DbLine( + lineId = 396, + type = StopLineType.Urban, + area = Area.UrbanTrento, + color = 0xc52720, + longName = "Cortesano Gardolo P.Dante Villazzano 3", + shortName = "3", + isFavorite = false + ), + DbLine( + lineId = 404, + type = StopLineType.Urban, + area = Area.UrbanTrento, + color = 0x52332a, + longName = "Centochiavi Piazza Dante Mattarello", + shortName = "8", + isFavorite = true + ), + DbLine( + lineId = 466, + type = StopLineType.Urban, + area = Area.UrbanTrento, + color = 0xbf6092, + longName = "P.Dante Rosmini S.Rocco Povo Polo Soc.", + shortName = "13", + isFavorite = false + ), + DbLine( + lineId = 415, + type = StopLineType.Urban, + area = Area.UrbanTrento, + color = 0xe490b0, + longName = "P.Dante Via Sanseverino Belvedere Ravina", + shortName = "14", + isFavorite = true + ), + DbLine( + lineId = 109, + type = StopLineType.Suburban, + area = Area.Suburban1, + color = null, + longName = "Cavalese - Masi di Cavalese", + shortName = "B109", + isFavorite = true + ), + DbLine( + lineId = 201, + type = StopLineType.Suburban, + area = Area.Suburban2, + color = null, + longName = "Trento-Vezzano-Sarche-Tione", + shortName = "B201", + isFavorite = false + ), + ) + val referenceDateTime = + OffsetDateTime.of(2022, 9, 26, 9, 33, 17, 328943849, ZoneOffset.UTC) + TripViewStops( + trip = UiTrip( + delay = 1, + direction = Direction.Backward, + lastEventReceivedAt = referenceDateTime.minusMinutes(3), + lineId = sampleDbLines.first().lineId, + line = sampleDbLines.first(), + headSign = "Conci \"Villazzano 3\"", + tripId = "0003726592022061120220911", + type = StopLineType.Urban, + completedStops = 2, + stopTimes = sampleDbStops.mapIndexed { index, dbStop -> + UiStopTime( + arrivalTime = referenceDateTime.plusMinutes((index - 2).toLong()), + departureTime = referenceDateTime.plusMinutes((index + index % 2 - 2).toLong()), + stop = dbStop + ) + }, + busId = 886, + ), + stopIdToHighlight = null, + stopTypeToHighlight = null + ) +} + +@OptIn(ExperimentalGlancePreviewApi::class) +// 3x2 Widget +@Preview(widthDp = 203, heightDp = 130) +// 3x3 Widget +@Preview(widthDp = 203, heightDp = 203) +// 3x4 Widget +@Preview(widthDp = 203, heightDp = 276) +@Composable +fun MyWidgetPreview() { + // Provide mock data to your content + MyContent() } \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidgetReceiver.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidgetReceiver.kt index ab781f1..5b2e3b2 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidgetReceiver.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidgetReceiver.kt @@ -5,14 +5,23 @@ import androidx.glance.appwidget.GlanceAppWidgetReceiver import dagger.hilt.android.AndroidEntryPoint import org.stypox.tridenta.db.LineDao import org.stypox.tridenta.db.StopDao +import org.stypox.tridenta.repo.LineTripsRepository +import org.stypox.tridenta.repo.LinesRepository import javax.inject.Inject @AndroidEntryPoint class MyAppWidgetReceiver : GlanceAppWidgetReceiver() { @Inject lateinit var stopDao: StopDao + @Inject lateinit var lineDao: LineDao - override val glanceAppWidget: GlanceAppWidget get() = MyAppWidget(stopDao,lineDao) + @Inject + lateinit var linesRepository: LinesRepository + + @Inject + lateinit var tripsRepository: LineTripsRepository + + override val glanceAppWidget: GlanceAppWidget get() = MyAppWidget(stopDao, lineDao,linesRepository,tripsRepository) } \ No newline at end of file diff --git a/app/src/main/res/drawable/arrow_right.xml b/app/src/main/res/drawable/arrow_right.xml new file mode 100644 index 0000000..8437cde --- /dev/null +++ b/app/src/main/res/drawable/arrow_right.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/check_circle.xml b/app/src/main/res/drawable/check_circle.xml new file mode 100644 index 0000000..7f747c6 --- /dev/null +++ b/app/src/main/res/drawable/check_circle.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/double_arrow.xml b/app/src/main/res/drawable/double_arrow.xml new file mode 100644 index 0000000..dfa8591 --- /dev/null +++ b/app/src/main/res/drawable/double_arrow.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/favorite.xml b/app/src/main/res/drawable/favorite.xml new file mode 100644 index 0000000..fdc35fb --- /dev/null +++ b/app/src/main/res/drawable/favorite.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/radio_button_unchecked.xml b/app/src/main/res/drawable/radio_button_unchecked.xml new file mode 100644 index 0000000..5fd804a --- /dev/null +++ b/app/src/main/res/drawable/radio_button_unchecked.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b7e05f1..37f5fa0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,8 +19,7 @@ compose = "1.7.8" kotlin = "2.1.10" ksp = "2.1.10-1.0.30" java = "11" -glanceAppWidget = "1.1.1" -glanceMaterial3 = "1.1.1" +glance = "1.1.1" [libraries] activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } @@ -51,8 +50,11 @@ compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } -glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = "glanceAppWidget" } -glance-material3 = { module = "androidx.glance:glance-material3", version.ref = "glanceMaterial3" } +glance = { module = "androidx.glance:glance", version.ref = "glance" } +glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = "glance" } +glance-material3 = { module = "androidx.glance:glance-material3", version.ref = "glance" } +glance-preview = { module = "androidx.glance:glance-preview", version.ref = "glance" } +glance-appwidget-preview = { module = "androidx.glance:glance-appwidget-preview", version.ref = "glance" } [plugins] com-android-application = { id = "com.android.application", version.ref = "agp" } From dd8f581110f40495d300201373f3f69e8fb0daf3 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Thu, 5 Mar 2026 12:45:05 +0100 Subject: [PATCH 03/43] feat: Now the widget is displayed correctly with all the data --- .../org/stypox/tridenta/widget/MyAppWidget.kt | 331 ++++---------- .../stypox/tridenta/widget/TripViewGlance.kt | 428 ++++++++++++++++++ .../tridenta/widget/TripViewStopsGlance.kt | 368 +++++++++++++++ app/src/main/res/drawable/arrow_left.xml | 5 + app/src/main/res/drawable/favorite.xml | 2 +- app/src/main/res/drawable/refresh.xml | 5 + app/src/main/res/xml/my_app_widget_info.xml | 4 +- 7 files changed, 886 insertions(+), 257 deletions(-) create mode 100644 app/src/main/java/org/stypox/tridenta/widget/TripViewGlance.kt create mode 100644 app/src/main/java/org/stypox/tridenta/widget/TripViewStopsGlance.kt create mode 100644 app/src/main/res/drawable/arrow_left.xml create mode 100644 app/src/main/res/drawable/refresh.xml diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt index 8688812..48d7bb1 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt @@ -1,42 +1,25 @@ package org.stypox.tridenta.widget import android.content.Context -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.glance.GlanceId import androidx.glance.GlanceModifier -import androidx.glance.Image -import androidx.glance.ImageProvider -import androidx.glance.action.clickable +import androidx.glance.GlanceTheme +import androidx.glance.action.actionStartActivity import androidx.glance.appwidget.GlanceAppWidget -import androidx.glance.appwidget.lazy.LazyColumn -import androidx.glance.appwidget.lazy.itemsIndexed import androidx.glance.appwidget.provideContent +import androidx.glance.background import androidx.glance.layout.Alignment import androidx.glance.layout.Column import androidx.glance.layout.Row import androidx.glance.layout.Spacer import androidx.glance.layout.fillMaxSize import androidx.glance.layout.fillMaxWidth -import androidx.glance.layout.height -import androidx.glance.layout.padding -import androidx.glance.layout.size import androidx.glance.preview.ExperimentalGlancePreviewApi import androidx.glance.preview.Preview -import androidx.glance.text.FontWeight import androidx.glance.text.Text -import androidx.glance.text.TextStyle import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.stypox.tridenta.R import org.stypox.tridenta.db.LineDao import org.stypox.tridenta.db.StopDao import org.stypox.tridenta.db.data.DbLine @@ -45,21 +28,23 @@ import org.stypox.tridenta.enums.Area import org.stypox.tridenta.enums.CardinalPoint import org.stypox.tridenta.enums.Direction import org.stypox.tridenta.enums.StopLineType -import org.stypox.tridenta.extractor.data.ExTrip +import org.stypox.tridenta.extractor.ROME_ZONE_ID import org.stypox.tridenta.log.logError import org.stypox.tridenta.repo.LineTripsRepository import org.stypox.tridenta.repo.LinesRepository import org.stypox.tridenta.repo.data.UiStopTime import org.stypox.tridenta.repo.data.UiTrip -import org.stypox.tridenta.sample.SampleUiTripProvider -import org.stypox.tridenta.ui.theme.LabelText -import org.stypox.tridenta.ui.trip.TripViewStops -import org.stypox.tridenta.util.formatConcatStrings -import org.stypox.tridenta.util.formatTime +import org.stypox.tridenta.ui.MainActivity import java.time.OffsetDateTime import java.time.ZoneOffset import java.time.ZonedDateTime +enum class State { + Loading, + Success, + Error +} + class MyAppWidget( private val stopDao: StopDao, private val lineDao: LineDao, @@ -73,34 +58,49 @@ class MyAppWidget( // In this method, load data needed to render the AppWidget. // Use `withContext` to switch to another thread for long running // operations. + var isError = false + var isLoading = true + var trip: UiTrip? = null lateinit var favoriteLines: List try { withContext(Dispatchers.IO) { - val lines = lineDao.getAllLines(); + val lines = lineDao.getAllLines() favoriteLines = lines.filter { l -> l.isFavorite } val line = favoriteLines[0] - tripsRepository.getUiTrip( + val tmp = tripsRepository.getUiTrip( line.lineId, line.type, - ZonedDateTime.now(), + ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID), Direction.ForwardAndBackward - ) - + ).third + trip = tmp + isLoading = false } } catch (e: Exception) { logError(e.message!!, e.cause) + isError = true } provideContent { - MyContent() + GlanceTheme() { + TripViewGlance( + trip, error = isError, loading = isLoading, + onReloadAction = actionStartActivity(), + onPrevAction = actionStartActivity(), + onNextAction = actionStartActivity(), + onLineClickAction = actionStartActivity(), + stopIdToHighlight = null, + stopTypeToHighlight = null, + ) + } } } } @Composable -fun MyContent() { +fun MyContent(trip: UiTrip) { Column( - modifier = GlanceModifier.fillMaxSize(), + modifier = GlanceModifier.fillMaxSize().background(GlanceTheme.colors.background), verticalAlignment = Alignment.Top, horizontalAlignment = Alignment.Start ) { @@ -109,197 +109,24 @@ fun MyContent() { Spacer() Text("> ") } - Image( - provider = ImageProvider(R.drawable.radio_button_unchecked), - contentDescription = null, - ) - } -} - -@Composable -fun TripViewStops( - trip: UiTrip, - stopIdToHighlight: Int?, - stopTypeToHighlight: StopLineType?, - modifier: GlanceModifier = GlanceModifier, - onStopClick: ((DbStop) -> Unit)? = null, -) { - LazyColumn( - modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - itemsIndexed(trip.stopTimes) { index, stopTime -> - TripViewStopItem( - trip = trip, - highlight = stopTime.stop != null && - stopTime.stop.stopId == stopIdToHighlight && - stopTime.stop.type == stopTypeToHighlight, - completed = index < trip.completedStops, - stopTime = stopTime, - modifier = if (stopTime.stop == null || onStopClick == null) { - GlanceModifier // not clickable, since there is no stop - } else { - GlanceModifier.clickable { onStopClick(stopTime.stop) } - } - ) - } - - item { - LabelText( - text = formatConcatStrings( - if (trip.lastEventReceivedAt == null) { - stringResource(R.string.no_update) - } else { - stringResource(R.string.last_update, formatTime(trip.lastEventReceivedAt)) - }, - if (trip.busId == ExTrip.BUS_ID_UNKNOWN) { - null - } else { - stringResource(R.string.bus_id, trip.busId) - } - ), - modifier = Modifier.padding(8.dp) - ) - } - - item { - // space for FABs - Spacer(modifier = GlanceModifier.size(height = 84.dp, width = 0.dp)) - } - } -} - -@Composable -private fun TripViewStopItem( - trip: UiTrip, - highlight: Boolean, - completed: Boolean, - stopTime: UiStopTime, - modifier: GlanceModifier = GlanceModifier, -) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = modifier.padding(horizontal = 16.dp, vertical = 0.dp) - ) { - Image( - provider = ImageProvider( - if (trip.lastEventReceivedAt == null) { - R.drawable.arrow_right - } else if (completed) { - R.drawable.check_circle - } else { - R.drawable.radio_button_unchecked - } - ), - contentDescription = null, - //tint = MaterialTheme.colorScheme.primary, TODO: Correct the color - modifier = GlanceModifier.padding( - end = 6.dp, - ), + TripViewStopsGlance( + trip, + stopIdToHighlight = null, + stopTypeToHighlight = trip.type, + modifier = GlanceModifier.defaultWeight() ) - - if (stopTime.stop?.isFavorite == true) { - Image( - provider = ImageProvider(R.drawable.favorite), - contentDescription = stringResource(R.string.favorite), - //tint = MaterialTheme.colorScheme.primary, TODO: Correct the color - modifier = GlanceModifier.padding(end = 3.dp) - .height(16.dp), - ) - } - - stopTime.stop?.cardinalPoint?.let { - Text( - text = stringResource(it.shortName), - //color = MaterialTheme.colorScheme.primary, TODO: Correct the color - modifier = GlanceModifier.padding(end = 3.dp), - ) - } - - Text( - text = stopTime.stop?.name ?: stringResource(R.string.error), - maxLines = 1, - style = TextStyle( - fontWeight = if (highlight) FontWeight.Bold else null - ), - // overflow = TextOverflow.Ellipsis, TODO: overflow text to ellipsis - modifier = GlanceModifier - .run { - if (stopTime.arrivalTime == null && stopTime.departureTime == null) { - this // do not apply end padding if there is nothing after - } else { - padding(end = 6.dp) - } - }, - /*color = if (stopTime.stop == null) { TODO: Correct the color - MaterialTheme.colorScheme.error - } else if (highlight) { - MaterialTheme.colorScheme.primary - } else { - Color.Unspecified - }*/ - ) - - val isLate = trip.lastEventReceivedAt != null - && !completed - && trip.delay > 0 - //val lateDecoration = if (isLate) TextDecoration.LineThrough else null - val highlightWeight = if (highlight) FontWeight.Bold else null - - if (stopTime.arrivalTime != null) { - // TODO `key` forces recompositions when `lateDecoration` changes, needed because of - // probably a bug in Compose (also see below) - Text( - text = formatTime(stopTime.arrivalTime), - maxLines = 1, - //textDecoration = lateDecoration, - style = TextStyle( - fontWeight = highlightWeight, - ), - ) - } - if (stopTime.arrivalTime != stopTime.departureTime) { - if (stopTime.arrivalTime != null) { - Image( - provider = ImageProvider(R.drawable.double_arrow), - contentDescription = null, - modifier = GlanceModifier.size(8.dp), - ) - } - if (stopTime.departureTime != null) { - Text( - text = formatTime(stopTime.departureTime), - maxLines = 1, - style = TextStyle( - fontWeight = highlightWeight, - ) - //textDecoration = lateDecoration, - ) - } - } - if (isLate) { - sequenceOf(stopTime.arrivalTime, stopTime.departureTime).firstOrNull { it != null } - ?.let { time -> - Text( - text = formatTime( - time.plusMinutes(trip.delay.toLong()) - ), - modifier = GlanceModifier.padding(start = 5.dp), - // color = MaterialTheme.colorScheme.error, TODO: Correct the color - maxLines = 1, - style = TextStyle( - fontWeight = highlightWeight, - ), - ) - } - } } } @OptIn(ExperimentalGlancePreviewApi::class) -@Preview(widthDp = 203, heightDp = 130) +// 3x2 Widget +@Preview(widthDp = 250, heightDp = 130) +// 3x3 Widget +@Preview(widthDp = 250, heightDp = 203) +// 3x4 Widget +@Preview(widthDp = 250, heightDp = 276) @Composable -fun TripViewStopsPreview() { +fun MyWidgetPreview() { val sampleDbStops: List = listOf( DbStop( stopId = 0, @@ -407,41 +234,35 @@ fun TripViewStopsPreview() { ), ) val referenceDateTime = - OffsetDateTime.of(2022, 9, 26, 9, 33, 17, 328943849, ZoneOffset.UTC) - TripViewStops( - trip = UiTrip( - delay = 1, - direction = Direction.Backward, - lastEventReceivedAt = referenceDateTime.minusMinutes(3), - lineId = sampleDbLines.first().lineId, - line = sampleDbLines.first(), - headSign = "Conci \"Villazzano 3\"", - tripId = "0003726592022061120220911", - type = StopLineType.Urban, - completedStops = 2, - stopTimes = sampleDbStops.mapIndexed { index, dbStop -> - UiStopTime( - arrivalTime = referenceDateTime.plusMinutes((index - 2).toLong()), - departureTime = referenceDateTime.plusMinutes((index + index % 2 - 2).toLong()), - stop = dbStop - ) - }, - busId = 886, - ), - stopIdToHighlight = null, - stopTypeToHighlight = null - ) -} - -@OptIn(ExperimentalGlancePreviewApi::class) -// 3x2 Widget -@Preview(widthDp = 203, heightDp = 130) -// 3x3 Widget -@Preview(widthDp = 203, heightDp = 203) -// 3x4 Widget -@Preview(widthDp = 203, heightDp = 276) -@Composable -fun MyWidgetPreview() { + OffsetDateTime.of(2022, 9, 26, 9, 33, 17, 328943849, ZoneOffset.UTC) // Provide mock data to your content - MyContent() + GlanceTheme() { + TripViewGlance( + UiTrip( + delay = 1, + direction = Direction.Backward, + lastEventReceivedAt = referenceDateTime.minusMinutes(3), + lineId = sampleDbLines.first().lineId, + line = sampleDbLines.first(), + headSign = "Conci \"Villazzano 3\"", + tripId = "0003726592022061120220911", + type = StopLineType.Urban, + completedStops = 2, + stopTimes = sampleDbStops.mapIndexed { index, dbStop -> + UiStopTime( + arrivalTime = referenceDateTime.plusMinutes((index - 2).toLong()), + departureTime = referenceDateTime.plusMinutes((index + index % 2 - 2).toLong()), + stop = dbStop + ) + }, + busId = 886, + ), error = false, loading = false, + onReloadAction = actionStartActivity(), + onPrevAction = actionStartActivity(), + onNextAction = actionStartActivity(), + onLineClickAction = actionStartActivity(), + stopIdToHighlight = null, + stopTypeToHighlight = null, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/TripViewGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/TripViewGlance.kt new file mode 100644 index 0000000..24c2436 --- /dev/null +++ b/app/src/main/java/org/stypox/tridenta/widget/TripViewGlance.kt @@ -0,0 +1,428 @@ +package org.stypox.tridenta.widget + +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.glance.Button +import androidx.glance.GlanceModifier +import androidx.glance.GlanceTheme +import androidx.glance.Image +import androidx.glance.ImageProvider +import androidx.glance.LocalContext +import androidx.glance.action.Action +import androidx.glance.action.clickable +import androidx.glance.appwidget.CircularProgressIndicator +import androidx.glance.background +import androidx.glance.layout.Alignment +import androidx.glance.layout.Box +import androidx.glance.layout.Column +import androidx.glance.layout.Row +import androidx.glance.layout.fillMaxSize +import androidx.glance.layout.fillMaxWidth +import androidx.glance.layout.padding +import androidx.glance.preview.ExperimentalGlancePreviewApi +import androidx.glance.preview.Preview +import androidx.glance.text.FontWeight +import androidx.glance.text.Text +import androidx.glance.text.TextAlign +import androidx.glance.text.TextStyle +import org.stypox.tridenta.R +import org.stypox.tridenta.db.data.DbLine +import org.stypox.tridenta.db.data.DbStop +import org.stypox.tridenta.enums.Area +import org.stypox.tridenta.enums.CardinalPoint +import org.stypox.tridenta.enums.Direction +import org.stypox.tridenta.enums.StopLineType +import org.stypox.tridenta.repo.data.UiStopTime +import org.stypox.tridenta.repo.data.UiTrip +import org.stypox.tridenta.sample.SampleDbStopProvider +import org.stypox.tridenta.util.formatDateFull +import org.stypox.tridenta.util.formatDurationMinutes +import java.time.OffsetDateTime +import java.time.ZoneOffset + +@Composable +fun TripViewGlance( + trip: UiTrip?, + error: Boolean, + loading: Boolean, + // Note: In Glance, clicks are handled by 'Action's (like actionRunCallback or actionStartActivity) + // rather than standard lambda functions, because they trigger background broadcasts. + onReloadAction: Action, + onPrevAction: Action, + onNextAction: Action, + onLineClickAction: Action, + stopIdToHighlight: Int?, + stopTypeToHighlight: StopLineType?, + modifier: GlanceModifier = GlanceModifier +) { + val context = LocalContext.current + + Box ( + modifier = modifier.fillMaxSize().background(GlanceTheme.colors.background), + contentAlignment = Alignment.Center + ) { + if (trip != null) { + Column( + modifier = GlanceModifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + TripViewTopRowGlance( + trip = trip, + onLineClickAction = onLineClickAction, + modifier = GlanceModifier.padding( + start = 12.dp, + top = 4.dp, + end = 12.dp, + bottom = 12.dp + ).fillMaxWidth() + ) + + if (error) { + // Replaced your custom ErrorRow with a simple Glance Text for the widget + Text( + text = context.getString(R.string.error), + style = TextStyle(color = GlanceTheme.colors.error), + modifier = GlanceModifier.padding(8.dp) + ) + } + + // Assuming this is already refactored to be Glance-compliant! + TripViewStopsGlance( + trip = trip, + stopIdToHighlight = stopIdToHighlight, + stopTypeToHighlight = stopTypeToHighlight, + modifier = GlanceModifier.defaultWeight() // Crucial for lists in Columns + ) + } + + } else if (loading) { + CircularProgressIndicator() + + } else if (error) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(text = context.getString(R.string.error), style = TextStyle(color = GlanceTheme.colors.error)) + Button(text = context.getString(R.string.reload), onClick = onReloadAction) + } + + } else { + Column( + modifier = GlanceModifier.padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = context.getString(R.string.no_trip_found), + style = TextStyle(fontWeight = FontWeight.Bold, color = GlanceTheme.colors.onBackground), + modifier = GlanceModifier.padding(bottom = 4.dp) + ) + Text( + text = context.getString(R.string.no_trip_found_description), + style = TextStyle(textAlign = TextAlign.Center, color = GlanceTheme.colors.onBackground) + ) + } + } + + // Bottom Row is aligned to the bottom using a Box setup + Box( + modifier = GlanceModifier.fillMaxSize(), + contentAlignment = Alignment.BottomCenter + ) { + TripViewBottomRowGlance( + loading = loading, + onReloadAction = onReloadAction, + onPrevAction = onPrevAction, + onNextAction = onNextAction, + modifier = GlanceModifier.fillMaxWidth() + ) + } + } +} + +@Composable +private fun TripViewTopRowGlance( + trip: UiTrip, + onLineClickAction: Action, + modifier: GlanceModifier = GlanceModifier +) { + val context = LocalContext.current + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + ) { + if (trip.line != null) { + // Replaced Surface with Box + background + Box( + modifier = GlanceModifier + .background(GlanceTheme.colors.primaryContainer) // Use a theme color instead of dynamic custom color for simplicity in Glance + .padding(8.dp) + .clickable(onLineClickAction) + ) { + Text( + text = trip.line.shortName, + maxLines = 1, + style = TextStyle( + color = GlanceTheme.colors.onPrimaryContainer, + fontWeight = FontWeight.Bold + ) + ) + } + } + + Column( + modifier = GlanceModifier.defaultWeight().padding(horizontal = 12.dp) + ) { + Text( + text = trip.headSign, + maxLines = 1, + style = TextStyle(color = GlanceTheme.colors.onBackground, fontWeight = FontWeight.Medium) + ) + + val dateOrDelayText = if (trip.lastEventReceivedAt == null) { + trip.stopTimes.asSequence() + .map { it.arrivalTime } + .filterNotNull() + .firstOrNull() + ?.let { firstArrival -> formatDateFull(firstArrival) } + ?: context.getString(R.string.no_date_time_information) + } else { + if (trip.delay < 0) + context.getString(R.string.early, formatDurationMinutes(-trip.delay)) + else if (trip.delay == 0) + context.getString(R.string.on_time) + else + context.getString(R.string.late, formatDurationMinutes(trip.delay)) + } + Text( + text = dateOrDelayText, + maxLines = 1, + style = TextStyle(color = GlanceTheme.colors.onSurfaceVariant) + ) + } + } +} + +@Composable +private fun TripViewBottomRowGlance( + loading: Boolean, + onReloadAction: Action, + onPrevAction: Action, + onNextAction: Action, + modifier: GlanceModifier = GlanceModifier +) { + val context = LocalContext.current + + // Widgets don't support FloatingActionButtons. Use standard Buttons or Images. + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .background(GlanceTheme.colors.surfaceVariant) + .padding(16.dp) + ) { + // You MUST replace R.drawable.ic_... with your actual XML drawables + Image( + provider = ImageProvider(R.drawable.arrow_left), // Placeholder + contentDescription = context.getString(R.string.previous), + modifier = GlanceModifier.clickable(onPrevAction).defaultWeight() + ) + + if (loading) { + CircularProgressIndicator(modifier = GlanceModifier.defaultWeight()) + } else { + Image( + provider = ImageProvider(R.drawable.refresh), // Placeholder + contentDescription = context.getString(R.string.reload), + modifier = GlanceModifier.clickable(onReloadAction).defaultWeight() + ) + } + + Image( + provider = ImageProvider(R.drawable.arrow_right), // Placeholder + contentDescription = context.getString(R.string.next), + modifier = GlanceModifier.clickable(onNextAction).defaultWeight() + ) + } +} + +/*@OptIn(ExperimentalGlancePreviewApi::class) +@Preview +@Composable +private fun TripViewPreview() { + val sampleDbStops: List = listOf( + DbStop( + stopId = 0, + latitude = 0.0, + longitude = 0.0, + name = "Sorni", + street = "Sorni", + town = "", + type = StopLineType.Urban, + wheelchairAccessible = false, + cardinalPoint = CardinalPoint.North, + isFavorite = false, + ), + DbStop( + stopId = 1, + latitude = 0.0, + longitude = 0.0, + name = "Funivia-Staz. di Monte-Sardagna lorem ipsum dolor sit amet", + street = "Cembra - Via 4 Novembre - Dir.Cavalese lorem ipsum dolor sit", + town = "Appiano sulla strada del vino", + type = StopLineType.Suburban, + wheelchairAccessible = true, + cardinalPoint = null, + isFavorite = true, + ), + DbStop( + stopId = 2, + latitude = 0.0, + longitude = 0.0, + name = "Pinè Bivio", + street = "Civezzano-Loc.La Mochena", + town = "Civezzano", + type = StopLineType.Suburban, + wheelchairAccessible = false, + cardinalPoint = CardinalPoint.NorthWest, + isFavorite = true, + ), + DbStop( + stopId = 3, + latitude = 0.0, + longitude = 0.0, + name = "Verona Big Center", + street = "Verona \"Big Center\"", + town = "Trento", + type = StopLineType.Suburban, + wheelchairAccessible = true, + cardinalPoint = null, + isFavorite = false, + ) + ) + val sampleDbLines: Sequence = sequenceOf( + DbLine( + lineId = 396, + type = StopLineType.Urban, + area = Area.UrbanTrento, + color = 0xc52720, + longName = "Cortesano Gardolo P.Dante Villazzano 3", + shortName = "3", + isFavorite = false + ), + DbLine( + lineId = 404, + type = StopLineType.Urban, + area = Area.UrbanTrento, + color = 0x52332a, + longName = "Centochiavi Piazza Dante Mattarello", + shortName = "8", + isFavorite = true + ), + DbLine( + lineId = 466, + type = StopLineType.Urban, + area = Area.UrbanTrento, + color = 0xbf6092, + longName = "P.Dante Rosmini S.Rocco Povo Polo Soc.", + shortName = "13", + isFavorite = false + ), + DbLine( + lineId = 415, + type = StopLineType.Urban, + area = Area.UrbanTrento, + color = 0xe490b0, + longName = "P.Dante Via Sanseverino Belvedere Ravina", + shortName = "14", + isFavorite = true + ), + DbLine( + lineId = 109, + type = StopLineType.Suburban, + area = Area.Suburban1, + color = null, + longName = "Cavalese - Masi di Cavalese", + shortName = "B109", + isFavorite = true + ), + DbLine( + lineId = 201, + type = StopLineType.Suburban, + area = Area.Suburban2, + color = null, + longName = "Trento-Vezzano-Sarche-Tione", + shortName = "B201", + isFavorite = false + ), + ) + val referenceDateTime = + OffsetDateTime.of(2022, 9, 26, 9, 33, 17, 328943849, ZoneOffset.UTC) + GlanceTheme { + Surface( + color = MaterialTheme.colorScheme.background + ) { + var loading by rememberSaveable { mutableStateOf(true) } + val stopToHighlight = SampleDbStopProvider().values.first() + TripView( + trip = UiTrip( + delay = 1, + direction = Direction.Backward, + lastEventReceivedAt = referenceDateTime.minusMinutes(3), + lineId = sampleDbLines.first().lineId, + line = sampleDbLines.first(), + headSign = "Conci \"Villazzano 3\"", + tripId = "0003726592022061120220911", + type = StopLineType.Urban, + completedStops = 2, + stopTimes = sampleDbStops.mapIndexed { index, dbStop -> + UiStopTime( + arrivalTime = referenceDateTime.plusMinutes((index - 2).toLong()), + departureTime = referenceDateTime.plusMinutes((index + index % 2 - 2).toLong()), + stop = dbStop + ) + }, + busId = 886, + ), + setReferenceDateTime = {}, + error = false, + loading = loading, + onReload = { loading = !loading }, + prevEnabled = true, + onPrevClicked = {}, + nextEnabled = false, + onNextClicked = {}, + stopIdToHighlight = stopToHighlight.stopId, + stopTypeToHighlight = stopToHighlight.type, + navigator = EmptyDestinationsNavigator, + onLineClick = {}, + ) + } + } +} + +@OptIn(ExperimentalGlancePreviewApi::class) +@Preview() +@Composable +private fun TripViewPreviewLoading() { + GlanceTheme { + Surface( + color = MaterialTheme.colorScheme.background + ) { + var loading by rememberSaveable { mutableStateOf(true) } + TripView( + trip = null, + setReferenceDateTime = {}, + error = false, + loading = loading, + onReload = { loading = !loading }, + prevEnabled = true, + onPrevClicked = {}, + nextEnabled = false, + onNextClicked = {}, + stopIdToHighlight = null, + stopTypeToHighlight = null, + navigator = EmptyDestinationsNavigator, + onLineClick = {}, + ) + } + } +}*/ \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/TripViewStopsGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/TripViewStopsGlance.kt new file mode 100644 index 0000000..e4456b3 --- /dev/null +++ b/app/src/main/java/org/stypox/tridenta/widget/TripViewStopsGlance.kt @@ -0,0 +1,368 @@ +package org.stypox.tridenta.widget + +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.glance.ColorFilter +import androidx.glance.GlanceModifier +import androidx.glance.GlanceTheme +import androidx.glance.Image +import androidx.glance.ImageProvider +import androidx.glance.LocalContext +import androidx.glance.appwidget.lazy.LazyColumn +import androidx.glance.appwidget.lazy.itemsIndexed +import androidx.glance.layout.Alignment +import androidx.glance.layout.Row +import androidx.glance.layout.Spacer +import androidx.glance.layout.height +import androidx.glance.layout.padding +import androidx.glance.layout.size +import androidx.glance.preview.ExperimentalGlancePreviewApi +import androidx.glance.preview.Preview +import androidx.glance.text.FontWeight +import androidx.glance.text.Text +import androidx.glance.text.TextDecoration +import androidx.glance.text.TextStyle +import org.stypox.tridenta.R +import org.stypox.tridenta.db.data.DbLine +import org.stypox.tridenta.db.data.DbStop +import org.stypox.tridenta.enums.Area +import org.stypox.tridenta.enums.CardinalPoint +import org.stypox.tridenta.enums.Direction +import org.stypox.tridenta.enums.StopLineType +import org.stypox.tridenta.extractor.data.ExTrip +import org.stypox.tridenta.repo.data.UiStopTime +import org.stypox.tridenta.repo.data.UiTrip +import org.stypox.tridenta.util.formatConcatStrings +import org.stypox.tridenta.util.formatTime +import java.time.OffsetDateTime +import java.time.ZoneOffset + +@Composable +fun TripViewStopsGlance( + trip: UiTrip, + stopIdToHighlight: Int?, + stopTypeToHighlight: StopLineType?, + modifier: GlanceModifier = GlanceModifier, + onStopClick: ((DbStop) -> Unit)? = null, +) { + val context = LocalContext.current + LazyColumn( + modifier = modifier, + horizontalAlignment = Alignment.Start, + ) { + itemsIndexed(trip.stopTimes) { index, stopTime -> + TripViewStopItemGlance( + trip = trip, + highlight = stopTime.stop != null && + stopTime.stop.stopId == stopIdToHighlight && + stopTime.stop.type == stopTypeToHighlight, + completed = index < trip.completedStops, + stopTime = stopTime, + modifier = if (stopTime.stop == null || onStopClick == null) { + GlanceModifier // not clickable, since there is no stop + } else { + GlanceModifier // TODO: aggiungere clickable + } + ) + } + + item { + Text( + text = formatConcatStrings( + if (trip.lastEventReceivedAt == null) { + context.getString(R.string.no_update) + } else { + context.getString( + R.string.last_update, + formatTime(trip.lastEventReceivedAt) + ) + }, + if (trip.busId == ExTrip.BUS_ID_UNKNOWN) { + null + } else { + context.getString(R.string.bus_id, trip.busId) + } + ), + style = TextStyle( + color = GlanceTheme.colors.onSurface + ), + modifier = GlanceModifier.padding(8.dp) + ) + } + + item { + // space for FABs + Spacer(modifier = GlanceModifier.size(height = 84.dp, width = 0.dp)) + } + } +} + +@Composable +private fun TripViewStopItemGlance( + trip: UiTrip, + highlight: Boolean, + completed: Boolean, + stopTime: UiStopTime, + modifier: GlanceModifier = GlanceModifier, +) { + val context = LocalContext.current + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.padding(horizontal = 16.dp, vertical = 0.dp) + ) { + Image( + provider = ImageProvider( + if (trip.lastEventReceivedAt == null) { + R.drawable.arrow_right + } else if (completed) { + R.drawable.check_circle + } else { + R.drawable.radio_button_unchecked + } + ), + contentDescription = null, + colorFilter = ColorFilter.tint(GlanceTheme.colors.primary), + modifier = GlanceModifier.padding( + end = 6.dp, + ), + ) + + if (stopTime.stop?.isFavorite == true) { + Image( + provider = ImageProvider(R.drawable.favorite), + contentDescription = context.getString(R.string.favorite), + colorFilter = ColorFilter.tint(GlanceTheme.colors.primary), + modifier = GlanceModifier.padding(end = 3.dp) + .height(16.dp), + ) + } + + stopTime.stop?.cardinalPoint?.let { + Text( + text = context.getString(it.shortName), + style = TextStyle( + color = GlanceTheme.colors.primary + ), + modifier = GlanceModifier.padding(end = 3.dp), + ) + } + + val textColor = if (stopTime.stop == null) { + GlanceTheme.colors.error + } else if (highlight) { + GlanceTheme.colors.primary + } else { + GlanceTheme.colors.onSurface + } + + Text( + text = stopTime.stop?.name ?: context.getString(R.string.error), + maxLines = 1, + style = TextStyle( + fontWeight = if (highlight) FontWeight.Bold else null, + color = textColor + ), + modifier = GlanceModifier + .run { + if (stopTime.arrivalTime == null && stopTime.departureTime == null) { + this // do not apply end padding if there is nothing after + } else { + padding(end = 6.dp) + } + }, + ) + + val isLate = trip.lastEventReceivedAt != null + && !completed + && trip.delay > 0 + val lateDecoration = if (isLate) TextDecoration.LineThrough else null + val highlightWeight = if (highlight) FontWeight.Bold else null + + if (stopTime.arrivalTime != null) { + // TODO `key` forces recompositions when `lateDecoration` changes, needed because of + // probably a bug in Compose (also see below) + Text( + text = formatTime(stopTime.arrivalTime), + maxLines = 1, + style = TextStyle( + fontWeight = highlightWeight, + textDecoration = lateDecoration + ), + ) + } + if (stopTime.arrivalTime != stopTime.departureTime) { + if (stopTime.arrivalTime != null) { + Image( + provider = ImageProvider(R.drawable.double_arrow), + contentDescription = null, + modifier = GlanceModifier.size(8.dp), + ) + } + if (stopTime.departureTime != null) { + Text( + text = formatTime(stopTime.departureTime), + maxLines = 1, + style = TextStyle( + fontWeight = highlightWeight, + textDecoration = lateDecoration + ) + ) + } + } + if (isLate) { + sequenceOf(stopTime.arrivalTime, stopTime.departureTime).firstOrNull { it != null } + ?.let { time -> + Text( + text = formatTime( + time.plusMinutes(trip.delay.toLong()) + ), + modifier = GlanceModifier.padding(start = 5.dp), + maxLines = 1, + style = TextStyle( + fontWeight = highlightWeight, + color = GlanceTheme.colors.error + ), + ) + } + } + } +} + +@OptIn(ExperimentalGlancePreviewApi::class) +@Preview(widthDp = 250, heightDp = 130) +@Composable +fun TripViewStopsPreview() { + val sampleDbStops: List = listOf( + DbStop( + stopId = 0, + latitude = 0.0, + longitude = 0.0, + name = "Sorni", + street = "Sorni", + town = "", + type = StopLineType.Urban, + wheelchairAccessible = false, + cardinalPoint = CardinalPoint.North, + isFavorite = false, + ), + DbStop( + stopId = 1, + latitude = 0.0, + longitude = 0.0, + name = "Funivia-Staz. di Monte-Sardagna lorem ipsum dolor sit amet", + street = "Cembra - Via 4 Novembre - Dir.Cavalese lorem ipsum dolor sit", + town = "Appiano sulla strada del vino", + type = StopLineType.Suburban, + wheelchairAccessible = true, + cardinalPoint = null, + isFavorite = true, + ), + DbStop( + stopId = 2, + latitude = 0.0, + longitude = 0.0, + name = "Pinè Bivio", + street = "Civezzano-Loc.La Mochena", + town = "Civezzano", + type = StopLineType.Suburban, + wheelchairAccessible = false, + cardinalPoint = CardinalPoint.NorthWest, + isFavorite = true, + ), + DbStop( + stopId = 3, + latitude = 0.0, + longitude = 0.0, + name = "Verona Big Center", + street = "Verona \"Big Center\"", + town = "Trento", + type = StopLineType.Suburban, + wheelchairAccessible = true, + cardinalPoint = null, + isFavorite = false, + ) + ) + val sampleDbLines: Sequence = sequenceOf( + DbLine( + lineId = 396, + type = StopLineType.Urban, + area = Area.UrbanTrento, + color = 0xc52720, + longName = "Cortesano Gardolo P.Dante Villazzano 3", + shortName = "3", + isFavorite = false + ), + DbLine( + lineId = 404, + type = StopLineType.Urban, + area = Area.UrbanTrento, + color = 0x52332a, + longName = "Centochiavi Piazza Dante Mattarello", + shortName = "8", + isFavorite = true + ), + DbLine( + lineId = 466, + type = StopLineType.Urban, + area = Area.UrbanTrento, + color = 0xbf6092, + longName = "P.Dante Rosmini S.Rocco Povo Polo Soc.", + shortName = "13", + isFavorite = false + ), + DbLine( + lineId = 415, + type = StopLineType.Urban, + area = Area.UrbanTrento, + color = 0xe490b0, + longName = "P.Dante Via Sanseverino Belvedere Ravina", + shortName = "14", + isFavorite = true + ), + DbLine( + lineId = 109, + type = StopLineType.Suburban, + area = Area.Suburban1, + color = null, + longName = "Cavalese - Masi di Cavalese", + shortName = "B109", + isFavorite = true + ), + DbLine( + lineId = 201, + type = StopLineType.Suburban, + area = Area.Suburban2, + color = null, + longName = "Trento-Vezzano-Sarche-Tione", + shortName = "B201", + isFavorite = false + ), + ) + val referenceDateTime = + OffsetDateTime.of(2022, 9, 26, 9, 33, 17, 328943849, ZoneOffset.UTC) + GlanceTheme { + TripViewStopsGlance( + trip = UiTrip( + delay = 1, + direction = Direction.Backward, + lastEventReceivedAt = referenceDateTime.minusMinutes(3), + lineId = sampleDbLines.first().lineId, + line = sampleDbLines.first(), + headSign = "Conci \"Villazzano 3\"", + tripId = "0003726592022061120220911", + type = StopLineType.Urban, + completedStops = 2, + stopTimes = sampleDbStops.mapIndexed { index, dbStop -> + UiStopTime( + arrivalTime = referenceDateTime.plusMinutes((index - 2).toLong()), + departureTime = referenceDateTime.plusMinutes((index + index % 2 - 2).toLong()), + stop = dbStop + ) + }, + busId = 886, + ), + stopIdToHighlight = null, + stopTypeToHighlight = null + ) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/arrow_left.xml b/app/src/main/res/drawable/arrow_left.xml new file mode 100644 index 0000000..95b0c36 --- /dev/null +++ b/app/src/main/res/drawable/arrow_left.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/favorite.xml b/app/src/main/res/drawable/favorite.xml index fdc35fb..2c2751e 100644 --- a/app/src/main/res/drawable/favorite.xml +++ b/app/src/main/res/drawable/favorite.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/drawable/refresh.xml b/app/src/main/res/drawable/refresh.xml new file mode 100644 index 0000000..536c0dd --- /dev/null +++ b/app/src/main/res/drawable/refresh.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/xml/my_app_widget_info.xml b/app/src/main/res/xml/my_app_widget_info.xml index cd2fed8..9a7a86c 100644 --- a/app/src/main/res/xml/my_app_widget_info.xml +++ b/app/src/main/res/xml/my_app_widget_info.xml @@ -1,4 +1,6 @@ + android:initialLayout="@layout/glance_default_loading_layout" + android:resizeMode="vertical|horizontal" + android:minWidth="250dp"> \ No newline at end of file From db940745013e54b602dc2680f2ebd1b049607dcd Mon Sep 17 00:00:00 2001 From: whyKVD Date: Thu, 5 Mar 2026 18:16:30 +0100 Subject: [PATCH 04/43] feat: Created a configuration activity for the widget --- app/src/main/AndroidManifest.xml | 7 + .../org/stypox/tridenta/widget/MyAppWidget.kt | 63 ++----- .../tridenta/widget/MyAppWidgetReceiver.kt | 14 +- .../tridenta/widget/WidgetConfigViewModel.kt | 38 +++++ .../widget/WidgetConfigurationActivity.kt | 108 ++++++++++++ .../tridenta/widget/actions/WidgetActions.kt | 89 ++++++++++ .../tridenta/widget/actions/WidgetKeys.kt | 29 ++++ .../widget/{ => ui}/TripViewGlance.kt | 156 +++++++++--------- .../widget/{ => ui}/TripViewStopsGlance.kt | 5 +- app/src/main/res/xml/my_app_widget_info.xml | 3 +- 10 files changed, 374 insertions(+), 138 deletions(-) create mode 100644 app/src/main/java/org/stypox/tridenta/widget/WidgetConfigViewModel.kt create mode 100644 app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt create mode 100644 app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt create mode 100644 app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt rename app/src/main/java/org/stypox/tridenta/widget/{ => ui}/TripViewGlance.kt (75%) rename app/src/main/java/org/stypox/tridenta/widget/{ => ui}/TripViewStopsGlance.kt (98%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 48232f7..ffcaa6a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -37,6 +37,13 @@ android:name="android.app.shortcuts" android:resource="@xml/shortcuts" /> + + + + + diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt index 48d7bb1..50d4687 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt @@ -3,21 +3,14 @@ package org.stypox.tridenta.widget import android.content.Context import androidx.compose.runtime.Composable import androidx.glance.GlanceId -import androidx.glance.GlanceModifier import androidx.glance.GlanceTheme import androidx.glance.action.actionStartActivity import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.action.actionRunCallback import androidx.glance.appwidget.provideContent -import androidx.glance.background -import androidx.glance.layout.Alignment -import androidx.glance.layout.Column -import androidx.glance.layout.Row -import androidx.glance.layout.Spacer -import androidx.glance.layout.fillMaxSize -import androidx.glance.layout.fillMaxWidth import androidx.glance.preview.ExperimentalGlancePreviewApi import androidx.glance.preview.Preview -import androidx.glance.text.Text +import dagger.hilt.android.EntryPointAccessors import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.stypox.tridenta.db.LineDao @@ -35,22 +28,16 @@ import org.stypox.tridenta.repo.LinesRepository import org.stypox.tridenta.repo.data.UiStopTime import org.stypox.tridenta.repo.data.UiTrip import org.stypox.tridenta.ui.MainActivity +import org.stypox.tridenta.widget.actions.NextTripAction +import org.stypox.tridenta.widget.actions.PrevTripAction +import org.stypox.tridenta.widget.actions.ReloadTripAction +import org.stypox.tridenta.widget.actions.WidgetEntryPoint +import org.stypox.tridenta.widget.ui.TripViewGlance import java.time.OffsetDateTime import java.time.ZoneOffset import java.time.ZonedDateTime -enum class State { - Loading, - Success, - Error -} - -class MyAppWidget( - private val stopDao: StopDao, - private val lineDao: LineDao, - private val linesRepository: LinesRepository, - private val tripsRepository: LineTripsRepository -) : GlanceAppWidget() { +class MyAppWidget() : GlanceAppWidget() { override suspend fun provideGlance( context: Context, id: GlanceId @@ -63,6 +50,9 @@ class MyAppWidget( var trip: UiTrip? = null lateinit var favoriteLines: List try { + val hiltEntryPoint = EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) + val tripsRepository = hiltEntryPoint.lineTripsRepository() + val lineDao = hiltEntryPoint.lineDao() withContext(Dispatchers.IO) { val lines = lineDao.getAllLines() favoriteLines = lines.filter { l -> l.isFavorite } @@ -79,15 +69,16 @@ class MyAppWidget( } catch (e: Exception) { logError(e.message!!, e.cause) isError = true + isLoading = false } provideContent { GlanceTheme() { TripViewGlance( trip, error = isError, loading = isLoading, - onReloadAction = actionStartActivity(), - onPrevAction = actionStartActivity(), - onNextAction = actionStartActivity(), + onReloadAction = actionRunCallback(), + onPrevAction = actionRunCallback(), + onNextAction = actionRunCallback(), onLineClickAction = actionStartActivity(), stopIdToHighlight = null, stopTypeToHighlight = null, @@ -97,27 +88,6 @@ class MyAppWidget( } } -@Composable -fun MyContent(trip: UiTrip) { - Column( - modifier = GlanceModifier.fillMaxSize().background(GlanceTheme.colors.background), - verticalAlignment = Alignment.Top, - horizontalAlignment = Alignment.Start - ) { - Row(GlanceModifier.fillMaxWidth()) { - Text("Trips -") - Spacer() - Text("> ") - } - TripViewStopsGlance( - trip, - stopIdToHighlight = null, - stopTypeToHighlight = trip.type, - modifier = GlanceModifier.defaultWeight() - ) - } -} - @OptIn(ExperimentalGlancePreviewApi::class) // 3x2 Widget @Preview(widthDp = 250, heightDp = 130) @@ -256,7 +226,8 @@ fun MyWidgetPreview() { ) }, busId = 886, - ), error = false, loading = false, + ), + error = false, loading = false, onReloadAction = actionStartActivity(), onPrevAction = actionStartActivity(), onNextAction = actionStartActivity(), diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidgetReceiver.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidgetReceiver.kt index 5b2e3b2..ac9fbec 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidgetReceiver.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidgetReceiver.kt @@ -11,17 +11,5 @@ import javax.inject.Inject @AndroidEntryPoint class MyAppWidgetReceiver : GlanceAppWidgetReceiver() { - @Inject - lateinit var stopDao: StopDao - - @Inject - lateinit var lineDao: LineDao - - @Inject - lateinit var linesRepository: LinesRepository - - @Inject - lateinit var tripsRepository: LineTripsRepository - - override val glanceAppWidget: GlanceAppWidget get() = MyAppWidget(stopDao, lineDao,linesRepository,tripsRepository) + override val glanceAppWidget: GlanceAppWidget get() = MyAppWidget() } \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigViewModel.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigViewModel.kt new file mode 100644 index 0000000..11c39d3 --- /dev/null +++ b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigViewModel.kt @@ -0,0 +1,38 @@ +package org.stypox.tridenta.widget + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.stypox.tridenta.db.LineDao +import org.stypox.tridenta.db.data.DbLine +import javax.inject.Inject +import kotlin.collections.emptyList + +@HiltViewModel +class WidgetConfigViewModel @Inject constructor( + private val lineDao: LineDao +) : ViewModel() { + // 1. Create a Mutable internal state + private val _availableLines = MutableStateFlow>(emptyList()) + // 2. Expose it as a read-only StateFlow for the UI + val availableLines = _availableLines.asStateFlow() + + init { + // 3. Fetch the data once when the ViewModel is created + viewModelScope.launch { + // Because your Dao is a suspend function, we can call it here + withContext(Dispatchers.IO) { + val lines = lineDao.getAllLines() + _availableLines.value = lines + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt new file mode 100644 index 0000000..81a1463 --- /dev/null +++ b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt @@ -0,0 +1,108 @@ +package org.stypox.tridenta.widget + +import android.app.Activity +import android.appwidget.AppWidgetManager +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.state.updateAppWidgetState +import androidx.hilt.navigation.compose.hiltViewModel +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.stypox.tridenta.db.data.DbLine +import org.stypox.tridenta.widget.actions.WidgetKeys + +@AndroidEntryPoint +class WidgetConfigurationActivity : ComponentActivity() { + + private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // 1. Get the Widget ID from the Intent that launched this Activity + appWidgetId = intent?.extras?.getInt( + AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID + ) ?: AppWidgetManager.INVALID_APPWIDGET_ID + + // If the intent doesn't have a valid ID, bail out early. + if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { + finish() + return + } + + // Set the result to CANCELED right away. This ensures that if the user backs + // out of the activity without picking a line, the widget is removed from the home screen. + setResult(Activity.RESULT_CANCELED) + + setContent { + val viewModel: WidgetConfigViewModel = hiltViewModel() + + val availableLines by viewModel.availableLines.collectAsState() + + LineSelectionScreen( + lines = availableLines, + onLineSelected = { selectedLine -> + saveWidgetConfiguration(selectedLine) + } + ) + } + } + + @OptIn(DelicateCoroutinesApi::class) + private fun saveWidgetConfiguration(line: DbLine) { + val context = this + + // We use GlobalScope or a dedicated CoroutineScope because the activity + // will finish immediately, and we need this save to complete. + GlobalScope.launch(Dispatchers.IO) { + // 2. Map the standard Android appWidgetId to a Jetpack GlanceId + val glanceManager = GlanceAppWidgetManager(context) + val glanceId = glanceManager.getGlanceIdBy(appWidgetId) + + // 3. Save the selected data to this specific widget's Preferences + updateAppWidgetState(context, glanceId) { prefs -> + prefs[WidgetKeys.LINE_ID] = line.lineId + prefs[WidgetKeys.LINE_TYPE] = line.type.name + prefs[WidgetKeys.IS_LOADED] = false // Forces the widget to fetch fresh data + } + + // 4. Tell the widget to redraw now that it has a configuration + MyAppWidget().update(context, glanceId) + + // 5. Tell the Android OS that the configuration was successful + val resultValue = Intent().apply { + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + } + setResult(Activity.RESULT_OK, resultValue) + finish() + } + } +} + +@Composable +fun LineSelectionScreen(lines: List, onLineSelected: (DbLine) -> Unit) { + LazyColumn(modifier = Modifier.fillMaxSize()) { + items(lines) { line -> + Text( + text = "Line ${line.shortName}", + modifier = Modifier.clickable { onLineSelected(line) } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt new file mode 100644 index 0000000..5ae3f58 --- /dev/null +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt @@ -0,0 +1,89 @@ +package org.stypox.tridenta.widget.actions + +import android.content.Context +import androidx.glance.GlanceId +import androidx.glance.action.ActionParameters +import androidx.glance.appwidget.action.ActionCallback +import androidx.glance.appwidget.state.updateAppWidgetState +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent +import org.stypox.tridenta.db.LineDao +import org.stypox.tridenta.db.StopDao +import org.stypox.tridenta.log.logInfo +import org.stypox.tridenta.repo.LineTripsRepository +import org.stypox.tridenta.repo.LinesRepository +import org.stypox.tridenta.widget.MyAppWidget + +// 1. Create a Hilt Entry point to access your Repositories inside Glance Actions +@EntryPoint +@InstallIn(SingletonComponent::class) +interface WidgetEntryPoint { + fun lineTripsRepository(): LineTripsRepository + fun lineDao(): LineDao + // Add HistoryDao and LinesRepository here too +} + +// 2. Refactor: onNextClicked() -> NextTripAction +class NextTripAction : ActionCallback { + override suspend fun onAction( + context: Context, + glanceId: GlanceId, + parameters: ActionParameters + ) { + val hiltEntryPoint = EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) + val tripsRepo = hiltEntryPoint.lineTripsRepository() + + /*// 1. Read current state (tripIndex) from Glance Preferences + updateAppWidgetState(context, glanceId) { prefs -> + val currentIndex = prefs[WidgetKeys.TRIP_INDEX] ?: 0 + val nextIndex = currentIndex + 1 + + // 2. Fetch the new data from your repository + // (Translating your loadIndexAsync logic here) + val nextTrip = tripsRepo.getUiTrip() // Use your repo logic + + // 3. Update the state in preferences + prefs[WidgetKeys.TRIP_INDEX] = nextIndex + // Save other necessary UI state strings/booleans + } + + // 4. Force the widget to redraw with the new state + MyAppWidget().update(context, glanceId)*/ + logInfo("NextTripAction performed") + } +} + +// 3. Refactor: onPrevClicked() -> PrevTripAction +class PrevTripAction : ActionCallback { + override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) { + val hiltEntryPoint = EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) + val tripsRepo = hiltEntryPoint.lineTripsRepository() + + /*updateAppWidgetState(context, glanceId) { prefs -> + val currentIndex = prefs[WidgetKeys.TRIP_INDEX] ?: 0 + if (currentIndex > 0) { + val prevIndex = currentIndex - 1 + // Fetch new trip and save to prefs... + prefs[WidgetKeys.TRIP_INDEX] = prevIndex + } + } + TridentaWidget().update(context, glanceId)*/ + logInfo("PrevTripAction performed") + } +} + +// 4. Refactor: onReload() -> ReloadTripAction +class ReloadTripAction : ActionCallback { + override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) { + // Handle reload logic, fetch fresh data, and update Widget + } +} + +// 5. Refactor: onDirectionClicked() -> ToggleDirectionAction +class ToggleDirectionAction : ActionCallback { + override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) { + // Read current direction from prefs, toggle it, fetch new trip, update prefs + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt new file mode 100644 index 0000000..e8839d8 --- /dev/null +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt @@ -0,0 +1,29 @@ +package org.stypox.tridenta.widget.actions + +import android.content.Context +import androidx.datastore.preferences.core.MutablePreferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.glance.GlanceId + +object WidgetKeys { + val TRIP_INDEX = intPreferencesKey("trip_index") + val LINE_ID = intPreferencesKey("line_id") + val TRIPS_IN_DAY_COUNT = intPreferencesKey("trips_in_day_count") + + // Save booleans + val IS_LOADED = booleanPreferencesKey("is_loading") + val HAS_ERROR = booleanPreferencesKey("has_error") + val PREV_ENABLED = booleanPreferencesKey("prev_enabled") + val NEXT_ENABLED = booleanPreferencesKey("next_enabled") + + // Save strings (Useful for Enums, IDs, or serialized JSON) + val LINE_TYPE = stringPreferencesKey("line_type") // e.g., "Urban", "Suburban" + val DIRECTION_FILTER = stringPreferencesKey("direction_filter") // e.g., "Forward", "Backward" + + // Complex objects like UiTrip cannot be saved directly in simple Preferences. + // You either have to serialize the UiTrip to a JSON string, or just save the tripId + // and let the Widget fetch it from the database every time it updates. + val CURRENT_TRIP_ID = stringPreferencesKey("current_trip_id") +} \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/TripViewGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt similarity index 75% rename from app/src/main/java/org/stypox/tridenta/widget/TripViewGlance.kt rename to app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt index 24c2436..8e3f9d7 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/TripViewGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt @@ -1,5 +1,6 @@ -package org.stypox.tridenta.widget +package org.stypox.tridenta.widget.ui +import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp import androidx.glance.Button @@ -9,6 +10,7 @@ import androidx.glance.Image import androidx.glance.ImageProvider import androidx.glance.LocalContext import androidx.glance.action.Action +import androidx.glance.action.actionStartActivity import androidx.glance.action.clickable import androidx.glance.appwidget.CircularProgressIndicator import androidx.glance.background @@ -25,6 +27,7 @@ import androidx.glance.text.FontWeight import androidx.glance.text.Text import androidx.glance.text.TextAlign import androidx.glance.text.TextStyle +import androidx.glance.unit.ColorProvider import org.stypox.tridenta.R import org.stypox.tridenta.db.data.DbLine import org.stypox.tridenta.db.data.DbStop @@ -34,12 +37,17 @@ import org.stypox.tridenta.enums.Direction import org.stypox.tridenta.enums.StopLineType import org.stypox.tridenta.repo.data.UiStopTime import org.stypox.tridenta.repo.data.UiTrip -import org.stypox.tridenta.sample.SampleDbStopProvider +import org.stypox.tridenta.ui.MainActivity import org.stypox.tridenta.util.formatDateFull -import org.stypox.tridenta.util.formatDurationMinutes +import org.stypox.tridenta.util.textColorOnBackground +import org.stypox.tridenta.util.toLineColor import java.time.OffsetDateTime import java.time.ZoneOffset +fun formatDurationMinutes(context: Context, minutes: Int): String { + return context.getString(R.string.short_minute_format, minutes) +} + @Composable fun TripViewGlance( trip: UiTrip?, @@ -57,7 +65,7 @@ fun TripViewGlance( ) { val context = LocalContext.current - Box ( + Box( modifier = modifier.fillMaxSize().background(GlanceTheme.colors.background), contentAlignment = Alignment.Center ) { @@ -100,7 +108,10 @@ fun TripViewGlance( } else if (error) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text(text = context.getString(R.string.error), style = TextStyle(color = GlanceTheme.colors.error)) + Text( + text = context.getString(R.string.error), + style = TextStyle(color = GlanceTheme.colors.error) + ) Button(text = context.getString(R.string.reload), onClick = onReloadAction) } @@ -111,12 +122,18 @@ fun TripViewGlance( ) { Text( text = context.getString(R.string.no_trip_found), - style = TextStyle(fontWeight = FontWeight.Bold, color = GlanceTheme.colors.onBackground), + style = TextStyle( + fontWeight = FontWeight.Bold, + color = GlanceTheme.colors.onBackground + ), modifier = GlanceModifier.padding(bottom = 4.dp) ) Text( text = context.getString(R.string.no_trip_found_description), - style = TextStyle(textAlign = TextAlign.Center, color = GlanceTheme.colors.onBackground) + style = TextStyle( + textAlign = TextAlign.Center, + color = GlanceTheme.colors.onBackground + ) ) } } @@ -151,9 +168,10 @@ private fun TripViewTopRowGlance( ) { if (trip.line != null) { // Replaced Surface with Box + background + val shortNameBackground = trip.line.color.toLineColor() Box( modifier = GlanceModifier - .background(GlanceTheme.colors.primaryContainer) // Use a theme color instead of dynamic custom color for simplicity in Glance + .background(shortNameBackground) // Use a theme color instead of dynamic custom color for simplicity in Glance .padding(8.dp) .clickable(onLineClickAction) ) { @@ -161,7 +179,7 @@ private fun TripViewTopRowGlance( text = trip.line.shortName, maxLines = 1, style = TextStyle( - color = GlanceTheme.colors.onPrimaryContainer, + color = ColorProvider(textColorOnBackground(shortNameBackground)), fontWeight = FontWeight.Bold ) ) @@ -174,7 +192,10 @@ private fun TripViewTopRowGlance( Text( text = trip.headSign, maxLines = 1, - style = TextStyle(color = GlanceTheme.colors.onBackground, fontWeight = FontWeight.Medium) + style = TextStyle( + color = GlanceTheme.colors.onBackground, + fontWeight = FontWeight.Medium + ) ) val dateOrDelayText = if (trip.lastEventReceivedAt == null) { @@ -186,11 +207,11 @@ private fun TripViewTopRowGlance( ?: context.getString(R.string.no_date_time_information) } else { if (trip.delay < 0) - context.getString(R.string.early, formatDurationMinutes(-trip.delay)) + context.getString(R.string.early, formatDurationMinutes(context,-trip.delay)) else if (trip.delay == 0) context.getString(R.string.on_time) else - context.getString(R.string.late, formatDurationMinutes(trip.delay)) + context.getString(R.string.late, formatDurationMinutes(context,trip.delay)) } Text( text = dateOrDelayText, @@ -219,9 +240,8 @@ private fun TripViewBottomRowGlance( .background(GlanceTheme.colors.surfaceVariant) .padding(16.dp) ) { - // You MUST replace R.drawable.ic_... with your actual XML drawables Image( - provider = ImageProvider(R.drawable.arrow_left), // Placeholder + provider = ImageProvider(R.drawable.arrow_left), contentDescription = context.getString(R.string.previous), modifier = GlanceModifier.clickable(onPrevAction).defaultWeight() ) @@ -230,21 +250,21 @@ private fun TripViewBottomRowGlance( CircularProgressIndicator(modifier = GlanceModifier.defaultWeight()) } else { Image( - provider = ImageProvider(R.drawable.refresh), // Placeholder + provider = ImageProvider(R.drawable.refresh), contentDescription = context.getString(R.string.reload), modifier = GlanceModifier.clickable(onReloadAction).defaultWeight() ) } Image( - provider = ImageProvider(R.drawable.arrow_right), // Placeholder + provider = ImageProvider(R.drawable.arrow_right), contentDescription = context.getString(R.string.next), modifier = GlanceModifier.clickable(onNextAction).defaultWeight() ) } } -/*@OptIn(ExperimentalGlancePreviewApi::class) +@OptIn(ExperimentalGlancePreviewApi::class) @Preview @Composable private fun TripViewPreview() { @@ -357,45 +377,35 @@ private fun TripViewPreview() { val referenceDateTime = OffsetDateTime.of(2022, 9, 26, 9, 33, 17, 328943849, ZoneOffset.UTC) GlanceTheme { - Surface( - color = MaterialTheme.colorScheme.background - ) { - var loading by rememberSaveable { mutableStateOf(true) } - val stopToHighlight = SampleDbStopProvider().values.first() - TripView( - trip = UiTrip( - delay = 1, - direction = Direction.Backward, - lastEventReceivedAt = referenceDateTime.minusMinutes(3), - lineId = sampleDbLines.first().lineId, - line = sampleDbLines.first(), - headSign = "Conci \"Villazzano 3\"", - tripId = "0003726592022061120220911", - type = StopLineType.Urban, - completedStops = 2, - stopTimes = sampleDbStops.mapIndexed { index, dbStop -> - UiStopTime( - arrivalTime = referenceDateTime.plusMinutes((index - 2).toLong()), - departureTime = referenceDateTime.plusMinutes((index + index % 2 - 2).toLong()), - stop = dbStop - ) - }, - busId = 886, - ), - setReferenceDateTime = {}, - error = false, - loading = loading, - onReload = { loading = !loading }, - prevEnabled = true, - onPrevClicked = {}, - nextEnabled = false, - onNextClicked = {}, - stopIdToHighlight = stopToHighlight.stopId, - stopTypeToHighlight = stopToHighlight.type, - navigator = EmptyDestinationsNavigator, - onLineClick = {}, - ) - } + TripViewGlance( + trip = UiTrip( + delay = 1, + direction = Direction.Backward, + lastEventReceivedAt = referenceDateTime.minusMinutes(3), + lineId = sampleDbLines.first().lineId, + line = sampleDbLines.first(), + headSign = "Conci \"Villazzano 3\"", + tripId = "0003726592022061120220911", + type = StopLineType.Urban, + completedStops = 2, + stopTimes = sampleDbStops.mapIndexed { index, dbStop -> + UiStopTime( + arrivalTime = referenceDateTime.plusMinutes((index - 2).toLong()), + departureTime = referenceDateTime.plusMinutes((index + index % 2 - 2).toLong()), + stop = dbStop + ) + }, + busId = 886, + ), + error = false, + loading = true, + stopIdToHighlight = null, + stopTypeToHighlight = null, + onReloadAction = actionStartActivity(), + onPrevAction = actionStartActivity(), + onNextAction = actionStartActivity(), + onLineClickAction = actionStartActivity() + ) } } @@ -403,26 +413,18 @@ private fun TripViewPreview() { @Preview() @Composable private fun TripViewPreviewLoading() { + val loading = true GlanceTheme { - Surface( - color = MaterialTheme.colorScheme.background - ) { - var loading by rememberSaveable { mutableStateOf(true) } - TripView( - trip = null, - setReferenceDateTime = {}, - error = false, - loading = loading, - onReload = { loading = !loading }, - prevEnabled = true, - onPrevClicked = {}, - nextEnabled = false, - onNextClicked = {}, - stopIdToHighlight = null, - stopTypeToHighlight = null, - navigator = EmptyDestinationsNavigator, - onLineClick = {}, - ) - } + TripViewGlance( + trip = null, + error = false, + loading = loading, + stopIdToHighlight = null, + stopTypeToHighlight = null, + onReloadAction = actionStartActivity(), + onPrevAction = actionStartActivity(), + onNextAction = actionStartActivity(), + onLineClickAction = actionStartActivity() + ) } -}*/ \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/TripViewStopsGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt similarity index 98% rename from app/src/main/java/org/stypox/tridenta/widget/TripViewStopsGlance.kt rename to app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt index e4456b3..9d7e3ba 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/TripViewStopsGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt @@ -1,4 +1,4 @@ -package org.stypox.tridenta.widget +package org.stypox.tridenta.widget.ui import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp @@ -22,6 +22,7 @@ import androidx.glance.text.FontWeight import androidx.glance.text.Text import androidx.glance.text.TextDecoration import androidx.glance.text.TextStyle +import androidx.glance.unit.ColorProvider import org.stypox.tridenta.R import org.stypox.tridenta.db.data.DbLine import org.stypox.tridenta.db.data.DbStop @@ -185,6 +186,7 @@ private fun TripViewStopItemGlance( text = formatTime(stopTime.arrivalTime), maxLines = 1, style = TextStyle( + color = GlanceTheme.colors.onSurface, fontWeight = highlightWeight, textDecoration = lateDecoration ), @@ -203,6 +205,7 @@ private fun TripViewStopItemGlance( text = formatTime(stopTime.departureTime), maxLines = 1, style = TextStyle( + color = GlanceTheme.colors.onSurface, fontWeight = highlightWeight, textDecoration = lateDecoration ) diff --git a/app/src/main/res/xml/my_app_widget_info.xml b/app/src/main/res/xml/my_app_widget_info.xml index 9a7a86c..e3c4d50 100644 --- a/app/src/main/res/xml/my_app_widget_info.xml +++ b/app/src/main/res/xml/my_app_widget_info.xml @@ -2,5 +2,6 @@ + android:minWidth="250dp" + android:configure="org.stypox.tridenta.widget.WidgetConfigurationActivity"> \ No newline at end of file From 05eeaa8b5bae90194dc09b9aadd769d1cb6c7781 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Fri, 6 Mar 2026 11:09:01 +0100 Subject: [PATCH 05/43] feat: Now the widget can be configured --- .../org/stypox/tridenta/widget/MyAppWidget.kt | 77 +++++++++++-------- .../widget/WidgetConfigurationActivity.kt | 64 +++++++++------ .../tridenta/widget/actions/WidgetKeys.kt | 3 +- app/src/main/res/xml/my_app_widget_info.xml | 2 + 4 files changed, 89 insertions(+), 57 deletions(-) diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt index 50d4687..fdbae43 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt @@ -2,19 +2,25 @@ package org.stypox.tridenta.widget import android.content.Context import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.datastore.preferences.core.Preferences import androidx.glance.GlanceId import androidx.glance.GlanceTheme import androidx.glance.action.actionStartActivity import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.action.actionRunCallback import androidx.glance.appwidget.provideContent +import androidx.glance.currentState import androidx.glance.preview.ExperimentalGlancePreviewApi import androidx.glance.preview.Preview +import androidx.glance.state.PreferencesGlanceStateDefinition import dagger.hilt.android.EntryPointAccessors import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.stypox.tridenta.db.LineDao -import org.stypox.tridenta.db.StopDao import org.stypox.tridenta.db.data.DbLine import org.stypox.tridenta.db.data.DbStop import org.stypox.tridenta.enums.Area @@ -23,8 +29,7 @@ import org.stypox.tridenta.enums.Direction import org.stypox.tridenta.enums.StopLineType import org.stypox.tridenta.extractor.ROME_ZONE_ID import org.stypox.tridenta.log.logError -import org.stypox.tridenta.repo.LineTripsRepository -import org.stypox.tridenta.repo.LinesRepository +import org.stypox.tridenta.log.logInfo import org.stypox.tridenta.repo.data.UiStopTime import org.stypox.tridenta.repo.data.UiTrip import org.stypox.tridenta.ui.MainActivity @@ -32,12 +37,14 @@ import org.stypox.tridenta.widget.actions.NextTripAction import org.stypox.tridenta.widget.actions.PrevTripAction import org.stypox.tridenta.widget.actions.ReloadTripAction import org.stypox.tridenta.widget.actions.WidgetEntryPoint +import org.stypox.tridenta.widget.actions.WidgetKeys import org.stypox.tridenta.widget.ui.TripViewGlance import java.time.OffsetDateTime import java.time.ZoneOffset import java.time.ZonedDateTime -class MyAppWidget() : GlanceAppWidget() { +class MyAppWidget : GlanceAppWidget() { + override val stateDefinition = PreferencesGlanceStateDefinition override suspend fun provideGlance( context: Context, id: GlanceId @@ -45,34 +52,40 @@ class MyAppWidget() : GlanceAppWidget() { // In this method, load data needed to render the AppWidget. // Use `withContext` to switch to another thread for long running // operations. - var isError = false - var isLoading = true - var trip: UiTrip? = null - lateinit var favoriteLines: List - try { - val hiltEntryPoint = EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) - val tripsRepository = hiltEntryPoint.lineTripsRepository() - val lineDao = hiltEntryPoint.lineDao() - withContext(Dispatchers.IO) { - val lines = lineDao.getAllLines() - favoriteLines = lines.filter { l -> l.isFavorite } - val line = favoriteLines[0] - val tmp = tripsRepository.getUiTrip( - line.lineId, - line.type, - ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID), - Direction.ForwardAndBackward - ).third - trip = tmp - isLoading = false - } - } catch (e: Exception) { - logError(e.message!!, e.cause) - isError = true - isLoading = false - } provideContent { + var trip by remember { mutableStateOf(null) } + var isLoading by remember { mutableStateOf(true) } + var isError by remember { mutableStateOf(false) } + val prefs = currentState() + val lineId = prefs[WidgetKeys.LINE_ID] + logInfo("lineId: ${lineId.toString()}") + val lineTypeString = prefs[WidgetKeys.LINE_TYPE] + logInfo("lineType: ${lineTypeString ?: "null"}") + LaunchedEffect(lineId, lineTypeString) { + try { + val hiltEntryPoint = + EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) + val tripsRepository = hiltEntryPoint.lineTripsRepository() + if (lineId != null && lineTypeString != null) { + val fetchedTrip = withContext(Dispatchers.IO) { + tripsRepository.getUiTrip( + lineId, + StopLineType.valueOf(lineTypeString), + ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID), + Direction.ForwardAndBackward + ).third + } + + trip = fetchedTrip + } + } catch (e: Exception) { + logError(e.message!!, e.cause) + isError = true + } finally { + isLoading = false + } + } GlanceTheme() { TripViewGlance( trip, error = isError, loading = isLoading, @@ -89,8 +102,6 @@ class MyAppWidget() : GlanceAppWidget() { } @OptIn(ExperimentalGlancePreviewApi::class) -// 3x2 Widget -@Preview(widthDp = 250, heightDp = 130) // 3x3 Widget @Preview(widthDp = 250, heightDp = 203) // 3x4 Widget diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt index 81a1463..b796e6b 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -17,17 +18,26 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.state.updateAppWidgetState +import androidx.glance.appwidget.updateAll import androidx.hilt.navigation.compose.hiltViewModel import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.stypox.tridenta.db.data.DbLine +import org.stypox.tridenta.enums.Direction +import org.stypox.tridenta.extractor.ROME_ZONE_ID +import org.stypox.tridenta.ui.lines.LineItem +import org.stypox.tridenta.ui.lines.LinesViewModel +import org.stypox.tridenta.ui.theme.TitleText import org.stypox.tridenta.widget.actions.WidgetKeys +import java.time.ZonedDateTime @AndroidEntryPoint -class WidgetConfigurationActivity : ComponentActivity() { +class WidgetConfigurationActivity: + ComponentActivity() { private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID @@ -51,22 +61,30 @@ class WidgetConfigurationActivity : ComponentActivity() { setResult(Activity.RESULT_CANCELED) setContent { - val viewModel: WidgetConfigViewModel = hiltViewModel() - - val availableLines by viewModel.availableLines.collectAsState() - - LineSelectionScreen( - lines = availableLines, - onLineSelected = { selectedLine -> - saveWidgetConfiguration(selectedLine) - } - ) + val viewModel: LinesViewModel = hiltViewModel() + + // 2. Collect the complex UI state + val uiState by viewModel.uiState.collectAsState() + + if (uiState.loading) { + CircularProgressIndicator() // Show loading spinner + } else if (uiState.error) { + Text("Error loading lines. Please try again.") + } else { + // 4. Pass the loaded lines to your selection screen + LineSelectionScreen( + lines = uiState.lines, + onLineSelected = { selectedLine -> + saveWidgetConfiguration(selectedLine) + } + ) + } } } @OptIn(DelicateCoroutinesApi::class) private fun saveWidgetConfiguration(line: DbLine) { - val context = this + val context = applicationContext // We use GlobalScope or a dedicated CoroutineScope because the activity // will finish immediately, and we need this save to complete. @@ -79,18 +97,17 @@ class WidgetConfigurationActivity : ComponentActivity() { updateAppWidgetState(context, glanceId) { prefs -> prefs[WidgetKeys.LINE_ID] = line.lineId prefs[WidgetKeys.LINE_TYPE] = line.type.name - prefs[WidgetKeys.IS_LOADED] = false // Forces the widget to fetch fresh data } - - // 4. Tell the widget to redraw now that it has a configuration - MyAppWidget().update(context, glanceId) + MyAppWidget().updateAll(this@WidgetConfigurationActivity) // 5. Tell the Android OS that the configuration was successful - val resultValue = Intent().apply { - putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + withContext(Dispatchers.Main) { + val resultValue = Intent().apply { + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + } + setResult(Activity.RESULT_OK, resultValue) + finish() } - setResult(Activity.RESULT_OK, resultValue) - finish() } } } @@ -98,10 +115,11 @@ class WidgetConfigurationActivity : ComponentActivity() { @Composable fun LineSelectionScreen(lines: List, onLineSelected: (DbLine) -> Unit) { LazyColumn(modifier = Modifier.fillMaxSize()) { + item { TitleText("Seleziona la linea:") } items(lines) { line -> - Text( - text = "Line ${line.shortName}", - modifier = Modifier.clickable { onLineSelected(line) } + LineItem( + line, true, + modifier = Modifier.clickable { onLineSelected(line) }, ) } } diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt index e8839d8..83e4e86 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt @@ -13,7 +13,8 @@ object WidgetKeys { val TRIPS_IN_DAY_COUNT = intPreferencesKey("trips_in_day_count") // Save booleans - val IS_LOADED = booleanPreferencesKey("is_loading") + val IS_LOADING = booleanPreferencesKey("is_loading") + val IS_INITIAL_DATA_LOADED = booleanPreferencesKey("is_initial_data_loaded") val HAS_ERROR = booleanPreferencesKey("has_error") val PREV_ENABLED = booleanPreferencesKey("prev_enabled") val NEXT_ENABLED = booleanPreferencesKey("next_enabled") diff --git a/app/src/main/res/xml/my_app_widget_info.xml b/app/src/main/res/xml/my_app_widget_info.xml index e3c4d50..2d94541 100644 --- a/app/src/main/res/xml/my_app_widget_info.xml +++ b/app/src/main/res/xml/my_app_widget_info.xml @@ -3,5 +3,7 @@ android:initialLayout="@layout/glance_default_loading_layout" android:resizeMode="vertical|horizontal" android:minWidth="250dp" + android:minHeight="150dp" + android:updatePeriodMillis="60000" android:configure="org.stypox.tridenta.widget.WidgetConfigurationActivity"> \ No newline at end of file From fe91af31d2a7694391fe00a572d1955232768588 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Fri, 6 Mar 2026 11:31:06 +0100 Subject: [PATCH 06/43] feat: Now the widget can be modified on the homescreen --- app/src/main/AndroidManifest.xml | 4 +++- .../org/stypox/tridenta/widget/MyAppWidget.kt | 20 +++++++++++++++---- .../widget/WidgetConfigurationActivity.kt | 4 +--- app/src/main/res/xml/my_app_widget_info.xml | 1 + 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ffcaa6a..ce00e4c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -39,7 +39,9 @@ + android:exported="true" + android:taskAffinity="" + android:excludeFromRecents="true"> diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt index fdbae43..24d339d 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt @@ -1,6 +1,8 @@ package org.stypox.tridenta.widget +import android.appwidget.AppWidgetManager import android.content.Context +import android.content.Intent import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -12,7 +14,9 @@ import androidx.glance.GlanceId import androidx.glance.GlanceTheme import androidx.glance.action.actionStartActivity import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.action.actionRunCallback +import androidx.glance.appwidget.action.actionStartActivity import androidx.glance.appwidget.provideContent import androidx.glance.currentState import androidx.glance.preview.ExperimentalGlancePreviewApi @@ -54,11 +58,19 @@ class MyAppWidget : GlanceAppWidget() { // operations. provideContent { + val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) + val configIntent = Intent(context, WidgetConfigurationActivity::class.java).apply { + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + + // These flags ensure the activity opens properly from the launcher context + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK + } var trip by remember { mutableStateOf(null) } var isLoading by remember { mutableStateOf(true) } var isError by remember { mutableStateOf(false) } val prefs = currentState() val lineId = prefs[WidgetKeys.LINE_ID] + val isInitalDataLoaded = prefs[WidgetKeys.IS_INITIAL_DATA_LOADED] ?: false logInfo("lineId: ${lineId.toString()}") val lineTypeString = prefs[WidgetKeys.LINE_TYPE] logInfo("lineType: ${lineTypeString ?: "null"}") @@ -86,13 +98,13 @@ class MyAppWidget : GlanceAppWidget() { isLoading = false } } - GlanceTheme() { + GlanceTheme { TripViewGlance( - trip, error = isError, loading = isLoading, + trip, error = isError, loading = isLoading && isInitalDataLoaded, onReloadAction = actionRunCallback(), onPrevAction = actionRunCallback(), onNextAction = actionRunCallback(), - onLineClickAction = actionStartActivity(), + onLineClickAction = actionStartActivity(configIntent), stopIdToHighlight = null, stopTypeToHighlight = null, ) @@ -217,7 +229,7 @@ fun MyWidgetPreview() { val referenceDateTime = OffsetDateTime.of(2022, 9, 26, 9, 33, 17, 328943849, ZoneOffset.UTC) // Provide mock data to your content - GlanceTheme() { + GlanceTheme { TripViewGlance( UiTrip( delay = 1, diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt index b796e6b..3737c4c 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt @@ -27,13 +27,10 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.stypox.tridenta.db.data.DbLine -import org.stypox.tridenta.enums.Direction -import org.stypox.tridenta.extractor.ROME_ZONE_ID import org.stypox.tridenta.ui.lines.LineItem import org.stypox.tridenta.ui.lines.LinesViewModel import org.stypox.tridenta.ui.theme.TitleText import org.stypox.tridenta.widget.actions.WidgetKeys -import java.time.ZonedDateTime @AndroidEntryPoint class WidgetConfigurationActivity: @@ -97,6 +94,7 @@ class WidgetConfigurationActivity: updateAppWidgetState(context, glanceId) { prefs -> prefs[WidgetKeys.LINE_ID] = line.lineId prefs[WidgetKeys.LINE_TYPE] = line.type.name + prefs[WidgetKeys.IS_INITIAL_DATA_LOADED] = true } MyAppWidget().updateAll(this@WidgetConfigurationActivity) diff --git a/app/src/main/res/xml/my_app_widget_info.xml b/app/src/main/res/xml/my_app_widget_info.xml index 2d94541..a3da815 100644 --- a/app/src/main/res/xml/my_app_widget_info.xml +++ b/app/src/main/res/xml/my_app_widget_info.xml @@ -5,5 +5,6 @@ android:minWidth="250dp" android:minHeight="150dp" android:updatePeriodMillis="60000" + android:widgetFeatures="reconfigurable" android:configure="org.stypox.tridenta.widget.WidgetConfigurationActivity"> \ No newline at end of file From e54eb74bae7f8f4fca4208e51cf1549933674dea Mon Sep 17 00:00:00 2001 From: whyKVD Date: Fri, 6 Mar 2026 14:13:51 +0100 Subject: [PATCH 07/43] feat: Added the direction icon and other fixes --- .../org/stypox/tridenta/widget/MyAppWidget.kt | 27 +++++++++-- .../widget/WidgetConfigurationActivity.kt | 3 ++ .../tridenta/widget/actions/WidgetActions.kt | 23 ++++++--- .../tridenta/widget/actions/WidgetKeys.kt | 6 +-- .../tridenta/widget/ui/TripViewGlance.kt | 47 +++++++++++++++++-- app/src/main/res/drawable/swap_calls.xml | 5 ++ .../main/res/drawable/turn_sharp_right.xml | 5 ++ app/src/main/res/drawable/u_turn_left.xml | 5 ++ app/src/main/res/xml/my_app_widget_info.xml | 2 +- 9 files changed, 101 insertions(+), 22 deletions(-) create mode 100644 app/src/main/res/drawable/swap_calls.xml create mode 100644 app/src/main/res/drawable/turn_sharp_right.xml create mode 100644 app/src/main/res/drawable/u_turn_left.xml diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt index 24d339d..73f1af9 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt @@ -18,6 +18,7 @@ import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.action.actionRunCallback import androidx.glance.appwidget.action.actionStartActivity import androidx.glance.appwidget.provideContent +import androidx.glance.appwidget.state.updateAppWidgetState import androidx.glance.currentState import androidx.glance.preview.ExperimentalGlancePreviewApi import androidx.glance.preview.Preview @@ -58,6 +59,7 @@ class MyAppWidget : GlanceAppWidget() { // operations. provideContent { + val prefs = currentState() val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) val configIntent = Intent(context, WidgetConfigurationActivity::class.java).apply { putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) @@ -66,9 +68,13 @@ class MyAppWidget : GlanceAppWidget() { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK } var trip by remember { mutableStateOf(null) } - var isLoading by remember { mutableStateOf(true) } var isError by remember { mutableStateOf(false) } - val prefs = currentState() + var isLoading by remember { mutableStateOf(prefs[WidgetKeys.IS_LOADING] ?: true) } + var directionFilter by remember { + mutableStateOf( + prefs[WidgetKeys.DIRECTION_FILTER] ?: Direction.ForwardAndBackward.name + ) + } val lineId = prefs[WidgetKeys.LINE_ID] val isInitalDataLoaded = prefs[WidgetKeys.IS_INITIAL_DATA_LOADED] ?: false logInfo("lineId: ${lineId.toString()}") @@ -85,10 +91,21 @@ class MyAppWidget : GlanceAppWidget() { lineId, StopLineType.valueOf(lineTypeString), ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID), - Direction.ForwardAndBackward + Direction.valueOf(directionFilter) ).third } - + withContext(Dispatchers.IO) { + updateAppWidgetState(context, id) { prefs -> + val fetchedTrip = tripsRepository.getUiTrip( + lineId, + StopLineType.valueOf(lineTypeString), + ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID), + Direction.valueOf(directionFilter) + ) + prefs[WidgetKeys.CURRENT_TRIP_ID] = fetchedTrip.second + prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] = fetchedTrip.first + } + } trip = fetchedTrip } } catch (e: Exception) { @@ -105,6 +122,7 @@ class MyAppWidget : GlanceAppWidget() { onPrevAction = actionRunCallback(), onNextAction = actionRunCallback(), onLineClickAction = actionStartActivity(configIntent), + //onDirectionClickAction = actionRunCallback(), stopIdToHighlight = null, stopTypeToHighlight = null, ) @@ -255,6 +273,7 @@ fun MyWidgetPreview() { onPrevAction = actionStartActivity(), onNextAction = actionStartActivity(), onLineClickAction = actionStartActivity(), + //onDirectionClickAction = actionStartActivity(), stopIdToHighlight = null, stopTypeToHighlight = null, ) diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt index 3737c4c..0da81e7 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.stypox.tridenta.db.data.DbLine +import org.stypox.tridenta.enums.Direction import org.stypox.tridenta.ui.lines.LineItem import org.stypox.tridenta.ui.lines.LinesViewModel import org.stypox.tridenta.ui.theme.TitleText @@ -95,6 +96,8 @@ class WidgetConfigurationActivity: prefs[WidgetKeys.LINE_ID] = line.lineId prefs[WidgetKeys.LINE_TYPE] = line.type.name prefs[WidgetKeys.IS_INITIAL_DATA_LOADED] = true + prefs[WidgetKeys.DIRECTION_FILTER] = Direction.ForwardAndBackward.name + prefs[WidgetKeys.IS_LOADING] = true } MyAppWidget().updateAll(this@WidgetConfigurationActivity) diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt index 5ae3f58..e689efc 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt @@ -5,12 +5,14 @@ import androidx.glance.GlanceId import androidx.glance.action.ActionParameters import androidx.glance.appwidget.action.ActionCallback import androidx.glance.appwidget.state.updateAppWidgetState +import androidx.glance.appwidget.updateAll import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent import org.stypox.tridenta.db.LineDao import org.stypox.tridenta.db.StopDao +import org.stypox.tridenta.enums.Direction import org.stypox.tridenta.log.logInfo import org.stypox.tridenta.repo.LineTripsRepository import org.stypox.tridenta.repo.LinesRepository @@ -35,22 +37,19 @@ class NextTripAction : ActionCallback { val hiltEntryPoint = EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) val tripsRepo = hiltEntryPoint.lineTripsRepository() - /*// 1. Read current state (tripIndex) from Glance Preferences + // 1. Read current state (tripIndex) from Glance Preferences updateAppWidgetState(context, glanceId) { prefs -> val currentIndex = prefs[WidgetKeys.TRIP_INDEX] ?: 0 + // TODO: Actually retrive the next index for the given trip val nextIndex = currentIndex + 1 - // 2. Fetch the new data from your repository - // (Translating your loadIndexAsync logic here) - val nextTrip = tripsRepo.getUiTrip() // Use your repo logic - // 3. Update the state in preferences prefs[WidgetKeys.TRIP_INDEX] = nextIndex // Save other necessary UI state strings/booleans } // 4. Force the widget to redraw with the new state - MyAppWidget().update(context, glanceId)*/ + MyAppWidget().updateAll(context) logInfo("NextTripAction performed") } } @@ -77,7 +76,7 @@ class PrevTripAction : ActionCallback { // 4. Refactor: onReload() -> ReloadTripAction class ReloadTripAction : ActionCallback { override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) { - // Handle reload logic, fetch fresh data, and update Widget + MyAppWidget().updateAll(context) } } @@ -85,5 +84,15 @@ class ReloadTripAction : ActionCallback { class ToggleDirectionAction : ActionCallback { override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) { // Read current direction from prefs, toggle it, fetch new trip, update prefs + updateAppWidgetState(context,glanceId) { prefs -> + val actualDirectionFilter = prefs[WidgetKeys.DIRECTION_FILTER] + val newDirectionFilter = when (Direction.valueOf(actualDirectionFilter ?: Direction.ForwardAndBackward.name)) { + Direction.Forward -> Direction.Backward + Direction.Backward -> Direction.ForwardAndBackward + Direction.ForwardAndBackward -> Direction.Forward + } + prefs[WidgetKeys.DIRECTION_FILTER] = newDirectionFilter.name + } + MyAppWidget().updateAll(context) } } \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt index 83e4e86..6091e0a 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt @@ -11,6 +11,7 @@ object WidgetKeys { val TRIP_INDEX = intPreferencesKey("trip_index") val LINE_ID = intPreferencesKey("line_id") val TRIPS_IN_DAY_COUNT = intPreferencesKey("trips_in_day_count") + val CURRENT_TRIP_ID = intPreferencesKey("current_trip_id") // Save booleans val IS_LOADING = booleanPreferencesKey("is_loading") @@ -22,9 +23,4 @@ object WidgetKeys { // Save strings (Useful for Enums, IDs, or serialized JSON) val LINE_TYPE = stringPreferencesKey("line_type") // e.g., "Urban", "Suburban" val DIRECTION_FILTER = stringPreferencesKey("direction_filter") // e.g., "Forward", "Backward" - - // Complex objects like UiTrip cannot be saved directly in simple Preferences. - // You either have to serialize the UiTrip to a JSON string, or just save the tripId - // and let the Widget fetch it from the database every time it updates. - val CURRENT_TRIP_ID = stringPreferencesKey("current_trip_id") } \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt index 8e3f9d7..8bad709 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp import androidx.glance.Button +import androidx.glance.ColorFilter import androidx.glance.GlanceModifier import androidx.glance.GlanceTheme import androidx.glance.Image @@ -59,6 +60,7 @@ fun TripViewGlance( onPrevAction: Action, onNextAction: Action, onLineClickAction: Action, + //onDirectionClickAction: Action, stopIdToHighlight: Int?, stopTypeToHighlight: StopLineType?, modifier: GlanceModifier = GlanceModifier @@ -77,6 +79,7 @@ fun TripViewGlance( TripViewTopRowGlance( trip = trip, onLineClickAction = onLineClickAction, + //onDirectionClickAction = onDirectionClickAction, modifier = GlanceModifier.padding( start = 12.dp, top = 4.dp, @@ -154,10 +157,37 @@ fun TripViewGlance( } } +@Composable +fun DirectionIconGlance( + direction: Direction, + context: Context, + modifier: GlanceModifier = GlanceModifier +) { + Image( + provider = ImageProvider( + when (direction) { + Direction.Forward -> R.drawable.turn_sharp_right + Direction.Backward -> R.drawable.u_turn_left + Direction.ForwardAndBackward -> R.drawable.swap_calls + } + ), + contentDescription = context.getString( + when (direction) { + Direction.Forward -> R.string.forward + Direction.Backward -> R.string.backward + Direction.ForwardAndBackward -> R.string.forward_and_backward + } + ), + colorFilter = ColorFilter.tint(GlanceTheme.colors.primary), + modifier = modifier + ) +} + @Composable private fun TripViewTopRowGlance( trip: UiTrip, onLineClickAction: Action, + //onDirectionClickAction: Action, modifier: GlanceModifier = GlanceModifier ) { val context = LocalContext.current @@ -169,6 +199,7 @@ private fun TripViewTopRowGlance( if (trip.line != null) { // Replaced Surface with Box + background val shortNameBackground = trip.line.color.toLineColor() + val textColor = textColorOnBackground(shortNameBackground) Box( modifier = GlanceModifier .background(shortNameBackground) // Use a theme color instead of dynamic custom color for simplicity in Glance @@ -179,7 +210,7 @@ private fun TripViewTopRowGlance( text = trip.line.shortName, maxLines = 1, style = TextStyle( - color = ColorProvider(textColorOnBackground(shortNameBackground)), + color = ColorProvider(textColor), fontWeight = FontWeight.Bold ) ) @@ -207,11 +238,11 @@ private fun TripViewTopRowGlance( ?: context.getString(R.string.no_date_time_information) } else { if (trip.delay < 0) - context.getString(R.string.early, formatDurationMinutes(context,-trip.delay)) + context.getString(R.string.early, formatDurationMinutes(context, -trip.delay)) else if (trip.delay == 0) context.getString(R.string.on_time) else - context.getString(R.string.late, formatDurationMinutes(context,trip.delay)) + context.getString(R.string.late, formatDurationMinutes(context, trip.delay)) } Text( text = dateOrDelayText, @@ -219,6 +250,10 @@ private fun TripViewTopRowGlance( style = TextStyle(color = GlanceTheme.colors.onSurfaceVariant) ) } + + Column { + DirectionIconGlance(trip.direction, context) + } } } @@ -404,7 +439,8 @@ private fun TripViewPreview() { onReloadAction = actionStartActivity(), onPrevAction = actionStartActivity(), onNextAction = actionStartActivity(), - onLineClickAction = actionStartActivity() + onLineClickAction = actionStartActivity(), + //onDirectionClickAction = actionStartActivity(), ) } } @@ -424,7 +460,8 @@ private fun TripViewPreviewLoading() { onReloadAction = actionStartActivity(), onPrevAction = actionStartActivity(), onNextAction = actionStartActivity(), - onLineClickAction = actionStartActivity() + onLineClickAction = actionStartActivity(), + //onDirectionClickAction = actionStartActivity() ) } } \ No newline at end of file diff --git a/app/src/main/res/drawable/swap_calls.xml b/app/src/main/res/drawable/swap_calls.xml new file mode 100644 index 0000000..df12c26 --- /dev/null +++ b/app/src/main/res/drawable/swap_calls.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/turn_sharp_right.xml b/app/src/main/res/drawable/turn_sharp_right.xml new file mode 100644 index 0000000..3597a99 --- /dev/null +++ b/app/src/main/res/drawable/turn_sharp_right.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/u_turn_left.xml b/app/src/main/res/drawable/u_turn_left.xml new file mode 100644 index 0000000..bc3bbe7 --- /dev/null +++ b/app/src/main/res/drawable/u_turn_left.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/xml/my_app_widget_info.xml b/app/src/main/res/xml/my_app_widget_info.xml index a3da815..6de72f9 100644 --- a/app/src/main/res/xml/my_app_widget_info.xml +++ b/app/src/main/res/xml/my_app_widget_info.xml @@ -4,7 +4,7 @@ android:resizeMode="vertical|horizontal" android:minWidth="250dp" android:minHeight="150dp" - android:updatePeriodMillis="60000" + android:updatePeriodMillis="1" android:widgetFeatures="reconfigurable" android:configure="org.stypox.tridenta.widget.WidgetConfigurationActivity"> \ No newline at end of file From c594d9c5e00d4d4ddc73cbe474957c34f6b69c4e Mon Sep 17 00:00:00 2001 From: whyKVD Date: Fri, 6 Mar 2026 14:35:48 +0100 Subject: [PATCH 08/43] fix: Fixed a problem gived byt the android studio linter --- .../main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt index 8bad709..6607053 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt @@ -15,6 +15,7 @@ import androidx.glance.action.actionStartActivity import androidx.glance.action.clickable import androidx.glance.appwidget.CircularProgressIndicator import androidx.glance.background +import androidx.glance.color.ColorProvider import androidx.glance.layout.Alignment import androidx.glance.layout.Box import androidx.glance.layout.Column @@ -28,7 +29,6 @@ import androidx.glance.text.FontWeight import androidx.glance.text.Text import androidx.glance.text.TextAlign import androidx.glance.text.TextStyle -import androidx.glance.unit.ColorProvider import org.stypox.tridenta.R import org.stypox.tridenta.db.data.DbLine import org.stypox.tridenta.db.data.DbStop @@ -210,7 +210,7 @@ private fun TripViewTopRowGlance( text = trip.line.shortName, maxLines = 1, style = TextStyle( - color = ColorProvider(textColor), + color = ColorProvider(day = textColor, night = textColor), fontWeight = FontWeight.Bold ) ) From caae9ebe4b961843b3534046ee6b0ece18c8aa41 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Fri, 6 Mar 2026 16:22:21 +0100 Subject: [PATCH 09/43] feat: Now the widget can perform next and prev trip calls, still not flawlessly --- .../org/stypox/tridenta/widget/MyAppWidget.kt | 31 +++++---- .../widget/WidgetConfigurationActivity.kt | 3 +- .../tridenta/widget/actions/WidgetActions.kt | 64 ++++++++++++------- 3 files changed, 62 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt index 73f1af9..7fc8036 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt @@ -79,21 +79,18 @@ class MyAppWidget : GlanceAppWidget() { val isInitalDataLoaded = prefs[WidgetKeys.IS_INITIAL_DATA_LOADED] ?: false logInfo("lineId: ${lineId.toString()}") val lineTypeString = prefs[WidgetKeys.LINE_TYPE] + val tripIndex = prefs[WidgetKeys.TRIP_INDEX] logInfo("lineType: ${lineTypeString ?: "null"}") LaunchedEffect(lineId, lineTypeString) { + if (lineId == null || lineTypeString == null) { + return@LaunchedEffect + } try { val hiltEntryPoint = EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) val tripsRepository = hiltEntryPoint.lineTripsRepository() - if (lineId != null && lineTypeString != null) { - val fetchedTrip = withContext(Dispatchers.IO) { - tripsRepository.getUiTrip( - lineId, - StopLineType.valueOf(lineTypeString), - ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID), - Direction.valueOf(directionFilter) - ).third - } + + if (tripIndex == null) { withContext(Dispatchers.IO) { updateAppWidgetState(context, id) { prefs -> val fetchedTrip = tripsRepository.getUiTrip( @@ -102,11 +99,23 @@ class MyAppWidget : GlanceAppWidget() { ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID), Direction.valueOf(directionFilter) ) - prefs[WidgetKeys.CURRENT_TRIP_ID] = fetchedTrip.second + prefs[WidgetKeys.TRIP_INDEX] = fetchedTrip.second prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] = fetchedTrip.first + trip = fetchedTrip.third } } - trip = fetchedTrip + return@LaunchedEffect + } + withContext(Dispatchers.IO) { + updateAppWidgetState(context, id) { prefs -> + val (fetchedTrip, _) = tripsRepository.getUiTrip( + lineId, + StopLineType.valueOf(lineTypeString), + ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID), + tripIndex + ) + trip = fetchedTrip + } } } catch (e: Exception) { logError(e.message!!, e.cause) diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt index 0da81e7..297995f 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt @@ -34,7 +34,7 @@ import org.stypox.tridenta.ui.theme.TitleText import org.stypox.tridenta.widget.actions.WidgetKeys @AndroidEntryPoint -class WidgetConfigurationActivity: +class WidgetConfigurationActivity : ComponentActivity() { private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID @@ -93,6 +93,7 @@ class WidgetConfigurationActivity: // 3. Save the selected data to this specific widget's Preferences updateAppWidgetState(context, glanceId) { prefs -> + prefs.clear() prefs[WidgetKeys.LINE_ID] = line.lineId prefs[WidgetKeys.LINE_TYPE] = line.type.name prefs[WidgetKeys.IS_INITIAL_DATA_LOADED] = true diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt index e689efc..0f3d37d 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt @@ -10,13 +10,19 @@ import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.withContext import org.stypox.tridenta.db.LineDao -import org.stypox.tridenta.db.StopDao import org.stypox.tridenta.enums.Direction +import org.stypox.tridenta.enums.StopLineType +import org.stypox.tridenta.extractor.ROME_ZONE_ID +import org.stypox.tridenta.log.logError import org.stypox.tridenta.log.logInfo import org.stypox.tridenta.repo.LineTripsRepository -import org.stypox.tridenta.repo.LinesRepository +import org.stypox.tridenta.repo.data.UiTrip import org.stypox.tridenta.widget.MyAppWidget +import java.time.ZonedDateTime // 1. Create a Hilt Entry point to access your Repositories inside Glance Actions @EntryPoint @@ -34,21 +40,17 @@ class NextTripAction : ActionCallback { glanceId: GlanceId, parameters: ActionParameters ) { - val hiltEntryPoint = EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) - val tripsRepo = hiltEntryPoint.lineTripsRepository() - - // 1. Read current state (tripIndex) from Glance Preferences updateAppWidgetState(context, glanceId) { prefs -> val currentIndex = prefs[WidgetKeys.TRIP_INDEX] ?: 0 - // TODO: Actually retrive the next index for the given trip + val tripsInDayCount = prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] ?: 0 val nextIndex = currentIndex + 1 + if (nextIndex !in 0.. PrevTripAction class PrevTripAction : ActionCallback { - override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) { - val hiltEntryPoint = EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) - val tripsRepo = hiltEntryPoint.lineTripsRepository() - - /*updateAppWidgetState(context, glanceId) { prefs -> + override suspend fun onAction( + context: Context, + glanceId: GlanceId, + parameters: ActionParameters + ) { + updateAppWidgetState(context, glanceId) { prefs -> val currentIndex = prefs[WidgetKeys.TRIP_INDEX] ?: 0 - if (currentIndex > 0) { - val prevIndex = currentIndex - 1 - // Fetch new trip and save to prefs... - prefs[WidgetKeys.TRIP_INDEX] = prevIndex + val tripsInDayCount = prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] ?: 0 + val nextIndex = currentIndex - 1 + if (nextIndex !in 0.. ReloadTripAction class ReloadTripAction : ActionCallback { - override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) { + override suspend fun onAction( + context: Context, + glanceId: GlanceId, + parameters: ActionParameters + ) { MyAppWidget().updateAll(context) } } // 5. Refactor: onDirectionClicked() -> ToggleDirectionAction class ToggleDirectionAction : ActionCallback { - override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) { + override suspend fun onAction( + context: Context, + glanceId: GlanceId, + parameters: ActionParameters + ) { // Read current direction from prefs, toggle it, fetch new trip, update prefs - updateAppWidgetState(context,glanceId) { prefs -> + updateAppWidgetState(context, glanceId) { prefs -> val actualDirectionFilter = prefs[WidgetKeys.DIRECTION_FILTER] - val newDirectionFilter = when (Direction.valueOf(actualDirectionFilter ?: Direction.ForwardAndBackward.name)) { + val newDirectionFilter = when (Direction.valueOf( + actualDirectionFilter ?: Direction.ForwardAndBackward.name + )) { Direction.Forward -> Direction.Backward Direction.Backward -> Direction.ForwardAndBackward Direction.ForwardAndBackward -> Direction.Forward From 0423ac96464d4ab151cd81462f6ea180612ee4ec Mon Sep 17 00:00:00 2001 From: whyKVD Date: Sat, 7 Mar 2026 12:30:50 +0100 Subject: [PATCH 10/43] feat: All the action, next, prev and refresh are completed almost simultaneously --- app/src/main/AndroidManifest.xml | 3 + .../org/stypox/tridenta/widget/MyAppWidget.kt | 109 +++++++++++++----- .../widget/WidgetConfigurationActivity.kt | 1 - .../tridenta/widget/actions/WidgetActions.kt | 14 +-- .../tridenta/widget/actions/WidgetKeys.kt | 4 +- 5 files changed, 92 insertions(+), 39 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ce00e4c..bf402bc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,6 +46,9 @@ + diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt index 7fc8036..08e62a6 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.datastore.preferences.core.Preferences import androidx.glance.GlanceId @@ -68,53 +69,85 @@ class MyAppWidget : GlanceAppWidget() { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK } var trip by remember { mutableStateOf(null) } - var isError by remember { mutableStateOf(false) } - var isLoading by remember { mutableStateOf(prefs[WidgetKeys.IS_LOADING] ?: true) } + var isError = false + var isLoading by remember { mutableStateOf(true) } var directionFilter by remember { mutableStateOf( - prefs[WidgetKeys.DIRECTION_FILTER] ?: Direction.ForwardAndBackward.name + Direction.ForwardAndBackward.name ) } + val storedDirectionFilter = prefs[WidgetKeys.DIRECTION_FILTER] + if (storedDirectionFilter != null) directionFilter = storedDirectionFilter val lineId = prefs[WidgetKeys.LINE_ID] - val isInitalDataLoaded = prefs[WidgetKeys.IS_INITIAL_DATA_LOADED] ?: false - logInfo("lineId: ${lineId.toString()}") val lineTypeString = prefs[WidgetKeys.LINE_TYPE] val tripIndex = prefs[WidgetKeys.TRIP_INDEX] - logInfo("lineType: ${lineTypeString ?: "null"}") - LaunchedEffect(lineId, lineTypeString) { + val prevTripIndex = prefs[WidgetKeys.PREV_TRIP_INDEX] + val refreshTimestamp = prefs[WidgetKeys.REFRESH_TIMESTAMP] ?: 0L + val hiltEntryPoint = + EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) + val tripsRepository = hiltEntryPoint.lineTripsRepository() + LaunchedEffect(lineId, lineTypeString, directionFilter, tripIndex) { if (lineId == null || lineTypeString == null) { return@LaunchedEffect } try { - val hiltEntryPoint = - EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) - val tripsRepository = hiltEntryPoint.lineTripsRepository() - if (tripIndex == null) { - withContext(Dispatchers.IO) { - updateAppWidgetState(context, id) { prefs -> - val fetchedTrip = tripsRepository.getUiTrip( - lineId, - StopLineType.valueOf(lineTypeString), - ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID), - Direction.valueOf(directionFilter) - ) - prefs[WidgetKeys.TRIP_INDEX] = fetchedTrip.second - prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] = fetchedTrip.first - trip = fetchedTrip.third - } + val fetchedTrip = withContext(Dispatchers.IO) { + tripsRepository.getUiTrip( + lineId, + StopLineType.valueOf(lineTypeString), + ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID), + Direction.valueOf(directionFilter) + ) + } + trip = fetchedTrip.third + updateAppWidgetState(context, id) { prefs -> + prefs[WidgetKeys.TRIP_INDEX] = fetchedTrip.second + prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] = fetchedTrip.first + prefs[WidgetKeys.IS_INITIAL_DATA_LOADED] = true } return@LaunchedEffect } - withContext(Dispatchers.IO) { - updateAppWidgetState(context, id) { prefs -> - val (fetchedTrip, _) = tripsRepository.getUiTrip( + if (Direction.valueOf(directionFilter) == Direction.ForwardAndBackward) { + val (fetchedTrip, network) = withContext(Dispatchers.IO) { + tripsRepository.getUiTrip( lineId, StopLineType.valueOf(lineTypeString), ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID), tripIndex ) - trip = fetchedTrip + } + trip = fetchedTrip + if (!network) { + updateAppWidgetState(context,id){ + prefs -> prefs[WidgetKeys.REFRESH_TIMESTAMP] = System.currentTimeMillis() + } + } + } else { + if (prevTripIndex == null) { + throw Error("PrevTripIndex cannot be null") + } + val data = withContext(Dispatchers.IO) { + tripsRepository.getUiTripWithDirection( + lineId, + StopLineType.valueOf(lineTypeString), + ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID), + Direction.valueOf(directionFilter), + tripIndex, + prevTripIndex + ) + } + if (data == null) { + throw Error("data cannot be null") + } + trip = data.first + if (!data.third) { + updateAppWidgetState(context,id){ + prefs -> prefs[WidgetKeys.REFRESH_TIMESTAMP] = System.currentTimeMillis() + } + } + updateAppWidgetState(context, id) { prefs -> + prefs[WidgetKeys.TRIP_INDEX] = data.second } } } catch (e: Exception) { @@ -124,9 +157,29 @@ class MyAppWidget : GlanceAppWidget() { isLoading = false } } + LaunchedEffect(refreshTimestamp) { + if (trip == null || tripIndex == null) return@LaunchedEffect + isLoading = true + try { + val freshTrip = withContext(Dispatchers.IO) { + tripsRepository.reloadUiTrip( + trip!!, + tripIndex, + ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID) + ) + } + + trip = freshTrip + } catch (e: Exception) { + logError(e.message!!, e.cause) + } finally { + isLoading = false + } + } + logInfo("Loading: $isLoading") GlanceTheme { TripViewGlance( - trip, error = isError, loading = isLoading && isInitalDataLoaded, + trip, error = isError, loading = isLoading, /*&& isInitalDataLoaded*/ onReloadAction = actionRunCallback(), onPrevAction = actionRunCallback(), onNextAction = actionRunCallback(), diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt index 297995f..6b6ef94 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt @@ -96,7 +96,6 @@ class WidgetConfigurationActivity : prefs.clear() prefs[WidgetKeys.LINE_ID] = line.lineId prefs[WidgetKeys.LINE_TYPE] = line.type.name - prefs[WidgetKeys.IS_INITIAL_DATA_LOADED] = true prefs[WidgetKeys.DIRECTION_FILTER] = Direction.ForwardAndBackward.name prefs[WidgetKeys.IS_LOADING] = true } diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt index 0f3d37d..2e2bb23 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt @@ -8,21 +8,12 @@ import androidx.glance.appwidget.state.updateAppWidgetState import androidx.glance.appwidget.updateAll import dagger.hilt.EntryPoint import dagger.hilt.InstallIn -import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.withContext import org.stypox.tridenta.db.LineDao import org.stypox.tridenta.enums.Direction -import org.stypox.tridenta.enums.StopLineType -import org.stypox.tridenta.extractor.ROME_ZONE_ID -import org.stypox.tridenta.log.logError import org.stypox.tridenta.log.logInfo import org.stypox.tridenta.repo.LineTripsRepository -import org.stypox.tridenta.repo.data.UiTrip import org.stypox.tridenta.widget.MyAppWidget -import java.time.ZonedDateTime // 1. Create a Hilt Entry point to access your Repositories inside Glance Actions @EntryPoint @@ -48,6 +39,7 @@ class NextTripAction : ActionCallback { return@updateAppWidgetState } + prefs[WidgetKeys.PREV_TRIP_INDEX] = currentIndex prefs[WidgetKeys.TRIP_INDEX] = nextIndex } @@ -71,6 +63,7 @@ class PrevTripAction : ActionCallback { return@updateAppWidgetState } + prefs[WidgetKeys.PREV_TRIP_INDEX] = currentIndex prefs[WidgetKeys.TRIP_INDEX] = nextIndex } @@ -86,6 +79,9 @@ class ReloadTripAction : ActionCallback { glanceId: GlanceId, parameters: ActionParameters ) { + updateAppWidgetState(context, glanceId) { prefs -> + prefs[WidgetKeys.REFRESH_TIMESTAMP] = System.currentTimeMillis() + } MyAppWidget().updateAll(context) } } diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt index 6091e0a..7611c8c 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt @@ -4,14 +4,16 @@ import android.content.Context import androidx.datastore.preferences.core.MutablePreferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.glance.GlanceId object WidgetKeys { + val REFRESH_TIMESTAMP = longPreferencesKey("refresh_timestamp") val TRIP_INDEX = intPreferencesKey("trip_index") val LINE_ID = intPreferencesKey("line_id") val TRIPS_IN_DAY_COUNT = intPreferencesKey("trips_in_day_count") - val CURRENT_TRIP_ID = intPreferencesKey("current_trip_id") + val PREV_TRIP_INDEX = intPreferencesKey("prev_trip_index") // Save booleans val IS_LOADING = booleanPreferencesKey("is_loading") From 5a5af718c12627fcbef3550382fae68744b496de Mon Sep 17 00:00:00 2001 From: whyKVD Date: Sat, 7 Mar 2026 12:32:37 +0100 Subject: [PATCH 11/43] fix: Deleted a receiver that does not exist anymore --- app/src/main/AndroidManifest.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bf402bc..ce00e4c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,9 +46,6 @@ - From 9302de55dc7efc49393fdc19c56843d5f153d341 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Sat, 7 Mar 2026 15:01:27 +0100 Subject: [PATCH 12/43] fix: Fixed the refresh call after the loading of a trip from the cache --- .../org/stypox/tridenta/widget/MyAppWidget.kt | 41 +++++++++++++++---- .../tridenta/widget/MyAppWidgetReceiver.kt | 5 --- .../tridenta/widget/WidgetConfigViewModel.kt | 38 ----------------- .../tridenta/widget/actions/WidgetKeys.kt | 3 -- .../tridenta/widget/ui/TripViewGlance.kt | 2 +- 5 files changed, 35 insertions(+), 54 deletions(-) delete mode 100644 app/src/main/java/org/stypox/tridenta/widget/WidgetConfigViewModel.kt diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt index 08e62a6..a28843b 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt @@ -8,7 +8,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.datastore.preferences.core.Preferences import androidx.glance.GlanceId @@ -119,8 +118,21 @@ class MyAppWidget : GlanceAppWidget() { } trip = fetchedTrip if (!network) { - updateAppWidgetState(context,id){ - prefs -> prefs[WidgetKeys.REFRESH_TIMESTAMP] = System.currentTimeMillis() + isLoading = true + try { + val freshTrip = withContext(Dispatchers.IO) { + tripsRepository.reloadUiTrip( + trip!!, + tripIndex, + ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID) + ) + } + + trip = freshTrip + } catch (e: Exception) { + logError(e.message!!, e.cause) + } finally { + isLoading = false } } } else { @@ -142,8 +154,21 @@ class MyAppWidget : GlanceAppWidget() { } trip = data.first if (!data.third) { - updateAppWidgetState(context,id){ - prefs -> prefs[WidgetKeys.REFRESH_TIMESTAMP] = System.currentTimeMillis() + isLoading = true + try { + val freshTrip = withContext(Dispatchers.IO) { + tripsRepository.reloadUiTrip( + trip!!, + tripIndex, + ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID) + ) + } + + trip = freshTrip + } catch (e: Exception) { + logError(e.message!!, e.cause) + } finally { + isLoading = false } } updateAppWidgetState(context, id) { prefs -> @@ -158,8 +183,9 @@ class MyAppWidget : GlanceAppWidget() { } } LaunchedEffect(refreshTimestamp) { - if (trip == null || tripIndex == null) return@LaunchedEffect + if (refreshTimestamp == 0L || trip == null || tripIndex == null) return@LaunchedEffect isLoading = true + logInfo("Performed refresh") try { val freshTrip = withContext(Dispatchers.IO) { tripsRepository.reloadUiTrip( @@ -176,7 +202,8 @@ class MyAppWidget : GlanceAppWidget() { isLoading = false } } - logInfo("Loading: $isLoading") + logInfo("tripIndex: $tripIndex") + logInfo("prevTripIndex: $prevTripIndex") GlanceTheme { TripViewGlance( trip, error = isError, loading = isLoading, /*&& isInitalDataLoaded*/ diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidgetReceiver.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidgetReceiver.kt index ac9fbec..5624058 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidgetReceiver.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidgetReceiver.kt @@ -3,11 +3,6 @@ package org.stypox.tridenta.widget import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidgetReceiver import dagger.hilt.android.AndroidEntryPoint -import org.stypox.tridenta.db.LineDao -import org.stypox.tridenta.db.StopDao -import org.stypox.tridenta.repo.LineTripsRepository -import org.stypox.tridenta.repo.LinesRepository -import javax.inject.Inject @AndroidEntryPoint class MyAppWidgetReceiver : GlanceAppWidgetReceiver() { diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigViewModel.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigViewModel.kt deleted file mode 100644 index 11c39d3..0000000 --- a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigViewModel.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.stypox.tridenta.widget - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.stypox.tridenta.db.LineDao -import org.stypox.tridenta.db.data.DbLine -import javax.inject.Inject -import kotlin.collections.emptyList - -@HiltViewModel -class WidgetConfigViewModel @Inject constructor( - private val lineDao: LineDao -) : ViewModel() { - // 1. Create a Mutable internal state - private val _availableLines = MutableStateFlow>(emptyList()) - // 2. Expose it as a read-only StateFlow for the UI - val availableLines = _availableLines.asStateFlow() - - init { - // 3. Fetch the data once when the ViewModel is created - viewModelScope.launch { - // Because your Dao is a suspend function, we can call it here - withContext(Dispatchers.IO) { - val lines = lineDao.getAllLines() - _availableLines.value = lines - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt index 7611c8c..893502e 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt @@ -1,12 +1,9 @@ package org.stypox.tridenta.widget.actions -import android.content.Context -import androidx.datastore.preferences.core.MutablePreferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.glance.GlanceId object WidgetKeys { val REFRESH_TIMESTAMP = longPreferencesKey("refresh_timestamp") diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt index 6607053..d28ba93 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt @@ -446,7 +446,7 @@ private fun TripViewPreview() { } @OptIn(ExperimentalGlancePreviewApi::class) -@Preview() +@Preview @Composable private fun TripViewPreviewLoading() { val loading = true From c0cd75457827e4e7f6ace5cd429916e41341c316 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Sat, 7 Mar 2026 16:31:54 +0100 Subject: [PATCH 13/43] feat: Added the line indication and the possibility to change the direction filter --- .../org/stypox/tridenta/widget/MyAppWidget.kt | 48 ++++- .../tridenta/widget/actions/WidgetActions.kt | 13 +- .../SmallCircularProgressIndicatorGlance.kt | 20 ++ .../tridenta/widget/ui/LineTripGlance.kt | 192 ++++++++++++++++++ .../tridenta/widget/ui/TripViewGlance.kt | 17 +- .../tridenta/widget/ui/TripViewStopsGlance.kt | 5 +- app/src/main/res/drawable/favorite.xml | 2 +- app/src/main/res/drawable/favorite_filled.xml | 5 + app/src/main/res/drawable/warning.xml | 5 + 9 files changed, 293 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/org/stypox/tridenta/widget/theme/SmallCircularProgressIndicatorGlance.kt create mode 100644 app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt create mode 100644 app/src/main/res/drawable/favorite_filled.xml create mode 100644 app/src/main/res/drawable/warning.xml diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt index a28843b..9ad7b2e 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt @@ -35,14 +35,17 @@ import org.stypox.tridenta.enums.StopLineType import org.stypox.tridenta.extractor.ROME_ZONE_ID import org.stypox.tridenta.log.logError import org.stypox.tridenta.log.logInfo +import org.stypox.tridenta.repo.data.UiLine import org.stypox.tridenta.repo.data.UiStopTime import org.stypox.tridenta.repo.data.UiTrip import org.stypox.tridenta.ui.MainActivity import org.stypox.tridenta.widget.actions.NextTripAction import org.stypox.tridenta.widget.actions.PrevTripAction import org.stypox.tridenta.widget.actions.ReloadTripAction +import org.stypox.tridenta.widget.actions.ToggleDirectionAction import org.stypox.tridenta.widget.actions.WidgetEntryPoint import org.stypox.tridenta.widget.actions.WidgetKeys +import org.stypox.tridenta.widget.ui.LineTripsWidgetScreen import org.stypox.tridenta.widget.ui.TripViewGlance import java.time.OffsetDateTime import java.time.ZoneOffset @@ -67,6 +70,8 @@ class MyAppWidget : GlanceAppWidget() { // These flags ensure the activity opens properly from the launcher context flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK } + var isFavorite by remember { mutableStateOf(false) } + var line by remember { mutableStateOf(null) } var trip by remember { mutableStateOf(null) } var isError = false var isLoading by remember { mutableStateOf(true) } @@ -80,12 +85,28 @@ class MyAppWidget : GlanceAppWidget() { val lineId = prefs[WidgetKeys.LINE_ID] val lineTypeString = prefs[WidgetKeys.LINE_TYPE] val tripIndex = prefs[WidgetKeys.TRIP_INDEX] - val prevTripIndex = prefs[WidgetKeys.PREV_TRIP_INDEX] + var prevTripIndex = prefs[WidgetKeys.PREV_TRIP_INDEX] val refreshTimestamp = prefs[WidgetKeys.REFRESH_TIMESTAMP] ?: 0L val hiltEntryPoint = EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) val tripsRepository = hiltEntryPoint.lineTripsRepository() - LaunchedEffect(lineId, lineTypeString, directionFilter, tripIndex) { + val linesRepository = hiltEntryPoint.lineRepository() + LaunchedEffect(lineId, lineTypeString) { + if (lineId == null || lineTypeString == null) { + return@LaunchedEffect + } + try { + val fetchedLine = withContext(Dispatchers.IO) { + linesRepository.getUiLine(lineId, StopLineType.valueOf(lineTypeString)) + } + + line = fetchedLine + line?.let { isFavorite = it.isFavorite } + } catch (e: Exception) { + logError(e.message!!, e.cause) + } + } + LaunchedEffect(lineId, directionFilter, tripIndex) { if (lineId == null || lineTypeString == null) { return@LaunchedEffect } @@ -137,7 +158,7 @@ class MyAppWidget : GlanceAppWidget() { } } else { if (prevTripIndex == null) { - throw Error("PrevTripIndex cannot be null") + prevTripIndex = tripIndex } val data = withContext(Dispatchers.IO) { tripsRepository.getUiTripWithDirection( @@ -205,16 +226,31 @@ class MyAppWidget : GlanceAppWidget() { logInfo("tripIndex: $tripIndex") logInfo("prevTripIndex: $prevTripIndex") GlanceTheme { - TripViewGlance( - trip, error = isError, loading = isLoading, /*&& isInitalDataLoaded*/ + LineTripsWidgetScreen( + line = line, + trip = trip, error = isError, loading = isLoading, onReloadAction = actionRunCallback(), onPrevAction = actionRunCallback(), onNextAction = actionRunCallback(), onLineClickAction = actionStartActivity(configIntent), - //onDirectionClickAction = actionRunCallback(), + directionFilter = Direction.valueOf(directionFilter), + onDirectionClickAction = actionRunCallback(), stopIdToHighlight = null, stopTypeToHighlight = null, + prevEnabled = true, + nextEnabled = true, + isFavorite = isFavorite ) + /*TripViewGlance( + trip, error = isError, loading = isLoading, + onReloadAction = actionRunCallback(), + onPrevAction = actionRunCallback(), + onNextAction = actionRunCallback(), + onLineClickAction = actionStartActivity(configIntent), + //onDirectionClickAction = actionRunCallback(), + stopIdToHighlight = null, + stopTypeToHighlight = null, + )*/ } } } diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt index 2e2bb23..a4f0ac2 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt @@ -13,6 +13,7 @@ import org.stypox.tridenta.db.LineDao import org.stypox.tridenta.enums.Direction import org.stypox.tridenta.log.logInfo import org.stypox.tridenta.repo.LineTripsRepository +import org.stypox.tridenta.repo.LinesRepository import org.stypox.tridenta.widget.MyAppWidget // 1. Create a Hilt Entry point to access your Repositories inside Glance Actions @@ -20,7 +21,7 @@ import org.stypox.tridenta.widget.MyAppWidget @InstallIn(SingletonComponent::class) interface WidgetEntryPoint { fun lineTripsRepository(): LineTripsRepository - fun lineDao(): LineDao + fun lineRepository(): LinesRepository // Add HistoryDao and LinesRepository here too } @@ -107,4 +108,14 @@ class ToggleDirectionAction : ActionCallback { } MyAppWidget().updateAll(context) } +} + +class ToggleFavoriteAction: ActionCallback{ + override suspend fun onAction( + context: Context, + glanceId: GlanceId, + parameters: ActionParameters + ) { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/theme/SmallCircularProgressIndicatorGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/theme/SmallCircularProgressIndicatorGlance.kt new file mode 100644 index 0000000..efb7dbd --- /dev/null +++ b/app/src/main/java/org/stypox/tridenta/widget/theme/SmallCircularProgressIndicatorGlance.kt @@ -0,0 +1,20 @@ +package org.stypox.tridenta.widget.theme + +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.glance.GlanceModifier +import androidx.glance.GlanceTheme +import androidx.glance.appwidget.CircularProgressIndicator +import androidx.glance.layout.size +import androidx.glance.preview.ExperimentalGlancePreviewApi +import androidx.glance.preview.Preview + +@OptIn(ExperimentalGlancePreviewApi::class) +@Preview +@Composable +fun SmallCircularProgressIndicatorGlance(modifier: GlanceModifier = GlanceModifier) { + CircularProgressIndicator( + color = GlanceTheme.colors.onSurface, + modifier = modifier.size(16.dp), + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt new file mode 100644 index 0000000..f6f7877 --- /dev/null +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt @@ -0,0 +1,192 @@ +package org.stypox.tridenta.widget.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.glance.ColorFilter +import androidx.glance.GlanceModifier +import androidx.glance.GlanceTheme +import androidx.glance.Image +import androidx.glance.ImageProvider +import androidx.glance.LocalContext +import androidx.glance.action.Action +import androidx.glance.action.clickable +import androidx.glance.appwidget.CircularProgressIndicator +import androidx.glance.appwidget.action.actionRunCallback +import androidx.glance.background +import androidx.glance.color.ColorProvider +import androidx.glance.layout.Alignment +import androidx.glance.layout.Box +import androidx.glance.layout.Column +import androidx.glance.layout.Row +import androidx.glance.layout.Spacer +import androidx.glance.layout.fillMaxSize +import androidx.glance.layout.fillMaxWidth +import androidx.glance.layout.padding +import androidx.glance.layout.size +import androidx.glance.text.FontWeight +import androidx.glance.text.Text +import androidx.glance.text.TextStyle +import org.stypox.tridenta.R +import org.stypox.tridenta.enums.Direction +import org.stypox.tridenta.enums.StopLineType +import org.stypox.tridenta.repo.data.UiLine +import org.stypox.tridenta.repo.data.UiTrip +import org.stypox.tridenta.util.textColorOnBackground +import org.stypox.tridenta.util.toLineColor +import org.stypox.tridenta.widget.actions.ToggleDirectionAction +import org.stypox.tridenta.widget.theme.SmallCircularProgressIndicatorGlance + +/** + * The Glance equivalent of your LineTripsScreen. + * * Notice that we don't pass ViewModels, Navigators, or lambda functions. + * In Glance, user interactions MUST be passed as `Action` objects (like actionRunCallback). + */ +@Composable +fun LineTripsWidgetScreen( + line: UiLine?, + trip: UiTrip?, + error: Boolean, + loading: Boolean, + prevEnabled: Boolean, + nextEnabled: Boolean, + stopIdToHighlight: Int?, + stopTypeToHighlight: StopLineType?, + isFavorite: Boolean, + directionFilter: Direction, + // Actions replace standard lambdas in Glance + onReloadAction: Action, + onPrevAction: Action, + onNextAction: Action, + onLineClickAction: Action, + onDirectionClickAction: Action +) { + // Glance's Scaffold is very basic. It gives you a background and layout structure. + Column( + modifier = GlanceModifier.fillMaxSize().background(GlanceTheme.colors.background) + ) { + WidgetAppBar( + line = line, + isFavorite = isFavorite, + directionFilter = directionFilter, + onDirectionClickAction + ) + TripViewGlance( + trip = trip, + error = error, + loading = loading, + onReloadAction = onReloadAction, + onPrevAction = onPrevAction, + onNextAction = onNextAction, + onLineClickAction = onLineClickAction, + stopIdToHighlight = stopIdToHighlight, + stopTypeToHighlight = stopTypeToHighlight + ) + } +} + +/** + * Glance equivalent of LineAppBar. + */ +@Composable +fun WidgetAppBar( + line: UiLine?, + isFavorite: Boolean, + directionFilter: Direction, + onDirectionAction: Action +) { + val context = LocalContext.current + // The main container Row + Row( + modifier = GlanceModifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + + // 1. The Title (Left Side) + // defaultWeight() makes the text take up all leftover space, + // pushing the icons to the far right. + + if (line == null) { + SmallCircularProgressIndicatorGlance() + } else { + val shortNameBackground = line.color.toLineColor() + val textColor = textColorOnBackground(shortNameBackground) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = context.getString(R.string.trips_for_line), + style = TextStyle( + color = GlanceTheme.colors.onSurface, + //fontWeight = FontWeight.Bold, + //fontSize = 16.sp + ), + modifier = GlanceModifier.defaultWeight() + ) + Spacer(GlanceModifier.size(8.dp)) + Box( + modifier = GlanceModifier + .background(shortNameBackground) // Use a theme color instead of dynamic custom color for simplicity in Glance + .padding(8.dp) + //.clickable(onLineClickAction) + ) { + Text( + text = line.shortName, + maxLines = 1, + style = TextStyle( + color = ColorProvider(day = textColor, night = textColor), + fontWeight = FontWeight.Bold + ) + ) + } + } + } + Spacer(GlanceModifier.defaultWeight()) + + // 2. The Action Icons (Right Side) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + // News/Warning Icon + if (line != null && line.newsItems.isNotEmpty()) { + Image( + provider = ImageProvider(R.drawable.warning), // Ensure you have this XML drawable + contentDescription = "News", + colorFilter = ColorFilter.tint(GlanceTheme.colors.primary), + modifier = GlanceModifier + .padding(end = 8.dp) + .clickable(onDirectionAction) + ) + } + + // Direction Toggle Icon + Image( + provider = ImageProvider(getDirectionDrawable(directionFilter)), + contentDescription = "Toggle Direction", + colorFilter = ColorFilter.tint(GlanceTheme.colors.primary), + modifier = GlanceModifier + .padding(end = 8.dp) + .clickable(actionRunCallback()) + ) + + // Favorite Toggle Icon + Image( + provider = ImageProvider( + if (isFavorite) R.drawable.favorite_filled else R.drawable.favorite + ), + colorFilter = ColorFilter.tint(GlanceTheme.colors.primary), + contentDescription = context.getString(R.string.favorite), + ) + } + } +} + +// Helper to resolve drawables for Glance +private fun getDirectionDrawable(direction: Direction): Int { + return when (direction) { + Direction.Forward -> R.drawable.turn_sharp_right // Replace with actual drawable IDs + Direction.Backward -> R.drawable.u_turn_left + Direction.ForwardAndBackward -> R.drawable.swap_calls + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt index d28ba93..2b95e06 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt @@ -19,10 +19,13 @@ import androidx.glance.color.ColorProvider import androidx.glance.layout.Alignment import androidx.glance.layout.Box import androidx.glance.layout.Column +import androidx.glance.layout.ContentScale import androidx.glance.layout.Row import androidx.glance.layout.fillMaxSize import androidx.glance.layout.fillMaxWidth +import androidx.glance.layout.height import androidx.glance.layout.padding +import androidx.glance.layout.size import androidx.glance.preview.ExperimentalGlancePreviewApi import androidx.glance.preview.Preview import androidx.glance.text.FontWeight @@ -42,6 +45,7 @@ import org.stypox.tridenta.ui.MainActivity import org.stypox.tridenta.util.formatDateFull import org.stypox.tridenta.util.textColorOnBackground import org.stypox.tridenta.util.toLineColor +import org.stypox.tridenta.widget.theme.SmallCircularProgressIndicatorGlance import java.time.OffsetDateTime import java.time.ZoneOffset @@ -68,7 +72,7 @@ fun TripViewGlance( val context = LocalContext.current Box( - modifier = modifier.fillMaxSize().background(GlanceTheme.colors.background), + modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { if (trip != null) { @@ -272,22 +276,27 @@ private fun TripViewBottomRowGlance( verticalAlignment = Alignment.CenterVertically, modifier = modifier .fillMaxWidth() - .background(GlanceTheme.colors.surfaceVariant) .padding(16.dp) ) { Image( provider = ImageProvider(R.drawable.arrow_left), contentDescription = context.getString(R.string.previous), modifier = GlanceModifier.clickable(onPrevAction).defaultWeight() + .height(32.dp) + .background(GlanceTheme.colors.primary) ) if (loading) { - CircularProgressIndicator(modifier = GlanceModifier.defaultWeight()) + SmallCircularProgressIndicatorGlance(modifier = GlanceModifier.defaultWeight() + .height(32.dp) + .background(GlanceTheme.colors.primary)) } else { Image( provider = ImageProvider(R.drawable.refresh), contentDescription = context.getString(R.string.reload), modifier = GlanceModifier.clickable(onReloadAction).defaultWeight() + .height(32.dp) + .background(GlanceTheme.colors.primary) ) } @@ -295,6 +304,8 @@ private fun TripViewBottomRowGlance( provider = ImageProvider(R.drawable.arrow_right), contentDescription = context.getString(R.string.next), modifier = GlanceModifier.clickable(onNextAction).defaultWeight() + .height(32.dp) + .background(GlanceTheme.colors.primary) ) } } diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt index 9d7e3ba..075f52f 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt @@ -22,7 +22,6 @@ import androidx.glance.text.FontWeight import androidx.glance.text.Text import androidx.glance.text.TextDecoration import androidx.glance.text.TextStyle -import androidx.glance.unit.ColorProvider import org.stypox.tridenta.R import org.stypox.tridenta.db.data.DbLine import org.stypox.tridenta.db.data.DbStop @@ -93,7 +92,7 @@ fun TripViewStopsGlance( item { // space for FABs - Spacer(modifier = GlanceModifier.size(height = 84.dp, width = 0.dp)) + Spacer(modifier = GlanceModifier.size(height = 64.dp, width = 0.dp)) } } } @@ -130,7 +129,7 @@ private fun TripViewStopItemGlance( if (stopTime.stop?.isFavorite == true) { Image( - provider = ImageProvider(R.drawable.favorite), + provider = ImageProvider(R.drawable.favorite_filled), contentDescription = context.getString(R.string.favorite), colorFilter = ColorFilter.tint(GlanceTheme.colors.primary), modifier = GlanceModifier.padding(end = 3.dp) diff --git a/app/src/main/res/drawable/favorite.xml b/app/src/main/res/drawable/favorite.xml index 2c2751e..fdc35fb 100644 --- a/app/src/main/res/drawable/favorite.xml +++ b/app/src/main/res/drawable/favorite.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/drawable/favorite_filled.xml b/app/src/main/res/drawable/favorite_filled.xml new file mode 100644 index 0000000..2c2751e --- /dev/null +++ b/app/src/main/res/drawable/favorite_filled.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/warning.xml b/app/src/main/res/drawable/warning.xml new file mode 100644 index 0000000..61b86d4 --- /dev/null +++ b/app/src/main/res/drawable/warning.xml @@ -0,0 +1,5 @@ + + + + + From dc0b68a645eed08307f95cdf191382d826dbaf46 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Sat, 7 Mar 2026 18:28:05 +0100 Subject: [PATCH 14/43] style: Created some seamless floatingbutton --- .../tridenta/widget/actions/WidgetActions.kt | 11 --- .../tridenta/widget/ui/TripViewGlance.kt | 76 ++++++++++++------- 2 files changed, 50 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt index a4f0ac2..4f4685c 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt @@ -9,7 +9,6 @@ import androidx.glance.appwidget.updateAll import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import org.stypox.tridenta.db.LineDao import org.stypox.tridenta.enums.Direction import org.stypox.tridenta.log.logInfo import org.stypox.tridenta.repo.LineTripsRepository @@ -108,14 +107,4 @@ class ToggleDirectionAction : ActionCallback { } MyAppWidget().updateAll(context) } -} - -class ToggleFavoriteAction: ActionCallback{ - override suspend fun onAction( - context: Context, - glanceId: GlanceId, - parameters: ActionParameters - ) { - TODO("Not yet implemented") - } } \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt index 2b95e06..33531cd 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt @@ -14,16 +14,16 @@ import androidx.glance.action.Action import androidx.glance.action.actionStartActivity import androidx.glance.action.clickable import androidx.glance.appwidget.CircularProgressIndicator +import androidx.glance.appwidget.cornerRadius import androidx.glance.background import androidx.glance.color.ColorProvider import androidx.glance.layout.Alignment import androidx.glance.layout.Box import androidx.glance.layout.Column -import androidx.glance.layout.ContentScale import androidx.glance.layout.Row +import androidx.glance.layout.Spacer import androidx.glance.layout.fillMaxSize import androidx.glance.layout.fillMaxWidth -import androidx.glance.layout.height import androidx.glance.layout.padding import androidx.glance.layout.size import androidx.glance.preview.ExperimentalGlancePreviewApi @@ -45,7 +45,6 @@ import org.stypox.tridenta.ui.MainActivity import org.stypox.tridenta.util.formatDateFull import org.stypox.tridenta.util.textColorOnBackground import org.stypox.tridenta.util.toLineColor -import org.stypox.tridenta.widget.theme.SmallCircularProgressIndicatorGlance import java.time.OffsetDateTime import java.time.ZoneOffset @@ -276,37 +275,62 @@ private fun TripViewBottomRowGlance( verticalAlignment = Alignment.CenterVertically, modifier = modifier .fillMaxWidth() + .background(GlanceTheme.colors.widgetBackground) .padding(16.dp) ) { - Image( - provider = ImageProvider(R.drawable.arrow_left), - contentDescription = context.getString(R.string.previous), - modifier = GlanceModifier.clickable(onPrevAction).defaultWeight() - .height(32.dp) + Box( + contentAlignment = Alignment.Center, + modifier = GlanceModifier.clickable(onPrevAction) + .size(48.dp) + .cornerRadius(12.dp) .background(GlanceTheme.colors.primary) - ) - - if (loading) { - SmallCircularProgressIndicatorGlance(modifier = GlanceModifier.defaultWeight() - .height(32.dp) - .background(GlanceTheme.colors.primary)) - } else { + ) { Image( - provider = ImageProvider(R.drawable.refresh), - contentDescription = context.getString(R.string.reload), - modifier = GlanceModifier.clickable(onReloadAction).defaultWeight() - .height(32.dp) - .background(GlanceTheme.colors.primary) + provider = ImageProvider(R.drawable.arrow_left), + contentDescription = context.getString(R.string.previous), + modifier = GlanceModifier + .size(32.dp) ) } + Spacer(GlanceModifier.defaultWeight()) - Image( - provider = ImageProvider(R.drawable.arrow_right), - contentDescription = context.getString(R.string.next), - modifier = GlanceModifier.clickable(onNextAction).defaultWeight() - .height(32.dp) + + Box( + contentAlignment = Alignment.Center, + modifier = GlanceModifier.clickable(onReloadAction) + .size(48.dp) + .cornerRadius(12.dp) .background(GlanceTheme.colors.primary) - ) + ) { + if (loading) { + CircularProgressIndicator( + modifier = GlanceModifier.defaultWeight() + .size(24.dp) + ) + } else { + Image( + provider = ImageProvider(R.drawable.refresh), + contentDescription = context.getString(R.string.reload), + modifier = GlanceModifier + .size(24.dp) + ) + } + } + Spacer(GlanceModifier.defaultWeight()) + + Box( + contentAlignment = Alignment.Center, + modifier = GlanceModifier.clickable(onNextAction) + .size(48.dp) + .cornerRadius(12.dp) + .background(GlanceTheme.colors.primary) + ) { + Image( + provider = ImageProvider(R.drawable.arrow_right), + contentDescription = context.getString(R.string.next), + GlanceModifier.size(32.dp) + ) + } } } From 40a1cd9314f3d8c57bac7988f20bd9ce29b6bceb Mon Sep 17 00:00:00 2001 From: whyKVD Date: Sun, 8 Mar 2026 21:20:36 +0100 Subject: [PATCH 15/43] feat: Configuration activity enhancement and some code cleanup --- .../org/stypox/tridenta/widget/MyAppWidget.kt | 196 +++++++++--------- .../widget/WidgetConfigurationActivity.kt | 89 ++++++-- .../tridenta/widget/actions/WidgetActions.kt | 19 +- .../tridenta/widget/ui/LineTripGlance.kt | 14 +- .../tridenta/widget/ui/TripViewGlance.kt | 55 +++-- .../tridenta/widget/ui/TripViewStopsGlance.kt | 2 +- 6 files changed, 206 insertions(+), 169 deletions(-) diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt index 9ad7b2e..e5b6f8e 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt @@ -35,6 +35,8 @@ import org.stypox.tridenta.enums.StopLineType import org.stypox.tridenta.extractor.ROME_ZONE_ID import org.stypox.tridenta.log.logError import org.stypox.tridenta.log.logInfo +import org.stypox.tridenta.repo.LineTripsRepository +import org.stypox.tridenta.repo.LinesRepository import org.stypox.tridenta.repo.data.UiLine import org.stypox.tridenta.repo.data.UiStopTime import org.stypox.tridenta.repo.data.UiTrip @@ -53,51 +55,61 @@ import java.time.ZonedDateTime class MyAppWidget : GlanceAppWidget() { override val stateDefinition = PreferencesGlanceStateDefinition + private lateinit var tripsRepository: LineTripsRepository + private lateinit var linesRepository: LinesRepository + private lateinit var referenceDateTime: ZonedDateTime + override suspend fun provideGlance( context: Context, id: GlanceId ) { - // In this method, load data needed to render the AppWidget. - // Use `withContext` to switch to another thread for long running - // operations. + val hiltEntryPoint = + EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) + tripsRepository = hiltEntryPoint.lineTripsRepository() + linesRepository = hiltEntryPoint.lineRepository() + referenceDateTime = ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID) + val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) + val configIntent = Intent(context, WidgetConfigurationActivity::class.java).apply { + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK + } provideContent { val prefs = currentState() - val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) - val configIntent = Intent(context, WidgetConfigurationActivity::class.java).apply { - putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) - - // These flags ensure the activity opens properly from the launcher context - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK - } var isFavorite by remember { mutableStateOf(false) } var line by remember { mutableStateOf(null) } - var trip by remember { mutableStateOf(null) } + val (trip, setTrip) = remember { mutableStateOf(null) } var isError = false - var isLoading by remember { mutableStateOf(true) } + val (isLoading, setIsLoading) = remember { mutableStateOf(true) } var directionFilter by remember { mutableStateOf( - Direction.ForwardAndBackward.name + Direction.ForwardAndBackward ) } + var lineType by remember { mutableStateOf(StopLineType.Urban) } + var prevEnabled by remember { mutableStateOf(true) } + var nextEnabled by remember { mutableStateOf(true) } + val storedDirectionFilter = prefs[WidgetKeys.DIRECTION_FILTER] - if (storedDirectionFilter != null) directionFilter = storedDirectionFilter + if (storedDirectionFilter != null) directionFilter = + Direction.valueOf(storedDirectionFilter) val lineId = prefs[WidgetKeys.LINE_ID] val lineTypeString = prefs[WidgetKeys.LINE_TYPE] + if (lineTypeString != null) lineType = StopLineType.valueOf(lineTypeString) val tripIndex = prefs[WidgetKeys.TRIP_INDEX] var prevTripIndex = prefs[WidgetKeys.PREV_TRIP_INDEX] val refreshTimestamp = prefs[WidgetKeys.REFRESH_TIMESTAMP] ?: 0L - val hiltEntryPoint = - EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) - val tripsRepository = hiltEntryPoint.lineTripsRepository() - val linesRepository = hiltEntryPoint.lineRepository() - LaunchedEffect(lineId, lineTypeString) { + val prevEnabledStored = prefs[WidgetKeys.PREV_ENABLED] + if (prevEnabledStored != null) prevEnabled = prevEnabledStored + val nextEnabledStored = prefs[WidgetKeys.NEXT_ENABLED] + if (nextEnabledStored != null) nextEnabled = nextEnabledStored + LaunchedEffect(lineId, lineTypeString) { // retrieving line if (lineId == null || lineTypeString == null) { return@LaunchedEffect } try { val fetchedLine = withContext(Dispatchers.IO) { - linesRepository.getUiLine(lineId, StopLineType.valueOf(lineTypeString)) + linesRepository.getUiLine(lineId, lineType) } line = fetchedLine @@ -106,7 +118,7 @@ class MyAppWidget : GlanceAppWidget() { logError(e.message!!, e.cause) } } - LaunchedEffect(lineId, directionFilter, tripIndex) { + LaunchedEffect(lineId, directionFilter, tripIndex) { // retrieving trip if (lineId == null || lineTypeString == null) { return@LaunchedEffect } @@ -115,12 +127,12 @@ class MyAppWidget : GlanceAppWidget() { val fetchedTrip = withContext(Dispatchers.IO) { tripsRepository.getUiTrip( lineId, - StopLineType.valueOf(lineTypeString), - ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID), - Direction.valueOf(directionFilter) + lineType, + referenceDateTime, + directionFilter ) } - trip = fetchedTrip.third + setTrip(fetchedTrip.third) updateAppWidgetState(context, id) { prefs -> prefs[WidgetKeys.TRIP_INDEX] = fetchedTrip.second prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] = fetchedTrip.first @@ -128,33 +140,18 @@ class MyAppWidget : GlanceAppWidget() { } return@LaunchedEffect } - if (Direction.valueOf(directionFilter) == Direction.ForwardAndBackward) { + if (directionFilter == Direction.ForwardAndBackward) { val (fetchedTrip, network) = withContext(Dispatchers.IO) { tripsRepository.getUiTrip( lineId, - StopLineType.valueOf(lineTypeString), - ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID), + lineType, + referenceDateTime, tripIndex ) } - trip = fetchedTrip + setTrip(fetchedTrip) if (!network) { - isLoading = true - try { - val freshTrip = withContext(Dispatchers.IO) { - tripsRepository.reloadUiTrip( - trip!!, - tripIndex, - ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID) - ) - } - - trip = freshTrip - } catch (e: Exception) { - logError(e.message!!, e.cause) - } finally { - isLoading = false - } + updateTrip(tripIndex, setIsLoading, fetchedTrip, setTrip) } } else { if (prevTripIndex == null) { @@ -163,68 +160,51 @@ class MyAppWidget : GlanceAppWidget() { val data = withContext(Dispatchers.IO) { tripsRepository.getUiTripWithDirection( lineId, - StopLineType.valueOf(lineTypeString), - ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID), - Direction.valueOf(directionFilter), + lineType, + referenceDateTime, + directionFilter, tripIndex, prevTripIndex ) } if (data == null) { - throw Error("data cannot be null") - } - trip = data.first - if (!data.third) { - isLoading = true - try { - val freshTrip = withContext(Dispatchers.IO) { - tripsRepository.reloadUiTrip( - trip!!, - tripIndex, - ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID) - ) + updateAppWidgetState(context, id) { prefs -> + prefs[WidgetKeys.TRIP_INDEX] = prevTripIndex + if (tripIndex < prevTripIndex) { + prefs[WidgetKeys.PREV_ENABLED] = false + prevEnabled = false + } + if (tripIndex > prevTripIndex) { + prefs[WidgetKeys.NEXT_ENABLED] = false + nextEnabled = false } - - trip = freshTrip - } catch (e: Exception) { - logError(e.message!!, e.cause) - } finally { - isLoading = false } + return@LaunchedEffect + } + setTrip(data.first) + if (!data.third) { + updateTrip(tripIndex, setIsLoading, data.first, setTrip) } updateAppWidgetState(context, id) { prefs -> prefs[WidgetKeys.TRIP_INDEX] = data.second + prefs[WidgetKeys.PREV_TRIP_INDEX] = tripIndex } } } catch (e: Exception) { logError(e.message!!, e.cause) isError = true } finally { - isLoading = false + setIsLoading(false) } } - LaunchedEffect(refreshTimestamp) { - if (refreshTimestamp == 0L || trip == null || tripIndex == null) return@LaunchedEffect - isLoading = true - logInfo("Performed refresh") - try { - val freshTrip = withContext(Dispatchers.IO) { - tripsRepository.reloadUiTrip( - trip!!, - tripIndex, - ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID) - ) - } - - trip = freshTrip - } catch (e: Exception) { - logError(e.message!!, e.cause) - } finally { - isLoading = false - } + LaunchedEffect(refreshTimestamp) { // updating trip + if (refreshTimestamp == 0L || tripIndex == null || trip == null) return@LaunchedEffect + updateTrip(tripIndex, setIsLoading, trip, setTrip) } + logInfo("${System.currentTimeMillis()}") logInfo("tripIndex: $tripIndex") logInfo("prevTripIndex: $prevTripIndex") + logInfo("directionFilter: $directionFilter") GlanceTheme { LineTripsWidgetScreen( line = line, @@ -233,27 +213,41 @@ class MyAppWidget : GlanceAppWidget() { onPrevAction = actionRunCallback(), onNextAction = actionRunCallback(), onLineClickAction = actionStartActivity(configIntent), - directionFilter = Direction.valueOf(directionFilter), + directionFilter = directionFilter, onDirectionClickAction = actionRunCallback(), stopIdToHighlight = null, stopTypeToHighlight = null, - prevEnabled = true, - nextEnabled = true, + prevEnabled = prevEnabled, + nextEnabled = nextEnabled, isFavorite = isFavorite ) - /*TripViewGlance( - trip, error = isError, loading = isLoading, - onReloadAction = actionRunCallback(), - onPrevAction = actionRunCallback(), - onNextAction = actionRunCallback(), - onLineClickAction = actionStartActivity(configIntent), - //onDirectionClickAction = actionRunCallback(), - stopIdToHighlight = null, - stopTypeToHighlight = null, - )*/ } } } + + private suspend fun updateTrip( + tripIndex: Int, + setIsLoading: (Boolean) -> Unit, + trip: UiTrip, + setTrip: (UiTrip) -> Unit + ) { + setIsLoading(true) + try { + val freshTrip = withContext(Dispatchers.IO) { + tripsRepository.reloadUiTrip( + trip, + tripIndex, + referenceDateTime + ) + } + + setTrip(freshTrip) + } catch (e: Exception) { + logError(e.message!!, e.cause) + } finally { + setIsLoading(false) + } + } } @OptIn(ExperimentalGlancePreviewApi::class) @@ -397,8 +391,8 @@ fun MyWidgetPreview() { onReloadAction = actionStartActivity(), onPrevAction = actionStartActivity(), onNextAction = actionStartActivity(), - onLineClickAction = actionStartActivity(), - //onDirectionClickAction = actionStartActivity(), + prevEnabled = true, + nextEnabled = true, stopIdToHighlight = null, stopTypeToHighlight = null, ) diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt index 6b6ef94..b6f0ea6 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt @@ -7,15 +7,27 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.state.updateAppWidgetState import androidx.glance.appwidget.updateAll @@ -26,11 +38,16 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.stypox.tridenta.R import org.stypox.tridenta.db.data.DbLine +import org.stypox.tridenta.enums.Area import org.stypox.tridenta.enums.Direction +import org.stypox.tridenta.ui.lines.AreaChip import org.stypox.tridenta.ui.lines.LineItem +import org.stypox.tridenta.ui.lines.LinesUiState import org.stypox.tridenta.ui.lines.LinesViewModel -import org.stypox.tridenta.ui.theme.TitleText +import org.stypox.tridenta.ui.lines.SelectAreaDialog +import org.stypox.tridenta.ui.theme.AppTheme import org.stypox.tridenta.widget.actions.WidgetKeys @AndroidEntryPoint @@ -64,18 +81,21 @@ class WidgetConfigurationActivity : // 2. Collect the complex UI state val uiState by viewModel.uiState.collectAsState() - if (uiState.loading) { - CircularProgressIndicator() // Show loading spinner - } else if (uiState.error) { - Text("Error loading lines. Please try again.") - } else { - // 4. Pass the loaded lines to your selection screen - LineSelectionScreen( - lines = uiState.lines, - onLineSelected = { selectedLine -> - saveWidgetConfiguration(selectedLine) - } - ) + AppTheme { + if (uiState.loading) { + CircularProgressIndicator() // Show loading spinner + } else if (uiState.error) { + Text("Error loading lines. Please try again.") + } else { + // 4. Pass the loaded lines to your selection screen + LineSelectionScreen( + state = uiState, + onLineSelected = { selectedLine -> + saveWidgetConfiguration(selectedLine) + }, + setSelectedArea = viewModel::setSelectedArea + ) + } } } } @@ -113,15 +133,42 @@ class WidgetConfigurationActivity : } } +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun LineSelectionScreen(lines: List, onLineSelected: (DbLine) -> Unit) { - LazyColumn(modifier = Modifier.fillMaxSize()) { - item { TitleText("Seleziona la linea:") } - items(lines) { line -> - LineItem( - line, true, - modifier = Modifier.clickable { onLineSelected(line) }, - ) +fun LineSelectionScreen(state: LinesUiState, onLineSelected: (DbLine) -> Unit, setSelectedArea: (Area) -> Unit) { + var showAreaDialog by rememberSaveable { mutableStateOf(false) } + if (showAreaDialog) { + SelectAreaDialog( + selectedArea = state.selectedArea, + setSelectedArea = setSelectedArea, + onDismiss = { showAreaDialog = false } + ) + } + Scaffold(topBar = { + TopAppBar(title = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text(text = stringResource(R.string.selected_area)) + AreaChip( + area = state.selectedArea, + onClick = { showAreaDialog = true } + ) + } + }) + }) { innerPadding -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) { + items(state.lines) { line -> + LineItem( + line, true, + modifier = Modifier.clickable { onLineSelected(line) }, + ) + } } } } \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt index 4f4685c..1643998 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt @@ -5,7 +5,6 @@ import androidx.glance.GlanceId import androidx.glance.action.ActionParameters import androidx.glance.appwidget.action.ActionCallback import androidx.glance.appwidget.state.updateAppWidgetState -import androidx.glance.appwidget.updateAll import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent @@ -15,7 +14,6 @@ import org.stypox.tridenta.repo.LineTripsRepository import org.stypox.tridenta.repo.LinesRepository import org.stypox.tridenta.widget.MyAppWidget -// 1. Create a Hilt Entry point to access your Repositories inside Glance Actions @EntryPoint @InstallIn(SingletonComponent::class) interface WidgetEntryPoint { @@ -24,7 +22,6 @@ interface WidgetEntryPoint { // Add HistoryDao and LinesRepository here too } -// 2. Refactor: onNextClicked() -> NextTripAction class NextTripAction : ActionCallback { override suspend fun onAction( context: Context, @@ -39,16 +36,17 @@ class NextTripAction : ActionCallback { return@updateAppWidgetState } + prefs[WidgetKeys.PREV_ENABLED] = nextIndex > 0 + prefs[WidgetKeys.NEXT_ENABLED] = nextIndex < tripsInDayCount - 1 prefs[WidgetKeys.PREV_TRIP_INDEX] = currentIndex prefs[WidgetKeys.TRIP_INDEX] = nextIndex } - MyAppWidget().updateAll(context) + MyAppWidget().update(context, glanceId) logInfo("NextTripAction performed") } } -// 3. Refactor: onPrevClicked() -> PrevTripAction class PrevTripAction : ActionCallback { override suspend fun onAction( context: Context, @@ -63,16 +61,17 @@ class PrevTripAction : ActionCallback { return@updateAppWidgetState } + prefs[WidgetKeys.PREV_ENABLED] = nextIndex > 0 + prefs[WidgetKeys.NEXT_ENABLED] = nextIndex < tripsInDayCount - 1 prefs[WidgetKeys.PREV_TRIP_INDEX] = currentIndex prefs[WidgetKeys.TRIP_INDEX] = nextIndex } - MyAppWidget().updateAll(context) + MyAppWidget().update(context, glanceId) logInfo("PrevTripAction performed") } } -// 4. Refactor: onReload() -> ReloadTripAction class ReloadTripAction : ActionCallback { override suspend fun onAction( context: Context, @@ -82,18 +81,16 @@ class ReloadTripAction : ActionCallback { updateAppWidgetState(context, glanceId) { prefs -> prefs[WidgetKeys.REFRESH_TIMESTAMP] = System.currentTimeMillis() } - MyAppWidget().updateAll(context) + MyAppWidget().update(context, glanceId) } } -// 5. Refactor: onDirectionClicked() -> ToggleDirectionAction class ToggleDirectionAction : ActionCallback { override suspend fun onAction( context: Context, glanceId: GlanceId, parameters: ActionParameters ) { - // Read current direction from prefs, toggle it, fetch new trip, update prefs updateAppWidgetState(context, glanceId) { prefs -> val actualDirectionFilter = prefs[WidgetKeys.DIRECTION_FILTER] val newDirectionFilter = when (Direction.valueOf( @@ -105,6 +102,6 @@ class ToggleDirectionAction : ActionCallback { } prefs[WidgetKeys.DIRECTION_FILTER] = newDirectionFilter.name } - MyAppWidget().updateAll(context) + MyAppWidget().update(context, glanceId) } } \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt index f6f7877..fcd91c1 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt @@ -10,7 +10,6 @@ import androidx.glance.ImageProvider import androidx.glance.LocalContext import androidx.glance.action.Action import androidx.glance.action.clickable -import androidx.glance.appwidget.CircularProgressIndicator import androidx.glance.appwidget.action.actionRunCallback import androidx.glance.background import androidx.glance.color.ColorProvider @@ -68,7 +67,8 @@ fun LineTripsWidgetScreen( line = line, isFavorite = isFavorite, directionFilter = directionFilter, - onDirectionClickAction + onDirectionAction = onDirectionClickAction, + onLineClickAction = onLineClickAction, ) TripViewGlance( trip = trip, @@ -77,28 +77,28 @@ fun LineTripsWidgetScreen( onReloadAction = onReloadAction, onPrevAction = onPrevAction, onNextAction = onNextAction, - onLineClickAction = onLineClickAction, + prevEnabled = prevEnabled, + nextEnabled = nextEnabled, stopIdToHighlight = stopIdToHighlight, stopTypeToHighlight = stopTypeToHighlight ) } } -/** - * Glance equivalent of LineAppBar. - */ @Composable fun WidgetAppBar( line: UiLine?, isFavorite: Boolean, directionFilter: Direction, - onDirectionAction: Action + onDirectionAction: Action, + onLineClickAction: Action, ) { val context = LocalContext.current // The main container Row Row( modifier = GlanceModifier .fillMaxWidth() + .clickable(onLineClickAction) .padding(horizontal = 12.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt index 33531cd..f9aecdd 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt @@ -10,6 +10,7 @@ import androidx.glance.GlanceTheme import androidx.glance.Image import androidx.glance.ImageProvider import androidx.glance.LocalContext +import androidx.glance.Visibility import androidx.glance.action.Action import androidx.glance.action.actionStartActivity import androidx.glance.action.clickable @@ -32,6 +33,7 @@ import androidx.glance.text.FontWeight import androidx.glance.text.Text import androidx.glance.text.TextAlign import androidx.glance.text.TextStyle +import androidx.glance.visibility import org.stypox.tridenta.R import org.stypox.tridenta.db.data.DbLine import org.stypox.tridenta.db.data.DbStop @@ -57,13 +59,11 @@ fun TripViewGlance( trip: UiTrip?, error: Boolean, loading: Boolean, - // Note: In Glance, clicks are handled by 'Action's (like actionRunCallback or actionStartActivity) - // rather than standard lambda functions, because they trigger background broadcasts. onReloadAction: Action, onPrevAction: Action, onNextAction: Action, - onLineClickAction: Action, - //onDirectionClickAction: Action, + prevEnabled: Boolean, + nextEnabled: Boolean, stopIdToHighlight: Int?, stopTypeToHighlight: StopLineType?, modifier: GlanceModifier = GlanceModifier @@ -81,8 +81,6 @@ fun TripViewGlance( ) { TripViewTopRowGlance( trip = trip, - onLineClickAction = onLineClickAction, - //onDirectionClickAction = onDirectionClickAction, modifier = GlanceModifier.padding( start = 12.dp, top = 4.dp, @@ -154,6 +152,8 @@ fun TripViewGlance( onReloadAction = onReloadAction, onPrevAction = onPrevAction, onNextAction = onNextAction, + prevEnabled = prevEnabled, + nextEnabled = nextEnabled, modifier = GlanceModifier.fillMaxWidth() ) } @@ -189,8 +189,6 @@ fun DirectionIconGlance( @Composable private fun TripViewTopRowGlance( trip: UiTrip, - onLineClickAction: Action, - //onDirectionClickAction: Action, modifier: GlanceModifier = GlanceModifier ) { val context = LocalContext.current @@ -205,9 +203,8 @@ private fun TripViewTopRowGlance( val textColor = textColorOnBackground(shortNameBackground) Box( modifier = GlanceModifier - .background(shortNameBackground) // Use a theme color instead of dynamic custom color for simplicity in Glance + .background(shortNameBackground) .padding(8.dp) - .clickable(onLineClickAction) ) { Text( text = trip.line.shortName, @@ -266,9 +263,14 @@ private fun TripViewBottomRowGlance( onReloadAction: Action, onPrevAction: Action, onNextAction: Action, + prevEnabled: Boolean, + nextEnabled: Boolean, modifier: GlanceModifier = GlanceModifier ) { val context = LocalContext.current + val buttonModifier = GlanceModifier.size(48.dp) + .cornerRadius(12.dp) + .background(GlanceTheme.colors.primary) // Widgets don't support FloatingActionButtons. Use standard Buttons or Images. Row( @@ -280,10 +282,9 @@ private fun TripViewBottomRowGlance( ) { Box( contentAlignment = Alignment.Center, - modifier = GlanceModifier.clickable(onPrevAction) - .size(48.dp) - .cornerRadius(12.dp) - .background(GlanceTheme.colors.primary) + modifier = if (prevEnabled) buttonModifier.clickable(onPrevAction) else buttonModifier.visibility( + Visibility.Invisible + ) ) { Image( provider = ImageProvider(R.drawable.arrow_left), @@ -297,10 +298,7 @@ private fun TripViewBottomRowGlance( Box( contentAlignment = Alignment.Center, - modifier = GlanceModifier.clickable(onReloadAction) - .size(48.dp) - .cornerRadius(12.dp) - .background(GlanceTheme.colors.primary) + modifier = buttonModifier.clickable(onReloadAction) ) { if (loading) { CircularProgressIndicator( @@ -320,10 +318,9 @@ private fun TripViewBottomRowGlance( Box( contentAlignment = Alignment.Center, - modifier = GlanceModifier.clickable(onNextAction) - .size(48.dp) - .cornerRadius(12.dp) - .background(GlanceTheme.colors.primary) + modifier = if (nextEnabled) buttonModifier.clickable(onNextAction) else buttonModifier.visibility( + Visibility.Invisible + ) ) { Image( provider = ImageProvider(R.drawable.arrow_right), @@ -469,12 +466,13 @@ private fun TripViewPreview() { ), error = false, loading = true, - stopIdToHighlight = null, - stopTypeToHighlight = null, onReloadAction = actionStartActivity(), onPrevAction = actionStartActivity(), onNextAction = actionStartActivity(), - onLineClickAction = actionStartActivity(), + prevEnabled = true, + nextEnabled = true, + stopIdToHighlight = null, + stopTypeToHighlight = null, //onDirectionClickAction = actionStartActivity(), ) } @@ -490,12 +488,13 @@ private fun TripViewPreviewLoading() { trip = null, error = false, loading = loading, - stopIdToHighlight = null, - stopTypeToHighlight = null, onReloadAction = actionStartActivity(), onPrevAction = actionStartActivity(), onNextAction = actionStartActivity(), - onLineClickAction = actionStartActivity(), + prevEnabled = true, + nextEnabled = true, + stopIdToHighlight = null, + stopTypeToHighlight = null, //onDirectionClickAction = actionStartActivity() ) } diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt index 075f52f..010a031 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt @@ -92,7 +92,7 @@ fun TripViewStopsGlance( item { // space for FABs - Spacer(modifier = GlanceModifier.size(height = 64.dp, width = 0.dp)) + Spacer(modifier = GlanceModifier.size(height = 84.dp, width = 0.dp)) } } } From 4737804be51bc7235394c1856328be13bdb30310 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Mon, 9 Mar 2026 11:41:28 +0100 Subject: [PATCH 16/43] feat: Started using mutablestateflow in the widget --- .../org/stypox/tridenta/widget/MyAppWidget.kt | 162 ++++++++++++------ .../widget/WidgetConfigurationActivity.kt | 76 ++++---- .../tridenta/widget/ui/LineTripGlance.kt | 7 +- app/src/main/res/drawable/warning.xml | 5 - app/src/main/res/drawable/warning_filled.xml | 5 + app/src/main/res/xml/my_app_widget_info.xml | 2 +- 6 files changed, 160 insertions(+), 97 deletions(-) delete mode 100644 app/src/main/res/drawable/warning.xml create mode 100644 app/src/main/res/drawable/warning_filled.xml diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt index e5b6f8e..6888416 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.Intent import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -25,6 +26,9 @@ import androidx.glance.preview.Preview import androidx.glance.state.PreferencesGlanceStateDefinition import dagger.hilt.android.EntryPointAccessors import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.withContext import org.stypox.tridenta.db.data.DbLine import org.stypox.tridenta.db.data.DbStop @@ -37,10 +41,10 @@ import org.stypox.tridenta.log.logError import org.stypox.tridenta.log.logInfo import org.stypox.tridenta.repo.LineTripsRepository import org.stypox.tridenta.repo.LinesRepository -import org.stypox.tridenta.repo.data.UiLine import org.stypox.tridenta.repo.data.UiStopTime import org.stypox.tridenta.repo.data.UiTrip import org.stypox.tridenta.ui.MainActivity +import org.stypox.tridenta.ui.line_trips.LineTripsUiState import org.stypox.tridenta.widget.actions.NextTripAction import org.stypox.tridenta.widget.actions.PrevTripAction import org.stypox.tridenta.widget.actions.ReloadTripAction @@ -57,7 +61,25 @@ class MyAppWidget : GlanceAppWidget() { override val stateDefinition = PreferencesGlanceStateDefinition private lateinit var tripsRepository: LineTripsRepository private lateinit var linesRepository: LinesRepository - private lateinit var referenceDateTime: ZonedDateTime + + //private lateinit var referenceDateTime: ZonedDateTime + private val mutableUiState = MutableStateFlow( + LineTripsUiState( + line = null, + tripsInDayCount = 0, + tripIndex = 0, + trip = null, + prevEnabled = false, + nextEnabled = false, + referenceDateTime = ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID), + directionFilter = Direction.ForwardAndBackward, + stopIdToHighlight = null, + stopTypeToHighlight = null, + loading = true, + error = false, + ) + ) + private val uiState = mutableUiState.asStateFlow() override suspend fun provideGlance( context: Context, @@ -67,7 +89,12 @@ class MyAppWidget : GlanceAppWidget() { EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) tripsRepository = hiltEntryPoint.lineTripsRepository() linesRepository = hiltEntryPoint.lineRepository() - referenceDateTime = ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID) + //referenceDateTime = ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID) + mutableUiState.update { + it.copy( + referenceDateTime = ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID) + ) + } val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) val configIntent = Intent(context, WidgetConfigurationActivity::class.java).apply { @@ -75,34 +102,36 @@ class MyAppWidget : GlanceAppWidget() { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK } provideContent { + val lineTripsUiState by uiState.collectAsState() val prefs = currentState() var isFavorite by remember { mutableStateOf(false) } - var line by remember { mutableStateOf(null) } - val (trip, setTrip) = remember { mutableStateOf(null) } - var isError = false - val (isLoading, setIsLoading) = remember { mutableStateOf(true) } - var directionFilter by remember { - mutableStateOf( - Direction.ForwardAndBackward - ) - } var lineType by remember { mutableStateOf(StopLineType.Urban) } - var prevEnabled by remember { mutableStateOf(true) } - var nextEnabled by remember { mutableStateOf(true) } - val storedDirectionFilter = prefs[WidgetKeys.DIRECTION_FILTER] - if (storedDirectionFilter != null) directionFilter = - Direction.valueOf(storedDirectionFilter) val lineId = prefs[WidgetKeys.LINE_ID] val lineTypeString = prefs[WidgetKeys.LINE_TYPE] if (lineTypeString != null) lineType = StopLineType.valueOf(lineTypeString) val tripIndex = prefs[WidgetKeys.TRIP_INDEX] var prevTripIndex = prefs[WidgetKeys.PREV_TRIP_INDEX] val refreshTimestamp = prefs[WidgetKeys.REFRESH_TIMESTAMP] ?: 0L + + val storedDirectionFilter = prefs[WidgetKeys.DIRECTION_FILTER] + if (storedDirectionFilter != null) mutableUiState.update { + it.copy( + directionFilter = + Direction.valueOf(storedDirectionFilter) + ) + } val prevEnabledStored = prefs[WidgetKeys.PREV_ENABLED] - if (prevEnabledStored != null) prevEnabled = prevEnabledStored + if (prevEnabledStored != null) mutableUiState.update { + it.copy(prevEnabled = prevEnabledStored) + } val nextEnabledStored = prefs[WidgetKeys.NEXT_ENABLED] - if (nextEnabledStored != null) nextEnabled = nextEnabledStored + if (nextEnabledStored != null) mutableUiState.update { + it.copy( + nextEnabled = nextEnabledStored + ) + } + LaunchedEffect(lineId, lineTypeString) { // retrieving line if (lineId == null || lineTypeString == null) { return@LaunchedEffect @@ -112,46 +141,64 @@ class MyAppWidget : GlanceAppWidget() { linesRepository.getUiLine(lineId, lineType) } - line = fetchedLine - line?.let { isFavorite = it.isFavorite } + mutableUiState.update { + it.copy(line = fetchedLine) + } + fetchedLine?.let { isFavorite = it.isFavorite } } catch (e: Exception) { + mutableUiState.update { it.copy(error = true) } logError(e.message!!, e.cause) } } - LaunchedEffect(lineId, directionFilter, tripIndex) { // retrieving trip + + LaunchedEffect( + lineId, + storedDirectionFilter, + tripIndex, + prevTripIndex + ) { // retrieving trip if (lineId == null || lineTypeString == null) { return@LaunchedEffect } try { if (tripIndex == null) { - val fetchedTrip = withContext(Dispatchers.IO) { + val data = withContext(Dispatchers.IO) { tripsRepository.getUiTrip( lineId, lineType, - referenceDateTime, - directionFilter + lineTripsUiState.referenceDateTime, + lineTripsUiState.directionFilter ) } - setTrip(fetchedTrip.third) + mutableUiState.update { it.copy(trip = data.third) } updateAppWidgetState(context, id) { prefs -> - prefs[WidgetKeys.TRIP_INDEX] = fetchedTrip.second - prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] = fetchedTrip.first + prefs[WidgetKeys.TRIP_INDEX] = data.second + prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] = data.first prefs[WidgetKeys.IS_INITIAL_DATA_LOADED] = true + prefs[WidgetKeys.PREV_ENABLED] = data.second > 0 + prefs[WidgetKeys.NEXT_ENABLED] = + data.second < (prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] ?: 0) - 1 + mutableUiState.update { + it.copy( + prevEnabled = prefs[WidgetKeys.PREV_ENABLED] ?: false, + nextEnabled = prefs[WidgetKeys.NEXT_ENABLED] ?: false + ) + } } return@LaunchedEffect } - if (directionFilter == Direction.ForwardAndBackward) { + if (lineTripsUiState.directionFilter == Direction.ForwardAndBackward) { val (fetchedTrip, network) = withContext(Dispatchers.IO) { tripsRepository.getUiTrip( lineId, lineType, - referenceDateTime, + lineTripsUiState.referenceDateTime, tripIndex ) } - setTrip(fetchedTrip) + mutableUiState.update { it.copy(trip = fetchedTrip) } if (!network) { - updateTrip(tripIndex, setIsLoading, fetchedTrip, setTrip) + updateTrip(tripIndex, mutableUiState, fetchedTrip) } } else { if (prevTripIndex == null) { @@ -161,8 +208,8 @@ class MyAppWidget : GlanceAppWidget() { tripsRepository.getUiTripWithDirection( lineId, lineType, - referenceDateTime, - directionFilter, + lineTripsUiState.referenceDateTime, + lineTripsUiState.directionFilter, tripIndex, prevTripIndex ) @@ -172,18 +219,18 @@ class MyAppWidget : GlanceAppWidget() { prefs[WidgetKeys.TRIP_INDEX] = prevTripIndex if (tripIndex < prevTripIndex) { prefs[WidgetKeys.PREV_ENABLED] = false - prevEnabled = false + mutableUiState.update { it.copy(prevEnabled = false) } } if (tripIndex > prevTripIndex) { prefs[WidgetKeys.NEXT_ENABLED] = false - nextEnabled = false + mutableUiState.update { it.copy(nextEnabled = false) } } } return@LaunchedEffect } - setTrip(data.first) + mutableUiState.update { it.copy(trip = data.first) } if (!data.third) { - updateTrip(tripIndex, setIsLoading, data.first, setTrip) + updateTrip(tripIndex, mutableUiState, data.first) } updateAppWidgetState(context, id) { prefs -> prefs[WidgetKeys.TRIP_INDEX] = data.second @@ -192,33 +239,38 @@ class MyAppWidget : GlanceAppWidget() { } } catch (e: Exception) { logError(e.message!!, e.cause) - isError = true + mutableUiState.update { it.copy(error = true) } } finally { - setIsLoading(false) + mutableUiState.update { it.copy(loading = false) } } } + LaunchedEffect(refreshTimestamp) { // updating trip - if (refreshTimestamp == 0L || tripIndex == null || trip == null) return@LaunchedEffect - updateTrip(tripIndex, setIsLoading, trip, setTrip) + if (refreshTimestamp == 0L || tripIndex == null || lineTripsUiState.trip == null) return@LaunchedEffect + updateTrip(tripIndex, mutableUiState, lineTripsUiState.trip!!) } + logInfo("${System.currentTimeMillis()}") logInfo("tripIndex: $tripIndex") logInfo("prevTripIndex: $prevTripIndex") - logInfo("directionFilter: $directionFilter") + logInfo("directionFilter: ${lineTripsUiState.directionFilter}") + GlanceTheme { LineTripsWidgetScreen( - line = line, - trip = trip, error = isError, loading = isLoading, + line = lineTripsUiState.line, + trip = lineTripsUiState.trip, + error = lineTripsUiState.error, + loading = lineTripsUiState.loading, onReloadAction = actionRunCallback(), onPrevAction = actionRunCallback(), onNextAction = actionRunCallback(), onLineClickAction = actionStartActivity(configIntent), - directionFilter = directionFilter, + directionFilter = lineTripsUiState.directionFilter, onDirectionClickAction = actionRunCallback(), stopIdToHighlight = null, stopTypeToHighlight = null, - prevEnabled = prevEnabled, - nextEnabled = nextEnabled, + prevEnabled = lineTripsUiState.prevEnabled, + nextEnabled = lineTripsUiState.nextEnabled, isFavorite = isFavorite ) } @@ -227,25 +279,25 @@ class MyAppWidget : GlanceAppWidget() { private suspend fun updateTrip( tripIndex: Int, - setIsLoading: (Boolean) -> Unit, - trip: UiTrip, - setTrip: (UiTrip) -> Unit + mutableUiState: MutableStateFlow, + trip: UiTrip ) { - setIsLoading(true) + mutableUiState.update { it.copy(loading = true) } try { val freshTrip = withContext(Dispatchers.IO) { tripsRepository.reloadUiTrip( trip, tripIndex, - referenceDateTime + mutableUiState.value.referenceDateTime ) } - setTrip(freshTrip) + mutableUiState.update { it.copy(trip = freshTrip) } } catch (e: Exception) { + mutableUiState.update { it.copy(error = true) } logError(e.message!!, e.cause) } finally { - setIsLoading(false) + mutableUiState.update { it.copy(loading = false) } } } } diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt index b6f0ea6..4cf286c 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt @@ -9,15 +9,17 @@ import androidx.activity.compose.setContent import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -30,7 +32,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.state.updateAppWidgetState -import androidx.glance.appwidget.updateAll import androidx.hilt.navigation.compose.hiltViewModel import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.DelicateCoroutinesApi @@ -76,26 +77,23 @@ class WidgetConfigurationActivity : setResult(Activity.RESULT_CANCELED) setContent { - val viewModel: LinesViewModel = hiltViewModel() + val linesViewModel: LinesViewModel = hiltViewModel() - // 2. Collect the complex UI state - val uiState by viewModel.uiState.collectAsState() + val linesUiState by linesViewModel.uiState.collectAsState() + val lastReloadWasError by linesViewModel.lastReloadWasError.collectAsState() AppTheme { - if (uiState.loading) { - CircularProgressIndicator() // Show loading spinner - } else if (uiState.error) { - Text("Error loading lines. Please try again.") - } else { - // 4. Pass the loaded lines to your selection screen - LineSelectionScreen( - state = uiState, - onLineSelected = { selectedLine -> - saveWidgetConfiguration(selectedLine) - }, - setSelectedArea = viewModel::setSelectedArea - ) - } + LineSelectionScreen( + criticalError = linesUiState.error, + minorError = lastReloadWasError, + loading = linesUiState.loading, + onReload = linesViewModel::onReload, + state = linesUiState, + onLineSelected = { selectedLine -> + saveWidgetConfiguration(selectedLine) + }, + setSelectedArea = linesViewModel::setSelectedArea + ) } } } @@ -119,7 +117,7 @@ class WidgetConfigurationActivity : prefs[WidgetKeys.DIRECTION_FILTER] = Direction.ForwardAndBackward.name prefs[WidgetKeys.IS_LOADING] = true } - MyAppWidget().updateAll(this@WidgetConfigurationActivity) + MyAppWidget().update(this@WidgetConfigurationActivity, glanceId) // 5. Tell the Android OS that the configuration was successful withContext(Dispatchers.Main) { @@ -135,7 +133,15 @@ class WidgetConfigurationActivity : @OptIn(ExperimentalMaterial3Api::class) @Composable -fun LineSelectionScreen(state: LinesUiState, onLineSelected: (DbLine) -> Unit, setSelectedArea: (Area) -> Unit) { +fun LineSelectionScreen( + criticalError: Boolean, + minorError: Boolean, + loading: Boolean, + onReload: () -> Unit, + state: LinesUiState, + onLineSelected: (DbLine) -> Unit, + setSelectedArea: (Area) -> Unit +) { var showAreaDialog by rememberSaveable { mutableStateOf(false) } if (showAreaDialog) { SelectAreaDialog( @@ -157,17 +163,25 @@ fun LineSelectionScreen(state: LinesUiState, onLineSelected: (DbLine) -> Unit, s ) } }) - }) { innerPadding -> - LazyColumn( + }) { paddingValues -> + PullToRefreshBox( + isRefreshing = loading, + state = rememberPullToRefreshState(), + onRefresh = onReload, modifier = Modifier - .fillMaxSize() - .padding(innerPadding) + .padding(paddingValues) + .fillMaxHeight() ) { - items(state.lines) { line -> - LineItem( - line, true, - modifier = Modifier.clickable { onLineSelected(line) }, - ) + LazyColumn( + modifier = Modifier + .fillMaxWidth() + ) { + items(state.lines) { line -> + LineItem( + line, true, + modifier = Modifier.clickable { onLineSelected(line) }, + ) + } } } } diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt index fcd91c1..e9db635 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt @@ -10,7 +10,6 @@ import androidx.glance.ImageProvider import androidx.glance.LocalContext import androidx.glance.action.Action import androidx.glance.action.clickable -import androidx.glance.appwidget.action.actionRunCallback import androidx.glance.background import androidx.glance.color.ColorProvider import androidx.glance.layout.Alignment @@ -32,7 +31,6 @@ import org.stypox.tridenta.repo.data.UiLine import org.stypox.tridenta.repo.data.UiTrip import org.stypox.tridenta.util.textColorOnBackground import org.stypox.tridenta.util.toLineColor -import org.stypox.tridenta.widget.actions.ToggleDirectionAction import org.stypox.tridenta.widget.theme.SmallCircularProgressIndicatorGlance /** @@ -151,12 +149,11 @@ fun WidgetAppBar( // News/Warning Icon if (line != null && line.newsItems.isNotEmpty()) { Image( - provider = ImageProvider(R.drawable.warning), // Ensure you have this XML drawable + provider = ImageProvider(R.drawable.warning_filled), // Ensure you have this XML drawable contentDescription = "News", colorFilter = ColorFilter.tint(GlanceTheme.colors.primary), modifier = GlanceModifier .padding(end = 8.dp) - .clickable(onDirectionAction) ) } @@ -167,7 +164,7 @@ fun WidgetAppBar( colorFilter = ColorFilter.tint(GlanceTheme.colors.primary), modifier = GlanceModifier .padding(end = 8.dp) - .clickable(actionRunCallback()) + .clickable(onDirectionAction) ) // Favorite Toggle Icon diff --git a/app/src/main/res/drawable/warning.xml b/app/src/main/res/drawable/warning.xml deleted file mode 100644 index 61b86d4..0000000 --- a/app/src/main/res/drawable/warning.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/warning_filled.xml b/app/src/main/res/drawable/warning_filled.xml new file mode 100644 index 0000000..7c1baad --- /dev/null +++ b/app/src/main/res/drawable/warning_filled.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/xml/my_app_widget_info.xml b/app/src/main/res/xml/my_app_widget_info.xml index 6de72f9..5f85f32 100644 --- a/app/src/main/res/xml/my_app_widget_info.xml +++ b/app/src/main/res/xml/my_app_widget_info.xml @@ -4,7 +4,7 @@ android:resizeMode="vertical|horizontal" android:minWidth="250dp" android:minHeight="150dp" - android:updatePeriodMillis="1" + android:updatePeriodMillis="1800000" android:widgetFeatures="reconfigurable" android:configure="org.stypox.tridenta.widget.WidgetConfigurationActivity"> \ No newline at end of file From 880b586c7fb930c69387cf8e596e5eaf83a8f42b Mon Sep 17 00:00:00 2001 From: whyKVD Date: Mon, 9 Mar 2026 21:32:57 +0100 Subject: [PATCH 17/43] feat: Refactoring how to retrieve trip data introduced a lot of bugs --- .../org/stypox/tridenta/widget/MyAppWidget.kt | 413 ++++++++++++++++-- .../tridenta/widget/actions/WidgetActions.kt | 3 + 2 files changed, 376 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt index 6888416..dc93e2f 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt @@ -25,11 +25,16 @@ import androidx.glance.preview.ExperimentalGlancePreviewApi import androidx.glance.preview.Preview import androidx.glance.state.PreferencesGlanceStateDefinition import dagger.hilt.android.EntryPointAccessors +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.stypox.tridenta.db.HistoryDao import org.stypox.tridenta.db.data.DbLine import org.stypox.tridenta.db.data.DbStop import org.stypox.tridenta.enums.Area @@ -56,11 +61,13 @@ import org.stypox.tridenta.widget.ui.TripViewGlance import java.time.OffsetDateTime import java.time.ZoneOffset import java.time.ZonedDateTime +import kotlin.properties.Delegates class MyAppWidget : GlanceAppWidget() { override val stateDefinition = PreferencesGlanceStateDefinition private lateinit var tripsRepository: LineTripsRepository private lateinit var linesRepository: LinesRepository + private lateinit var historyDao: HistoryDao //private lateinit var referenceDateTime: ZonedDateTime private val mutableUiState = MutableStateFlow( @@ -79,7 +86,11 @@ class MyAppWidget : GlanceAppWidget() { error = false, ) ) - private val uiState = mutableUiState.asStateFlow() + val uiState = mutableUiState.asStateFlow() + private var lineId by Delegates.notNull() + private lateinit var lineType: StopLineType + + private var tripReloadJob: Job? = null override suspend fun provideGlance( context: Context, @@ -89,6 +100,7 @@ class MyAppWidget : GlanceAppWidget() { EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) tripsRepository = hiltEntryPoint.lineTripsRepository() linesRepository = hiltEntryPoint.lineRepository() + historyDao = hiltEntryPoint.historyDao() //referenceDateTime = ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID) mutableUiState.update { it.copy( @@ -105,22 +117,22 @@ class MyAppWidget : GlanceAppWidget() { val lineTripsUiState by uiState.collectAsState() val prefs = currentState() var isFavorite by remember { mutableStateOf(false) } - var lineType by remember { mutableStateOf(StopLineType.Urban) } + //var lineType by remember { mutableStateOf(StopLineType.Urban) } - val lineId = prefs[WidgetKeys.LINE_ID] + lineId = prefs[WidgetKeys.LINE_ID] ?: -1 val lineTypeString = prefs[WidgetKeys.LINE_TYPE] if (lineTypeString != null) lineType = StopLineType.valueOf(lineTypeString) val tripIndex = prefs[WidgetKeys.TRIP_INDEX] - var prevTripIndex = prefs[WidgetKeys.PREV_TRIP_INDEX] + val prevTripIndex = prefs[WidgetKeys.PREV_TRIP_INDEX] val refreshTimestamp = prefs[WidgetKeys.REFRESH_TIMESTAMP] ?: 0L val storedDirectionFilter = prefs[WidgetKeys.DIRECTION_FILTER] - if (storedDirectionFilter != null) mutableUiState.update { + /*if (storedDirectionFilter != null) mutableUiState.update { it.copy( directionFilter = Direction.valueOf(storedDirectionFilter) ) - } + }*/ val prevEnabledStored = prefs[WidgetKeys.PREV_ENABLED] if (prevEnabledStored != null) mutableUiState.update { it.copy(prevEnabled = prevEnabledStored) @@ -131,8 +143,13 @@ class MyAppWidget : GlanceAppWidget() { nextEnabled = nextEnabledStored ) } + /*if(lineId != -1 && lineTypeString != null) isFavorite = historyDao.isFavorite( + isLine = true, + id = lineId, + type = lineType + ).value ?: false*/ - LaunchedEffect(lineId, lineTypeString) { // retrieving line + /*LaunchedEffect(lineId, lineTypeString) { // retrieving line if (lineId == null || lineTypeString == null) { return@LaunchedEffect } @@ -149,44 +166,36 @@ class MyAppWidget : GlanceAppWidget() { mutableUiState.update { it.copy(error = true) } logError(e.message!!, e.cause) } - } + }*/ LaunchedEffect( lineId, - storedDirectionFilter, tripIndex, prevTripIndex ) { // retrieving trip - if (lineId == null || lineTypeString == null) { + if (lineId == -1 || lineTypeString == null) { return@LaunchedEffect } - try { - if (tripIndex == null) { - val data = withContext(Dispatchers.IO) { - tripsRepository.getUiTrip( - lineId, - lineType, - lineTripsUiState.referenceDateTime, - lineTripsUiState.directionFilter - ) - } - mutableUiState.update { it.copy(trip = data.third) } - updateAppWidgetState(context, id) { prefs -> - prefs[WidgetKeys.TRIP_INDEX] = data.second - prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] = data.first - prefs[WidgetKeys.IS_INITIAL_DATA_LOADED] = true - prefs[WidgetKeys.PREV_ENABLED] = data.second > 0 - prefs[WidgetKeys.NEXT_ENABLED] = - data.second < (prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] ?: 0) - 1 - mutableUiState.update { - it.copy( - prevEnabled = prefs[WidgetKeys.PREV_ENABLED] ?: false, - nextEnabled = prefs[WidgetKeys.NEXT_ENABLED] ?: false - ) - } - } - return@LaunchedEffect + isFavorite = historyDao.isFavorite( + isLine = true, + id = lineId, + type = lineType + ).value ?: false + if (lineTripsUiState.line == null || lineTripsUiState.line!!.lineId != lineId) { + loadLine(lineId, lineType) + } + if (tripIndex == null) { + mutableUiState.update { + it.copy(loading = true, error = false) + } + setReferenceDateTimeAsync(lineTripsUiState.referenceDateTime, context, id) + mutableUiState.update { + it.copy(loading = false) } + return@LaunchedEffect + } + loadIndex(tripIndex, context, id) + /*try { if (lineTripsUiState.directionFilter == Direction.ForwardAndBackward) { val (fetchedTrip, network) = withContext(Dispatchers.IO) { tripsRepository.getUiTrip( @@ -196,7 +205,7 @@ class MyAppWidget : GlanceAppWidget() { tripIndex ) } - mutableUiState.update { it.copy(trip = fetchedTrip) } + mutableUiState.update { it.copy(trip = fetchedTrip, tripIndex = tripIndex) } if (!network) { updateTrip(tripIndex, mutableUiState, fetchedTrip) } @@ -228,7 +237,12 @@ class MyAppWidget : GlanceAppWidget() { } return@LaunchedEffect } - mutableUiState.update { it.copy(trip = data.first) } + mutableUiState.update { + it.copy( + trip = data.first, + tripIndex = data.second + ) + } if (!data.third) { updateTrip(tripIndex, mutableUiState, data.first) } @@ -242,18 +256,59 @@ class MyAppWidget : GlanceAppWidget() { mutableUiState.update { it.copy(error = true) } } finally { mutableUiState.update { it.copy(loading = false) } + }*/ + } + + LaunchedEffect(storedDirectionFilter) { + if (storedDirectionFilter == null) return@LaunchedEffect + val newDirectionFilter = Direction.valueOf(storedDirectionFilter) + mutableUiState.update { it.copy(directionFilter = newDirectionFilter) } + + if (newDirectionFilter == Direction.ForwardAndBackward) { + val state = uiState.value + if (state.trip == null) { + // the trip can be null if there is no trip in that direction + loadIndex(state.tripIndex, context, id) + } else { + // no need to load the trip, as it's already loaded + mutableUiState.update { + it.copy( + prevEnabled = state.tripIndex > 0, + nextEnabled = state.tripIndex < uiState.value.tripsInDayCount - 1, + directionFilter = newDirectionFilter, + ) + } + } + + } else { + val state = uiState.value + if (state.trip?.direction != newDirectionFilter) { + // we need to load another trip, since the current one has the wrong direction + loadIndex(state.tripIndex, context, id) + updateAppWidgetState(context, id) { prefs -> + prefs[WidgetKeys.TRIP_INDEX] = lineTripsUiState.tripIndex + prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] = lineTripsUiState.tripsInDayCount + prefs[WidgetKeys.IS_INITIAL_DATA_LOADED] = true + prefs[WidgetKeys.PREV_ENABLED] = lineTripsUiState.tripIndex > 0 + prefs[WidgetKeys.NEXT_ENABLED] = + lineTripsUiState.tripIndex < (prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] + ?: 0) - 1 + } + } } } LaunchedEffect(refreshTimestamp) { // updating trip if (refreshTimestamp == 0L || tripIndex == null || lineTripsUiState.trip == null) return@LaunchedEffect - updateTrip(tripIndex, mutableUiState, lineTripsUiState.trip!!) + //updateTrip(tripIndex, mutableUiState, lineTripsUiState.trip!!) + cancelTripReloadJobAndLaunch { onReloadAsync(context, id) } } logInfo("${System.currentTimeMillis()}") logInfo("tripIndex: $tripIndex") logInfo("prevTripIndex: $prevTripIndex") logInfo("directionFilter: ${lineTripsUiState.directionFilter}") + logInfo("isFavorite: $isFavorite") GlanceTheme { LineTripsWidgetScreen( @@ -271,10 +326,288 @@ class MyAppWidget : GlanceAppWidget() { stopTypeToHighlight = null, prevEnabled = lineTripsUiState.prevEnabled, nextEnabled = lineTripsUiState.nextEnabled, - isFavorite = isFavorite + isFavorite = lineTripsUiState.line?.isFavorite ?: false + ) + } + } + } + + private fun loadLine(lineId: Int, lineType: StopLineType) { + // this job is independent from trip reloading jobs, so don't use cancelReloadJobAndLaunch + CoroutineScope(Dispatchers.IO).launch { + val line = withContext(Dispatchers.IO) { + try { + linesRepository.getUiLine(lineId, lineType) + .also { + if (it == null) { + logError( + "UI line (${lineId}, ${lineType}) not found" + ) + } + + // register a view for this line (assuming loadLine is called once) + historyDao.registerAccessed(true, lineId, lineType) + } + } catch (e: Throwable) { + logError("Could not load UI line (${lineId}, ${lineType})", e) + null + } + } + mutableUiState.update { it.copy(line = line) } + } + } + + private fun cancelTripReloadJobAndLaunch(tripLoadingFunction: suspend () -> Unit) { + tripReloadJob?.cancel() + tripReloadJob = CoroutineScope(Dispatchers.IO).launch { + // clear errors and set the state to "loading" before starting to load + mutableUiState.update { it.copy(loading = true, error = false) } + tripLoadingFunction() + // set "loading" to false when loading finishes (errors are set inside the function) + mutableUiState.update { it.copy(loading = false) } + } + } + + private fun loadIndex(index: Int, context: Context, id: GlanceId) { + logInfo("loadIndex: $index") + if (index >= 0 && index < uiState.value.tripsInDayCount) { + // only cancel any currently running job if there is something to do; the above + // condition will be false e.g. when there are no trips in a day (but not only for that) + cancelTripReloadJobAndLaunch { + loadIndexAsync(index, context, id) + } + } + } + + private suspend fun loadIndexAsync(index: Int, context: Context, id: GlanceId) { + // hide the current trip, as it's going to change + val prevState = mutableUiState.getAndUpdate { + it.copy( + tripIndex = index, + trip = null, + prevEnabled = index > 0, + nextEnabled = index < uiState.value.tripsInDayCount - 1, + ) + } + + // load the trip, and show it right after it gets loaded (see the related functions) + val (trip, network) = if (prevState.directionFilter == Direction.ForwardAndBackward) { + loadIndexNoFilterAsync(index,context,id) + } else { + loadIndexDirectionAsync(index, prevState, context, id) + } + + if (!network && trip != null && trip.completedStops < trip.stopTimes.size) { + // after showing the (possibly) outdated trip fast, reload it to show latest updates + // (but reload it only if there actually is a trip and it is not completed) + onReloadAsync(context, id) + } + } + + private suspend fun setReferenceDateTimeAsync( + referenceDateTimeCurrentZone: ZonedDateTime, + context: Context, + id: GlanceId + ) { + val referenceDateTime = referenceDateTimeCurrentZone.withZoneSameInstant(ROME_ZONE_ID) + mutableUiState.update { + it.copy( + tripsInDayCount = 0, + tripIndex = 0, + trip = null, + prevEnabled = false, + nextEnabled = false, + referenceDateTime = referenceDateTime, + ) + } + + var error = false + val (tripsInDayCount, tripIndex, trip) = withContext(Dispatchers.IO) { + try { + tripsRepository.getUiTrip( + lineId = lineId, + lineType = lineType, + referenceDateTime = referenceDateTime, + directionFilter = uiState.value.directionFilter, + ) + } catch (e: Throwable) { + logError( + "Could not load trip for UI line (${mutableUiState.value.line?.lineId}, " + + "${mutableUiState.value.line?.type}) at time $referenceDateTime", + e ) + error = true + Triple(0, 0, null) } } + + mutableUiState.update { + it.copy( + tripsInDayCount = tripsInDayCount, + tripIndex = tripIndex, + trip = trip, + prevEnabled = tripIndex > 0, + nextEnabled = tripIndex < tripsInDayCount - 1, + referenceDateTime = referenceDateTime, + error = error, + ) + } + updateAppWidgetState(context, id) { prefs -> + prefs[WidgetKeys.TRIP_INDEX] = tripIndex + prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] = tripsInDayCount + prefs[WidgetKeys.IS_INITIAL_DATA_LOADED] = true + prefs[WidgetKeys.PREV_ENABLED] = tripIndex > 0 + prefs[WidgetKeys.NEXT_ENABLED] = tripIndex < tripsInDayCount - 1 + } + } + + private suspend fun onReloadAsync(context: Context, id: GlanceId) { + val previousTrip = uiState.value.trip + if (previousTrip == null) { + // this could happen if an error happened while loading initial/more trips + if (uiState.value.tripsInDayCount > 0) { + // more trips failed loading, try to load again the currently set index + loadIndexAsync(uiState.value.tripIndex, context, id) + } else { + // initial trips failed loading, try to load again the current day + setReferenceDateTimeAsync(uiState.value.referenceDateTime, context, id) + } + return + } + + val trip = withContext(Dispatchers.IO) { + try { + tripsRepository.reloadUiTrip( + uiTrip = previousTrip, + index = uiState.value.tripIndex, + referenceDateTime = uiState.value.referenceDateTime + ) + } catch (e: Throwable) { + logError( + "Could not load trip ${previousTrip.tripId} for UI line " + + "(${mutableUiState.value.line?.lineId}, ${mutableUiState.value.line?.type})", + e + ) + null + } + } + + if (trip == null) { + // keep previous trip intact, we don't want to hide information that we do have! + mutableUiState.update { it.copy(error = true) } + } else { + mutableUiState.update { it.copy(trip = trip) } + } + } + + private suspend fun loadIndexNoFilterAsync(index: Int,context: Context,id: GlanceId): Pair { + val res = withContext(Dispatchers.IO) { + try { + tripsRepository.getUiTrip( + lineId = mutableUiState.value.line?.lineId!!, + lineType = mutableUiState.value.line?.type!!, + referenceDateTime = uiState.value.referenceDateTime, + index = index, + ) + } catch (e: Throwable) { + logError( + "Could not load trip at index $index for UI line " + + "(${mutableUiState.value.line?.lineId}, ${mutableUiState.value.line?.type})", + e + ) + Pair(null, true /* <- useless when trip == null */) + } + } + + // show the trip even if loadedFromNetwork is false (in which case it could be outdated) + mutableUiState.update { + it.copy( + trip = res.first, + error = res.first == null, + ) + } + if (res.first != null) { + updateAppWidgetState(context, id) { prefs -> + prefs[WidgetKeys.TRIP_INDEX] = index + prefs[WidgetKeys.PREV_ENABLED] = index > 0 + prefs[WidgetKeys.NEXT_ENABLED] = + index < uiState.value.tripsInDayCount - 1 + } + } + + return res + } + + + private suspend fun loadIndexDirectionAsync( + index: Int, + prevState: LineTripsUiState, + context: Context, + id: GlanceId + ): Pair { + val res = withContext(Dispatchers.IO) { + try { + tripsRepository.getUiTripWithDirection( + lineId = mutableUiState.value.line?.lineId!!, + lineType = mutableUiState.value.line?.type!!, + referenceDateTime = uiState.value.referenceDateTime, + index = index, + direction = prevState.directionFilter, + prevIndex = prevState.tripIndex, + ) + } catch (e: Throwable) { + logError( + "Could not load trip at index $index for UI line " + + "(${mutableUiState.value.line?.lineId}, ${mutableUiState.value.line?.type})" + + "in direction ${prevState.directionFilter}", + e + ) + null + } + } + + if (res == null) { + // no trip could be loaded in the set direction, restore previous state, + // but update prevEnabled or nextEnabled + mutableUiState.update { + it.copy( + tripIndex = prevState.tripIndex, + trip = prevState.trip, + prevEnabled = if (index < prevState.tripIndex) false else prevState.prevEnabled, + nextEnabled = if (index > prevState.tripIndex) false else prevState.nextEnabled, + ) + } + updateAppWidgetState(context, id) { prefs -> + prefs[WidgetKeys.TRIP_INDEX] = prevState.tripIndex + if (index < prevState.tripIndex) { + prefs[WidgetKeys.PREV_ENABLED] = false + } + if (index > prevState.tripIndex) { + prefs[WidgetKeys.NEXT_ENABLED] = false + } + } + + return Pair(null, true /* <- useless when trip == null */) + + } else { + val (trip, newIndex, loadedFromNetwork) = res + mutableUiState.update { + it.copy( + tripIndex = newIndex, + trip = trip, + prevEnabled = newIndex > 0, + nextEnabled = newIndex < uiState.value.tripsInDayCount - 1, + ) + } + updateAppWidgetState(context, id) { prefs -> + prefs[WidgetKeys.TRIP_INDEX] = newIndex + prefs[WidgetKeys.PREV_ENABLED] = newIndex > 0 + prefs[WidgetKeys.NEXT_ENABLED] = + newIndex < uiState.value.tripsInDayCount - 1 + } + + return Pair(trip, loadedFromNetwork) + } } private suspend fun updateTrip( diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt index 1643998..a243d1e 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt @@ -8,6 +8,7 @@ import androidx.glance.appwidget.state.updateAppWidgetState import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import org.stypox.tridenta.db.HistoryDao import org.stypox.tridenta.enums.Direction import org.stypox.tridenta.log.logInfo import org.stypox.tridenta.repo.LineTripsRepository @@ -19,6 +20,7 @@ import org.stypox.tridenta.widget.MyAppWidget interface WidgetEntryPoint { fun lineTripsRepository(): LineTripsRepository fun lineRepository(): LinesRepository + fun historyDao(): HistoryDao // Add HistoryDao and LinesRepository here too } @@ -103,5 +105,6 @@ class ToggleDirectionAction : ActionCallback { prefs[WidgetKeys.DIRECTION_FILTER] = newDirectionFilter.name } MyAppWidget().update(context, glanceId) + logInfo("ToggleDirectionAction performed") } } \ No newline at end of file From cef83c09f8ef73896e6bb1ba29ceb47334ace9a1 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Tue, 10 Mar 2026 09:37:14 +0100 Subject: [PATCH 18/43] fix: Resolved the infinite loading problem --- .../org/stypox/tridenta/widget/MyAppWidget.kt | 163 ++++++------------ .../tridenta/widget/actions/WidgetActions.kt | 17 +- .../tridenta/widget/actions/WidgetKeys.kt | 1 + 3 files changed, 58 insertions(+), 123 deletions(-) diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt index dc93e2f..279697b 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt @@ -7,9 +7,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.datastore.preferences.core.Preferences import androidx.glance.GlanceId import androidx.glance.GlanceTheme @@ -93,8 +90,7 @@ class MyAppWidget : GlanceAppWidget() { private var tripReloadJob: Job? = null override suspend fun provideGlance( - context: Context, - id: GlanceId + context: Context, id: GlanceId ) { val hiltEntryPoint = EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) @@ -116,23 +112,23 @@ class MyAppWidget : GlanceAppWidget() { provideContent { val lineTripsUiState by uiState.collectAsState() val prefs = currentState() - var isFavorite by remember { mutableStateOf(false) } //var lineType by remember { mutableStateOf(StopLineType.Urban) } lineId = prefs[WidgetKeys.LINE_ID] ?: -1 val lineTypeString = prefs[WidgetKeys.LINE_TYPE] if (lineTypeString != null) lineType = StopLineType.valueOf(lineTypeString) val tripIndex = prefs[WidgetKeys.TRIP_INDEX] + if (tripIndex != null && lineTripsUiState.tripIndex != tripIndex) { + mutableUiState.update { + it.copy( + tripIndex = tripIndex + ) + } + } + val toggledDirection = prefs[WidgetKeys.TOGGLED_DIRECTION] ?: false val prevTripIndex = prefs[WidgetKeys.PREV_TRIP_INDEX] val refreshTimestamp = prefs[WidgetKeys.REFRESH_TIMESTAMP] ?: 0L - val storedDirectionFilter = prefs[WidgetKeys.DIRECTION_FILTER] - /*if (storedDirectionFilter != null) mutableUiState.update { - it.copy( - directionFilter = - Direction.valueOf(storedDirectionFilter) - ) - }*/ val prevEnabledStored = prefs[WidgetKeys.PREV_ENABLED] if (prevEnabledStored != null) mutableUiState.update { it.copy(prevEnabled = prevEnabledStored) @@ -143,59 +139,36 @@ class MyAppWidget : GlanceAppWidget() { nextEnabled = nextEnabledStored ) } - /*if(lineId != -1 && lineTypeString != null) isFavorite = historyDao.isFavorite( + val tripsInDayCount = prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] + if (tripsInDayCount != null) { + mutableUiState.update { + it.copy(tripsInDayCount = tripsInDayCount) + } + }/*if(lineId != -1 && lineTypeString != null) isFavorite = historyDao.isFavorite( isLine = true, id = lineId, type = lineType ).value ?: false*/ - /*LaunchedEffect(lineId, lineTypeString) { // retrieving line - if (lineId == null || lineTypeString == null) { - return@LaunchedEffect - } - try { - val fetchedLine = withContext(Dispatchers.IO) { - linesRepository.getUiLine(lineId, lineType) - } - - mutableUiState.update { - it.copy(line = fetchedLine) - } - fetchedLine?.let { isFavorite = it.isFavorite } - } catch (e: Exception) { - mutableUiState.update { it.copy(error = true) } - logError(e.message!!, e.cause) - } - }*/ - - LaunchedEffect( - lineId, - tripIndex, - prevTripIndex - ) { // retrieving trip + LaunchedEffect(lineId, lineTypeString) { // retrieving line if (lineId == -1 || lineTypeString == null) { return@LaunchedEffect } - isFavorite = historyDao.isFavorite( - isLine = true, - id = lineId, - type = lineType - ).value ?: false if (lineTripsUiState.line == null || lineTripsUiState.line!!.lineId != lineId) { loadLine(lineId, lineType) } + } + + LaunchedEffect( + lineId, tripIndex, prevTripIndex, tripsInDayCount + ) { // retrieving trip if (tripIndex == null) { - mutableUiState.update { - it.copy(loading = true, error = false) - } - setReferenceDateTimeAsync(lineTripsUiState.referenceDateTime, context, id) - mutableUiState.update { - it.copy(loading = false) + cancelTripReloadJobAndLaunch { + setReferenceDateTimeAsync(lineTripsUiState.referenceDateTime, context, id) } return@LaunchedEffect } - loadIndex(tripIndex, context, id) - /*try { + loadIndex(tripIndex, context, id)/*try { if (lineTripsUiState.directionFilter == Direction.ForwardAndBackward) { val (fetchedTrip, network) = withContext(Dispatchers.IO) { tripsRepository.getUiTrip( @@ -260,13 +233,14 @@ class MyAppWidget : GlanceAppWidget() { } LaunchedEffect(storedDirectionFilter) { - if (storedDirectionFilter == null) return@LaunchedEffect + if (storedDirectionFilter == null || !toggledDirection) return@LaunchedEffect val newDirectionFilter = Direction.valueOf(storedDirectionFilter) mutableUiState.update { it.copy(directionFilter = newDirectionFilter) } if (newDirectionFilter == Direction.ForwardAndBackward) { val state = uiState.value if (state.trip == null) { + logInfo("updating direction if") // the trip can be null if there is no trip in that direction loadIndex(state.tripIndex, context, id) } else { @@ -283,6 +257,7 @@ class MyAppWidget : GlanceAppWidget() { } else { val state = uiState.value if (state.trip?.direction != newDirectionFilter) { + logInfo("updating direction else") // we need to load another trip, since the current one has the wrong direction loadIndex(state.tripIndex, context, id) updateAppWidgetState(context, id) { prefs -> @@ -296,6 +271,9 @@ class MyAppWidget : GlanceAppWidget() { } } } + updateAppWidgetState(context, id) { prefs -> + prefs[WidgetKeys.TOGGLED_DIRECTION] = false + } } LaunchedEffect(refreshTimestamp) { // updating trip @@ -306,9 +284,11 @@ class MyAppWidget : GlanceAppWidget() { logInfo("${System.currentTimeMillis()}") logInfo("tripIndex: $tripIndex") + logInfo("trip: ${lineTripsUiState.trip}") logInfo("prevTripIndex: $prevTripIndex") logInfo("directionFilter: ${lineTripsUiState.directionFilter}") - logInfo("isFavorite: $isFavorite") + logInfo("isFavorite: ${lineTripsUiState.line?.isFavorite}") + logInfo("isLoading: ${lineTripsUiState.loading}") GlanceTheme { LineTripsWidgetScreen( @@ -337,8 +317,7 @@ class MyAppWidget : GlanceAppWidget() { CoroutineScope(Dispatchers.IO).launch { val line = withContext(Dispatchers.IO) { try { - linesRepository.getUiLine(lineId, lineType) - .also { + linesRepository.getUiLine(lineId, lineType).also { if (it == null) { logError( "UI line (${lineId}, ${lineType}) not found" @@ -392,7 +371,7 @@ class MyAppWidget : GlanceAppWidget() { // load the trip, and show it right after it gets loaded (see the related functions) val (trip, network) = if (prevState.directionFilter == Direction.ForwardAndBackward) { - loadIndexNoFilterAsync(index,context,id) + loadIndexNoFilterAsync(index, context, id) } else { loadIndexDirectionAsync(index, prevState, context, id) } @@ -405,9 +384,7 @@ class MyAppWidget : GlanceAppWidget() { } private suspend fun setReferenceDateTimeAsync( - referenceDateTimeCurrentZone: ZonedDateTime, - context: Context, - id: GlanceId + referenceDateTimeCurrentZone: ZonedDateTime, context: Context, id: GlanceId ) { val referenceDateTime = referenceDateTimeCurrentZone.withZoneSameInstant(ROME_ZONE_ID) mutableUiState.update { @@ -432,8 +409,7 @@ class MyAppWidget : GlanceAppWidget() { ) } catch (e: Throwable) { logError( - "Could not load trip for UI line (${mutableUiState.value.line?.lineId}, " + - "${mutableUiState.value.line?.type}) at time $referenceDateTime", + "Could not load trip for UI line (${mutableUiState.value.line?.lineId}, " + "${mutableUiState.value.line?.type}) at time $referenceDateTime", e ) error = true @@ -484,8 +460,7 @@ class MyAppWidget : GlanceAppWidget() { ) } catch (e: Throwable) { logError( - "Could not load trip ${previousTrip.tripId} for UI line " + - "(${mutableUiState.value.line?.lineId}, ${mutableUiState.value.line?.type})", + "Could not load trip ${previousTrip.tripId} for UI line " + "(${mutableUiState.value.line?.lineId}, ${mutableUiState.value.line?.type})", e ) null @@ -500,19 +475,20 @@ class MyAppWidget : GlanceAppWidget() { } } - private suspend fun loadIndexNoFilterAsync(index: Int,context: Context,id: GlanceId): Pair { + private suspend fun loadIndexNoFilterAsync( + index: Int, context: Context, id: GlanceId + ): Pair { val res = withContext(Dispatchers.IO) { try { tripsRepository.getUiTrip( - lineId = mutableUiState.value.line?.lineId!!, - lineType = mutableUiState.value.line?.type!!, + lineId = lineId, + lineType = lineType, referenceDateTime = uiState.value.referenceDateTime, index = index, ) } catch (e: Throwable) { logError( - "Could not load trip at index $index for UI line " + - "(${mutableUiState.value.line?.lineId}, ${mutableUiState.value.line?.type})", + "Could not load trip at index $index for UI line " + "(${lineId}, ${lineType})", e ) Pair(null, true /* <- useless when trip == null */) @@ -530,8 +506,7 @@ class MyAppWidget : GlanceAppWidget() { updateAppWidgetState(context, id) { prefs -> prefs[WidgetKeys.TRIP_INDEX] = index prefs[WidgetKeys.PREV_ENABLED] = index > 0 - prefs[WidgetKeys.NEXT_ENABLED] = - index < uiState.value.tripsInDayCount - 1 + prefs[WidgetKeys.NEXT_ENABLED] = index < uiState.value.tripsInDayCount - 1 } } @@ -540,10 +515,7 @@ class MyAppWidget : GlanceAppWidget() { private suspend fun loadIndexDirectionAsync( - index: Int, - prevState: LineTripsUiState, - context: Context, - id: GlanceId + index: Int, prevState: LineTripsUiState, context: Context, id: GlanceId ): Pair { val res = withContext(Dispatchers.IO) { try { @@ -557,9 +529,7 @@ class MyAppWidget : GlanceAppWidget() { ) } catch (e: Throwable) { logError( - "Could not load trip at index $index for UI line " + - "(${mutableUiState.value.line?.lineId}, ${mutableUiState.value.line?.type})" + - "in direction ${prevState.directionFilter}", + "Could not load trip at index $index for UI line " + "(${mutableUiState.value.line?.lineId}, ${mutableUiState.value.line?.type})" + "in direction ${prevState.directionFilter}", e ) null @@ -602,37 +572,12 @@ class MyAppWidget : GlanceAppWidget() { updateAppWidgetState(context, id) { prefs -> prefs[WidgetKeys.TRIP_INDEX] = newIndex prefs[WidgetKeys.PREV_ENABLED] = newIndex > 0 - prefs[WidgetKeys.NEXT_ENABLED] = - newIndex < uiState.value.tripsInDayCount - 1 + prefs[WidgetKeys.NEXT_ENABLED] = newIndex < uiState.value.tripsInDayCount - 1 } return Pair(trip, loadedFromNetwork) } } - - private suspend fun updateTrip( - tripIndex: Int, - mutableUiState: MutableStateFlow, - trip: UiTrip - ) { - mutableUiState.update { it.copy(loading = true) } - try { - val freshTrip = withContext(Dispatchers.IO) { - tripsRepository.reloadUiTrip( - trip, - tripIndex, - mutableUiState.value.referenceDateTime - ) - } - - mutableUiState.update { it.copy(trip = freshTrip) } - } catch (e: Exception) { - mutableUiState.update { it.copy(error = true) } - logError(e.message!!, e.cause) - } finally { - mutableUiState.update { it.copy(loading = false) } - } - } } @OptIn(ExperimentalGlancePreviewApi::class) @@ -654,8 +599,7 @@ fun MyWidgetPreview() { wheelchairAccessible = false, cardinalPoint = CardinalPoint.North, isFavorite = false, - ), - DbStop( + ), DbStop( stopId = 1, latitude = 0.0, longitude = 0.0, @@ -666,8 +610,7 @@ fun MyWidgetPreview() { wheelchairAccessible = true, cardinalPoint = null, isFavorite = true, - ), - DbStop( + ), DbStop( stopId = 2, latitude = 0.0, longitude = 0.0, @@ -678,8 +621,7 @@ fun MyWidgetPreview() { wheelchairAccessible = false, cardinalPoint = CardinalPoint.NorthWest, isFavorite = true, - ), - DbStop( + ), DbStop( stopId = 3, latitude = 0.0, longitude = 0.0, @@ -748,8 +690,7 @@ fun MyWidgetPreview() { isFavorite = false ), ) - val referenceDateTime = - OffsetDateTime.of(2022, 9, 26, 9, 33, 17, 328943849, ZoneOffset.UTC) + val referenceDateTime = OffsetDateTime.of(2022, 9, 26, 9, 33, 17, 328943849, ZoneOffset.UTC) // Provide mock data to your content GlanceTheme { TripViewGlance( diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt index a243d1e..71387b4 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt @@ -26,9 +26,7 @@ interface WidgetEntryPoint { class NextTripAction : ActionCallback { override suspend fun onAction( - context: Context, - glanceId: GlanceId, - parameters: ActionParameters + context: Context, glanceId: GlanceId, parameters: ActionParameters ) { updateAppWidgetState(context, glanceId) { prefs -> val currentIndex = prefs[WidgetKeys.TRIP_INDEX] ?: 0 @@ -51,9 +49,7 @@ class NextTripAction : ActionCallback { class PrevTripAction : ActionCallback { override suspend fun onAction( - context: Context, - glanceId: GlanceId, - parameters: ActionParameters + context: Context, glanceId: GlanceId, parameters: ActionParameters ) { updateAppWidgetState(context, glanceId) { prefs -> val currentIndex = prefs[WidgetKeys.TRIP_INDEX] ?: 0 @@ -76,9 +72,7 @@ class PrevTripAction : ActionCallback { class ReloadTripAction : ActionCallback { override suspend fun onAction( - context: Context, - glanceId: GlanceId, - parameters: ActionParameters + context: Context, glanceId: GlanceId, parameters: ActionParameters ) { updateAppWidgetState(context, glanceId) { prefs -> prefs[WidgetKeys.REFRESH_TIMESTAMP] = System.currentTimeMillis() @@ -89,9 +83,7 @@ class ReloadTripAction : ActionCallback { class ToggleDirectionAction : ActionCallback { override suspend fun onAction( - context: Context, - glanceId: GlanceId, - parameters: ActionParameters + context: Context, glanceId: GlanceId, parameters: ActionParameters ) { updateAppWidgetState(context, glanceId) { prefs -> val actualDirectionFilter = prefs[WidgetKeys.DIRECTION_FILTER] @@ -103,6 +95,7 @@ class ToggleDirectionAction : ActionCallback { Direction.ForwardAndBackward -> Direction.Forward } prefs[WidgetKeys.DIRECTION_FILTER] = newDirectionFilter.name + prefs[WidgetKeys.TOGGLED_DIRECTION] = true } MyAppWidget().update(context, glanceId) logInfo("ToggleDirectionAction performed") diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt index 893502e..8a9d6fa 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt @@ -16,6 +16,7 @@ object WidgetKeys { val IS_LOADING = booleanPreferencesKey("is_loading") val IS_INITIAL_DATA_LOADED = booleanPreferencesKey("is_initial_data_loaded") val HAS_ERROR = booleanPreferencesKey("has_error") + val TOGGLED_DIRECTION = booleanPreferencesKey("toggled_direction") val PREV_ENABLED = booleanPreferencesKey("prev_enabled") val NEXT_ENABLED = booleanPreferencesKey("next_enabled") From 70d7985e84b5cf4fdefffcb473cc01ecba246b0a Mon Sep 17 00:00:00 2001 From: whyKVD Date: Tue, 10 Mar 2026 12:23:56 +0100 Subject: [PATCH 19/43] fix: Fixed the wrong update of the tripIndex --- .../org/stypox/tridenta/widget/MyAppWidget.kt | 109 ++++-------------- 1 file changed, 21 insertions(+), 88 deletions(-) diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt index 279697b..a0afd2e 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt @@ -118,13 +118,6 @@ class MyAppWidget : GlanceAppWidget() { val lineTypeString = prefs[WidgetKeys.LINE_TYPE] if (lineTypeString != null) lineType = StopLineType.valueOf(lineTypeString) val tripIndex = prefs[WidgetKeys.TRIP_INDEX] - if (tripIndex != null && lineTripsUiState.tripIndex != tripIndex) { - mutableUiState.update { - it.copy( - tripIndex = tripIndex - ) - } - } val toggledDirection = prefs[WidgetKeys.TOGGLED_DIRECTION] ?: false val prevTripIndex = prefs[WidgetKeys.PREV_TRIP_INDEX] val refreshTimestamp = prefs[WidgetKeys.REFRESH_TIMESTAMP] ?: 0L @@ -144,11 +137,7 @@ class MyAppWidget : GlanceAppWidget() { mutableUiState.update { it.copy(tripsInDayCount = tripsInDayCount) } - }/*if(lineId != -1 && lineTypeString != null) isFavorite = historyDao.isFavorite( - isLine = true, - id = lineId, - type = lineType - ).value ?: false*/ + } LaunchedEffect(lineId, lineTypeString) { // retrieving line if (lineId == -1 || lineTypeString == null) { @@ -168,68 +157,7 @@ class MyAppWidget : GlanceAppWidget() { } return@LaunchedEffect } - loadIndex(tripIndex, context, id)/*try { - if (lineTripsUiState.directionFilter == Direction.ForwardAndBackward) { - val (fetchedTrip, network) = withContext(Dispatchers.IO) { - tripsRepository.getUiTrip( - lineId, - lineType, - lineTripsUiState.referenceDateTime, - tripIndex - ) - } - mutableUiState.update { it.copy(trip = fetchedTrip, tripIndex = tripIndex) } - if (!network) { - updateTrip(tripIndex, mutableUiState, fetchedTrip) - } - } else { - if (prevTripIndex == null) { - prevTripIndex = tripIndex - } - val data = withContext(Dispatchers.IO) { - tripsRepository.getUiTripWithDirection( - lineId, - lineType, - lineTripsUiState.referenceDateTime, - lineTripsUiState.directionFilter, - tripIndex, - prevTripIndex - ) - } - if (data == null) { - updateAppWidgetState(context, id) { prefs -> - prefs[WidgetKeys.TRIP_INDEX] = prevTripIndex - if (tripIndex < prevTripIndex) { - prefs[WidgetKeys.PREV_ENABLED] = false - mutableUiState.update { it.copy(prevEnabled = false) } - } - if (tripIndex > prevTripIndex) { - prefs[WidgetKeys.NEXT_ENABLED] = false - mutableUiState.update { it.copy(nextEnabled = false) } - } - } - return@LaunchedEffect - } - mutableUiState.update { - it.copy( - trip = data.first, - tripIndex = data.second - ) - } - if (!data.third) { - updateTrip(tripIndex, mutableUiState, data.first) - } - updateAppWidgetState(context, id) { prefs -> - prefs[WidgetKeys.TRIP_INDEX] = data.second - prefs[WidgetKeys.PREV_TRIP_INDEX] = tripIndex - } - } - } catch (e: Exception) { - logError(e.message!!, e.cause) - mutableUiState.update { it.copy(error = true) } - } finally { - mutableUiState.update { it.copy(loading = false) } - }*/ + loadIndex(tripIndex, context, id) } LaunchedEffect(storedDirectionFilter) { @@ -240,7 +168,6 @@ class MyAppWidget : GlanceAppWidget() { if (newDirectionFilter == Direction.ForwardAndBackward) { val state = uiState.value if (state.trip == null) { - logInfo("updating direction if") // the trip can be null if there is no trip in that direction loadIndex(state.tripIndex, context, id) } else { @@ -257,18 +184,21 @@ class MyAppWidget : GlanceAppWidget() { } else { val state = uiState.value if (state.trip?.direction != newDirectionFilter) { - logInfo("updating direction else") // we need to load another trip, since the current one has the wrong direction loadIndex(state.tripIndex, context, id) + logInfo("state.tripIndex: ${state.tripIndex}") + logInfo("lineTripsUiState.tripIndex: ${lineTripsUiState.tripIndex}") updateAppWidgetState(context, id) { prefs -> prefs[WidgetKeys.TRIP_INDEX] = lineTripsUiState.tripIndex + prefs[WidgetKeys.PREV_TRIP_INDEX] = state.tripIndex prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] = lineTripsUiState.tripsInDayCount prefs[WidgetKeys.IS_INITIAL_DATA_LOADED] = true + prefs[WidgetKeys.DIRECTION_FILTER] = lineTripsUiState.directionFilter.name prefs[WidgetKeys.PREV_ENABLED] = lineTripsUiState.tripIndex > 0 prefs[WidgetKeys.NEXT_ENABLED] = - lineTripsUiState.tripIndex < (prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] - ?: 0) - 1 + lineTripsUiState.tripIndex < lineTripsUiState.tripsInDayCount - 1 } + this@MyAppWidget.update(context,id) } } updateAppWidgetState(context, id) { prefs -> @@ -284,6 +214,7 @@ class MyAppWidget : GlanceAppWidget() { logInfo("${System.currentTimeMillis()}") logInfo("tripIndex: $tripIndex") + logInfo("lineTripsUiState.tripIndex: ${lineTripsUiState.tripIndex}") logInfo("trip: ${lineTripsUiState.trip}") logInfo("prevTripIndex: $prevTripIndex") logInfo("directionFilter: ${lineTripsUiState.directionFilter}") @@ -318,15 +249,15 @@ class MyAppWidget : GlanceAppWidget() { val line = withContext(Dispatchers.IO) { try { linesRepository.getUiLine(lineId, lineType).also { - if (it == null) { - logError( - "UI line (${lineId}, ${lineType}) not found" - ) - } - - // register a view for this line (assuming loadLine is called once) - historyDao.registerAccessed(true, lineId, lineType) + if (it == null) { + logError( + "UI line (${lineId}, ${lineType}) not found" + ) } + + // register a view for this line (assuming loadLine is called once) + historyDao.registerAccessed(true, lineId, lineType) + } } catch (e: Throwable) { logError("Could not load UI line (${lineId}, ${lineType})", e) null @@ -435,6 +366,7 @@ class MyAppWidget : GlanceAppWidget() { prefs[WidgetKeys.PREV_ENABLED] = tripIndex > 0 prefs[WidgetKeys.NEXT_ENABLED] = tripIndex < tripsInDayCount - 1 } + logInfo("setReferenceDateTimeAsync tripIndex: $tripIndex") } private suspend fun onReloadAsync(context: Context, id: GlanceId) { @@ -520,8 +452,8 @@ class MyAppWidget : GlanceAppWidget() { val res = withContext(Dispatchers.IO) { try { tripsRepository.getUiTripWithDirection( - lineId = mutableUiState.value.line?.lineId!!, - lineType = mutableUiState.value.line?.type!!, + lineId = lineId, + lineType = lineType, referenceDateTime = uiState.value.referenceDateTime, index = index, direction = prevState.directionFilter, @@ -571,6 +503,7 @@ class MyAppWidget : GlanceAppWidget() { } updateAppWidgetState(context, id) { prefs -> prefs[WidgetKeys.TRIP_INDEX] = newIndex + prefs[WidgetKeys.PREV_TRIP_INDEX] = prevState.tripIndex prefs[WidgetKeys.PREV_ENABLED] = newIndex > 0 prefs[WidgetKeys.NEXT_ENABLED] = newIndex < uiState.value.tripsInDayCount - 1 } From fd345270fdbab95a8819661ebf942fcfcbf3bc68 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Wed, 11 Mar 2026 15:29:35 +0100 Subject: [PATCH 20/43] refactoring: Added the StopLineTypeIcon and changed some tint --- .../org/stypox/tridenta/widget/MyAppWidget.kt | 2 +- .../tridenta/widget/ui/LineTripGlance.kt | 11 ++++---- .../tridenta/widget/ui/TripViewGlance.kt | 26 ++++++++++++++++--- .../tridenta/widget/ui/TripViewStopsGlance.kt | 10 +++---- app/src/main/res/drawable/landscape.xml | 5 ++++ app/src/main/res/drawable/location_city.xml | 5 ++++ 6 files changed, 42 insertions(+), 17 deletions(-) create mode 100644 app/src/main/res/drawable/landscape.xml create mode 100644 app/src/main/res/drawable/location_city.xml diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt index a0afd2e..dbb1c63 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt @@ -97,7 +97,7 @@ class MyAppWidget : GlanceAppWidget() { tripsRepository = hiltEntryPoint.lineTripsRepository() linesRepository = hiltEntryPoint.lineRepository() historyDao = hiltEntryPoint.historyDao() - //referenceDateTime = ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID) + mutableUiState.update { it.copy( referenceDateTime = ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID) diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt index e9db635..a2a9f97 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt @@ -96,7 +96,6 @@ fun WidgetAppBar( Row( modifier = GlanceModifier .fillMaxWidth() - .clickable(onLineClickAction) .padding(horizontal = 12.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { @@ -111,6 +110,8 @@ fun WidgetAppBar( val shortNameBackground = line.color.toLineColor() val textColor = textColorOnBackground(shortNameBackground) Row( + modifier = GlanceModifier + .clickable(onLineClickAction), verticalAlignment = Alignment.CenterVertically, ) { Text( @@ -149,9 +150,9 @@ fun WidgetAppBar( // News/Warning Icon if (line != null && line.newsItems.isNotEmpty()) { Image( - provider = ImageProvider(R.drawable.warning_filled), // Ensure you have this XML drawable + provider = ImageProvider(R.drawable.warning_filled), contentDescription = "News", - colorFilter = ColorFilter.tint(GlanceTheme.colors.primary), + colorFilter = ColorFilter.tint(GlanceTheme.colors.onSurface), modifier = GlanceModifier .padding(end = 8.dp) ) @@ -161,7 +162,7 @@ fun WidgetAppBar( Image( provider = ImageProvider(getDirectionDrawable(directionFilter)), contentDescription = "Toggle Direction", - colorFilter = ColorFilter.tint(GlanceTheme.colors.primary), + colorFilter = ColorFilter.tint(GlanceTheme.colors.onSurfaceVariant), modifier = GlanceModifier .padding(end = 8.dp) .clickable(onDirectionAction) @@ -172,7 +173,7 @@ fun WidgetAppBar( provider = ImageProvider( if (isFavorite) R.drawable.favorite_filled else R.drawable.favorite ), - colorFilter = ColorFilter.tint(GlanceTheme.colors.primary), + colorFilter = ColorFilter.tint(GlanceTheme.colors.onSurface), contentDescription = context.getString(R.string.favorite), ) } diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt index f9aecdd..6d68960 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt @@ -160,6 +160,25 @@ fun TripViewGlance( } } +@Composable +fun StopLineTypeIcon(stopLineType: StopLineType,context: Context, modifier: GlanceModifier = GlanceModifier) { + Image( + provider = ImageProvider( + when (stopLineType) { + StopLineType.Urban -> R.drawable.location_city + StopLineType.Suburban -> R.drawable.landscape + } + ), + contentDescription = context.getString( + when (stopLineType) { + StopLineType.Urban -> R.string.urban + StopLineType.Suburban -> R.string.suburban + } + ), + colorFilter = ColorFilter.tint(GlanceTheme.colors.onSurface), + modifier = modifier, + ) +} @Composable fun DirectionIconGlance( direction: Direction, @@ -181,7 +200,7 @@ fun DirectionIconGlance( Direction.ForwardAndBackward -> R.string.forward_and_backward } ), - colorFilter = ColorFilter.tint(GlanceTheme.colors.primary), + colorFilter = ColorFilter.tint(GlanceTheme.colors.onSurface), modifier = modifier ) } @@ -252,6 +271,7 @@ private fun TripViewTopRowGlance( } Column { + StopLineTypeIcon(trip.type,context) DirectionIconGlance(trip.direction, context) } } @@ -465,7 +485,7 @@ private fun TripViewPreview() { busId = 886, ), error = false, - loading = true, + loading = false, onReloadAction = actionStartActivity(), onPrevAction = actionStartActivity(), onNextAction = actionStartActivity(), @@ -473,7 +493,6 @@ private fun TripViewPreview() { nextEnabled = true, stopIdToHighlight = null, stopTypeToHighlight = null, - //onDirectionClickAction = actionStartActivity(), ) } } @@ -495,7 +514,6 @@ private fun TripViewPreviewLoading() { nextEnabled = true, stopIdToHighlight = null, stopTypeToHighlight = null, - //onDirectionClickAction = actionStartActivity() ) } } \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt index 010a031..dcfef90 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt @@ -13,6 +13,7 @@ import androidx.glance.appwidget.lazy.itemsIndexed import androidx.glance.layout.Alignment import androidx.glance.layout.Row import androidx.glance.layout.Spacer +import androidx.glance.layout.fillMaxWidth import androidx.glance.layout.height import androidx.glance.layout.padding import androidx.glance.layout.size @@ -58,11 +59,6 @@ fun TripViewStopsGlance( stopTime.stop.type == stopTypeToHighlight, completed = index < trip.completedStops, stopTime = stopTime, - modifier = if (stopTime.stop == null || onStopClick == null) { - GlanceModifier // not clickable, since there is no stop - } else { - GlanceModifier // TODO: aggiungere clickable - } ) } @@ -108,7 +104,7 @@ private fun TripViewStopItemGlance( val context = LocalContext.current Row( verticalAlignment = Alignment.CenterVertically, - modifier = modifier.padding(horizontal = 16.dp, vertical = 0.dp) + modifier = modifier.padding(horizontal = 16.dp, vertical = 0.dp).fillMaxWidth() ) { Image( provider = ImageProvider( @@ -162,7 +158,7 @@ private fun TripViewStopItemGlance( fontWeight = if (highlight) FontWeight.Bold else null, color = textColor ), - modifier = GlanceModifier + modifier = GlanceModifier.defaultWeight() .run { if (stopTime.arrivalTime == null && stopTime.departureTime == null) { this // do not apply end padding if there is nothing after diff --git a/app/src/main/res/drawable/landscape.xml b/app/src/main/res/drawable/landscape.xml new file mode 100644 index 0000000..6f1d078 --- /dev/null +++ b/app/src/main/res/drawable/landscape.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/location_city.xml b/app/src/main/res/drawable/location_city.xml new file mode 100644 index 0000000..9a13d6f --- /dev/null +++ b/app/src/main/res/drawable/location_city.xml @@ -0,0 +1,5 @@ + + + + + From 7cc8821dd579bfeb0bb864432c139bbcfc0a945c Mon Sep 17 00:00:00 2001 From: whyKVD Date: Thu, 12 Mar 2026 12:44:44 +0100 Subject: [PATCH 21/43] refactoring: tried to decuple the state from the ui --- .../org/stypox/tridenta/widget/MyAppWidget.kt | 394 ++---------------- .../org/stypox/tridenta/widget/WidgetModel.kt | 359 ++++++++++++++++ .../tridenta/widget/actions/WidgetKeys.kt | 2 + 3 files changed, 393 insertions(+), 362 deletions(-) create mode 100644 app/src/main/java/org/stypox/tridenta/widget/WidgetModel.kt diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt index dbb1c63..19eb8e8 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt @@ -20,89 +20,34 @@ import androidx.glance.appwidget.state.updateAppWidgetState import androidx.glance.currentState import androidx.glance.preview.ExperimentalGlancePreviewApi import androidx.glance.preview.Preview -import androidx.glance.state.PreferencesGlanceStateDefinition -import dagger.hilt.android.EntryPointAccessors -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.stypox.tridenta.db.HistoryDao import org.stypox.tridenta.db.data.DbLine import org.stypox.tridenta.db.data.DbStop import org.stypox.tridenta.enums.Area import org.stypox.tridenta.enums.CardinalPoint import org.stypox.tridenta.enums.Direction import org.stypox.tridenta.enums.StopLineType -import org.stypox.tridenta.extractor.ROME_ZONE_ID -import org.stypox.tridenta.log.logError import org.stypox.tridenta.log.logInfo -import org.stypox.tridenta.repo.LineTripsRepository -import org.stypox.tridenta.repo.LinesRepository import org.stypox.tridenta.repo.data.UiStopTime import org.stypox.tridenta.repo.data.UiTrip import org.stypox.tridenta.ui.MainActivity -import org.stypox.tridenta.ui.line_trips.LineTripsUiState import org.stypox.tridenta.widget.actions.NextTripAction import org.stypox.tridenta.widget.actions.PrevTripAction import org.stypox.tridenta.widget.actions.ReloadTripAction import org.stypox.tridenta.widget.actions.ToggleDirectionAction -import org.stypox.tridenta.widget.actions.WidgetEntryPoint import org.stypox.tridenta.widget.actions.WidgetKeys import org.stypox.tridenta.widget.ui.LineTripsWidgetScreen import org.stypox.tridenta.widget.ui.TripViewGlance import java.time.OffsetDateTime import java.time.ZoneOffset -import java.time.ZonedDateTime -import kotlin.properties.Delegates class MyAppWidget : GlanceAppWidget() { - override val stateDefinition = PreferencesGlanceStateDefinition - private lateinit var tripsRepository: LineTripsRepository - private lateinit var linesRepository: LinesRepository - private lateinit var historyDao: HistoryDao - - //private lateinit var referenceDateTime: ZonedDateTime - private val mutableUiState = MutableStateFlow( - LineTripsUiState( - line = null, - tripsInDayCount = 0, - tripIndex = 0, - trip = null, - prevEnabled = false, - nextEnabled = false, - referenceDateTime = ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID), - directionFilter = Direction.ForwardAndBackward, - stopIdToHighlight = null, - stopTypeToHighlight = null, - loading = true, - error = false, - ) - ) - val uiState = mutableUiState.asStateFlow() - private var lineId by Delegates.notNull() - private lateinit var lineType: StopLineType - - private var tripReloadJob: Job? = null - + lateinit var model: WidgetModel override suspend fun provideGlance( context: Context, id: GlanceId ) { - val hiltEntryPoint = - EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) - tripsRepository = hiltEntryPoint.lineTripsRepository() - linesRepository = hiltEntryPoint.lineRepository() - historyDao = hiltEntryPoint.historyDao() - - mutableUiState.update { - it.copy( - referenceDateTime = ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID) - ) - } + model = WidgetModel(context, id) + model.initState() val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) val configIntent = Intent(context, WidgetConfigurationActivity::class.java).apply { @@ -110,82 +55,67 @@ class MyAppWidget : GlanceAppWidget() { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK } provideContent { - val lineTripsUiState by uiState.collectAsState() + val lineTripsUiState by model.uiState.collectAsState() val prefs = currentState() - //var lineType by remember { mutableStateOf(StopLineType.Urban) } - lineId = prefs[WidgetKeys.LINE_ID] ?: -1 + val lineId = prefs[WidgetKeys.LINE_ID] ?: -1 val lineTypeString = prefs[WidgetKeys.LINE_TYPE] - if (lineTypeString != null) lineType = StopLineType.valueOf(lineTypeString) val tripIndex = prefs[WidgetKeys.TRIP_INDEX] val toggledDirection = prefs[WidgetKeys.TOGGLED_DIRECTION] ?: false val prevTripIndex = prefs[WidgetKeys.PREV_TRIP_INDEX] val refreshTimestamp = prefs[WidgetKeys.REFRESH_TIMESTAMP] ?: 0L val storedDirectionFilter = prefs[WidgetKeys.DIRECTION_FILTER] - val prevEnabledStored = prefs[WidgetKeys.PREV_ENABLED] - if (prevEnabledStored != null) mutableUiState.update { - it.copy(prevEnabled = prevEnabledStored) - } - val nextEnabledStored = prefs[WidgetKeys.NEXT_ENABLED] - if (nextEnabledStored != null) mutableUiState.update { - it.copy( - nextEnabled = nextEnabledStored - ) - } val tripsInDayCount = prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] - if (tripsInDayCount != null) { - mutableUiState.update { - it.copy(tripsInDayCount = tripsInDayCount) - } - } - LaunchedEffect(lineId, lineTypeString) { // retrieving line + LaunchedEffect(lineId) { // retrieving line if (lineId == -1 || lineTypeString == null) { return@LaunchedEffect } + model.initState() if (lineTripsUiState.line == null || lineTripsUiState.line!!.lineId != lineId) { - loadLine(lineId, lineType) + model.loadLine() } } LaunchedEffect( - lineId, tripIndex, prevTripIndex, tripsInDayCount + model.lineId, tripIndex, prevTripIndex, tripsInDayCount ) { // retrieving trip if (tripIndex == null) { - cancelTripReloadJobAndLaunch { - setReferenceDateTimeAsync(lineTripsUiState.referenceDateTime, context, id) + model.cancelTripReloadJobAndLaunch { + model.setReferenceDateTimeAsync(lineTripsUiState.referenceDateTime) } return@LaunchedEffect } - loadIndex(tripIndex, context, id) + model.loadIndex(tripIndex) + model.initState() } LaunchedEffect(storedDirectionFilter) { if (storedDirectionFilter == null || !toggledDirection) return@LaunchedEffect val newDirectionFilter = Direction.valueOf(storedDirectionFilter) - mutableUiState.update { it.copy(directionFilter = newDirectionFilter) } + model.mutableUiState.update { it.copy(directionFilter = newDirectionFilter) } if (newDirectionFilter == Direction.ForwardAndBackward) { - val state = uiState.value + val state = model.uiState.value if (state.trip == null) { // the trip can be null if there is no trip in that direction - loadIndex(state.tripIndex, context, id) + model.loadIndex(state.tripIndex) } else { // no need to load the trip, as it's already loaded - mutableUiState.update { + model.mutableUiState.update { it.copy( prevEnabled = state.tripIndex > 0, - nextEnabled = state.tripIndex < uiState.value.tripsInDayCount - 1, + nextEnabled = state.tripIndex < model.uiState.value.tripsInDayCount - 1, directionFilter = newDirectionFilter, ) } } } else { - val state = uiState.value + val state = model.uiState.value if (state.trip?.direction != newDirectionFilter) { // we need to load another trip, since the current one has the wrong direction - loadIndex(state.tripIndex, context, id) + model.loadIndex(state.tripIndex) logInfo("state.tripIndex: ${state.tripIndex}") logInfo("lineTripsUiState.tripIndex: ${lineTripsUiState.tripIndex}") updateAppWidgetState(context, id) { prefs -> @@ -193,12 +123,13 @@ class MyAppWidget : GlanceAppWidget() { prefs[WidgetKeys.PREV_TRIP_INDEX] = state.tripIndex prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] = lineTripsUiState.tripsInDayCount prefs[WidgetKeys.IS_INITIAL_DATA_LOADED] = true - prefs[WidgetKeys.DIRECTION_FILTER] = lineTripsUiState.directionFilter.name + prefs[WidgetKeys.DIRECTION_FILTER] = + lineTripsUiState.directionFilter.name prefs[WidgetKeys.PREV_ENABLED] = lineTripsUiState.tripIndex > 0 prefs[WidgetKeys.NEXT_ENABLED] = lineTripsUiState.tripIndex < lineTripsUiState.tripsInDayCount - 1 } - this@MyAppWidget.update(context,id) + this@MyAppWidget.update(context, id) } } updateAppWidgetState(context, id) { prefs -> @@ -206,10 +137,18 @@ class MyAppWidget : GlanceAppWidget() { } } + if (storedDirectionFilter != null && Direction.valueOf(storedDirectionFilter) != lineTripsUiState.directionFilter) { + model.mutableUiState.update { + it.copy( + directionFilter = Direction.valueOf(storedDirectionFilter) + ) + } + } + LaunchedEffect(refreshTimestamp) { // updating trip if (refreshTimestamp == 0L || tripIndex == null || lineTripsUiState.trip == null) return@LaunchedEffect //updateTrip(tripIndex, mutableUiState, lineTripsUiState.trip!!) - cancelTripReloadJobAndLaunch { onReloadAsync(context, id) } + model.cancelTripReloadJobAndLaunch { model.onReloadAsync() } } logInfo("${System.currentTimeMillis()}") @@ -242,275 +181,6 @@ class MyAppWidget : GlanceAppWidget() { } } } - - private fun loadLine(lineId: Int, lineType: StopLineType) { - // this job is independent from trip reloading jobs, so don't use cancelReloadJobAndLaunch - CoroutineScope(Dispatchers.IO).launch { - val line = withContext(Dispatchers.IO) { - try { - linesRepository.getUiLine(lineId, lineType).also { - if (it == null) { - logError( - "UI line (${lineId}, ${lineType}) not found" - ) - } - - // register a view for this line (assuming loadLine is called once) - historyDao.registerAccessed(true, lineId, lineType) - } - } catch (e: Throwable) { - logError("Could not load UI line (${lineId}, ${lineType})", e) - null - } - } - mutableUiState.update { it.copy(line = line) } - } - } - - private fun cancelTripReloadJobAndLaunch(tripLoadingFunction: suspend () -> Unit) { - tripReloadJob?.cancel() - tripReloadJob = CoroutineScope(Dispatchers.IO).launch { - // clear errors and set the state to "loading" before starting to load - mutableUiState.update { it.copy(loading = true, error = false) } - tripLoadingFunction() - // set "loading" to false when loading finishes (errors are set inside the function) - mutableUiState.update { it.copy(loading = false) } - } - } - - private fun loadIndex(index: Int, context: Context, id: GlanceId) { - logInfo("loadIndex: $index") - if (index >= 0 && index < uiState.value.tripsInDayCount) { - // only cancel any currently running job if there is something to do; the above - // condition will be false e.g. when there are no trips in a day (but not only for that) - cancelTripReloadJobAndLaunch { - loadIndexAsync(index, context, id) - } - } - } - - private suspend fun loadIndexAsync(index: Int, context: Context, id: GlanceId) { - // hide the current trip, as it's going to change - val prevState = mutableUiState.getAndUpdate { - it.copy( - tripIndex = index, - trip = null, - prevEnabled = index > 0, - nextEnabled = index < uiState.value.tripsInDayCount - 1, - ) - } - - // load the trip, and show it right after it gets loaded (see the related functions) - val (trip, network) = if (prevState.directionFilter == Direction.ForwardAndBackward) { - loadIndexNoFilterAsync(index, context, id) - } else { - loadIndexDirectionAsync(index, prevState, context, id) - } - - if (!network && trip != null && trip.completedStops < trip.stopTimes.size) { - // after showing the (possibly) outdated trip fast, reload it to show latest updates - // (but reload it only if there actually is a trip and it is not completed) - onReloadAsync(context, id) - } - } - - private suspend fun setReferenceDateTimeAsync( - referenceDateTimeCurrentZone: ZonedDateTime, context: Context, id: GlanceId - ) { - val referenceDateTime = referenceDateTimeCurrentZone.withZoneSameInstant(ROME_ZONE_ID) - mutableUiState.update { - it.copy( - tripsInDayCount = 0, - tripIndex = 0, - trip = null, - prevEnabled = false, - nextEnabled = false, - referenceDateTime = referenceDateTime, - ) - } - - var error = false - val (tripsInDayCount, tripIndex, trip) = withContext(Dispatchers.IO) { - try { - tripsRepository.getUiTrip( - lineId = lineId, - lineType = lineType, - referenceDateTime = referenceDateTime, - directionFilter = uiState.value.directionFilter, - ) - } catch (e: Throwable) { - logError( - "Could not load trip for UI line (${mutableUiState.value.line?.lineId}, " + "${mutableUiState.value.line?.type}) at time $referenceDateTime", - e - ) - error = true - Triple(0, 0, null) - } - } - - mutableUiState.update { - it.copy( - tripsInDayCount = tripsInDayCount, - tripIndex = tripIndex, - trip = trip, - prevEnabled = tripIndex > 0, - nextEnabled = tripIndex < tripsInDayCount - 1, - referenceDateTime = referenceDateTime, - error = error, - ) - } - updateAppWidgetState(context, id) { prefs -> - prefs[WidgetKeys.TRIP_INDEX] = tripIndex - prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] = tripsInDayCount - prefs[WidgetKeys.IS_INITIAL_DATA_LOADED] = true - prefs[WidgetKeys.PREV_ENABLED] = tripIndex > 0 - prefs[WidgetKeys.NEXT_ENABLED] = tripIndex < tripsInDayCount - 1 - } - logInfo("setReferenceDateTimeAsync tripIndex: $tripIndex") - } - - private suspend fun onReloadAsync(context: Context, id: GlanceId) { - val previousTrip = uiState.value.trip - if (previousTrip == null) { - // this could happen if an error happened while loading initial/more trips - if (uiState.value.tripsInDayCount > 0) { - // more trips failed loading, try to load again the currently set index - loadIndexAsync(uiState.value.tripIndex, context, id) - } else { - // initial trips failed loading, try to load again the current day - setReferenceDateTimeAsync(uiState.value.referenceDateTime, context, id) - } - return - } - - val trip = withContext(Dispatchers.IO) { - try { - tripsRepository.reloadUiTrip( - uiTrip = previousTrip, - index = uiState.value.tripIndex, - referenceDateTime = uiState.value.referenceDateTime - ) - } catch (e: Throwable) { - logError( - "Could not load trip ${previousTrip.tripId} for UI line " + "(${mutableUiState.value.line?.lineId}, ${mutableUiState.value.line?.type})", - e - ) - null - } - } - - if (trip == null) { - // keep previous trip intact, we don't want to hide information that we do have! - mutableUiState.update { it.copy(error = true) } - } else { - mutableUiState.update { it.copy(trip = trip) } - } - } - - private suspend fun loadIndexNoFilterAsync( - index: Int, context: Context, id: GlanceId - ): Pair { - val res = withContext(Dispatchers.IO) { - try { - tripsRepository.getUiTrip( - lineId = lineId, - lineType = lineType, - referenceDateTime = uiState.value.referenceDateTime, - index = index, - ) - } catch (e: Throwable) { - logError( - "Could not load trip at index $index for UI line " + "(${lineId}, ${lineType})", - e - ) - Pair(null, true /* <- useless when trip == null */) - } - } - - // show the trip even if loadedFromNetwork is false (in which case it could be outdated) - mutableUiState.update { - it.copy( - trip = res.first, - error = res.first == null, - ) - } - if (res.first != null) { - updateAppWidgetState(context, id) { prefs -> - prefs[WidgetKeys.TRIP_INDEX] = index - prefs[WidgetKeys.PREV_ENABLED] = index > 0 - prefs[WidgetKeys.NEXT_ENABLED] = index < uiState.value.tripsInDayCount - 1 - } - } - - return res - } - - - private suspend fun loadIndexDirectionAsync( - index: Int, prevState: LineTripsUiState, context: Context, id: GlanceId - ): Pair { - val res = withContext(Dispatchers.IO) { - try { - tripsRepository.getUiTripWithDirection( - lineId = lineId, - lineType = lineType, - referenceDateTime = uiState.value.referenceDateTime, - index = index, - direction = prevState.directionFilter, - prevIndex = prevState.tripIndex, - ) - } catch (e: Throwable) { - logError( - "Could not load trip at index $index for UI line " + "(${mutableUiState.value.line?.lineId}, ${mutableUiState.value.line?.type})" + "in direction ${prevState.directionFilter}", - e - ) - null - } - } - - if (res == null) { - // no trip could be loaded in the set direction, restore previous state, - // but update prevEnabled or nextEnabled - mutableUiState.update { - it.copy( - tripIndex = prevState.tripIndex, - trip = prevState.trip, - prevEnabled = if (index < prevState.tripIndex) false else prevState.prevEnabled, - nextEnabled = if (index > prevState.tripIndex) false else prevState.nextEnabled, - ) - } - updateAppWidgetState(context, id) { prefs -> - prefs[WidgetKeys.TRIP_INDEX] = prevState.tripIndex - if (index < prevState.tripIndex) { - prefs[WidgetKeys.PREV_ENABLED] = false - } - if (index > prevState.tripIndex) { - prefs[WidgetKeys.NEXT_ENABLED] = false - } - } - - return Pair(null, true /* <- useless when trip == null */) - - } else { - val (trip, newIndex, loadedFromNetwork) = res - mutableUiState.update { - it.copy( - tripIndex = newIndex, - trip = trip, - prevEnabled = newIndex > 0, - nextEnabled = newIndex < uiState.value.tripsInDayCount - 1, - ) - } - updateAppWidgetState(context, id) { prefs -> - prefs[WidgetKeys.TRIP_INDEX] = newIndex - prefs[WidgetKeys.PREV_TRIP_INDEX] = prevState.tripIndex - prefs[WidgetKeys.PREV_ENABLED] = newIndex > 0 - prefs[WidgetKeys.NEXT_ENABLED] = newIndex < uiState.value.tripsInDayCount - 1 - } - - return Pair(trip, loadedFromNetwork) - } - } } @OptIn(ExperimentalGlancePreviewApi::class) diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetModel.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetModel.kt new file mode 100644 index 0000000..3337e34 --- /dev/null +++ b/app/src/main/java/org/stypox/tridenta/widget/WidgetModel.kt @@ -0,0 +1,359 @@ +package org.stypox.tridenta.widget + +import android.content.Context +import androidx.glance.GlanceId +import androidx.glance.appwidget.state.getAppWidgetState +import androidx.glance.appwidget.state.updateAppWidgetState +import androidx.glance.state.PreferencesGlanceStateDefinition +import dagger.hilt.android.EntryPointAccessors +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.getAndUpdate +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.stypox.tridenta.db.HistoryDao +import org.stypox.tridenta.enums.Direction +import org.stypox.tridenta.enums.StopLineType +import org.stypox.tridenta.extractor.ROME_ZONE_ID +import org.stypox.tridenta.log.logError +import org.stypox.tridenta.log.logInfo +import org.stypox.tridenta.repo.LineTripsRepository +import org.stypox.tridenta.repo.LinesRepository +import org.stypox.tridenta.repo.data.UiTrip +import org.stypox.tridenta.ui.line_trips.LineTripsUiState +import org.stypox.tridenta.widget.actions.WidgetEntryPoint +import org.stypox.tridenta.widget.actions.WidgetKeys +import java.time.ZonedDateTime +import kotlin.properties.Delegates + +class WidgetModel { + private val context: Context + private val glanceId: GlanceId + val tripsRepository: LineTripsRepository + val linesRepository: LinesRepository + val historyDao: HistoryDao + + val mutableUiState = MutableStateFlow( + LineTripsUiState( + line = null, + tripsInDayCount = 0, + tripIndex = 0, + trip = null, + prevEnabled = false, + nextEnabled = false, + referenceDateTime = ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID), + directionFilter = Direction.ForwardAndBackward, + stopIdToHighlight = null, + stopTypeToHighlight = null, + loading = true, + error = false, + ) + ) + val uiState = mutableUiState.asStateFlow() + var lineId by Delegates.notNull() + private lateinit var lineType: StopLineType + + private var tripReloadJob: Job? = null + + constructor(aContext: Context, aGlanceId: GlanceId) { + context = aContext + glanceId = aGlanceId + val hiltEntryPoint = + EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) + tripsRepository = hiltEntryPoint.lineTripsRepository() + linesRepository = hiltEntryPoint.lineRepository() + historyDao = hiltEntryPoint.historyDao() + } + + suspend fun initState() { + val prefs = getAppWidgetState( + context, + PreferencesGlanceStateDefinition, glanceId + ) + lineId = prefs[WidgetKeys.LINE_ID] ?: -1 + val lineTypeString = prefs[WidgetKeys.LINE_TYPE] ?: StopLineType.Urban.name + lineType = StopLineType.valueOf(lineTypeString) + mutableUiState.update { + it.copy( + tripIndex = prefs[WidgetKeys.TRIP_INDEX] ?: 0, + tripsInDayCount = prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] ?: 0, + prevEnabled = prefs[WidgetKeys.PREV_ENABLED] ?: false, + nextEnabled = prefs[WidgetKeys.NEXT_ENABLED] ?: false, + ) + } + } + + + fun loadLine() { + // this job is independent from trip reloading jobs, so don't use cancelReloadJobAndLaunch + CoroutineScope(Dispatchers.IO).launch { + val line = withContext(Dispatchers.IO) { + try { + linesRepository.getUiLine(lineId, lineType).also { + if (it == null) { + logError( + "UI line (${lineId}, ${lineType}) not found" + ) + } + + // register a view for this line (assuming loadLine is called once) + historyDao.registerAccessed(true, lineId, lineType) + } + } catch (e: Throwable) { + logError("Could not load UI line (${lineId}, ${lineType})", e) + null + } + } + mutableUiState.update { it.copy(line = line) } + } + } + + fun cancelTripReloadJobAndLaunch(tripLoadingFunction: suspend () -> Unit) { + tripReloadJob?.cancel() + tripReloadJob = CoroutineScope(Dispatchers.IO).launch { + // clear errors and set the state to "loading" before starting to load + mutableUiState.update { it.copy(loading = true, error = false) } + tripLoadingFunction() + // set "loading" to false when loading finishes (errors are set inside the function) + mutableUiState.update { it.copy(loading = false) } + } + } + + fun loadIndex(index: Int) { + logInfo("loadIndex: $index") + if (index >= 0 && index < uiState.value.tripsInDayCount) { + // only cancel any currently running job if there is something to do; the above + // condition will be false e.g. when there are no trips in a day (but not only for that) + cancelTripReloadJobAndLaunch { + loadIndexAsync(index) + } + } + } + + private suspend fun loadIndexAsync(index: Int) { + // hide the current trip, as it's going to change + val prevState = mutableUiState.getAndUpdate { + it.copy( + tripIndex = index, + trip = null, + prevEnabled = index > 0, + nextEnabled = index < uiState.value.tripsInDayCount - 1, + ) + } + + // load the trip, and show it right after it gets loaded (see the related functions) + val (trip, network) = if (prevState.directionFilter == Direction.ForwardAndBackward) { + loadIndexNoFilterAsync(index) + } else { + loadIndexDirectionAsync(index, prevState) + } + + if (!network && trip != null && trip.completedStops < trip.stopTimes.size) { + // after showing the (possibly) outdated trip fast, reload it to show latest updates + // (but reload it only if there actually is a trip and it is not completed) + onReloadAsync() + } + } + + suspend fun setReferenceDateTimeAsync( + referenceDateTimeCurrentZone: ZonedDateTime, + ) { + val referenceDateTime = referenceDateTimeCurrentZone.withZoneSameInstant(ROME_ZONE_ID) + mutableUiState.update { + it.copy( + tripsInDayCount = 0, + tripIndex = 0, + trip = null, + prevEnabled = false, + nextEnabled = false, + referenceDateTime = referenceDateTime, + ) + } + + var error = false + val (tripsInDayCount, tripIndex, trip) = withContext(Dispatchers.IO) { + try { + tripsRepository.getUiTrip( + lineId = lineId, + lineType = lineType, + referenceDateTime = referenceDateTime, + directionFilter = uiState.value.directionFilter, + ) + } catch (e: Throwable) { + logError( + "Could not load trip for UI line (${mutableUiState.value.line?.lineId}, " + "${mutableUiState.value.line?.type}) at time $referenceDateTime", + e + ) + error = true + Triple(0, 0, null) + } + } + + mutableUiState.update { + it.copy( + tripsInDayCount = tripsInDayCount, + tripIndex = tripIndex, + trip = trip, + prevEnabled = tripIndex > 0, + nextEnabled = tripIndex < tripsInDayCount - 1, + referenceDateTime = referenceDateTime, + error = error, + ) + } + updateAppWidgetState(context, glanceId) { prefs -> + prefs[WidgetKeys.TRIP_INDEX] = tripIndex + prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] = tripsInDayCount + prefs[WidgetKeys.IS_INITIAL_DATA_LOADED] = true + prefs[WidgetKeys.PREV_ENABLED] = tripIndex > 0 + prefs[WidgetKeys.NEXT_ENABLED] = tripIndex < tripsInDayCount - 1 + } + logInfo("setReferenceDateTimeAsync tripIndex: $tripIndex") + } + + suspend fun onReloadAsync() { + val previousTrip = uiState.value.trip + if (previousTrip == null) { + // this could happen if an error happened while loading initial/more trips + if (uiState.value.tripsInDayCount > 0) { + // more trips failed loading, try to load again the currently set index + loadIndexAsync(uiState.value.tripIndex) + } else { + // initial trips failed loading, try to load again the current day + setReferenceDateTimeAsync(uiState.value.referenceDateTime) + } + return + } + + val trip = withContext(Dispatchers.IO) { + try { + tripsRepository.reloadUiTrip( + uiTrip = previousTrip, + index = uiState.value.tripIndex, + referenceDateTime = uiState.value.referenceDateTime + ) + } catch (e: Throwable) { + logError( + "Could not load trip ${previousTrip.tripId} for UI line " + "(${mutableUiState.value.line?.lineId}, ${mutableUiState.value.line?.type})", + e + ) + null + } + } + + if (trip == null) { + // keep previous trip intact, we don't want to hide information that we do have! + mutableUiState.update { it.copy(error = true) } + } else { + mutableUiState.update { it.copy(trip = trip) } + } + } + + private suspend fun loadIndexNoFilterAsync( + index: Int + ): Pair { + val res = withContext(Dispatchers.IO) { + try { + tripsRepository.getUiTrip( + lineId = lineId, + lineType = lineType, + referenceDateTime = uiState.value.referenceDateTime, + index = index, + ) + } catch (e: Throwable) { + logError( + "Could not load trip at index $index for UI line " + "(${lineId}, ${lineType})", + e + ) + Pair(null, true /* <- useless when trip == null */) + } + } + + // show the trip even if loadedFromNetwork is false (in which case it could be outdated) + mutableUiState.update { + it.copy( + trip = res.first, + error = res.first == null, + ) + } + if (res.first != null) { + updateAppWidgetState(context, glanceId) { prefs -> + prefs[WidgetKeys.TRIP_INDEX] = index + prefs[WidgetKeys.PREV_ENABLED] = index > 0 + prefs[WidgetKeys.NEXT_ENABLED] = index < uiState.value.tripsInDayCount - 1 + } + } + + return res + } + + + private suspend fun loadIndexDirectionAsync( + index: Int, prevState: LineTripsUiState + ): Pair { + val res = withContext(Dispatchers.IO) { + try { + tripsRepository.getUiTripWithDirection( + lineId = lineId, + lineType = lineType, + referenceDateTime = uiState.value.referenceDateTime, + index = index, + direction = prevState.directionFilter, + prevIndex = prevState.tripIndex, + ) + } catch (e: Throwable) { + logError( + "Could not load trip at index $index for UI line " + "(${mutableUiState.value.line?.lineId}, ${mutableUiState.value.line?.type})" + "in direction ${prevState.directionFilter}", + e + ) + null + } + } + + if (res == null) { + // no trip could be loaded in the set direction, restore previous state, + // but update prevEnabled or nextEnabled + mutableUiState.update { + it.copy( + tripIndex = prevState.tripIndex, + trip = prevState.trip, + prevEnabled = if (index < prevState.tripIndex) false else prevState.prevEnabled, + nextEnabled = if (index > prevState.tripIndex) false else prevState.nextEnabled, + ) + } + updateAppWidgetState(context, glanceId) { prefs -> + prefs[WidgetKeys.TRIP_INDEX] = prevState.tripIndex + if (index < prevState.tripIndex) { + prefs[WidgetKeys.PREV_ENABLED] = false + } + if (index > prevState.tripIndex) { + prefs[WidgetKeys.NEXT_ENABLED] = false + } + } + + return Pair(null, true /* <- useless when trip == null */) + + } else { + val (trip, newIndex, loadedFromNetwork) = res + mutableUiState.update { + it.copy( + tripIndex = newIndex, + trip = trip, + prevEnabled = newIndex > 0, + nextEnabled = newIndex < uiState.value.tripsInDayCount - 1, + ) + } + updateAppWidgetState(context, glanceId) { prefs -> + prefs[WidgetKeys.TRIP_INDEX] = newIndex + prefs[WidgetKeys.PREV_TRIP_INDEX] = prevState.tripIndex + prefs[WidgetKeys.PREV_ENABLED] = newIndex > 0 + prefs[WidgetKeys.NEXT_ENABLED] = newIndex < uiState.value.tripsInDayCount - 1 + } + + return Pair(trip, loadedFromNetwork) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt index 8a9d6fa..a4b6727 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt @@ -17,6 +17,8 @@ object WidgetKeys { val IS_INITIAL_DATA_LOADED = booleanPreferencesKey("is_initial_data_loaded") val HAS_ERROR = booleanPreferencesKey("has_error") val TOGGLED_DIRECTION = booleanPreferencesKey("toggled_direction") + val CHANGED_LINE = booleanPreferencesKey("changed_line") + val CHANGED_TRIP = booleanPreferencesKey("changed_trip") val PREV_ENABLED = booleanPreferencesKey("prev_enabled") val NEXT_ENABLED = booleanPreferencesKey("next_enabled") From b7bf11452feba3901315dd5fbdb73cab1975cdde Mon Sep 17 00:00:00 2001 From: whyKVD Date: Fri, 20 Mar 2026 15:34:01 +0100 Subject: [PATCH 22/43] feat: Changed the minSdk from 21 to 23 Changed the minSdk from 21 to 23 because of library compatibility --- app/build.gradle | 13 ++++++++++++- build.gradle | 1 + gradle/libs.versions.toml | 20 +++++++++++++++++--- gradle/wrapper/gradle-wrapper.properties | 2 +- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index edf0f8a..9986467 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,6 +4,7 @@ plugins { alias(libs.plugins.com.google.devtools.ksp) alias(libs.plugins.com.google.dagger.hilt.android) alias(libs.plugins.compose.compiler) + alias(libs.plugins.kotlin.serialization) } android { @@ -11,7 +12,7 @@ android { defaultConfig { applicationId "org.stypox.tridenta" - minSdk 21 + minSdk 23 targetSdk 36 versionCode 60 versionName "1.5" @@ -65,8 +66,14 @@ hilt { } dependencies { + implementation libs.androidx.work.runtime.ktx + implementation libs.androidx.hilt.common + implementation libs.androidx.hilt.work + implementation libs.androidx.hilt.compiler coreLibraryDesugaring libs.desugar.jdk.libs + implementation libs.work + // Core implementation libs.core.ktx implementation libs.lifecycle.runtime.ktx @@ -74,6 +81,10 @@ dependencies { implementation libs.okhttp implementation libs.preference.ktx + // Serialization + implementation libs.kotlin.serialization + implementation libs.kotlin.serialization.json + // Compose and Material3 implementation libs.activity.compose implementation libs.compose.ui diff --git a/build.gradle b/build.gradle index 00a9d59..8a2d9f3 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,7 @@ plugins { alias(libs.plugins.org.jetbrains.kotlin.android) apply false alias(libs.plugins.com.google.devtools.ksp) apply false alias(libs.plugins.compose.compiler) apply false + alias(libs.plugins.kotlin.serialization) apply false } tasks.register('clean', Delete) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 37f5fa0..511cb48 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.9.1" +agp = "8.13.0" composeDestinations = "1.9.54" espressoCore = "3.6.1" hilt = "2.55" @@ -16,10 +16,16 @@ material3 = "1.3.1" okhttp = "4.12.0" preference = "1.2.1" compose = "1.7.8" -kotlin = "2.1.10" -ksp = "2.1.10-1.0.30" +kotlin = "2.2.21" +ksp = "2.3.0" java = "11" glance = "1.1.1" +kotlinSerializationPlugin = "2.2.21" +kotlinSerialization = "1.9.0" +work = "2.10.0" +workRuntimeKtx = "2.11.1" +hiltCommon = "1.2.0" +hiltWork = "1.2.0" [libraries] activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } @@ -55,8 +61,16 @@ glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = glance-material3 = { module = "androidx.glance:glance-material3", version.ref = "glance" } glance-preview = { module = "androidx.glance:glance-preview", version.ref = "glance" } glance-appwidget-preview = { module = "androidx.glance:glance-appwidget-preview", version.ref = "glance" } +kotlin-serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-core", version.ref = "kotlinSerialization" } +kotlin-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinSerialization" } +work = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" } +androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workRuntimeKtx" } +androidx-hilt-common = { group = "androidx.hilt", name = "hilt-common", version.ref = "hiltCommon" } +androidx-hilt-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "hiltWork" } +androidx-hilt-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltWork"} [plugins] +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerializationPlugin"} com-android-application = { id = "com.android.application", version.ref = "agp" } org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } com-google-devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e2847c8..37f853b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 9af08796f824a47350268312c89af7e793f15322 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Fri, 20 Mar 2026 15:38:08 +0100 Subject: [PATCH 23/43] feat: Created WidgetState and modified some models Created a WidgetState class and modified some models to make them serializable and also created some utils to serialize ZoneDateTime and OffsetDateTime classes --- .../org/stypox/tridenta/db/data/DbLine.kt | 6 +++ .../org/stypox/tridenta/db/data/DbStop.kt | 2 + .../org/stypox/tridenta/repo/data/UiLine.kt | 3 +- .../org/stypox/tridenta/repo/data/UiTrip.kt | 7 +++ .../stypox/tridenta/util/SerializerUtils.kt | 36 +++++++++++++ .../widget/LineTripWidgetStateDefinition.kt | 53 +++++++++++++++++++ .../org/stypox/tridenta/widget/WidgetState.kt | 32 +++++++++++ 7 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/org/stypox/tridenta/util/SerializerUtils.kt create mode 100644 app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetStateDefinition.kt create mode 100644 app/src/main/java/org/stypox/tridenta/widget/WidgetState.kt diff --git a/app/src/main/java/org/stypox/tridenta/db/data/DbLine.kt b/app/src/main/java/org/stypox/tridenta/db/data/DbLine.kt index 8d099a1..8c991bf 100644 --- a/app/src/main/java/org/stypox/tridenta/db/data/DbLine.kt +++ b/app/src/main/java/org/stypox/tridenta/db/data/DbLine.kt @@ -5,13 +5,16 @@ import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.Ignore import androidx.room.Index +import kotlinx.serialization.Serializable import org.stypox.tridenta.enums.Area import org.stypox.tridenta.enums.StopLineType +import org.stypox.tridenta.util.OffsetDateTimeSerializer import java.time.OffsetDateTime @Entity( primaryKeys = ["lineId", "type"] ) +@Serializable data class DbLine( // some testing exposed that a line is always identified by the (lineId, type) tuple val lineId: Int, @@ -49,9 +52,12 @@ data class DbLine( Index("lineId", "lineType") ] ) +@Serializable data class DbNewsItem( val serviceType: String, + @Serializable(with = OffsetDateTimeSerializer::class) val startDate: OffsetDateTime, + @Serializable(with = OffsetDateTimeSerializer::class) val endDate: OffsetDateTime, val header: String, val details: String, diff --git a/app/src/main/java/org/stypox/tridenta/db/data/DbStop.kt b/app/src/main/java/org/stypox/tridenta/db/data/DbStop.kt index 59ce92e..b7709a0 100644 --- a/app/src/main/java/org/stypox/tridenta/db/data/DbStop.kt +++ b/app/src/main/java/org/stypox/tridenta/db/data/DbStop.kt @@ -1,12 +1,14 @@ package org.stypox.tridenta.db.data import androidx.room.* +import kotlinx.serialization.Serializable import org.stypox.tridenta.enums.CardinalPoint import org.stypox.tridenta.enums.StopLineType @Entity( primaryKeys = ["stopId", "type"] ) +@Serializable data class DbStop( // some testing exposed that a stop is always identified by the (stopId, type) tuple val stopId: Int, diff --git a/app/src/main/java/org/stypox/tridenta/repo/data/UiLine.kt b/app/src/main/java/org/stypox/tridenta/repo/data/UiLine.kt index b872708..0059ca8 100644 --- a/app/src/main/java/org/stypox/tridenta/repo/data/UiLine.kt +++ b/app/src/main/java/org/stypox/tridenta/repo/data/UiLine.kt @@ -1,13 +1,14 @@ package org.stypox.tridenta.repo.data import androidx.annotation.ColorInt +import kotlinx.serialization.Serializable import org.stypox.tridenta.db.data.DbLine import org.stypox.tridenta.db.data.DbNewsItem import org.stypox.tridenta.enums.Area import org.stypox.tridenta.enums.StopLineType import java.util.regex.Pattern import kotlin.math.min - +@Serializable data class UiLine( val lineId: Int, val type: StopLineType, diff --git a/app/src/main/java/org/stypox/tridenta/repo/data/UiTrip.kt b/app/src/main/java/org/stypox/tridenta/repo/data/UiTrip.kt index a6e8bb6..7b88448 100644 --- a/app/src/main/java/org/stypox/tridenta/repo/data/UiTrip.kt +++ b/app/src/main/java/org/stypox/tridenta/repo/data/UiTrip.kt @@ -1,19 +1,23 @@ package org.stypox.tridenta.repo.data +import kotlinx.serialization.Serializable import org.stypox.tridenta.db.data.DbLine import org.stypox.tridenta.db.data.DbStop import org.stypox.tridenta.enums.Direction import org.stypox.tridenta.enums.StopLineType import org.stypox.tridenta.extractor.data.ExStopTime import org.stypox.tridenta.extractor.data.ExTrip +import org.stypox.tridenta.util.OffsetDateTimeSerializer import java.time.OffsetDateTime /** * Same as [ExTrip] but with line and stop data loaded */ +@Serializable data class UiTrip( val delay: Int, val direction: Direction, + @Serializable(with = OffsetDateTimeSerializer::class) val lastEventReceivedAt: OffsetDateTime?, val lineId: Int, // needed for when DbLine could not be loaded, and so it is null val line: DbLine?, @@ -28,8 +32,11 @@ data class UiTrip( /** * Same as [ExStopTime] but with stop data loaded */ + @Serializable data class UiStopTime( + @Serializable(with = OffsetDateTimeSerializer::class) val arrivalTime: OffsetDateTime?, + @Serializable(with = OffsetDateTimeSerializer::class) val departureTime: OffsetDateTime?, val stop: DbStop?, ) diff --git a/app/src/main/java/org/stypox/tridenta/util/SerializerUtils.kt b/app/src/main/java/org/stypox/tridenta/util/SerializerUtils.kt new file mode 100644 index 0000000..976457f --- /dev/null +++ b/app/src/main/java/org/stypox/tridenta/util/SerializerUtils.kt @@ -0,0 +1,36 @@ +package org.stypox.tridenta.util + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.time.OffsetDateTime +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +object ZonedDateTimeSerializer : KSerializer { + override val descriptor = PrimitiveSerialDescriptor("ZonedDateTime", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: ZonedDateTime) { + encoder.encodeString(value.toString()) + } + + override fun deserialize(decoder: Decoder): ZonedDateTime { + return ZonedDateTime.parse(decoder.decodeString()) + } +} + +object OffsetDateTimeSerializer : KSerializer { + override val descriptor = PrimitiveSerialDescriptor("OffsetDateTime", PrimitiveKind.STRING) + + private val formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME + + override fun serialize(encoder: Encoder, value: OffsetDateTime) { + encoder.encodeString(value.format(formatter)) + } + + override fun deserialize(decoder: Decoder): OffsetDateTime { + return OffsetDateTime.parse(decoder.decodeString(), formatter) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetStateDefinition.kt b/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetStateDefinition.kt new file mode 100644 index 0000000..b6f1b5f --- /dev/null +++ b/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetStateDefinition.kt @@ -0,0 +1,53 @@ +package org.stypox.tridenta.widget + +import android.content.Context +import androidx.datastore.core.CorruptionException +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.core.Serializer +import androidx.datastore.dataStoreFile +import androidx.glance.state.GlanceStateDefinition +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import org.stypox.tridenta.log.logError +import java.io.File +import java.io.InputStream +import java.io.OutputStream + +object LineTripWidgetStateDefinition : GlanceStateDefinition { + private const val DATA_STORE_FILENAME_PREFIX = "line_trip_widget_state_" + override suspend fun getDataStore( + context: Context, + fileKey: String + ): DataStore = DataStoreFactory.create( + serializer = WidgetStateSerializer, + produceFile = { getLocation(context, fileKey) } + ) + + override fun getLocation( + context: Context, + fileKey: String + ): File = + context.dataStoreFile(DATA_STORE_FILENAME_PREFIX + fileKey.lowercase()) + + object WidgetStateSerializer : Serializer { + override suspend fun readFrom(input: InputStream): WidgetState = try { + Json.decodeFromString(WidgetState.serializer(), input.readBytes().decodeToString()) + } catch (exception: SerializationException) { + logError(exception.message ?: "Could not read widget state", exception.cause) + throw CorruptionException("Could not read widget state: ${exception.message}") + } + + override suspend fun writeTo( + t: WidgetState, + output: OutputStream + ) { + output.use { + it.write(Json.encodeToString(WidgetState.serializer(), t).encodeToByteArray()) + } + } + + override val defaultValue: WidgetState + get() = WidgetState.Unavailable("Something went wrong") + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetState.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetState.kt new file mode 100644 index 0000000..33c60e0 --- /dev/null +++ b/app/src/main/java/org/stypox/tridenta/widget/WidgetState.kt @@ -0,0 +1,32 @@ +package org.stypox.tridenta.widget + +import kotlinx.serialization.Serializable +import org.stypox.tridenta.enums.Direction +import org.stypox.tridenta.repo.data.UiLine +import org.stypox.tridenta.repo.data.UiTrip +import org.stypox.tridenta.util.ZonedDateTimeSerializer +import java.time.ZonedDateTime + +@Serializable +sealed interface WidgetState { + @Serializable + data object Loading : WidgetState + + @Serializable + data class Available( + val line: UiLine?, + val trip: UiTrip?, + @Serializable(with = ZonedDateTimeSerializer::class) + val referenceDateTime: ZonedDateTime, + val tripsInDayCount: Int = 0, + val tripIndex: Int = 0, + val prevEnabled: Boolean = false, + val nextEnabled: Boolean = false, + val directionFilter: Direction = Direction.ForwardAndBackward, + val error: Boolean = false, + val loading: Boolean = true, + ) : WidgetState + + @Serializable + data class Unavailable(val message: String) : WidgetState +} \ No newline at end of file From 2cd1438c70acf08df3f8aa1e531a2733aaffc567 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Fri, 20 Mar 2026 15:41:37 +0100 Subject: [PATCH 24/43] feat: Created a worker to update the widget Created a worker to update the widget every 15 minutes, renamed the classes to something a bit more self-explanatory and started using the WidgetState class --- .../{MyAppWidget.kt => LineTripWidget.kt} | 166 ++++-------------- .../tridenta/widget/LineTripWidgetReceiver.kt | 26 +++ .../tridenta/widget/LineTripWidgetWorker.kt | 107 +++++++++++ .../tridenta/widget/MyAppWidgetReceiver.kt | 10 -- 4 files changed, 168 insertions(+), 141 deletions(-) rename app/src/main/java/org/stypox/tridenta/widget/{MyAppWidget.kt => LineTripWidget.kt} (50%) create mode 100644 app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetReceiver.kt create mode 100644 app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetWorker.kt delete mode 100644 app/src/main/java/org/stypox/tridenta/widget/MyAppWidgetReceiver.kt diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/LineTripWidget.kt similarity index 50% rename from app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt rename to app/src/main/java/org/stypox/tridenta/widget/LineTripWidget.kt index 19eb8e8..8f31d2c 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/LineTripWidget.kt @@ -4,11 +4,9 @@ import android.appwidget.AppWidgetManager import android.content.Context import android.content.Intent import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.datastore.preferences.core.Preferences +import androidx.compose.ui.unit.dp import androidx.glance.GlanceId +import androidx.glance.GlanceModifier import androidx.glance.GlanceTheme import androidx.glance.action.actionStartActivity import androidx.glance.appwidget.GlanceAppWidget @@ -16,18 +14,19 @@ import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.action.actionRunCallback import androidx.glance.appwidget.action.actionStartActivity import androidx.glance.appwidget.provideContent -import androidx.glance.appwidget.state.updateAppWidgetState import androidx.glance.currentState +import androidx.glance.layout.padding import androidx.glance.preview.ExperimentalGlancePreviewApi import androidx.glance.preview.Preview -import kotlinx.coroutines.flow.update +import androidx.glance.text.Text +import androidx.glance.text.TextStyle +import org.stypox.tridenta.R import org.stypox.tridenta.db.data.DbLine import org.stypox.tridenta.db.data.DbStop import org.stypox.tridenta.enums.Area import org.stypox.tridenta.enums.CardinalPoint import org.stypox.tridenta.enums.Direction import org.stypox.tridenta.enums.StopLineType -import org.stypox.tridenta.log.logInfo import org.stypox.tridenta.repo.data.UiStopTime import org.stypox.tridenta.repo.data.UiTrip import org.stypox.tridenta.ui.MainActivity @@ -35,19 +34,17 @@ import org.stypox.tridenta.widget.actions.NextTripAction import org.stypox.tridenta.widget.actions.PrevTripAction import org.stypox.tridenta.widget.actions.ReloadTripAction import org.stypox.tridenta.widget.actions.ToggleDirectionAction -import org.stypox.tridenta.widget.actions.WidgetKeys +import org.stypox.tridenta.widget.theme.SmallCircularProgressIndicatorGlance import org.stypox.tridenta.widget.ui.LineTripsWidgetScreen import org.stypox.tridenta.widget.ui.TripViewGlance import java.time.OffsetDateTime import java.time.ZoneOffset -class MyAppWidget : GlanceAppWidget() { - lateinit var model: WidgetModel +class LineTripWidget : GlanceAppWidget() { + override val stateDefinition = LineTripWidgetStateDefinition override suspend fun provideGlance( context: Context, id: GlanceId ) { - model = WidgetModel(context, id) - model.initState() val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) val configIntent = Intent(context, WidgetConfigurationActivity::class.java).apply { @@ -55,130 +52,37 @@ class MyAppWidget : GlanceAppWidget() { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK } provideContent { - val lineTripsUiState by model.uiState.collectAsState() - val prefs = currentState() + val state = currentState() - val lineId = prefs[WidgetKeys.LINE_ID] ?: -1 - val lineTypeString = prefs[WidgetKeys.LINE_TYPE] - val tripIndex = prefs[WidgetKeys.TRIP_INDEX] - val toggledDirection = prefs[WidgetKeys.TOGGLED_DIRECTION] ?: false - val prevTripIndex = prefs[WidgetKeys.PREV_TRIP_INDEX] - val refreshTimestamp = prefs[WidgetKeys.REFRESH_TIMESTAMP] ?: 0L - val storedDirectionFilter = prefs[WidgetKeys.DIRECTION_FILTER] - val tripsInDayCount = prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] - - LaunchedEffect(lineId) { // retrieving line - if (lineId == -1 || lineTypeString == null) { - return@LaunchedEffect - } - model.initState() - if (lineTripsUiState.line == null || lineTripsUiState.line!!.lineId != lineId) { - model.loadLine() - } - } - - LaunchedEffect( - model.lineId, tripIndex, prevTripIndex, tripsInDayCount - ) { // retrieving trip - if (tripIndex == null) { - model.cancelTripReloadJobAndLaunch { - model.setReferenceDateTimeAsync(lineTripsUiState.referenceDateTime) - } - return@LaunchedEffect - } - model.loadIndex(tripIndex) - model.initState() - } - - LaunchedEffect(storedDirectionFilter) { - if (storedDirectionFilter == null || !toggledDirection) return@LaunchedEffect - val newDirectionFilter = Direction.valueOf(storedDirectionFilter) - model.mutableUiState.update { it.copy(directionFilter = newDirectionFilter) } - - if (newDirectionFilter == Direction.ForwardAndBackward) { - val state = model.uiState.value - if (state.trip == null) { - // the trip can be null if there is no trip in that direction - model.loadIndex(state.tripIndex) - } else { - // no need to load the trip, as it's already loaded - model.mutableUiState.update { - it.copy( - prevEnabled = state.tripIndex > 0, - nextEnabled = state.tripIndex < model.uiState.value.tripsInDayCount - 1, - directionFilter = newDirectionFilter, - ) - } - } - - } else { - val state = model.uiState.value - if (state.trip?.direction != newDirectionFilter) { - // we need to load another trip, since the current one has the wrong direction - model.loadIndex(state.tripIndex) - logInfo("state.tripIndex: ${state.tripIndex}") - logInfo("lineTripsUiState.tripIndex: ${lineTripsUiState.tripIndex}") - updateAppWidgetState(context, id) { prefs -> - prefs[WidgetKeys.TRIP_INDEX] = lineTripsUiState.tripIndex - prefs[WidgetKeys.PREV_TRIP_INDEX] = state.tripIndex - prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] = lineTripsUiState.tripsInDayCount - prefs[WidgetKeys.IS_INITIAL_DATA_LOADED] = true - prefs[WidgetKeys.DIRECTION_FILTER] = - lineTripsUiState.directionFilter.name - prefs[WidgetKeys.PREV_ENABLED] = lineTripsUiState.tripIndex > 0 - prefs[WidgetKeys.NEXT_ENABLED] = - lineTripsUiState.tripIndex < lineTripsUiState.tripsInDayCount - 1 - } - this@MyAppWidget.update(context, id) - } - } - updateAppWidgetState(context, id) { prefs -> - prefs[WidgetKeys.TOGGLED_DIRECTION] = false - } - } + GlanceTheme { + when (state) { + is WidgetState.Loading -> SmallCircularProgressIndicatorGlance() + is WidgetState.Available -> + LineTripsWidgetScreen( + line = state.line, + trip = state.trip, + error = state.error, + loading = state.loading, + onReloadAction = actionRunCallback(), + onPrevAction = actionRunCallback(), + onNextAction = actionRunCallback(), + onLineClickAction = actionStartActivity(configIntent), + directionFilter = state.directionFilter, + onDirectionClickAction = actionRunCallback(), + stopIdToHighlight = null, + stopTypeToHighlight = null, + prevEnabled = state.prevEnabled, + nextEnabled = state.nextEnabled, + isFavorite = state.line?.isFavorite ?: false + ) - if (storedDirectionFilter != null && Direction.valueOf(storedDirectionFilter) != lineTripsUiState.directionFilter) { - model.mutableUiState.update { - it.copy( - directionFilter = Direction.valueOf(storedDirectionFilter) + is WidgetState.Unavailable -> Text( + text = context.getString(R.string.error), + style = TextStyle(color = GlanceTheme.colors.error), + modifier = GlanceModifier.padding(8.dp) ) } } - - LaunchedEffect(refreshTimestamp) { // updating trip - if (refreshTimestamp == 0L || tripIndex == null || lineTripsUiState.trip == null) return@LaunchedEffect - //updateTrip(tripIndex, mutableUiState, lineTripsUiState.trip!!) - model.cancelTripReloadJobAndLaunch { model.onReloadAsync() } - } - - logInfo("${System.currentTimeMillis()}") - logInfo("tripIndex: $tripIndex") - logInfo("lineTripsUiState.tripIndex: ${lineTripsUiState.tripIndex}") - logInfo("trip: ${lineTripsUiState.trip}") - logInfo("prevTripIndex: $prevTripIndex") - logInfo("directionFilter: ${lineTripsUiState.directionFilter}") - logInfo("isFavorite: ${lineTripsUiState.line?.isFavorite}") - logInfo("isLoading: ${lineTripsUiState.loading}") - - GlanceTheme { - LineTripsWidgetScreen( - line = lineTripsUiState.line, - trip = lineTripsUiState.trip, - error = lineTripsUiState.error, - loading = lineTripsUiState.loading, - onReloadAction = actionRunCallback(), - onPrevAction = actionRunCallback(), - onNextAction = actionRunCallback(), - onLineClickAction = actionStartActivity(configIntent), - directionFilter = lineTripsUiState.directionFilter, - onDirectionClickAction = actionRunCallback(), - stopIdToHighlight = null, - stopTypeToHighlight = null, - prevEnabled = lineTripsUiState.prevEnabled, - nextEnabled = lineTripsUiState.nextEnabled, - isFavorite = lineTripsUiState.line?.isFavorite ?: false - ) - } } } } diff --git a/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetReceiver.kt b/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetReceiver.kt new file mode 100644 index 0000000..cd5b121 --- /dev/null +++ b/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetReceiver.kt @@ -0,0 +1,26 @@ +package org.stypox.tridenta.widget + +import android.appwidget.AppWidgetManager +import android.content.Context +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetReceiver +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class LineTripWidgetReceiver : GlanceAppWidgetReceiver() { + override val glanceAppWidget: GlanceAppWidget get() = LineTripWidget() + + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + super.onUpdate(context, appWidgetManager, appWidgetIds) + LineTripWidgetWorker.enqueue(context) + } + + override fun onDeleted(context: Context, appWidgetIds: IntArray) { + super.onDeleted(context, appWidgetIds) + LineTripWidgetWorker.cancel(context) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetWorker.kt b/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetWorker.kt new file mode 100644 index 0000000..e65366a --- /dev/null +++ b/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetWorker.kt @@ -0,0 +1,107 @@ +package org.stypox.tridenta.widget + +import android.content.Context +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.state.getAppWidgetState +import androidx.glance.appwidget.state.updateAppWidgetState +import androidx.work.CoroutineWorker +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import dagger.hilt.android.EntryPointAccessors +import org.stypox.tridenta.extractor.ROME_ZONE_ID +import org.stypox.tridenta.log.logInfo +import org.stypox.tridenta.widget.actions.WidgetEntryPoint +import java.time.ZonedDateTime +import java.util.concurrent.TimeUnit + +class LineTripWidgetWorker( + private val context: Context, params: WorkerParameters, + //private val tripsRepository: LineTripsRepository +) : + CoroutineWorker(context, params) { + companion object { + private val uniqueWorkName = LineTripWidgetWorker::class.java.simpleName + + fun enqueue(context: Context, force: Boolean = false) { + val workManager = WorkManager.getInstance(context) + val request = + PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES).build() + + workManager.enqueueUniquePeriodicWork( + uniqueWorkName = uniqueWorkName, existingPeriodicWorkPolicy = if (force) { + ExistingPeriodicWorkPolicy.UPDATE + } else { + ExistingPeriodicWorkPolicy.KEEP + }, request = request + ) + } + + fun cancel(context: Context) { + WorkManager.getInstance(context).cancelUniqueWork(uniqueWorkName) + } + } + + override suspend fun doWork(): Result { + val appWidgetManager = GlanceAppWidgetManager(context) + appWidgetManager.getGlanceIds(LineTripWidget::class.java).forEach { glanceId -> + val currentState: WidgetState = getAppWidgetState( + context, + LineTripWidgetStateDefinition, glanceId + ) + if (currentState is WidgetState.Available) { + updateAppWidgetState( + context, + LineTripWidgetStateDefinition, + glanceId + ) { WidgetState.Loading } + LineTripWidget().update(context, glanceId) + + // TODO Retrieve the updated state + if (currentState.line == null) { + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { + WidgetState.Unavailable("Something went wrong") + } + LineTripWidget().update(context, glanceId) + return@forEach + } + + val hiltEntryPoint = + EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) + val tripsRepository = hiltEntryPoint.lineTripsRepository() + val referenceDateTime = ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID) + val (tripsInDayCount, tripIndex, trip) = tripsRepository.getUiTrip( + currentState.line.lineId, + currentState.line.type, + referenceDateTime, + currentState.directionFilter + ) + logInfo("trip: $trip") + + updateAppWidgetState( + context, + LineTripWidgetStateDefinition, + glanceId + ) { + if (trip != null) { + WidgetState.Available( + currentState.line, + trip, + referenceDateTime, + tripsInDayCount, + tripIndex, + prevEnabled = tripIndex > 0, + nextEnabled = tripIndex < tripsInDayCount - 1, + currentState.directionFilter, + ) + } else { + WidgetState.Unavailable(message = "Something went wrong") + } + } + } + LineTripWidget().update(context, glanceId) + } + return Result.success() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidgetReceiver.kt b/app/src/main/java/org/stypox/tridenta/widget/MyAppWidgetReceiver.kt deleted file mode 100644 index 5624058..0000000 --- a/app/src/main/java/org/stypox/tridenta/widget/MyAppWidgetReceiver.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.stypox.tridenta.widget - -import androidx.glance.appwidget.GlanceAppWidget -import androidx.glance.appwidget.GlanceAppWidgetReceiver -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class MyAppWidgetReceiver : GlanceAppWidgetReceiver() { - override val glanceAppWidget: GlanceAppWidget get() = MyAppWidget() -} \ No newline at end of file From dd39bee847f64aef0134f9fafd518accdbd24905 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Fri, 20 Mar 2026 15:43:01 +0100 Subject: [PATCH 25/43] feat: Modified the Manifest and the application file Modified the manifest and the application for the worker --- app/src/main/AndroidManifest.xml | 16 +++++++++++++- .../stypox/tridenta/TridentaApplication.kt | 22 ++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ce00e4c..26a936c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,7 +46,7 @@ - @@ -55,6 +55,20 @@ android:name="android.appwidget.provider" android:resource="@xml/my_app_widget_info" /> + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/TridentaApplication.kt b/app/src/main/java/org/stypox/tridenta/TridentaApplication.kt index cc1bf58..fa914b0 100644 --- a/app/src/main/java/org/stypox/tridenta/TridentaApplication.kt +++ b/app/src/main/java/org/stypox/tridenta/TridentaApplication.kt @@ -1,7 +1,14 @@ package org.stypox.tridenta import android.app.Application +import android.util.Log +import androidx.hilt.work.HiltWorkerFactory +import androidx.work.Configuration +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors import dagger.hilt.android.HiltAndroidApp +import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.MainScope import org.stypox.tridenta.db.LogDao import org.stypox.tridenta.log.AppUncaughtExceptionHandler @@ -10,7 +17,7 @@ import org.stypox.tridenta.log.setupLogger import javax.inject.Inject @HiltAndroidApp -class TridentaApplication : Application() { +class TridentaApplication : Application(), Configuration.Provider { // store logger's DAO and scope here, so that they are correctly garbage collected when // Application is destroyed (probably this is not needed, but let's be sure) @@ -25,4 +32,17 @@ class TridentaApplication : Application() { // cleanup old logs (we don't want the database to be cluttered with those) clearOldLogs() } + + @EntryPoint + @InstallIn(SingletonComponent::class) + interface HiltWorkerFactoryEntryPoint { + fun workerFactory(): HiltWorkerFactory + } + + override val workManagerConfiguration by lazy {Configuration.Builder() + .setWorkerFactory( + EntryPointAccessors.fromApplication(this, HiltWorkerFactoryEntryPoint::class.java).workerFactory() + ) + .setMinimumLoggingLevel(Log.INFO) + .build()} } \ No newline at end of file From 68538b7a90e9644d99f8af4943276a5090ea5291 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Fri, 20 Mar 2026 15:44:43 +0100 Subject: [PATCH 26/43] feat: Modified the ConfigurationActivity and the Actions Modified the activity and the actions to work with the WidgetState class --- .../widget/WidgetConfigurationActivity.kt | 57 ++- .../tridenta/widget/actions/WidgetActions.kt | 377 +++++++++++++++--- 2 files changed, 381 insertions(+), 53 deletions(-) diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt index 4cf286c..7840421 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt @@ -34,6 +34,7 @@ import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.state.updateAppWidgetState import androidx.hilt.navigation.compose.hiltViewModel import dagger.hilt.android.AndroidEntryPoint +import dagger.hilt.android.EntryPointAccessors import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -43,13 +44,16 @@ import org.stypox.tridenta.R import org.stypox.tridenta.db.data.DbLine import org.stypox.tridenta.enums.Area import org.stypox.tridenta.enums.Direction +import org.stypox.tridenta.extractor.ROME_ZONE_ID +import org.stypox.tridenta.log.logError import org.stypox.tridenta.ui.lines.AreaChip import org.stypox.tridenta.ui.lines.LineItem import org.stypox.tridenta.ui.lines.LinesUiState import org.stypox.tridenta.ui.lines.LinesViewModel import org.stypox.tridenta.ui.lines.SelectAreaDialog import org.stypox.tridenta.ui.theme.AppTheme -import org.stypox.tridenta.widget.actions.WidgetKeys +import org.stypox.tridenta.widget.actions.WidgetEntryPoint +import java.time.ZonedDateTime @AndroidEntryPoint class WidgetConfigurationActivity : @@ -108,16 +112,53 @@ class WidgetConfigurationActivity : // 2. Map the standard Android appWidgetId to a Jetpack GlanceId val glanceManager = GlanceAppWidgetManager(context) val glanceId = glanceManager.getGlanceIdBy(appWidgetId) + val hiltEntryPoint = + EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) + val line = withContext(Dispatchers.IO) { + try { + hiltEntryPoint.lineRepository().getUiLine(line.lineId, line.type).also { + if (it == null) { + logError( + "UI line (${line.lineId}, ${line.type}) not found" + ) + } + + // register a view for this line (assuming loadLine is called once) + hiltEntryPoint.historyDao().registerAccessed(true, line.lineId, line.type) + } + } catch (e: Throwable) { + logError("Could not load UI line (${line.lineId}, ${line.type})", e) + null + } + } + if (line == null) { + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { + WidgetState.Unavailable(message = "Something went wrong") + } + return@launch + } + val tripsRepository = hiltEntryPoint.lineTripsRepository() + val referenceDateTime = ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID) + val (tripsInDayCount, tripIndex, trip) = tripsRepository.getUiTrip( + line.lineId, + line.type, + referenceDateTime, + Direction.ForwardAndBackward + ) // 3. Save the selected data to this specific widget's Preferences - updateAppWidgetState(context, glanceId) { prefs -> - prefs.clear() - prefs[WidgetKeys.LINE_ID] = line.lineId - prefs[WidgetKeys.LINE_TYPE] = line.type.name - prefs[WidgetKeys.DIRECTION_FILTER] = Direction.ForwardAndBackward.name - prefs[WidgetKeys.IS_LOADING] = true + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { + WidgetState.Available( + line, + trip, + ZonedDateTime.now(), + tripsInDayCount, + tripIndex, + prevEnabled = tripIndex > 0, + nextEnabled = tripIndex < tripsInDayCount - 1, + ) } - MyAppWidget().update(this@WidgetConfigurationActivity, glanceId) + LineTripWidget().update(this@WidgetConfigurationActivity, glanceId) // 5. Tell the Android OS that the configuration was successful withContext(Dispatchers.Main) { diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt index 71387b4..faf00ef 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt @@ -4,16 +4,27 @@ import android.content.Context import androidx.glance.GlanceId import androidx.glance.action.ActionParameters import androidx.glance.appwidget.action.ActionCallback +import androidx.glance.appwidget.state.getAppWidgetState import androidx.glance.appwidget.state.updateAppWidgetState import dagger.hilt.EntryPoint import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.stypox.tridenta.db.HistoryDao import org.stypox.tridenta.enums.Direction +import org.stypox.tridenta.extractor.ROME_ZONE_ID +import org.stypox.tridenta.log.logError import org.stypox.tridenta.log.logInfo +import org.stypox.tridenta.log.logWarning import org.stypox.tridenta.repo.LineTripsRepository import org.stypox.tridenta.repo.LinesRepository -import org.stypox.tridenta.widget.MyAppWidget +import org.stypox.tridenta.repo.data.UiTrip +import org.stypox.tridenta.widget.LineTripWidget +import org.stypox.tridenta.widget.LineTripWidgetStateDefinition +import org.stypox.tridenta.widget.WidgetState +import java.time.ZonedDateTime @EntryPoint @InstallIn(SingletonComponent::class) @@ -21,28 +32,266 @@ interface WidgetEntryPoint { fun lineTripsRepository(): LineTripsRepository fun lineRepository(): LinesRepository fun historyDao(): HistoryDao - // Add HistoryDao and LinesRepository here too + + suspend fun loadIndex(index: Int, glanceId: GlanceId, context: Context) { + logInfo("loadIndex: $index") + val state: WidgetState = getAppWidgetState( + context, + LineTripWidgetStateDefinition, glanceId + ) + if (state !is WidgetState.Available) return + if (index < 0) { + logWarning("index must be positive") + return + } + if (index >= state.tripsInDayCount - 1) { + logWarning("index must be less than tripsInDayCount") + return + } + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { + state.copy(loading = true) + } + LineTripWidget().update(context, glanceId) + // only cancel any currently running job if there is something to do; the above + // condition will be false e.g. when there are no trips in a day (but not only for that) + loadIndexAsync(index, state, context, glanceId) + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { s -> + if (s is WidgetState.Available) s.copy(loading = false) else s + } + LineTripWidget().update(context, glanceId) + } + + private suspend fun loadIndexAsync( + index: Int, + state: WidgetState.Available, + context: Context, + glanceId: GlanceId + ) { + // hide the current trip, as it's going to change + val prevState = state + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { state -> + if (state is WidgetState.Available) state.copy( + tripIndex = index, + trip = null, + prevEnabled = index > 0, + nextEnabled = index < state.tripsInDayCount - 1, + ) else state + } + LineTripWidget().update(context, glanceId) + + // load the trip, and show it right after it gets loaded (see the related functions) + val (trip, network) = if (prevState.directionFilter == Direction.ForwardAndBackward) { + loadIndexNoFilterAsync(index, prevState, context, glanceId) + } else { + loadIndexDirectionAsync(index, prevState, context, glanceId) + } + + if (!network && trip != null && trip.completedStops < trip.stopTimes.size) { + // after showing the (possibly) outdated trip fast, reload it to show latest updates + // (but reload it only if there actually is a trip and it is not completed) + onReloadAsync(context, glanceId) + } + } + + suspend fun setReferenceDateTimeAsync( + referenceDateTimeCurrentZone: ZonedDateTime, + state: WidgetState.Available, + context: Context, + glanceId: GlanceId + ) { + val referenceDateTime = referenceDateTimeCurrentZone.withZoneSameInstant(ROME_ZONE_ID) + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { state -> + if (state is WidgetState.Available) state.copy( + tripsInDayCount = 0, + tripIndex = 0, + trip = null, + prevEnabled = false, + nextEnabled = false, + referenceDateTime = referenceDateTime, + ) else state + } + LineTripWidget().update(context, glanceId) + + var error = false + val (tripsInDayCount, tripIndex, trip) = withContext(Dispatchers.IO) { + try { + lineTripsRepository().getUiTrip( + lineId = state.line?.lineId!!, + lineType = state.line.type, + referenceDateTime = referenceDateTime, + directionFilter = state.directionFilter, + ) + } catch (e: Throwable) { + logError( + "Could not load trip for UI line (${state.line?.lineId}, " + "${state.line?.type}) at time $referenceDateTime", + e + ) + error = true + Triple(0, 0, null) + } + } + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { state -> + if (state is WidgetState.Available) state.copy( + tripsInDayCount = tripsInDayCount, tripIndex = tripIndex, trip = trip, + prevEnabled = tripIndex > 0, + nextEnabled = tripIndex < tripsInDayCount - 1, + referenceDateTime = referenceDateTime, + error = error + ) else state + } + LineTripWidget().update(context, glanceId) + } + + suspend fun onReloadAsync(context: Context, glanceId: GlanceId) { + val state: WidgetState = getAppWidgetState( + context, + LineTripWidgetStateDefinition, glanceId + ) + if (state !is WidgetState.Available) return + val previousTrip = state.trip + if (previousTrip == null) {// this could happen if an error happened while loading initial/more trips + if (state.tripsInDayCount > 0) { + // more trips failed loading, try to load again the currently set index + loadIndexAsync(state.tripIndex, state, context, glanceId) + } else { + // initial trips failed loading, try to load again the current day + setReferenceDateTimeAsync(state.referenceDateTime, state, context, glanceId) + } + return + } + + val trip = withContext(Dispatchers.IO) { + logInfo("guilty") + try { + lineTripsRepository().reloadUiTrip( + uiTrip = previousTrip, + index = state.tripIndex, + referenceDateTime = state.referenceDateTime + ) + } catch (e: Throwable) { + logError( + "Could not load trip ${previousTrip.tripId} for UI line " + "(${state.line?.lineId}, ${state.line?.type})", + e + ) + null + } + } + + if (trip == null) { + // keep previous trip intact, we don't want to hide information that we do have! + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { + state.copy(error = true) + } + LineTripWidget().update(context, glanceId) + } else { + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { + state.copy(trip = trip) + } + LineTripWidget().update(context, glanceId) + } + } + + private suspend fun loadIndexNoFilterAsync( + index: Int, + state: WidgetState.Available, + context: Context, + glanceId: GlanceId + ): Pair { + val res = withContext(Dispatchers.IO) { + try { + lineTripsRepository().getUiTrip( + lineId = state.line?.lineId!!, + lineType = state.line.type, + referenceDateTime = state.referenceDateTime, + index = index, + ) + } catch (e: Throwable) { + logError( + "Could not load trip at index $index for UI line " + "(${state.line?.lineId}, ${state.line?.type})", + e + ) + Pair(null, true /* <- useless when trip == null */) + } + } + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { state -> + if (state is WidgetState.Available) state.copy( + trip = res.first, + error = res.first == null + ) else state + } + LineTripWidget().update(context, glanceId) + logInfo("loadIndexNoFilterAsync") + + return res + } + + private suspend fun loadIndexDirectionAsync( + index: Int, prevState: WidgetState.Available, context: Context, glanceId: GlanceId + ): Pair { + val res = withContext(Dispatchers.IO) { + try { + lineTripsRepository().getUiTripWithDirection( + lineId = prevState.line?.lineId!!, + lineType = prevState.line.type, + referenceDateTime = prevState.referenceDateTime, + index = index, + direction = prevState.directionFilter, + prevIndex = prevState.tripIndex, + ) + } catch (e: Throwable) { + logError( + "Could not load trip at index $index for UI line " + "(${prevState.line?.lineId}, ${prevState.line?.type})" + "in direction ${prevState.directionFilter}", + e + ) + null + } + } + + if (res == null) { + // no trip could be loaded in the set direction, restore previous state, + // but update prevEnabled or nextEnabled + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { state -> + if (state is WidgetState.Available) state.copy( + tripIndex = prevState.tripIndex, + trip = prevState.trip, + prevEnabled = if (index < prevState.tripIndex) false else prevState.prevEnabled, + nextEnabled = if (index > prevState.tripIndex) false else prevState.nextEnabled, + ) else state + } + LineTripWidget().update(context, glanceId) + + return Pair(null, true /* <- useless when trip == null */) + + } else { + val (trip, newIndex, loadedFromNetwork) = res + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { state -> + if (state is WidgetState.Available) state.copy( + tripIndex = newIndex, + trip = trip, + prevEnabled = newIndex > 0, + nextEnabled = newIndex < prevState.tripsInDayCount - 1 + ) else state + } + LineTripWidget().update(context, glanceId) + + return Pair(trip, loadedFromNetwork) + } + } } class NextTripAction : ActionCallback { override suspend fun onAction( context: Context, glanceId: GlanceId, parameters: ActionParameters ) { - updateAppWidgetState(context, glanceId) { prefs -> - val currentIndex = prefs[WidgetKeys.TRIP_INDEX] ?: 0 - val tripsInDayCount = prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] ?: 0 - val nextIndex = currentIndex + 1 - if (nextIndex !in 0.. 0 - prefs[WidgetKeys.NEXT_ENABLED] = nextIndex < tripsInDayCount - 1 - prefs[WidgetKeys.PREV_TRIP_INDEX] = currentIndex - prefs[WidgetKeys.TRIP_INDEX] = nextIndex - } + val currentState: WidgetState = getAppWidgetState( + context, + LineTripWidgetStateDefinition, glanceId + ) + if (currentState !is WidgetState.Available) return + val hiltEntryPoint = + EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) + hiltEntryPoint.loadIndex(currentState.tripIndex + 1, glanceId, context) - MyAppWidget().update(context, glanceId) logInfo("NextTripAction performed") } } @@ -51,21 +300,15 @@ class PrevTripAction : ActionCallback { override suspend fun onAction( context: Context, glanceId: GlanceId, parameters: ActionParameters ) { - updateAppWidgetState(context, glanceId) { prefs -> - val currentIndex = prefs[WidgetKeys.TRIP_INDEX] ?: 0 - val tripsInDayCount = prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] ?: 0 - val nextIndex = currentIndex - 1 - if (nextIndex !in 0.. 0 - prefs[WidgetKeys.NEXT_ENABLED] = nextIndex < tripsInDayCount - 1 - prefs[WidgetKeys.PREV_TRIP_INDEX] = currentIndex - prefs[WidgetKeys.TRIP_INDEX] = nextIndex - } + val currentState: WidgetState = getAppWidgetState( + context, + LineTripWidgetStateDefinition, glanceId + ) + if (currentState !is WidgetState.Available) return + val hiltEntryPoint = + EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) + hiltEntryPoint.loadIndex(currentState.tripIndex - 1, glanceId, context) - MyAppWidget().update(context, glanceId) logInfo("PrevTripAction performed") } } @@ -74,10 +317,22 @@ class ReloadTripAction : ActionCallback { override suspend fun onAction( context: Context, glanceId: GlanceId, parameters: ActionParameters ) { - updateAppWidgetState(context, glanceId) { prefs -> - prefs[WidgetKeys.REFRESH_TIMESTAMP] = System.currentTimeMillis() + val currentState: WidgetState = getAppWidgetState( + context, + LineTripWidgetStateDefinition, glanceId + ) + if (currentState !is WidgetState.Available) return + val hiltEntryPoint = + EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { + currentState.copy(loading = true) } - MyAppWidget().update(context, glanceId) + LineTripWidget().update(context, glanceId) + hiltEntryPoint.onReloadAsync(context, glanceId) + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { state -> + if (state is WidgetState.Available) state.copy(loading = false) else state + } + LineTripWidget().update(context, glanceId) } } @@ -85,19 +340,51 @@ class ToggleDirectionAction : ActionCallback { override suspend fun onAction( context: Context, glanceId: GlanceId, parameters: ActionParameters ) { - updateAppWidgetState(context, glanceId) { prefs -> - val actualDirectionFilter = prefs[WidgetKeys.DIRECTION_FILTER] - val newDirectionFilter = when (Direction.valueOf( - actualDirectionFilter ?: Direction.ForwardAndBackward.name - )) { - Direction.Forward -> Direction.Backward - Direction.Backward -> Direction.ForwardAndBackward - Direction.ForwardAndBackward -> Direction.Forward + val currentState: WidgetState = getAppWidgetState( + context, + LineTripWidgetStateDefinition, glanceId + ) + + if (currentState !is WidgetState.Available) return + val hiltEntryPoint = + EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) + val newDirectionFilter = when (currentState.directionFilter) { + Direction.Forward -> Direction.Backward + Direction.Backward -> Direction.ForwardAndBackward + Direction.ForwardAndBackward -> Direction.Forward + } + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { + currentState.copy(directionFilter = newDirectionFilter) + } + LineTripWidget().update(context, glanceId) + + if (newDirectionFilter == Direction.ForwardAndBackward) { + val state = currentState + if (state.trip == null) { + // the trip can be null if there is no trip in that direction + hiltEntryPoint.loadIndex(state.tripIndex, glanceId, context) + } else { + // no need to load the trip, as it's already loaded + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { + state.copy( + prevEnabled = state.tripIndex > 0, + nextEnabled = state.tripIndex < state.tripsInDayCount - 1, + directionFilter = newDirectionFilter, + ) + } + LineTripWidget().update(context, glanceId) + } + + } else { + if (currentState.trip?.direction != newDirectionFilter) { + // we need to load another trip, since the current one has the wrong direction + hiltEntryPoint.loadIndex( + currentState.tripIndex, + glanceId, + context + ) } - prefs[WidgetKeys.DIRECTION_FILTER] = newDirectionFilter.name - prefs[WidgetKeys.TOGGLED_DIRECTION] = true } - MyAppWidget().update(context, glanceId) - logInfo("ToggleDirectionAction performed") + logInfo("ToggleDirectionAction performed, directionFilter: ${currentState.directionFilter}") } } \ No newline at end of file From 46039fb5c8751494efc1f12bdacaf9c74c379579 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Mon, 23 Mar 2026 12:14:58 +0100 Subject: [PATCH 27/43] style: Minor changes and some refactoring Deleted the object WidgetState.Loading , changed how the prev and next button behave to the disable state from disappearing to fade and some code refactoring here and there --- .../stypox/tridenta/widget/LineTripWidget.kt | 2 - .../tridenta/widget/LineTripWidgetWorker.kt | 20 +++++----- .../widget/WidgetConfigurationActivity.kt | 1 + .../org/stypox/tridenta/widget/WidgetState.kt | 3 -- .../tridenta/widget/actions/WidgetActions.kt | 2 +- .../tridenta/widget/ui/LineTripGlance.kt | 14 +------ .../tridenta/widget/ui/TripViewGlance.kt | 38 +++++++++++-------- 7 files changed, 37 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/org/stypox/tridenta/widget/LineTripWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/LineTripWidget.kt index 8f31d2c..51bd04a 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/LineTripWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/LineTripWidget.kt @@ -34,7 +34,6 @@ import org.stypox.tridenta.widget.actions.NextTripAction import org.stypox.tridenta.widget.actions.PrevTripAction import org.stypox.tridenta.widget.actions.ReloadTripAction import org.stypox.tridenta.widget.actions.ToggleDirectionAction -import org.stypox.tridenta.widget.theme.SmallCircularProgressIndicatorGlance import org.stypox.tridenta.widget.ui.LineTripsWidgetScreen import org.stypox.tridenta.widget.ui.TripViewGlance import java.time.OffsetDateTime @@ -56,7 +55,6 @@ class LineTripWidget : GlanceAppWidget() { GlanceTheme { when (state) { - is WidgetState.Loading -> SmallCircularProgressIndicatorGlance() is WidgetState.Available -> LineTripsWidgetScreen( line = state.line, diff --git a/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetWorker.kt b/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetWorker.kt index e65366a..892df1d 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetWorker.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetWorker.kt @@ -55,7 +55,7 @@ class LineTripWidgetWorker( context, LineTripWidgetStateDefinition, glanceId - ) { WidgetState.Loading } + ) { oldState -> if (oldState is WidgetState.Available) oldState.copy(loading = true) else oldState } LineTripWidget().update(context, glanceId) // TODO Retrieve the updated state @@ -83,20 +83,20 @@ class LineTripWidgetWorker( context, LineTripWidgetStateDefinition, glanceId - ) { + ) { oldState -> + oldState as WidgetState.Available if (trip != null) { - WidgetState.Available( - currentState.line, - trip, - referenceDateTime, - tripsInDayCount, - tripIndex, + oldState.copy( + trip = trip, + referenceDateTime = referenceDateTime, + tripsInDayCount = tripsInDayCount, + tripIndex = tripIndex, prevEnabled = tripIndex > 0, nextEnabled = tripIndex < tripsInDayCount - 1, - currentState.directionFilter, + loading = false ) } else { - WidgetState.Unavailable(message = "Something went wrong") + oldState.copy(error = true, loading = false) } } } diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt index 7840421..7eb6f1e 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt @@ -156,6 +156,7 @@ class WidgetConfigurationActivity : tripIndex, prevEnabled = tripIndex > 0, nextEnabled = tripIndex < tripsInDayCount - 1, + loading = false, ) } LineTripWidget().update(this@WidgetConfigurationActivity, glanceId) diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetState.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetState.kt index 33c60e0..8b6b56f 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/WidgetState.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/WidgetState.kt @@ -9,9 +9,6 @@ import java.time.ZonedDateTime @Serializable sealed interface WidgetState { - @Serializable - data object Loading : WidgetState - @Serializable data class Available( val line: UiLine?, diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt index faf00ef..be0d2b2 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt @@ -44,7 +44,7 @@ interface WidgetEntryPoint { logWarning("index must be positive") return } - if (index >= state.tripsInDayCount - 1) { + if (index > state.tripsInDayCount - 1) { logWarning("index must be less than tripsInDayCount") return } diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt index a2a9f97..061373b 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt @@ -33,11 +33,6 @@ import org.stypox.tridenta.util.textColorOnBackground import org.stypox.tridenta.util.toLineColor import org.stypox.tridenta.widget.theme.SmallCircularProgressIndicatorGlance -/** - * The Glance equivalent of your LineTripsScreen. - * * Notice that we don't pass ViewModels, Navigators, or lambda functions. - * In Glance, user interactions MUST be passed as `Action` objects (like actionRunCallback). - */ @Composable fun LineTripsWidgetScreen( line: UiLine?, @@ -50,14 +45,12 @@ fun LineTripsWidgetScreen( stopTypeToHighlight: StopLineType?, isFavorite: Boolean, directionFilter: Direction, - // Actions replace standard lambdas in Glance onReloadAction: Action, onPrevAction: Action, onNextAction: Action, onLineClickAction: Action, onDirectionClickAction: Action ) { - // Glance's Scaffold is very basic. It gives you a background and layout structure. Column( modifier = GlanceModifier.fillMaxSize().background(GlanceTheme.colors.background) ) { @@ -118,17 +111,14 @@ fun WidgetAppBar( text = context.getString(R.string.trips_for_line), style = TextStyle( color = GlanceTheme.colors.onSurface, - //fontWeight = FontWeight.Bold, - //fontSize = 16.sp ), modifier = GlanceModifier.defaultWeight() ) Spacer(GlanceModifier.size(8.dp)) Box( modifier = GlanceModifier - .background(shortNameBackground) // Use a theme color instead of dynamic custom color for simplicity in Glance + .background(shortNameBackground) .padding(8.dp) - //.clickable(onLineClickAction) ) { Text( text = line.shortName, @@ -183,7 +173,7 @@ fun WidgetAppBar( // Helper to resolve drawables for Glance private fun getDirectionDrawable(direction: Direction): Int { return when (direction) { - Direction.Forward -> R.drawable.turn_sharp_right // Replace with actual drawable IDs + Direction.Forward -> R.drawable.turn_sharp_right Direction.Backward -> R.drawable.u_turn_left Direction.ForwardAndBackward -> R.drawable.swap_calls } diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt index 6d68960..eb807c8 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt @@ -10,7 +10,6 @@ import androidx.glance.GlanceTheme import androidx.glance.Image import androidx.glance.ImageProvider import androidx.glance.LocalContext -import androidx.glance.Visibility import androidx.glance.action.Action import androidx.glance.action.actionStartActivity import androidx.glance.action.clickable @@ -33,7 +32,6 @@ import androidx.glance.text.FontWeight import androidx.glance.text.Text import androidx.glance.text.TextAlign import androidx.glance.text.TextStyle -import androidx.glance.visibility import org.stypox.tridenta.R import org.stypox.tridenta.db.data.DbLine import org.stypox.tridenta.db.data.DbStop @@ -161,7 +159,11 @@ fun TripViewGlance( } @Composable -fun StopLineTypeIcon(stopLineType: StopLineType,context: Context, modifier: GlanceModifier = GlanceModifier) { +fun StopLineTypeIcon( + stopLineType: StopLineType, + context: Context, + modifier: GlanceModifier = GlanceModifier +) { Image( provider = ImageProvider( when (stopLineType) { @@ -179,6 +181,7 @@ fun StopLineTypeIcon(stopLineType: StopLineType,context: Context, modifier: Glan modifier = modifier, ) } + @Composable fun DirectionIconGlance( direction: Direction, @@ -271,7 +274,7 @@ private fun TripViewTopRowGlance( } Column { - StopLineTypeIcon(trip.type,context) + StopLineTypeIcon(trip.type, context) DirectionIconGlance(trip.direction, context) } } @@ -290,7 +293,10 @@ private fun TripViewBottomRowGlance( val context = LocalContext.current val buttonModifier = GlanceModifier.size(48.dp) .cornerRadius(12.dp) - .background(GlanceTheme.colors.primary) + .background(GlanceTheme.colors.primaryContainer) + val buttonModifierDisabled = buttonModifier.background( + GlanceTheme.colors.primaryContainer.getColor(context).copy(alpha = 0.5f) + ) // Widgets don't support FloatingActionButtons. Use standard Buttons or Images. Row( @@ -302,18 +308,19 @@ private fun TripViewBottomRowGlance( ) { Box( contentAlignment = Alignment.Center, - modifier = if (prevEnabled) buttonModifier.clickable(onPrevAction) else buttonModifier.visibility( - Visibility.Invisible - ) + modifier = if (prevEnabled) buttonModifier.clickable(onPrevAction) else buttonModifierDisabled ) { Image( provider = ImageProvider(R.drawable.arrow_left), contentDescription = context.getString(R.string.previous), modifier = GlanceModifier - .size(32.dp) + .size(32.dp), + colorFilter = ColorFilter.tint(GlanceTheme.colors.onPrimaryContainer), + //alpha = if (prevEnabled) 1.0f else 0.5f //TODO upgrade to glance 1.2 for this feature ) } - Spacer(GlanceModifier.defaultWeight()) + + //is WidgetState.Loading -> SmallCircularProgressIndicatorGlance() Spacer(GlanceModifier.defaultWeight()) Box( @@ -330,7 +337,8 @@ private fun TripViewBottomRowGlance( provider = ImageProvider(R.drawable.refresh), contentDescription = context.getString(R.string.reload), modifier = GlanceModifier - .size(24.dp) + .size(24.dp), + colorFilter = ColorFilter.tint(GlanceTheme.colors.onPrimaryContainer) ) } } @@ -338,14 +346,14 @@ private fun TripViewBottomRowGlance( Box( contentAlignment = Alignment.Center, - modifier = if (nextEnabled) buttonModifier.clickable(onNextAction) else buttonModifier.visibility( - Visibility.Invisible - ) + modifier = if (nextEnabled) buttonModifier.clickable(onNextAction) else buttonModifierDisabled ) { Image( provider = ImageProvider(R.drawable.arrow_right), contentDescription = context.getString(R.string.next), - GlanceModifier.size(32.dp) + GlanceModifier.size(32.dp), + colorFilter = ColorFilter.tint(GlanceTheme.colors.onPrimaryContainer) + //alpha = if (prevEnabled) 1.0f else 0.5f //TODO upgrade to glance 1.2 for this feature ) } } From 8fd2232b76d7e698745f3980c14bb888813caae4 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Wed, 25 Mar 2026 15:59:16 +0100 Subject: [PATCH 28/43] refactoring: General code refactoring --- .../ui/line_trips/LineTripsViewModel.kt | 4 +- .../widget/WidgetConfigurationActivity.kt | 5 +- .../org/stypox/tridenta/widget/WidgetModel.kt | 359 ------------------ .../tridenta/widget/actions/WidgetActions.kt | 11 +- .../SmallCircularProgressIndicatorGlance.kt | 2 +- .../tridenta/widget/ui/TripViewGlance.kt | 9 +- 6 files changed, 13 insertions(+), 377 deletions(-) delete mode 100644 app/src/main/java/org/stypox/tridenta/widget/WidgetModel.kt diff --git a/app/src/main/java/org/stypox/tridenta/ui/line_trips/LineTripsViewModel.kt b/app/src/main/java/org/stypox/tridenta/ui/line_trips/LineTripsViewModel.kt index 8eb956f..2bb5a58 100644 --- a/app/src/main/java/org/stypox/tridenta/ui/line_trips/LineTripsViewModel.kt +++ b/app/src/main/java/org/stypox/tridenta/ui/line_trips/LineTripsViewModel.kt @@ -78,7 +78,7 @@ class LineTripsViewModel @Inject constructor( mutableUiState.update { it.copy(directionFilter = newDirectionFilter) } if (newDirectionFilter == Direction.ForwardAndBackward) { - val state = uiState.value; + val state = uiState.value if (state.trip == null) { // the trip can be null if there is no trip in that direction loadIndex(state.tripIndex) @@ -94,7 +94,7 @@ class LineTripsViewModel @Inject constructor( } } else { - val state = uiState.value; + val state = uiState.value if (state.trip?.direction != newDirectionFilter) { // we need to load another trip, since the current one has the wrong direction loadIndex(state.tripIndex) diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt index 7eb6f1e..42e5c6b 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt @@ -1,6 +1,5 @@ package org.stypox.tridenta.widget -import android.app.Activity import android.appwidget.AppWidgetManager import android.content.Intent import android.os.Bundle @@ -78,7 +77,7 @@ class WidgetConfigurationActivity : // Set the result to CANCELED right away. This ensures that if the user backs // out of the activity without picking a line, the widget is removed from the home screen. - setResult(Activity.RESULT_CANCELED) + setResult(RESULT_CANCELED) setContent { val linesViewModel: LinesViewModel = hiltViewModel() @@ -166,7 +165,7 @@ class WidgetConfigurationActivity : val resultValue = Intent().apply { putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) } - setResult(Activity.RESULT_OK, resultValue) + setResult(RESULT_OK, resultValue) finish() } } diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetModel.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetModel.kt deleted file mode 100644 index 3337e34..0000000 --- a/app/src/main/java/org/stypox/tridenta/widget/WidgetModel.kt +++ /dev/null @@ -1,359 +0,0 @@ -package org.stypox.tridenta.widget - -import android.content.Context -import androidx.glance.GlanceId -import androidx.glance.appwidget.state.getAppWidgetState -import androidx.glance.appwidget.state.updateAppWidgetState -import androidx.glance.state.PreferencesGlanceStateDefinition -import dagger.hilt.android.EntryPointAccessors -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.getAndUpdate -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.stypox.tridenta.db.HistoryDao -import org.stypox.tridenta.enums.Direction -import org.stypox.tridenta.enums.StopLineType -import org.stypox.tridenta.extractor.ROME_ZONE_ID -import org.stypox.tridenta.log.logError -import org.stypox.tridenta.log.logInfo -import org.stypox.tridenta.repo.LineTripsRepository -import org.stypox.tridenta.repo.LinesRepository -import org.stypox.tridenta.repo.data.UiTrip -import org.stypox.tridenta.ui.line_trips.LineTripsUiState -import org.stypox.tridenta.widget.actions.WidgetEntryPoint -import org.stypox.tridenta.widget.actions.WidgetKeys -import java.time.ZonedDateTime -import kotlin.properties.Delegates - -class WidgetModel { - private val context: Context - private val glanceId: GlanceId - val tripsRepository: LineTripsRepository - val linesRepository: LinesRepository - val historyDao: HistoryDao - - val mutableUiState = MutableStateFlow( - LineTripsUiState( - line = null, - tripsInDayCount = 0, - tripIndex = 0, - trip = null, - prevEnabled = false, - nextEnabled = false, - referenceDateTime = ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID), - directionFilter = Direction.ForwardAndBackward, - stopIdToHighlight = null, - stopTypeToHighlight = null, - loading = true, - error = false, - ) - ) - val uiState = mutableUiState.asStateFlow() - var lineId by Delegates.notNull() - private lateinit var lineType: StopLineType - - private var tripReloadJob: Job? = null - - constructor(aContext: Context, aGlanceId: GlanceId) { - context = aContext - glanceId = aGlanceId - val hiltEntryPoint = - EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) - tripsRepository = hiltEntryPoint.lineTripsRepository() - linesRepository = hiltEntryPoint.lineRepository() - historyDao = hiltEntryPoint.historyDao() - } - - suspend fun initState() { - val prefs = getAppWidgetState( - context, - PreferencesGlanceStateDefinition, glanceId - ) - lineId = prefs[WidgetKeys.LINE_ID] ?: -1 - val lineTypeString = prefs[WidgetKeys.LINE_TYPE] ?: StopLineType.Urban.name - lineType = StopLineType.valueOf(lineTypeString) - mutableUiState.update { - it.copy( - tripIndex = prefs[WidgetKeys.TRIP_INDEX] ?: 0, - tripsInDayCount = prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] ?: 0, - prevEnabled = prefs[WidgetKeys.PREV_ENABLED] ?: false, - nextEnabled = prefs[WidgetKeys.NEXT_ENABLED] ?: false, - ) - } - } - - - fun loadLine() { - // this job is independent from trip reloading jobs, so don't use cancelReloadJobAndLaunch - CoroutineScope(Dispatchers.IO).launch { - val line = withContext(Dispatchers.IO) { - try { - linesRepository.getUiLine(lineId, lineType).also { - if (it == null) { - logError( - "UI line (${lineId}, ${lineType}) not found" - ) - } - - // register a view for this line (assuming loadLine is called once) - historyDao.registerAccessed(true, lineId, lineType) - } - } catch (e: Throwable) { - logError("Could not load UI line (${lineId}, ${lineType})", e) - null - } - } - mutableUiState.update { it.copy(line = line) } - } - } - - fun cancelTripReloadJobAndLaunch(tripLoadingFunction: suspend () -> Unit) { - tripReloadJob?.cancel() - tripReloadJob = CoroutineScope(Dispatchers.IO).launch { - // clear errors and set the state to "loading" before starting to load - mutableUiState.update { it.copy(loading = true, error = false) } - tripLoadingFunction() - // set "loading" to false when loading finishes (errors are set inside the function) - mutableUiState.update { it.copy(loading = false) } - } - } - - fun loadIndex(index: Int) { - logInfo("loadIndex: $index") - if (index >= 0 && index < uiState.value.tripsInDayCount) { - // only cancel any currently running job if there is something to do; the above - // condition will be false e.g. when there are no trips in a day (but not only for that) - cancelTripReloadJobAndLaunch { - loadIndexAsync(index) - } - } - } - - private suspend fun loadIndexAsync(index: Int) { - // hide the current trip, as it's going to change - val prevState = mutableUiState.getAndUpdate { - it.copy( - tripIndex = index, - trip = null, - prevEnabled = index > 0, - nextEnabled = index < uiState.value.tripsInDayCount - 1, - ) - } - - // load the trip, and show it right after it gets loaded (see the related functions) - val (trip, network) = if (prevState.directionFilter == Direction.ForwardAndBackward) { - loadIndexNoFilterAsync(index) - } else { - loadIndexDirectionAsync(index, prevState) - } - - if (!network && trip != null && trip.completedStops < trip.stopTimes.size) { - // after showing the (possibly) outdated trip fast, reload it to show latest updates - // (but reload it only if there actually is a trip and it is not completed) - onReloadAsync() - } - } - - suspend fun setReferenceDateTimeAsync( - referenceDateTimeCurrentZone: ZonedDateTime, - ) { - val referenceDateTime = referenceDateTimeCurrentZone.withZoneSameInstant(ROME_ZONE_ID) - mutableUiState.update { - it.copy( - tripsInDayCount = 0, - tripIndex = 0, - trip = null, - prevEnabled = false, - nextEnabled = false, - referenceDateTime = referenceDateTime, - ) - } - - var error = false - val (tripsInDayCount, tripIndex, trip) = withContext(Dispatchers.IO) { - try { - tripsRepository.getUiTrip( - lineId = lineId, - lineType = lineType, - referenceDateTime = referenceDateTime, - directionFilter = uiState.value.directionFilter, - ) - } catch (e: Throwable) { - logError( - "Could not load trip for UI line (${mutableUiState.value.line?.lineId}, " + "${mutableUiState.value.line?.type}) at time $referenceDateTime", - e - ) - error = true - Triple(0, 0, null) - } - } - - mutableUiState.update { - it.copy( - tripsInDayCount = tripsInDayCount, - tripIndex = tripIndex, - trip = trip, - prevEnabled = tripIndex > 0, - nextEnabled = tripIndex < tripsInDayCount - 1, - referenceDateTime = referenceDateTime, - error = error, - ) - } - updateAppWidgetState(context, glanceId) { prefs -> - prefs[WidgetKeys.TRIP_INDEX] = tripIndex - prefs[WidgetKeys.TRIPS_IN_DAY_COUNT] = tripsInDayCount - prefs[WidgetKeys.IS_INITIAL_DATA_LOADED] = true - prefs[WidgetKeys.PREV_ENABLED] = tripIndex > 0 - prefs[WidgetKeys.NEXT_ENABLED] = tripIndex < tripsInDayCount - 1 - } - logInfo("setReferenceDateTimeAsync tripIndex: $tripIndex") - } - - suspend fun onReloadAsync() { - val previousTrip = uiState.value.trip - if (previousTrip == null) { - // this could happen if an error happened while loading initial/more trips - if (uiState.value.tripsInDayCount > 0) { - // more trips failed loading, try to load again the currently set index - loadIndexAsync(uiState.value.tripIndex) - } else { - // initial trips failed loading, try to load again the current day - setReferenceDateTimeAsync(uiState.value.referenceDateTime) - } - return - } - - val trip = withContext(Dispatchers.IO) { - try { - tripsRepository.reloadUiTrip( - uiTrip = previousTrip, - index = uiState.value.tripIndex, - referenceDateTime = uiState.value.referenceDateTime - ) - } catch (e: Throwable) { - logError( - "Could not load trip ${previousTrip.tripId} for UI line " + "(${mutableUiState.value.line?.lineId}, ${mutableUiState.value.line?.type})", - e - ) - null - } - } - - if (trip == null) { - // keep previous trip intact, we don't want to hide information that we do have! - mutableUiState.update { it.copy(error = true) } - } else { - mutableUiState.update { it.copy(trip = trip) } - } - } - - private suspend fun loadIndexNoFilterAsync( - index: Int - ): Pair { - val res = withContext(Dispatchers.IO) { - try { - tripsRepository.getUiTrip( - lineId = lineId, - lineType = lineType, - referenceDateTime = uiState.value.referenceDateTime, - index = index, - ) - } catch (e: Throwable) { - logError( - "Could not load trip at index $index for UI line " + "(${lineId}, ${lineType})", - e - ) - Pair(null, true /* <- useless when trip == null */) - } - } - - // show the trip even if loadedFromNetwork is false (in which case it could be outdated) - mutableUiState.update { - it.copy( - trip = res.first, - error = res.first == null, - ) - } - if (res.first != null) { - updateAppWidgetState(context, glanceId) { prefs -> - prefs[WidgetKeys.TRIP_INDEX] = index - prefs[WidgetKeys.PREV_ENABLED] = index > 0 - prefs[WidgetKeys.NEXT_ENABLED] = index < uiState.value.tripsInDayCount - 1 - } - } - - return res - } - - - private suspend fun loadIndexDirectionAsync( - index: Int, prevState: LineTripsUiState - ): Pair { - val res = withContext(Dispatchers.IO) { - try { - tripsRepository.getUiTripWithDirection( - lineId = lineId, - lineType = lineType, - referenceDateTime = uiState.value.referenceDateTime, - index = index, - direction = prevState.directionFilter, - prevIndex = prevState.tripIndex, - ) - } catch (e: Throwable) { - logError( - "Could not load trip at index $index for UI line " + "(${mutableUiState.value.line?.lineId}, ${mutableUiState.value.line?.type})" + "in direction ${prevState.directionFilter}", - e - ) - null - } - } - - if (res == null) { - // no trip could be loaded in the set direction, restore previous state, - // but update prevEnabled or nextEnabled - mutableUiState.update { - it.copy( - tripIndex = prevState.tripIndex, - trip = prevState.trip, - prevEnabled = if (index < prevState.tripIndex) false else prevState.prevEnabled, - nextEnabled = if (index > prevState.tripIndex) false else prevState.nextEnabled, - ) - } - updateAppWidgetState(context, glanceId) { prefs -> - prefs[WidgetKeys.TRIP_INDEX] = prevState.tripIndex - if (index < prevState.tripIndex) { - prefs[WidgetKeys.PREV_ENABLED] = false - } - if (index > prevState.tripIndex) { - prefs[WidgetKeys.NEXT_ENABLED] = false - } - } - - return Pair(null, true /* <- useless when trip == null */) - - } else { - val (trip, newIndex, loadedFromNetwork) = res - mutableUiState.update { - it.copy( - tripIndex = newIndex, - trip = trip, - prevEnabled = newIndex > 0, - nextEnabled = newIndex < uiState.value.tripsInDayCount - 1, - ) - } - updateAppWidgetState(context, glanceId) { prefs -> - prefs[WidgetKeys.TRIP_INDEX] = newIndex - prefs[WidgetKeys.PREV_TRIP_INDEX] = prevState.tripIndex - prefs[WidgetKeys.PREV_ENABLED] = newIndex > 0 - prefs[WidgetKeys.NEXT_ENABLED] = newIndex < uiState.value.tripsInDayCount - 1 - } - - return Pair(trip, loadedFromNetwork) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt index be0d2b2..4cf3759 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt @@ -359,16 +359,15 @@ class ToggleDirectionAction : ActionCallback { LineTripWidget().update(context, glanceId) if (newDirectionFilter == Direction.ForwardAndBackward) { - val state = currentState - if (state.trip == null) { + if (currentState.trip == null) { // the trip can be null if there is no trip in that direction - hiltEntryPoint.loadIndex(state.tripIndex, glanceId, context) + hiltEntryPoint.loadIndex(currentState.tripIndex, glanceId, context) } else { // no need to load the trip, as it's already loaded updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { - state.copy( - prevEnabled = state.tripIndex > 0, - nextEnabled = state.tripIndex < state.tripsInDayCount - 1, + currentState.copy( + prevEnabled = currentState.tripIndex > 0, + nextEnabled = currentState.tripIndex < currentState.tripsInDayCount - 1, directionFilter = newDirectionFilter, ) } diff --git a/app/src/main/java/org/stypox/tridenta/widget/theme/SmallCircularProgressIndicatorGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/theme/SmallCircularProgressIndicatorGlance.kt index efb7dbd..0e7136d 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/theme/SmallCircularProgressIndicatorGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/theme/SmallCircularProgressIndicatorGlance.kt @@ -14,7 +14,7 @@ import androidx.glance.preview.Preview @Composable fun SmallCircularProgressIndicatorGlance(modifier: GlanceModifier = GlanceModifier) { CircularProgressIndicator( - color = GlanceTheme.colors.onSurface, + color = GlanceTheme.colors.onPrimaryContainer, modifier = modifier.size(16.dp), ) } \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt index eb807c8..f9c8d1f 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt @@ -252,10 +252,7 @@ private fun TripViewTopRowGlance( ) val dateOrDelayText = if (trip.lastEventReceivedAt == null) { - trip.stopTimes.asSequence() - .map { it.arrivalTime } - .filterNotNull() - .firstOrNull() + trip.stopTimes.firstNotNullOfOrNull { it.arrivalTime } ?.let { firstArrival -> formatDateFull(firstArrival) } ?: context.getString(R.string.no_date_time_information) } else { @@ -319,8 +316,7 @@ private fun TripViewBottomRowGlance( //alpha = if (prevEnabled) 1.0f else 0.5f //TODO upgrade to glance 1.2 for this feature ) } - - //is WidgetState.Loading -> SmallCircularProgressIndicatorGlance() Spacer(GlanceModifier.defaultWeight()) + Spacer(GlanceModifier.defaultWeight()) Box( @@ -329,6 +325,7 @@ private fun TripViewBottomRowGlance( ) { if (loading) { CircularProgressIndicator( + color = GlanceTheme.colors.onPrimaryContainer, modifier = GlanceModifier.defaultWeight() .size(24.dp) ) From baef5d1de23d91bed81308885bbcc4a6bca82bd3 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Sun, 29 Mar 2026 20:21:56 +0200 Subject: [PATCH 29/43] refactoring: Added menu icon Added a menu icon to the widget to open the configuration menu --- ...TripGlance.kt => LineTripsScreenGlance.kt} | 19 ++++++++++--------- app/src/main/res/drawable/menu.xml | 5 +++++ 2 files changed, 15 insertions(+), 9 deletions(-) rename app/src/main/java/org/stypox/tridenta/widget/ui/{LineTripGlance.kt => LineTripsScreenGlance.kt} (93%) create mode 100644 app/src/main/res/drawable/menu.xml diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripsScreenGlance.kt similarity index 93% rename from app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt rename to app/src/main/java/org/stypox/tridenta/widget/ui/LineTripsScreenGlance.kt index 061373b..da6c48d 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripsScreenGlance.kt @@ -26,7 +26,6 @@ import androidx.glance.text.Text import androidx.glance.text.TextStyle import org.stypox.tridenta.R import org.stypox.tridenta.enums.Direction -import org.stypox.tridenta.enums.StopLineType import org.stypox.tridenta.repo.data.UiLine import org.stypox.tridenta.repo.data.UiTrip import org.stypox.tridenta.util.textColorOnBackground @@ -41,8 +40,6 @@ fun LineTripsWidgetScreen( loading: Boolean, prevEnabled: Boolean, nextEnabled: Boolean, - stopIdToHighlight: Int?, - stopTypeToHighlight: StopLineType?, isFavorite: Boolean, directionFilter: Direction, onReloadAction: Action, @@ -54,7 +51,7 @@ fun LineTripsWidgetScreen( Column( modifier = GlanceModifier.fillMaxSize().background(GlanceTheme.colors.background) ) { - WidgetAppBar( + LineAppBar( line = line, isFavorite = isFavorite, directionFilter = directionFilter, @@ -70,14 +67,14 @@ fun LineTripsWidgetScreen( onNextAction = onNextAction, prevEnabled = prevEnabled, nextEnabled = nextEnabled, - stopIdToHighlight = stopIdToHighlight, - stopTypeToHighlight = stopTypeToHighlight + stopIdToHighlight = null, + stopTypeToHighlight = null ) } } @Composable -fun WidgetAppBar( +fun LineAppBar( line: UiLine?, isFavorite: Boolean, directionFilter: Direction, @@ -92,6 +89,12 @@ fun WidgetAppBar( .padding(horizontal = 12.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { + Image( + provider = ImageProvider(R.drawable.menu), + contentDescription = "menu", + modifier = GlanceModifier.clickable(onLineClickAction).padding(end = 8.dp), + colorFilter = ColorFilter.tint(GlanceTheme.colors.onSurfaceVariant), + ) // 1. The Title (Left Side) // defaultWeight() makes the text take up all leftover space, @@ -103,8 +106,6 @@ fun WidgetAppBar( val shortNameBackground = line.color.toLineColor() val textColor = textColorOnBackground(shortNameBackground) Row( - modifier = GlanceModifier - .clickable(onLineClickAction), verticalAlignment = Alignment.CenterVertically, ) { Text( diff --git a/app/src/main/res/drawable/menu.xml b/app/src/main/res/drawable/menu.xml new file mode 100644 index 0000000..68a77c2 --- /dev/null +++ b/app/src/main/res/drawable/menu.xml @@ -0,0 +1,5 @@ + + + + + From 1dc20b4dd025491fdfacf61fb929c06c1da5d47a Mon Sep 17 00:00:00 2001 From: whyKVD Date: Sun, 29 Mar 2026 20:25:33 +0200 Subject: [PATCH 30/43] refactoring: Deleted the unused keys --- .../tridenta/widget/actions/WidgetKeys.kt | 26 +++---------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt index a4b6727..51e26ca 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetKeys.kt @@ -1,28 +1,8 @@ package org.stypox.tridenta.widget.actions -import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.intPreferencesKey -import androidx.datastore.preferences.core.longPreferencesKey -import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.glance.action.ActionParameters object WidgetKeys { - val REFRESH_TIMESTAMP = longPreferencesKey("refresh_timestamp") - val TRIP_INDEX = intPreferencesKey("trip_index") - val LINE_ID = intPreferencesKey("line_id") - val TRIPS_IN_DAY_COUNT = intPreferencesKey("trips_in_day_count") - val PREV_TRIP_INDEX = intPreferencesKey("prev_trip_index") - - // Save booleans - val IS_LOADING = booleanPreferencesKey("is_loading") - val IS_INITIAL_DATA_LOADED = booleanPreferencesKey("is_initial_data_loaded") - val HAS_ERROR = booleanPreferencesKey("has_error") - val TOGGLED_DIRECTION = booleanPreferencesKey("toggled_direction") - val CHANGED_LINE = booleanPreferencesKey("changed_line") - val CHANGED_TRIP = booleanPreferencesKey("changed_trip") - val PREV_ENABLED = booleanPreferencesKey("prev_enabled") - val NEXT_ENABLED = booleanPreferencesKey("next_enabled") - - // Save strings (Useful for Enums, IDs, or serialized JSON) - val LINE_TYPE = stringPreferencesKey("line_type") // e.g., "Urban", "Suburban" - val DIRECTION_FILTER = stringPreferencesKey("direction_filter") // e.g., "Forward", "Backward" + val STOP_ID = ActionParameters.Key("stop_id") + val STOP_TYPE = ActionParameters.Key("stop_type") } \ No newline at end of file From 5d69830b081d1b58f8d79486a4b8b059f3c33f82 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Sun, 29 Mar 2026 20:32:33 +0200 Subject: [PATCH 31/43] refactoring: Renamed Available to LineTripsAvailable --- .../tridenta/widget/{LineTripWidget.kt => TripWidget.kt} | 4 +--- .../org/stypox/tridenta/widget/WidgetConfigurationActivity.kt | 2 +- app/src/main/java/org/stypox/tridenta/widget/WidgetState.kt | 4 +++- 3 files changed, 5 insertions(+), 5 deletions(-) rename app/src/main/java/org/stypox/tridenta/widget/{LineTripWidget.kt => TripWidget.kt} (98%) diff --git a/app/src/main/java/org/stypox/tridenta/widget/LineTripWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/TripWidget.kt similarity index 98% rename from app/src/main/java/org/stypox/tridenta/widget/LineTripWidget.kt rename to app/src/main/java/org/stypox/tridenta/widget/TripWidget.kt index 51bd04a..bc79054 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/LineTripWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/TripWidget.kt @@ -55,7 +55,7 @@ class LineTripWidget : GlanceAppWidget() { GlanceTheme { when (state) { - is WidgetState.Available -> + is WidgetState.LineTripsAvailable -> LineTripsWidgetScreen( line = state.line, trip = state.trip, @@ -67,8 +67,6 @@ class LineTripWidget : GlanceAppWidget() { onLineClickAction = actionStartActivity(configIntent), directionFilter = state.directionFilter, onDirectionClickAction = actionRunCallback(), - stopIdToHighlight = null, - stopTypeToHighlight = null, prevEnabled = state.prevEnabled, nextEnabled = state.nextEnabled, isFavorite = state.line?.isFavorite ?: false diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt index 42e5c6b..1c55384 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt @@ -147,7 +147,7 @@ class WidgetConfigurationActivity : // 3. Save the selected data to this specific widget's Preferences updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { - WidgetState.Available( + WidgetState.LineTripsAvailable( line, trip, ZonedDateTime.now(), diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetState.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetState.kt index 8b6b56f..b4a1b56 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/WidgetState.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/WidgetState.kt @@ -1,7 +1,9 @@ package org.stypox.tridenta.widget import kotlinx.serialization.Serializable +import org.stypox.tridenta.db.data.DbStop import org.stypox.tridenta.enums.Direction +import org.stypox.tridenta.extractor.ROME_ZONE_ID import org.stypox.tridenta.repo.data.UiLine import org.stypox.tridenta.repo.data.UiTrip import org.stypox.tridenta.util.ZonedDateTimeSerializer @@ -10,7 +12,7 @@ import java.time.ZonedDateTime @Serializable sealed interface WidgetState { @Serializable - data class Available( + data class LineTripsAvailable( val line: UiLine?, val trip: UiTrip?, @Serializable(with = ZonedDateTimeSerializer::class) From 4adf5b312e0546bcfbd2302f4a40d555c933cf33 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Sun, 29 Mar 2026 20:36:06 +0200 Subject: [PATCH 32/43] feat: Added StopTripsState Created a view also for the stops in the widget with his own state --- .../org/stypox/tridenta/widget/TripWidget.kt | 36 +++++- .../org/stypox/tridenta/widget/WidgetState.kt | 12 ++ .../widget/ui/StopTripsScreenGlance.kt | 103 ++++++++++++++++++ 3 files changed, 146 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/org/stypox/tridenta/widget/ui/StopTripsScreenGlance.kt diff --git a/app/src/main/java/org/stypox/tridenta/widget/TripWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/TripWidget.kt index bc79054..080a03c 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/TripWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/TripWidget.kt @@ -14,7 +14,11 @@ import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.action.actionRunCallback import androidx.glance.appwidget.action.actionStartActivity import androidx.glance.appwidget.provideContent +import androidx.glance.background import androidx.glance.currentState +import androidx.glance.layout.Alignment +import androidx.glance.layout.Box +import androidx.glance.layout.fillMaxSize import androidx.glance.layout.padding import androidx.glance.preview.ExperimentalGlancePreviewApi import androidx.glance.preview.Preview @@ -35,6 +39,7 @@ import org.stypox.tridenta.widget.actions.PrevTripAction import org.stypox.tridenta.widget.actions.ReloadTripAction import org.stypox.tridenta.widget.actions.ToggleDirectionAction import org.stypox.tridenta.widget.ui.LineTripsWidgetScreen +import org.stypox.tridenta.widget.ui.StopTripsScreenGlance import org.stypox.tridenta.widget.ui.TripViewGlance import java.time.OffsetDateTime import java.time.ZoneOffset @@ -72,11 +77,32 @@ class LineTripWidget : GlanceAppWidget() { isFavorite = state.line?.isFavorite ?: false ) - is WidgetState.Unavailable -> Text( - text = context.getString(R.string.error), - style = TextStyle(color = GlanceTheme.colors.error), - modifier = GlanceModifier.padding(8.dp) - ) + is WidgetState.StopTripsAvailable -> { + StopTripsScreenGlance( + stop = state.stop, + trip = state.trip, + error = state.error, + loading = state.loading, + onReloadAction = actionRunCallback(), + onPrevAction = actionRunCallback(), + onNextAction = actionRunCallback(), + onLineClickAction = actionStartActivity(configIntent), + prevEnabled = state.prevEnabled, + nextEnabled = state.nextEnabled, + isFavorite = state.stop?.isFavorite ?: false + ) + } + + is WidgetState.Unavailable -> Box( + contentAlignment = Alignment.Center, + modifier = GlanceModifier.fillMaxSize().background(GlanceTheme.colors.background) + ) { + Text( + text = context.getString(R.string.error), + style = TextStyle(color = GlanceTheme.colors.error), + modifier = GlanceModifier.padding(8.dp) + ) + } } } } diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetState.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetState.kt index b4a1b56..e90d6d1 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/WidgetState.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/WidgetState.kt @@ -26,6 +26,18 @@ sealed interface WidgetState { val loading: Boolean = true, ) : WidgetState + @Serializable + data class StopTripsAvailable( + val stop: DbStop? = null, // <- when null, nothing was loaded yet + val tripIndex: Int = 0, // <- makes sense only if trip != null + val trip: UiTrip? = null, + val prevEnabled: Boolean = false, + val nextEnabled: Boolean = false, + @Serializable(with = ZonedDateTimeSerializer::class) + val referenceDateTime: ZonedDateTime = ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID), // <- reuse only when trip != null + val loading: Boolean = true, + val error: Boolean = false, + ) : WidgetState @Serializable data class Unavailable(val message: String) : WidgetState } \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/StopTripsScreenGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/StopTripsScreenGlance.kt new file mode 100644 index 0000000..2f7c20c --- /dev/null +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/StopTripsScreenGlance.kt @@ -0,0 +1,103 @@ +package org.stypox.tridenta.widget.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.glance.ColorFilter +import androidx.glance.GlanceModifier +import androidx.glance.GlanceTheme +import androidx.glance.Image +import androidx.glance.ImageProvider +import androidx.glance.LocalContext +import androidx.glance.action.Action +import androidx.glance.action.clickable +import androidx.glance.background +import androidx.glance.layout.Alignment +import androidx.glance.layout.Column +import androidx.glance.layout.Row +import androidx.glance.layout.Spacer +import androidx.glance.layout.fillMaxSize +import androidx.glance.layout.fillMaxWidth +import androidx.glance.layout.padding +import androidx.glance.text.Text +import androidx.glance.text.TextStyle +import org.stypox.tridenta.R +import org.stypox.tridenta.db.data.DbStop +import org.stypox.tridenta.repo.data.UiTrip +import org.stypox.tridenta.widget.theme.SmallCircularProgressIndicatorGlance + +@Composable +fun StopTripsScreenGlance( + stop: DbStop?, + trip: UiTrip?, + error: Boolean, + loading: Boolean, + prevEnabled: Boolean, + nextEnabled: Boolean, + onReloadAction: Action, + onPrevAction: Action, + onNextAction: Action, + onLineClickAction: Action, + isFavorite: Boolean, +) { + Column( + modifier = GlanceModifier.fillMaxSize().background(GlanceTheme.colors.background) + ) { + StopAppBar(stop, isFavorite, onLineClickAction) + TripViewGlance( + trip = trip, + error = error, + loading = loading, + onReloadAction = onReloadAction, + onPrevAction = onPrevAction, + onNextAction = onNextAction, + prevEnabled = prevEnabled, + nextEnabled = nextEnabled, + stopIdToHighlight = stop?.stopId, + stopTypeToHighlight = stop?.type + ) + } +} + +@Composable +fun StopAppBar( + stop: DbStop?, + isFavorite: Boolean, + onLineClickAction: Action, +) { + val context = LocalContext.current + Row( + modifier = GlanceModifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + provider = ImageProvider(R.drawable.menu), + contentDescription = "menu", + modifier = GlanceModifier.clickable(onLineClickAction).padding(end = 8.dp), + colorFilter = ColorFilter.tint(GlanceTheme.colors.onSurfaceVariant), + ) + if (stop == null) { + SmallCircularProgressIndicatorGlance() + } else { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stop.name, + maxLines = 1, + style = TextStyle(color = GlanceTheme.colors.onSurface) + ) + } + } + Spacer(GlanceModifier.defaultWeight()) + + Image( + provider = ImageProvider( + if (isFavorite) R.drawable.favorite_filled else R.drawable.favorite + ), + colorFilter = ColorFilter.tint(GlanceTheme.colors.onSurface), + contentDescription = context.getString(R.string.favorite), + ) + } +} \ No newline at end of file From d2984e259a34a3dfdf94d2791a4b5cfcf7ccfc23 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Sun, 29 Mar 2026 20:40:11 +0200 Subject: [PATCH 33/43] feat: Updated the actions and created a new entry point Created a new action to switch from line to stop view and also updated the old actions to react at different states, Also added a new entry point to work with the stop state --- .../tridenta/widget/actions/WidgetActions.kt | 390 ++++++++++++++++-- 1 file changed, 357 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt index 4cf3759..980f088 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt @@ -14,12 +14,15 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.stypox.tridenta.db.HistoryDao import org.stypox.tridenta.enums.Direction +import org.stypox.tridenta.enums.StopLineType import org.stypox.tridenta.extractor.ROME_ZONE_ID import org.stypox.tridenta.log.logError import org.stypox.tridenta.log.logInfo import org.stypox.tridenta.log.logWarning import org.stypox.tridenta.repo.LineTripsRepository import org.stypox.tridenta.repo.LinesRepository +import org.stypox.tridenta.repo.StopTripsRepository +import org.stypox.tridenta.repo.StopsRepository import org.stypox.tridenta.repo.data.UiTrip import org.stypox.tridenta.widget.LineTripWidget import org.stypox.tridenta.widget.LineTripWidgetStateDefinition @@ -39,7 +42,7 @@ interface WidgetEntryPoint { context, LineTripWidgetStateDefinition, glanceId ) - if (state !is WidgetState.Available) return + if (state !is WidgetState.LineTripsAvailable) return if (index < 0) { logWarning("index must be positive") return @@ -56,21 +59,21 @@ interface WidgetEntryPoint { // condition will be false e.g. when there are no trips in a day (but not only for that) loadIndexAsync(index, state, context, glanceId) updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { s -> - if (s is WidgetState.Available) s.copy(loading = false) else s + if (s is WidgetState.LineTripsAvailable) s.copy(loading = false) else s } LineTripWidget().update(context, glanceId) } private suspend fun loadIndexAsync( index: Int, - state: WidgetState.Available, + state: WidgetState.LineTripsAvailable, context: Context, glanceId: GlanceId ) { // hide the current trip, as it's going to change val prevState = state updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { state -> - if (state is WidgetState.Available) state.copy( + if (state is WidgetState.LineTripsAvailable) state.copy( tripIndex = index, trip = null, prevEnabled = index > 0, @@ -95,13 +98,13 @@ interface WidgetEntryPoint { suspend fun setReferenceDateTimeAsync( referenceDateTimeCurrentZone: ZonedDateTime, - state: WidgetState.Available, + state: WidgetState.LineTripsAvailable, context: Context, glanceId: GlanceId ) { val referenceDateTime = referenceDateTimeCurrentZone.withZoneSameInstant(ROME_ZONE_ID) updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { state -> - if (state is WidgetState.Available) state.copy( + if (state is WidgetState.LineTripsAvailable) state.copy( tripsInDayCount = 0, tripIndex = 0, trip = null, @@ -131,7 +134,7 @@ interface WidgetEntryPoint { } } updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { state -> - if (state is WidgetState.Available) state.copy( + if (state is WidgetState.LineTripsAvailable) state.copy( tripsInDayCount = tripsInDayCount, tripIndex = tripIndex, trip = trip, prevEnabled = tripIndex > 0, nextEnabled = tripIndex < tripsInDayCount - 1, @@ -147,7 +150,7 @@ interface WidgetEntryPoint { context, LineTripWidgetStateDefinition, glanceId ) - if (state !is WidgetState.Available) return + if (state !is WidgetState.LineTripsAvailable) return val previousTrip = state.trip if (previousTrip == null) {// this could happen if an error happened while loading initial/more trips if (state.tripsInDayCount > 0) { @@ -193,7 +196,7 @@ interface WidgetEntryPoint { private suspend fun loadIndexNoFilterAsync( index: Int, - state: WidgetState.Available, + state: WidgetState.LineTripsAvailable, context: Context, glanceId: GlanceId ): Pair { @@ -214,7 +217,7 @@ interface WidgetEntryPoint { } } updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { state -> - if (state is WidgetState.Available) state.copy( + if (state is WidgetState.LineTripsAvailable) state.copy( trip = res.first, error = res.first == null ) else state @@ -226,7 +229,7 @@ interface WidgetEntryPoint { } private suspend fun loadIndexDirectionAsync( - index: Int, prevState: WidgetState.Available, context: Context, glanceId: GlanceId + index: Int, prevState: WidgetState.LineTripsAvailable, context: Context, glanceId: GlanceId ): Pair { val res = withContext(Dispatchers.IO) { try { @@ -251,7 +254,7 @@ interface WidgetEntryPoint { // no trip could be loaded in the set direction, restore previous state, // but update prevEnabled or nextEnabled updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { state -> - if (state is WidgetState.Available) state.copy( + if (state is WidgetState.LineTripsAvailable) state.copy( tripIndex = prevState.tripIndex, trip = prevState.trip, prevEnabled = if (index < prevState.tripIndex) false else prevState.prevEnabled, @@ -265,7 +268,7 @@ interface WidgetEntryPoint { } else { val (trip, newIndex, loadedFromNetwork) = res updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { state -> - if (state is WidgetState.Available) state.copy( + if (state is WidgetState.LineTripsAvailable) state.copy( tripIndex = newIndex, trip = trip, prevEnabled = newIndex > 0, @@ -279,6 +282,225 @@ interface WidgetEntryPoint { } } +@EntryPoint +@InstallIn(SingletonComponent::class) +interface WidgetStopTripsEntryPoint { + fun stopsRepository(): StopsRepository + fun tripsRepository(): StopTripsRepository + fun historyDao(): HistoryDao + + suspend fun loadStop( + stopId: Int, + stopType: StopLineType, + context: Context, + glanceId: GlanceId + ) { + val stop = withContext(Dispatchers.IO) { + try { + stopsRepository().getDbStop(stopId, stopType).also { + if (it == null) { + logError( + "DB stop (${stopId}, ${stopType}) not found" + ) + } + + // register a view for this stop (assuming loadStop is called once) + historyDao().registerAccessed(false, stopId, stopType) + } + } catch (e: Throwable) { + logError("Could not load DB stop (${stopId}, ${stopType})", e) + null + } + } + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { + if (stop == null) { + WidgetState.Unavailable("Some error has occurred") + } else { + WidgetState.StopTripsAvailable(stop = stop) + } + } + } + + suspend fun setReferenceDateTimeAsync( + referenceDateTimeCurrentZone: ZonedDateTime, + stopId: Int, + stopType: StopLineType, + context: Context, + glanceId: GlanceId + ) { + val referenceDateTime = referenceDateTimeCurrentZone.withZoneSameInstant(ROME_ZONE_ID) + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { oldState -> + if (oldState is WidgetState.StopTripsAvailable) oldState.copy( + tripIndex = 0, + trip = null, + prevEnabled = false, + nextEnabled = false, + referenceDateTime = referenceDateTime + ) else oldState + } + LineTripWidget().update(context, glanceId) + + val tripsAtDateTimeList = withContext(Dispatchers.IO) { + try { + tripsRepository().getTrips( + stopId = stopId, + stopType = stopType, + referenceDateTime = referenceDateTime + ) + } catch (e: Throwable) { + logError( + "Could not load trips for DB stop (${stopId}, " + + "${stopType}) at time $referenceDateTime", + e + ) + null + } + } + + if (tripsAtDateTimeList == null) { + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { oldState -> + if (oldState is WidgetState.StopTripsAvailable) oldState.copy(error = true) else oldState + } + } else { + // show the first trip, which should be the next one arriving at the stop; + // requestedByUser is false since the trip is surely up-to-date, as it was just fetched + loadIndexAsync(0, false, stopId, stopType, context, glanceId) + } + } + + suspend fun loadIndex( + index: Int, + context: Context, + glanceId: GlanceId, + stopId: Int, + stopType: StopLineType + ) { + val tripsAtDateTimeList = tripsRepository().getTrips( + stopId = stopId, stopType = stopType, + ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID) + ) + if (index >= 0 && index < (tripsAtDateTimeList.tripCount)) { + // only cancel any currently running job if there is something to do; the above + // condition will be false e.g. when there are no trips in a day (but not only for that) + loadIndexAsync(index, true, stopId, stopType, context, glanceId) + } + } + + private suspend fun loadIndexAsync( + index: Int, + requestedByUser: Boolean, + stopId: Int, + stopType: StopLineType, + context: Context, + glanceId: GlanceId + ) { + val tripsAtDateTimeList = tripsRepository().getTrips( + stopId = stopId, stopType = stopType, + ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID) + ) + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { oldState -> + if (oldState is WidgetState.StopTripsAvailable) oldState.copy( + tripIndex = index, + trip = null, + prevEnabled = index > 0, + nextEnabled = index < (tripsAtDateTimeList.tripCount) - 1, + ) else oldState + } + LineTripWidget().update(context, glanceId) + + val trip = withContext(Dispatchers.IO) { + try { + tripsAtDateTimeList.getUiTripAtIndex(index) + } catch (e: Throwable) { + logError( + "Could not load trip at index $index for DB stop " + + "(${stopId}, ${stopType})", + e + ) + null + } + } + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { oldState -> + if (oldState is WidgetState.StopTripsAvailable) oldState.copy( + trip = trip, + loading = false, + error = trip == null, + ) else oldState + } + LineTripWidget().update(context, glanceId) + + if (requestedByUser && trip != null && trip.completedStops < trip.stopTimes.size) { + // after showing the (possibly) outdated trip fast, reload it to show latest updates + // (but reload it only if there actually is a trip and it is not completed) + onReloadAsync(context, glanceId, stopId, stopType) + } + } + + + suspend fun onReloadAsync( + context: Context, + glanceId: GlanceId, + stopId: Int, + stopType: StopLineType + ) { + val currentState = getAppWidgetState( + context, + LineTripWidgetStateDefinition, glanceId + ) + if (currentState !is WidgetState.StopTripsAvailable) return + val previousTrip = currentState.trip + if (previousTrip == null) { + // initial trips failed loading, try to load again the current day + setReferenceDateTimeAsync( + currentState.referenceDateTime, + stopId, + stopType, + context, + glanceId + ) + return + } + val tripsAtDateTimeList = tripsRepository().getTrips( + stopId = stopId, stopType = stopType, + ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID) + ) + + val trip = withContext(Dispatchers.IO) { + try { + tripsAtDateTimeList.reloadUiTrip( + uiTrip = previousTrip, + index = currentState.tripIndex, + referenceDateTime = currentState.referenceDateTime + ) + } catch (e: Throwable) { + logError( + "Could not load trip ${previousTrip.tripId} for DB stop " + + "(${stopId}, ${stopType})", + e + ) + null + } + } + + if (trip == null) { + // keep previous trip intact, we don't want to hide information that we do have! + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { oldState -> + if (oldState is WidgetState.StopTripsAvailable) oldState.copy( + error = true + ) else oldState + } + LineTripWidget().update(context, glanceId) + } else { + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { oldState -> + if (oldState is WidgetState.StopTripsAvailable) oldState.copy( + trip = trip + ) else oldState + } + LineTripWidget().update(context, glanceId) + } + } +} + class NextTripAction : ActionCallback { override suspend fun onAction( context: Context, glanceId: GlanceId, parameters: ActionParameters @@ -287,10 +509,31 @@ class NextTripAction : ActionCallback { context, LineTripWidgetStateDefinition, glanceId ) - if (currentState !is WidgetState.Available) return - val hiltEntryPoint = - EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) - hiltEntryPoint.loadIndex(currentState.tripIndex + 1, glanceId, context) + when (currentState) { + is WidgetState.LineTripsAvailable -> { + val hiltEntryPoint = + EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) + hiltEntryPoint.loadIndex(currentState.tripIndex + 1, glanceId, context) + } + + is WidgetState.StopTripsAvailable -> { + if (currentState.stop == null) return + val hiltEntryPoint = + EntryPointAccessors.fromApplication( + context, + WidgetStopTripsEntryPoint::class.java + ) + hiltEntryPoint.loadIndex( + currentState.tripIndex + 1, + context, + glanceId, + currentState.stop.stopId, + currentState.stop.type + ) + } + + else -> {} + } logInfo("NextTripAction performed") } @@ -304,10 +547,31 @@ class PrevTripAction : ActionCallback { context, LineTripWidgetStateDefinition, glanceId ) - if (currentState !is WidgetState.Available) return - val hiltEntryPoint = - EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) - hiltEntryPoint.loadIndex(currentState.tripIndex - 1, glanceId, context) + when (currentState) { + is WidgetState.LineTripsAvailable -> { + val hiltEntryPoint = + EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) + hiltEntryPoint.loadIndex(currentState.tripIndex - 1, glanceId, context) + } + + is WidgetState.StopTripsAvailable -> { + if (currentState.stop == null) return + val hiltEntryPoint = + EntryPointAccessors.fromApplication( + context, + WidgetStopTripsEntryPoint::class.java + ) + hiltEntryPoint.loadIndex( + currentState.tripIndex - 1, + context, + glanceId, + currentState.stop.stopId, + currentState.stop.type + ) + } + + else -> {} + } logInfo("PrevTripAction performed") } @@ -321,18 +585,46 @@ class ReloadTripAction : ActionCallback { context, LineTripWidgetStateDefinition, glanceId ) - if (currentState !is WidgetState.Available) return - val hiltEntryPoint = - EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) - updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { - currentState.copy(loading = true) - } - LineTripWidget().update(context, glanceId) - hiltEntryPoint.onReloadAsync(context, glanceId) - updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { state -> - if (state is WidgetState.Available) state.copy(loading = false) else state + when (currentState) { + is WidgetState.LineTripsAvailable -> { + val hiltEntryPoint = + EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { + currentState.copy(loading = true) + } + LineTripWidget().update(context, glanceId) + hiltEntryPoint.onReloadAsync(context, glanceId) + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { state -> + if (state is WidgetState.LineTripsAvailable) state.copy(loading = false) else state + } + LineTripWidget().update(context, glanceId) + } + + is WidgetState.StopTripsAvailable -> { + if (currentState.stop == null) return + val hiltEntryPoint = + EntryPointAccessors.fromApplication( + context, + WidgetStopTripsEntryPoint::class.java + ) + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { + currentState.copy(loading = true) + } + LineTripWidget().update(context, glanceId) + hiltEntryPoint.onReloadAsync( + context, + glanceId, + currentState.stop.stopId, + currentState.stop.type + ) + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { state -> + if (state is WidgetState.StopTripsAvailable) state.copy(loading = false) else state + } + LineTripWidget().update(context, glanceId) + } + + else -> {} } - LineTripWidget().update(context, glanceId) } } @@ -345,7 +637,7 @@ class ToggleDirectionAction : ActionCallback { LineTripWidgetStateDefinition, glanceId ) - if (currentState !is WidgetState.Available) return + if (currentState !is WidgetState.LineTripsAvailable) return val hiltEntryPoint = EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) val newDirectionFilter = when (currentState.directionFilter) { @@ -386,4 +678,36 @@ class ToggleDirectionAction : ActionCallback { } logInfo("ToggleDirectionAction performed, directionFilter: ${currentState.directionFilter}") } +} + +class OnStopClickAction : ActionCallback { + override suspend fun onAction( + context: Context, + glanceId: GlanceId, + parameters: ActionParameters + ) { + logInfo("OnStopClickAction") + val stopId = parameters[WidgetKeys.STOP_ID] ?: return + val stopTypeRaw = parameters[WidgetKeys.STOP_TYPE] ?: return + val stopType = StopLineType.valueOf(stopTypeRaw) + logInfo("OnStopClickAction") + val currentState: WidgetState = getAppWidgetState( + context, + LineTripWidgetStateDefinition, glanceId + ) + + if (currentState is WidgetState.Unavailable) return + val hiltEntryPoint = + EntryPointAccessors.fromApplication(context, WidgetStopTripsEntryPoint::class.java) + hiltEntryPoint.loadStop(stopId, stopType, context, glanceId) + hiltEntryPoint.setReferenceDateTimeAsync( + ZonedDateTime.now(), + stopId, + stopType, + context, + glanceId + ) + + LineTripWidget().update(context, glanceId) + } } \ No newline at end of file From 3ad45b5536794e24f3c9c43bab52c55a6aee9aa2 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Sun, 29 Mar 2026 20:41:56 +0200 Subject: [PATCH 34/43] feat: Added the stopClickAction run callback --- .../tridenta/widget/ui/TripViewStopsGlance.kt | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt index dcfef90..99eb689 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt @@ -8,11 +8,13 @@ import androidx.glance.GlanceTheme import androidx.glance.Image import androidx.glance.ImageProvider import androidx.glance.LocalContext +import androidx.glance.action.actionParametersOf +import androidx.glance.action.clickable +import androidx.glance.appwidget.action.actionRunCallback import androidx.glance.appwidget.lazy.LazyColumn import androidx.glance.appwidget.lazy.itemsIndexed import androidx.glance.layout.Alignment import androidx.glance.layout.Row -import androidx.glance.layout.Spacer import androidx.glance.layout.fillMaxWidth import androidx.glance.layout.height import androidx.glance.layout.padding @@ -35,6 +37,8 @@ import org.stypox.tridenta.repo.data.UiStopTime import org.stypox.tridenta.repo.data.UiTrip import org.stypox.tridenta.util.formatConcatStrings import org.stypox.tridenta.util.formatTime +import org.stypox.tridenta.widget.actions.OnStopClickAction +import org.stypox.tridenta.widget.actions.WidgetKeys import java.time.OffsetDateTime import java.time.ZoneOffset @@ -44,7 +48,6 @@ fun TripViewStopsGlance( stopIdToHighlight: Int?, stopTypeToHighlight: StopLineType?, modifier: GlanceModifier = GlanceModifier, - onStopClick: ((DbStop) -> Unit)? = null, ) { val context = LocalContext.current LazyColumn( @@ -59,6 +62,18 @@ fun TripViewStopsGlance( stopTime.stop.type == stopTypeToHighlight, completed = index < trip.completedStops, stopTime = stopTime, + modifier = if (stopTime.stop == null) { + GlanceModifier // not clickable, since there is no stop + } else { + GlanceModifier.clickable( + actionRunCallback( + actionParametersOf( + WidgetKeys.STOP_ID to stopTime.stop.stopId, + WidgetKeys.STOP_TYPE to stopTime.stop.type.name + ) + ) + ) + } ) } @@ -360,7 +375,7 @@ fun TripViewStopsPreview() { busId = 886, ), stopIdToHighlight = null, - stopTypeToHighlight = null + stopTypeToHighlight = null, ) } } \ No newline at end of file From f3bea5af9730aef691998855184e38f3e0be7820 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Sun, 29 Mar 2026 20:42:57 +0200 Subject: [PATCH 35/43] refactoring: Refactored the view to work better --- .../tridenta/widget/ui/TripViewGlance.kt | 141 ++++++++---------- .../tridenta/widget/ui/TripViewStopsGlance.kt | 7 +- 2 files changed, 67 insertions(+), 81 deletions(-) diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt index f9c8d1f..30cf42c 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt @@ -68,93 +68,84 @@ fun TripViewGlance( ) { val context = LocalContext.current - Box( + Column( modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.Center ) { - if (trip != null) { - Column( - modifier = GlanceModifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - TripViewTopRowGlance( - trip = trip, - modifier = GlanceModifier.padding( - start = 12.dp, - top = 4.dp, - end = 12.dp, - bottom = 12.dp - ).fillMaxWidth() - ) + Box(modifier = GlanceModifier.defaultWeight().fillMaxWidth(), contentAlignment = Alignment.Center) { + if (trip != null) { + Column( + modifier = GlanceModifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + TripViewTopRowGlance( + trip = trip, + modifier = GlanceModifier.padding( + start = 12.dp, + top = 4.dp, + end = 12.dp, + bottom = 12.dp + ).fillMaxWidth() + ) - if (error) { - // Replaced your custom ErrorRow with a simple Glance Text for the widget + if (error) { + Text( + text = context.getString(R.string.error), + style = TextStyle(color = GlanceTheme.colors.error), + modifier = GlanceModifier.padding(8.dp) + ) + } + + TripViewStopsGlance( + trip = trip, + stopIdToHighlight = stopIdToHighlight, + stopTypeToHighlight = stopTypeToHighlight, + modifier = GlanceModifier.defaultWeight() // Crucial for lists in Columns + ) + } + } else if (loading) { + CircularProgressIndicator() + } else if (error) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( text = context.getString(R.string.error), - style = TextStyle(color = GlanceTheme.colors.error), - modifier = GlanceModifier.padding(8.dp) + style = TextStyle(color = GlanceTheme.colors.error) ) + Button(text = context.getString(R.string.reload), onClick = onReloadAction) } - - // Assuming this is already refactored to be Glance-compliant! - TripViewStopsGlance( - trip = trip, - stopIdToHighlight = stopIdToHighlight, - stopTypeToHighlight = stopTypeToHighlight, - modifier = GlanceModifier.defaultWeight() // Crucial for lists in Columns - ) - } - - } else if (loading) { - CircularProgressIndicator() - - } else if (error) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - text = context.getString(R.string.error), - style = TextStyle(color = GlanceTheme.colors.error) - ) - Button(text = context.getString(R.string.reload), onClick = onReloadAction) - } - - } else { - Column( - modifier = GlanceModifier.padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = context.getString(R.string.no_trip_found), - style = TextStyle( - fontWeight = FontWeight.Bold, - color = GlanceTheme.colors.onBackground - ), - modifier = GlanceModifier.padding(bottom = 4.dp) - ) - Text( - text = context.getString(R.string.no_trip_found_description), - style = TextStyle( - textAlign = TextAlign.Center, - color = GlanceTheme.colors.onBackground + } else { + Column( + modifier = GlanceModifier.padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = context.getString(R.string.no_trip_found), + style = TextStyle( + fontWeight = FontWeight.Bold, + color = GlanceTheme.colors.onBackground + ), + modifier = GlanceModifier.padding(bottom = 4.dp) ) - ) + Text( + text = context.getString(R.string.no_trip_found_description), + style = TextStyle( + textAlign = TextAlign.Center, + color = GlanceTheme.colors.onBackground + ) + ) + } } } // Bottom Row is aligned to the bottom using a Box setup - Box( - modifier = GlanceModifier.fillMaxSize(), - contentAlignment = Alignment.BottomCenter - ) { - TripViewBottomRowGlance( - loading = loading, - onReloadAction = onReloadAction, - onPrevAction = onPrevAction, - onNextAction = onNextAction, - prevEnabled = prevEnabled, - nextEnabled = nextEnabled, - modifier = GlanceModifier.fillMaxWidth() - ) - } + TripViewBottomRowGlance( + loading = loading, + onReloadAction = onReloadAction, + onPrevAction = onPrevAction, + onNextAction = onNextAction, + prevEnabled = prevEnabled, + nextEnabled = nextEnabled, + modifier = GlanceModifier.fillMaxWidth() + ) } } diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt index 99eb689..5e31cfd 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt @@ -52,7 +52,7 @@ fun TripViewStopsGlance( val context = LocalContext.current LazyColumn( modifier = modifier, - horizontalAlignment = Alignment.Start, + horizontalAlignment = Alignment.CenterHorizontally, ) { itemsIndexed(trip.stopTimes) { index, stopTime -> TripViewStopItemGlance( @@ -100,11 +100,6 @@ fun TripViewStopsGlance( modifier = GlanceModifier.padding(8.dp) ) } - - item { - // space for FABs - Spacer(modifier = GlanceModifier.size(height = 84.dp, width = 0.dp)) - } } } From 0aa28673dc2fa08ec563453ed2ff905b1c5b3345 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Sun, 29 Mar 2026 20:44:02 +0200 Subject: [PATCH 36/43] refactoring: Renamed the state --- .../java/org/stypox/tridenta/widget/LineTripWidgetWorker.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetWorker.kt b/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetWorker.kt index 892df1d..7307110 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetWorker.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetWorker.kt @@ -50,12 +50,12 @@ class LineTripWidgetWorker( context, LineTripWidgetStateDefinition, glanceId ) - if (currentState is WidgetState.Available) { + if (currentState is WidgetState.LineTripsAvailable) { updateAppWidgetState( context, LineTripWidgetStateDefinition, glanceId - ) { oldState -> if (oldState is WidgetState.Available) oldState.copy(loading = true) else oldState } + ) { oldState -> if (oldState is WidgetState.LineTripsAvailable) oldState.copy(loading = true) else oldState } LineTripWidget().update(context, glanceId) // TODO Retrieve the updated state @@ -84,7 +84,7 @@ class LineTripWidgetWorker( LineTripWidgetStateDefinition, glanceId ) { oldState -> - oldState as WidgetState.Available + oldState as WidgetState.LineTripsAvailable if (trip != null) { oldState.copy( trip = trip, From 3766b0c8a8ab24c8b8bcbfc1b0d48dab921c679b Mon Sep 17 00:00:00 2001 From: whyKVD Date: Wed, 1 Apr 2026 10:45:51 +0200 Subject: [PATCH 37/43] refactoring: Added some loading behavior --- .../tridenta/widget/actions/WidgetActions.kt | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt index 980f088..1062e12 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt @@ -511,13 +511,25 @@ class NextTripAction : ActionCallback { ) when (currentState) { is WidgetState.LineTripsAvailable -> { + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { + currentState.copy(loading = true) + } + LineTripWidget().update(context, glanceId) val hiltEntryPoint = EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) hiltEntryPoint.loadIndex(currentState.tripIndex + 1, glanceId, context) + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { + oldState -> if(oldState is WidgetState.LineTripsAvailable) oldState.copy(loading = false) else oldState + } + LineTripWidget().update(context, glanceId) } is WidgetState.StopTripsAvailable -> { if (currentState.stop == null) return + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { + currentState.copy(loading = true) + } + LineTripWidget().update(context, glanceId) val hiltEntryPoint = EntryPointAccessors.fromApplication( context, @@ -530,6 +542,10 @@ class NextTripAction : ActionCallback { currentState.stop.stopId, currentState.stop.type ) + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { + oldState -> if(oldState is WidgetState.StopTripsAvailable) oldState.copy(loading = false) else oldState + } + LineTripWidget().update(context, glanceId) } else -> {} @@ -549,13 +565,25 @@ class PrevTripAction : ActionCallback { ) when (currentState) { is WidgetState.LineTripsAvailable -> { + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { + currentState.copy(loading = true) + } + LineTripWidget().update(context, glanceId) val hiltEntryPoint = EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) hiltEntryPoint.loadIndex(currentState.tripIndex - 1, glanceId, context) + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { + oldState -> if(oldState is WidgetState.LineTripsAvailable) oldState.copy(loading = false) else oldState + } + LineTripWidget().update(context, glanceId) } is WidgetState.StopTripsAvailable -> { if (currentState.stop == null) return + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { + currentState.copy(loading = true) + } + LineTripWidget().update(context, glanceId) val hiltEntryPoint = EntryPointAccessors.fromApplication( context, @@ -568,6 +596,10 @@ class PrevTripAction : ActionCallback { currentState.stop.stopId, currentState.stop.type ) + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { + oldState -> if(oldState is WidgetState.StopTripsAvailable) oldState.copy(loading = false) else oldState + } + LineTripWidget().update(context, glanceId) } else -> {} From a46dd570f699008b12c755c24616eaf185868831 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Wed, 1 Apr 2026 10:57:34 +0200 Subject: [PATCH 38/43] refactoring: Added the updating feature for the stop state --- .../tridenta/widget/LineTripWidgetWorker.kt | 134 ++++++++++++------ 1 file changed, 93 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetWorker.kt b/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetWorker.kt index 7307110..40bcecb 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetWorker.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetWorker.kt @@ -13,6 +13,7 @@ import dagger.hilt.android.EntryPointAccessors import org.stypox.tridenta.extractor.ROME_ZONE_ID import org.stypox.tridenta.log.logInfo import org.stypox.tridenta.widget.actions.WidgetEntryPoint +import org.stypox.tridenta.widget.actions.WidgetStopTripsEntryPoint import java.time.ZonedDateTime import java.util.concurrent.TimeUnit @@ -50,55 +51,106 @@ class LineTripWidgetWorker( context, LineTripWidgetStateDefinition, glanceId ) - if (currentState is WidgetState.LineTripsAvailable) { - updateAppWidgetState( - context, - LineTripWidgetStateDefinition, - glanceId - ) { oldState -> if (oldState is WidgetState.LineTripsAvailable) oldState.copy(loading = true) else oldState } - LineTripWidget().update(context, glanceId) - - // TODO Retrieve the updated state - if (currentState.line == null) { - updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { - WidgetState.Unavailable("Something went wrong") + when (currentState) { + is WidgetState.LineTripsAvailable -> { + updateAppWidgetState( + context, + LineTripWidgetStateDefinition, + glanceId + ) { oldState -> + if (oldState is WidgetState.LineTripsAvailable) oldState.copy( + loading = true + ) else oldState } LineTripWidget().update(context, glanceId) - return@forEach + + if (currentState.line == null) { + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { + WidgetState.Unavailable("Something went wrong") + } + LineTripWidget().update(context, glanceId) + return@forEach + } + + val hiltEntryPoint = + EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) + val tripsRepository = hiltEntryPoint.lineTripsRepository() + val referenceDateTime = ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID) + val (tripsInDayCount, tripIndex, trip) = tripsRepository.getUiTrip( + currentState.line.lineId, + currentState.line.type, + referenceDateTime, + currentState.directionFilter + ) + logInfo("trip: $trip") + + updateAppWidgetState( + context, + LineTripWidgetStateDefinition, + glanceId + ) { oldState -> + oldState as WidgetState.LineTripsAvailable + if (trip != null) { + oldState.copy( + trip = trip, + referenceDateTime = referenceDateTime, + tripsInDayCount = tripsInDayCount, + tripIndex = tripIndex, + prevEnabled = tripIndex > 0, + nextEnabled = tripIndex < tripsInDayCount - 1, + loading = false + ) + } else { + oldState.copy(error = true, loading = false) + } + } } - val hiltEntryPoint = - EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) - val tripsRepository = hiltEntryPoint.lineTripsRepository() - val referenceDateTime = ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID) - val (tripsInDayCount, tripIndex, trip) = tripsRepository.getUiTrip( - currentState.line.lineId, - currentState.line.type, - referenceDateTime, - currentState.directionFilter - ) - logInfo("trip: $trip") + is WidgetState.StopTripsAvailable -> { + updateAppWidgetState( + context, + LineTripWidgetStateDefinition, + glanceId + ) { oldState -> + if (oldState is WidgetState.StopTripsAvailable) oldState.copy( + loading = true + ) else oldState + } + LineTripWidget().update(context, glanceId) - updateAppWidgetState( - context, - LineTripWidgetStateDefinition, - glanceId - ) { oldState -> - oldState as WidgetState.LineTripsAvailable - if (trip != null) { - oldState.copy( - trip = trip, - referenceDateTime = referenceDateTime, - tripsInDayCount = tripsInDayCount, - tripIndex = tripIndex, - prevEnabled = tripIndex > 0, - nextEnabled = tripIndex < tripsInDayCount - 1, - loading = false + if (currentState.stop == null) { + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { + WidgetState.Unavailable("Something went wrong") + } + LineTripWidget().update(context, glanceId) + return@forEach + } + + val hiltEntryPoint = + EntryPointAccessors.fromApplication( + context, + WidgetStopTripsEntryPoint::class.java ) - } else { - oldState.copy(error = true, loading = false) + val referenceDateTime = ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID) + hiltEntryPoint.setReferenceDateTimeAsync( + referenceDateTime, + currentState.stop.stopId, + currentState.stop.type, + context, + glanceId + ) + updateAppWidgetState( + context, + LineTripWidgetStateDefinition, + glanceId + ) { oldState -> + if (oldState is WidgetState.StopTripsAvailable) oldState.copy( + loading = false + ) else oldState } } + + else -> {} } LineTripWidget().update(context, glanceId) } From f0f02c334c2598756814cf7e5d8217dc11eea1ae Mon Sep 17 00:00:00 2001 From: whyKVD Date: Fri, 3 Apr 2026 09:34:48 +0200 Subject: [PATCH 39/43] feat: Limited Widget worker updates Now the Worker update the widgets only if is passed 15 minutes from referenceDateTime update --- .../stypox/tridenta/widget/LineTripWidgetWorker.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetWorker.kt b/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetWorker.kt index 40bcecb..fd43334 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetWorker.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/LineTripWidgetWorker.kt @@ -53,6 +53,12 @@ class LineTripWidgetWorker( ) when (currentState) { is WidgetState.LineTripsAvailable -> { + if (currentState.referenceDateTime.isAfter( + ZonedDateTime.now().minusMinutes(15) + ) + ) { + return@forEach + } updateAppWidgetState( context, LineTripWidgetStateDefinition, @@ -107,6 +113,12 @@ class LineTripWidgetWorker( } is WidgetState.StopTripsAvailable -> { + if (currentState.referenceDateTime.isAfter( + ZonedDateTime.now().minusMinutes(15) + ) + ) { + return@forEach + } updateAppWidgetState( context, LineTripWidgetStateDefinition, From 9b8d9546ce67c589c6b64d1d7eb70684d22899d3 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Fri, 3 Apr 2026 10:38:11 +0200 Subject: [PATCH 40/43] feat: Added a show hide feature for the completed stops --- .../org/stypox/tridenta/widget/TripWidget.kt | 10 ++- .../org/stypox/tridenta/widget/WidgetState.kt | 8 +- .../tridenta/widget/actions/WidgetActions.kt | 34 ++++++-- .../widget/ui/LineTripsScreenGlance.kt | 4 +- .../widget/ui/StopTripsScreenGlance.kt | 4 +- .../tridenta/widget/ui/TripViewGlance.kt | 9 ++- .../tridenta/widget/ui/TripViewStopsGlance.kt | 81 +++++++++++++++++++ app/src/main/res/drawable/arrow_down.xml | 5 ++ app/src/main/res/drawable/arrow_up.xml | 5 ++ 9 files changed, 143 insertions(+), 17 deletions(-) create mode 100644 app/src/main/res/drawable/arrow_down.xml create mode 100644 app/src/main/res/drawable/arrow_up.xml diff --git a/app/src/main/java/org/stypox/tridenta/widget/TripWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/TripWidget.kt index 080a03c..664a609 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/TripWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/TripWidget.kt @@ -74,7 +74,8 @@ class LineTripWidget : GlanceAppWidget() { onDirectionClickAction = actionRunCallback(), prevEnabled = state.prevEnabled, nextEnabled = state.nextEnabled, - isFavorite = state.line?.isFavorite ?: false + isFavorite = state.line?.isFavorite ?: false, + showPrevStop = state.showPrevStop, ) is WidgetState.StopTripsAvailable -> { @@ -89,13 +90,15 @@ class LineTripWidget : GlanceAppWidget() { onLineClickAction = actionStartActivity(configIntent), prevEnabled = state.prevEnabled, nextEnabled = state.nextEnabled, - isFavorite = state.stop?.isFavorite ?: false + isFavorite = state.stop?.isFavorite ?: false, + showPrevStop = state.showPrevStop ) } is WidgetState.Unavailable -> Box( contentAlignment = Alignment.Center, - modifier = GlanceModifier.fillMaxSize().background(GlanceTheme.colors.background) + modifier = GlanceModifier.fillMaxSize() + .background(GlanceTheme.colors.background) ) { Text( text = context.getString(R.string.error), @@ -250,6 +253,7 @@ fun MyWidgetPreview() { nextEnabled = true, stopIdToHighlight = null, stopTypeToHighlight = null, + showPrevStop = false ) } } \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetState.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetState.kt index e90d6d1..0e47465 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/WidgetState.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/WidgetState.kt @@ -24,19 +24,21 @@ sealed interface WidgetState { val directionFilter: Direction = Direction.ForwardAndBackward, val error: Boolean = false, val loading: Boolean = true, + val showPrevStop: Boolean = false, ) : WidgetState @Serializable data class StopTripsAvailable( - val stop: DbStop? = null, // <- when null, nothing was loaded yet - val tripIndex: Int = 0, // <- makes sense only if trip != null + val stop: DbStop? = null, + val tripIndex: Int = 0, val trip: UiTrip? = null, val prevEnabled: Boolean = false, val nextEnabled: Boolean = false, @Serializable(with = ZonedDateTimeSerializer::class) - val referenceDateTime: ZonedDateTime = ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID), // <- reuse only when trip != null + val referenceDateTime: ZonedDateTime = ZonedDateTime.now().withZoneSameInstant(ROME_ZONE_ID), val loading: Boolean = true, val error: Boolean = false, + val showPrevStop: Boolean = false, ) : WidgetState @Serializable data class Unavailable(val message: String) : WidgetState diff --git a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt index 1062e12..3acf6ef 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/actions/WidgetActions.kt @@ -518,8 +518,8 @@ class NextTripAction : ActionCallback { val hiltEntryPoint = EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) hiltEntryPoint.loadIndex(currentState.tripIndex + 1, glanceId, context) - updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { - oldState -> if(oldState is WidgetState.LineTripsAvailable) oldState.copy(loading = false) else oldState + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { oldState -> + if (oldState is WidgetState.LineTripsAvailable) oldState.copy(loading = false) else oldState } LineTripWidget().update(context, glanceId) } @@ -542,8 +542,8 @@ class NextTripAction : ActionCallback { currentState.stop.stopId, currentState.stop.type ) - updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { - oldState -> if(oldState is WidgetState.StopTripsAvailable) oldState.copy(loading = false) else oldState + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { oldState -> + if (oldState is WidgetState.StopTripsAvailable) oldState.copy(loading = false) else oldState } LineTripWidget().update(context, glanceId) } @@ -572,8 +572,8 @@ class PrevTripAction : ActionCallback { val hiltEntryPoint = EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java) hiltEntryPoint.loadIndex(currentState.tripIndex - 1, glanceId, context) - updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { - oldState -> if(oldState is WidgetState.LineTripsAvailable) oldState.copy(loading = false) else oldState + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { oldState -> + if (oldState is WidgetState.LineTripsAvailable) oldState.copy(loading = false) else oldState } LineTripWidget().update(context, glanceId) } @@ -596,8 +596,8 @@ class PrevTripAction : ActionCallback { currentState.stop.stopId, currentState.stop.type ) - updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { - oldState -> if(oldState is WidgetState.StopTripsAvailable) oldState.copy(loading = false) else oldState + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { oldState -> + if (oldState is WidgetState.StopTripsAvailable) oldState.copy(loading = false) else oldState } LineTripWidget().update(context, glanceId) } @@ -740,6 +740,24 @@ class OnStopClickAction : ActionCallback { glanceId ) + LineTripWidget().update(context, glanceId) + } +} + +class ToggleShowPrevStop : ActionCallback { + override suspend fun onAction( + context: Context, + glanceId: GlanceId, + parameters: ActionParameters + ) { + logInfo("ToggleShowPrevStop") + updateAppWidgetState(context, LineTripWidgetStateDefinition, glanceId) { oldState -> + when (oldState) { + is WidgetState.LineTripsAvailable -> oldState.copy(showPrevStop = !oldState.showPrevStop) + is WidgetState.StopTripsAvailable -> oldState.copy(showPrevStop = !oldState.showPrevStop) + else -> oldState + } + } LineTripWidget().update(context, glanceId) } } \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripsScreenGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripsScreenGlance.kt index da6c48d..09eb1a4 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripsScreenGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/LineTripsScreenGlance.kt @@ -41,6 +41,7 @@ fun LineTripsWidgetScreen( prevEnabled: Boolean, nextEnabled: Boolean, isFavorite: Boolean, + showPrevStop: Boolean, directionFilter: Direction, onReloadAction: Action, onPrevAction: Action, @@ -68,7 +69,8 @@ fun LineTripsWidgetScreen( prevEnabled = prevEnabled, nextEnabled = nextEnabled, stopIdToHighlight = null, - stopTypeToHighlight = null + stopTypeToHighlight = null, + showPrevStop ) } } diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/StopTripsScreenGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/StopTripsScreenGlance.kt index 2f7c20c..a9fc9fb 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/StopTripsScreenGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/StopTripsScreenGlance.kt @@ -38,6 +38,7 @@ fun StopTripsScreenGlance( onNextAction: Action, onLineClickAction: Action, isFavorite: Boolean, + showPrevStop: Boolean, ) { Column( modifier = GlanceModifier.fillMaxSize().background(GlanceTheme.colors.background) @@ -53,7 +54,8 @@ fun StopTripsScreenGlance( prevEnabled = prevEnabled, nextEnabled = nextEnabled, stopIdToHighlight = stop?.stopId, - stopTypeToHighlight = stop?.type + stopTypeToHighlight = stop?.type, + showPrevStop, ) } } diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt index 30cf42c..ab577c1 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewGlance.kt @@ -64,6 +64,7 @@ fun TripViewGlance( nextEnabled: Boolean, stopIdToHighlight: Int?, stopTypeToHighlight: StopLineType?, + showPrevStop: Boolean, modifier: GlanceModifier = GlanceModifier ) { val context = LocalContext.current @@ -71,7 +72,10 @@ fun TripViewGlance( Column( modifier = modifier.fillMaxSize(), ) { - Box(modifier = GlanceModifier.defaultWeight().fillMaxWidth(), contentAlignment = Alignment.Center) { + Box( + modifier = GlanceModifier.defaultWeight().fillMaxWidth(), + contentAlignment = Alignment.Center + ) { if (trip != null) { Column( modifier = GlanceModifier.fillMaxSize(), @@ -99,6 +103,7 @@ fun TripViewGlance( trip = trip, stopIdToHighlight = stopIdToHighlight, stopTypeToHighlight = stopTypeToHighlight, + showPrevStop, modifier = GlanceModifier.defaultWeight() // Crucial for lists in Columns ) } @@ -489,6 +494,7 @@ private fun TripViewPreview() { nextEnabled = true, stopIdToHighlight = null, stopTypeToHighlight = null, + showPrevStop = false, ) } } @@ -510,6 +516,7 @@ private fun TripViewPreviewLoading() { nextEnabled = true, stopIdToHighlight = null, stopTypeToHighlight = null, + showPrevStop = false ) } } \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt index 5e31cfd..e2f44bf 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt @@ -8,6 +8,7 @@ import androidx.glance.GlanceTheme import androidx.glance.Image import androidx.glance.ImageProvider import androidx.glance.LocalContext +import androidx.glance.LocalGlanceId import androidx.glance.action.actionParametersOf import androidx.glance.action.clickable import androidx.glance.appwidget.action.actionRunCallback @@ -38,6 +39,7 @@ import org.stypox.tridenta.repo.data.UiTrip import org.stypox.tridenta.util.formatConcatStrings import org.stypox.tridenta.util.formatTime import org.stypox.tridenta.widget.actions.OnStopClickAction +import org.stypox.tridenta.widget.actions.ToggleShowPrevStop import org.stypox.tridenta.widget.actions.WidgetKeys import java.time.OffsetDateTime import java.time.ZoneOffset @@ -47,14 +49,92 @@ fun TripViewStopsGlance( trip: UiTrip, stopIdToHighlight: Int?, stopTypeToHighlight: StopLineType?, + showPrevStop: Boolean, modifier: GlanceModifier = GlanceModifier, ) { val context = LocalContext.current + val glanceId = LocalGlanceId.current LazyColumn( modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally, ) { + item { + val index = 0 + val stopTime = trip.stopTimes[index] + TripViewStopItemGlance( + trip = trip, + highlight = stopTime.stop != null && + stopTime.stop.stopId == stopIdToHighlight && + stopTime.stop.type == stopTypeToHighlight, + completed = index < trip.completedStops, + stopTime = stopTime, + modifier = if (stopTime.stop == null) { + GlanceModifier // not clickable, since there is no stop + } else { + GlanceModifier.clickable( + actionRunCallback( + actionParametersOf( + WidgetKeys.STOP_ID to stopTime.stop.stopId, + WidgetKeys.STOP_TYPE to stopTime.stop.type.name + ) + ) + ) + } + ) + } + + item { + if (trip.completedStops < 2) return@item + Row( + horizontalAlignment = Alignment.CenterHorizontally, + verticalAlignment = Alignment.CenterVertically, + modifier = GlanceModifier.fillMaxWidth() + .clickable(actionRunCallback()) + ) { + Text( + if (showPrevStop) "Hide completed stops" else "Show completed stops", + style = TextStyle( + color = GlanceTheme.colors.onSurface + ), + modifier = GlanceModifier.padding(8.dp) + ) + Image( + ImageProvider(if (showPrevStop) R.drawable.arrow_up else R.drawable.arrow_down), + contentDescription = "toggle show stops", + colorFilter = ColorFilter.tint(GlanceTheme.colors.onSurfaceVariant), + modifier = GlanceModifier.padding(8.dp) + ) + } + } + itemsIndexed(trip.stopTimes) { index, stopTime -> + if (!(trip.stopTimes.size - 1 > index && index > 0)) return@itemsIndexed + if (!showPrevStop && index < trip.completedStops) return@itemsIndexed + TripViewStopItemGlance( + trip = trip, + highlight = stopTime.stop != null && + stopTime.stop.stopId == stopIdToHighlight && + stopTime.stop.type == stopTypeToHighlight, + completed = index < trip.completedStops, + stopTime = stopTime, + modifier = if (stopTime.stop == null) { + GlanceModifier // not clickable, since there is no stop + } else { + GlanceModifier.clickable( + actionRunCallback( + actionParametersOf( + WidgetKeys.STOP_ID to stopTime.stop.stopId, + WidgetKeys.STOP_TYPE to stopTime.stop.type.name + ) + ) + ) + } + ) + } + + item { + val index = trip.stopTimes.size - 1 + val stopTime = trip.stopTimes[index] TripViewStopItemGlance( trip = trip, highlight = stopTime.stop != null && @@ -371,6 +451,7 @@ fun TripViewStopsPreview() { ), stopIdToHighlight = null, stopTypeToHighlight = null, + showPrevStop = false, ) } } \ No newline at end of file diff --git a/app/src/main/res/drawable/arrow_down.xml b/app/src/main/res/drawable/arrow_down.xml new file mode 100644 index 0000000..4eba1ba --- /dev/null +++ b/app/src/main/res/drawable/arrow_down.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/arrow_up.xml b/app/src/main/res/drawable/arrow_up.xml new file mode 100644 index 0000000..cda82d6 --- /dev/null +++ b/app/src/main/res/drawable/arrow_up.xml @@ -0,0 +1,5 @@ + + + + + From d0d05d8c51f3b5092e49fd3b2064011a1e8f35e8 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Fri, 3 Apr 2026 10:40:05 +0200 Subject: [PATCH 41/43] refactoring: Deleted an unused line --- .../java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt index e2f44bf..a6f1991 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/TripViewStopsGlance.kt @@ -8,7 +8,6 @@ import androidx.glance.GlanceTheme import androidx.glance.Image import androidx.glance.ImageProvider import androidx.glance.LocalContext -import androidx.glance.LocalGlanceId import androidx.glance.action.actionParametersOf import androidx.glance.action.clickable import androidx.glance.appwidget.action.actionRunCallback @@ -53,7 +52,6 @@ fun TripViewStopsGlance( modifier: GlanceModifier = GlanceModifier, ) { val context = LocalContext.current - val glanceId = LocalGlanceId.current LazyColumn( modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally, From 479785b04f0a9656c191aa72e281976589081961 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Sun, 5 Apr 2026 20:57:47 +0200 Subject: [PATCH 42/43] refactoring: Refactoring of ConfigurationActivity Moved LineSelectionScreenWidget to a separate file and created a destination for the screen so i can work on creating a drawer like the one in MainActivity to enable to configure also the widget for the stops and not only the lines --- .../stypox/tridenta/ui/error/ErrorPanel.kt | 2 +- .../org/stypox/tridenta/ui/error/ErrorRow.kt | 2 +- .../tridenta/ui/line_trips/LineTripsScreen.kt | 2 +- .../ui/line_trips/LineTripsViewModel.kt | 2 +- .../stypox/tridenta/ui/lines/LinesScreen.kt | 2 +- .../java/org/stypox/tridenta/ui/nav/Drawer.kt | 2 +- .../org/stypox/tridenta/ui/nav/Navigation.kt | 4 +- .../tridenta/ui/stop_trips/StopTripsScreen.kt | 2 +- .../ui/stop_trips/StopTripsViewModel.kt | 2 +- .../stypox/tridenta/ui/stops/StopsScreen.kt | 4 +- .../org/stypox/tridenta/ui/trip/TripView.kt | 4 +- .../org/stypox/tridenta/util/ShortcutUtils.kt | 4 +- .../widget/WidgetConfigurationActivity.kt | 118 ++++-------------- .../widget/ui/LineSelectionScreenWidget.kt | 94 ++++++++++++++ 14 files changed, 134 insertions(+), 110 deletions(-) create mode 100644 app/src/main/java/org/stypox/tridenta/widget/ui/LineSelectionScreenWidget.kt diff --git a/app/src/main/java/org/stypox/tridenta/ui/error/ErrorPanel.kt b/app/src/main/java/org/stypox/tridenta/ui/error/ErrorPanel.kt index 6080bdc..6776921 100644 --- a/app/src/main/java/org/stypox/tridenta/ui/error/ErrorPanel.kt +++ b/app/src/main/java/org/stypox/tridenta/ui/error/ErrorPanel.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.unit.dp import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator import org.stypox.tridenta.R -import org.stypox.tridenta.ui.destinations.LogsScreenDestination +import org.stypox.tridenta.destinations.LogsScreenDestination import org.stypox.tridenta.ui.theme.TitleText diff --git a/app/src/main/java/org/stypox/tridenta/ui/error/ErrorRow.kt b/app/src/main/java/org/stypox/tridenta/ui/error/ErrorRow.kt index e6539a6..ea7f8d9 100644 --- a/app/src/main/java/org/stypox/tridenta/ui/error/ErrorRow.kt +++ b/app/src/main/java/org/stypox/tridenta/ui/error/ErrorRow.kt @@ -19,7 +19,7 @@ import androidx.compose.ui.unit.dp import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator import org.stypox.tridenta.R -import org.stypox.tridenta.ui.destinations.LogsScreenDestination +import org.stypox.tridenta.destinations.LogsScreenDestination import org.stypox.tridenta.ui.theme.BodyText @Composable diff --git a/app/src/main/java/org/stypox/tridenta/ui/line_trips/LineTripsScreen.kt b/app/src/main/java/org/stypox/tridenta/ui/line_trips/LineTripsScreen.kt index 041d227..7e29616 100644 --- a/app/src/main/java/org/stypox/tridenta/ui/line_trips/LineTripsScreen.kt +++ b/app/src/main/java/org/stypox/tridenta/ui/line_trips/LineTripsScreen.kt @@ -27,7 +27,7 @@ import org.stypox.tridenta.repo.data.UiLine import org.stypox.tridenta.repo.data.UiTrip import org.stypox.tridenta.sample.SampleUiLineProvider import org.stypox.tridenta.sample.SampleUiTripProvider -import org.stypox.tridenta.ui.destinations.LineTripsScreenDestination +import org.stypox.tridenta.destinations.LineTripsScreenDestination import org.stypox.tridenta.ui.lines.LineShortName import org.stypox.tridenta.ui.nav.AppBarDrawerIcon import org.stypox.tridenta.ui.nav.AppBarFavoriteIcon diff --git a/app/src/main/java/org/stypox/tridenta/ui/line_trips/LineTripsViewModel.kt b/app/src/main/java/org/stypox/tridenta/ui/line_trips/LineTripsViewModel.kt index 2bb5a58..d2aad02 100644 --- a/app/src/main/java/org/stypox/tridenta/ui/line_trips/LineTripsViewModel.kt +++ b/app/src/main/java/org/stypox/tridenta/ui/line_trips/LineTripsViewModel.kt @@ -17,7 +17,7 @@ import org.stypox.tridenta.log.logError import org.stypox.tridenta.repo.LineTripsRepository import org.stypox.tridenta.repo.LinesRepository import org.stypox.tridenta.repo.data.UiTrip -import org.stypox.tridenta.ui.navArgs +import org.stypox.tridenta.navArgs import java.time.ZonedDateTime import javax.inject.Inject diff --git a/app/src/main/java/org/stypox/tridenta/ui/lines/LinesScreen.kt b/app/src/main/java/org/stypox/tridenta/ui/lines/LinesScreen.kt index cc94c16..d3f0d0a 100644 --- a/app/src/main/java/org/stypox/tridenta/ui/lines/LinesScreen.kt +++ b/app/src/main/java/org/stypox/tridenta/ui/lines/LinesScreen.kt @@ -25,7 +25,7 @@ import org.stypox.tridenta.R import org.stypox.tridenta.db.data.DbLine import org.stypox.tridenta.enums.Area import org.stypox.tridenta.sample.SampleDbLineProvider -import org.stypox.tridenta.ui.destinations.LineTripsScreenDestination +import org.stypox.tridenta.destinations.LineTripsScreenDestination import org.stypox.tridenta.ui.error.ErrorPanel import org.stypox.tridenta.ui.error.ErrorRow import org.stypox.tridenta.ui.nav.AppBarDrawerIcon diff --git a/app/src/main/java/org/stypox/tridenta/ui/nav/Drawer.kt b/app/src/main/java/org/stypox/tridenta/ui/nav/Drawer.kt index e2afa97..4689070 100644 --- a/app/src/main/java/org/stypox/tridenta/ui/nav/Drawer.kt +++ b/app/src/main/java/org/stypox/tridenta/ui/nav/Drawer.kt @@ -30,7 +30,7 @@ import org.stypox.tridenta.R import org.stypox.tridenta.db.data.DbLine import org.stypox.tridenta.db.data.DbStop import org.stypox.tridenta.db.views.HistoryLineOrStop -import org.stypox.tridenta.ui.destinations.* +import org.stypox.tridenta.destinations.* import org.stypox.tridenta.ui.lines.LineShortName import org.stypox.tridenta.ui.theme.* diff --git a/app/src/main/java/org/stypox/tridenta/ui/nav/Navigation.kt b/app/src/main/java/org/stypox/tridenta/ui/nav/Navigation.kt index f3a144e..c41f267 100644 --- a/app/src/main/java/org/stypox/tridenta/ui/nav/Navigation.kt +++ b/app/src/main/java/org/stypox/tridenta/ui/nav/Navigation.kt @@ -30,11 +30,11 @@ import com.ramcosta.composedestinations.rememberNavHostEngine import com.ramcosta.composedestinations.spec.Direction import kotlinx.coroutines.launch import org.stypox.tridenta.R -import org.stypox.tridenta.ui.NavGraphs -import org.stypox.tridenta.ui.destinations.LinesScreenDestination import org.stypox.tridenta.ui.theme.HeadlineText import org.stypox.tridenta.util.PreferenceKeys import androidx.core.content.edit +import org.stypox.tridenta.NavGraphs +import org.stypox.tridenta.destinations.LinesScreenDestination const val DEEP_LINK_PREFIX = "tridenta://" const val DEEP_LINK_URL_PATTERN = DEEP_LINK_PREFIX + FULL_ROUTE_PLACEHOLDER diff --git a/app/src/main/java/org/stypox/tridenta/ui/stop_trips/StopTripsScreen.kt b/app/src/main/java/org/stypox/tridenta/ui/stop_trips/StopTripsScreen.kt index ea74018..3c40c02 100644 --- a/app/src/main/java/org/stypox/tridenta/ui/stop_trips/StopTripsScreen.kt +++ b/app/src/main/java/org/stypox/tridenta/ui/stop_trips/StopTripsScreen.kt @@ -26,7 +26,7 @@ import org.stypox.tridenta.db.data.DbStop import org.stypox.tridenta.repo.data.UiTrip import org.stypox.tridenta.sample.SampleDbStopProvider import org.stypox.tridenta.sample.SampleUiTripProvider -import org.stypox.tridenta.ui.destinations.LineTripsScreenDestination +import org.stypox.tridenta.destinations.LineTripsScreenDestination import org.stypox.tridenta.ui.nav.AppBarDrawerIcon import org.stypox.tridenta.ui.nav.AppBarFavoriteIcon import org.stypox.tridenta.ui.nav.DEEP_LINK_URL_PATTERN diff --git a/app/src/main/java/org/stypox/tridenta/ui/stop_trips/StopTripsViewModel.kt b/app/src/main/java/org/stypox/tridenta/ui/stop_trips/StopTripsViewModel.kt index 5b24e86..14b067a 100644 --- a/app/src/main/java/org/stypox/tridenta/ui/stop_trips/StopTripsViewModel.kt +++ b/app/src/main/java/org/stypox/tridenta/ui/stop_trips/StopTripsViewModel.kt @@ -14,7 +14,7 @@ import org.stypox.tridenta.extractor.ROME_ZONE_ID import org.stypox.tridenta.log.logError import org.stypox.tridenta.repo.StopTripsRepository import org.stypox.tridenta.repo.StopsRepository -import org.stypox.tridenta.ui.navArgs +import org.stypox.tridenta.navArgs import java.time.ZonedDateTime import javax.inject.Inject diff --git a/app/src/main/java/org/stypox/tridenta/ui/stops/StopsScreen.kt b/app/src/main/java/org/stypox/tridenta/ui/stops/StopsScreen.kt index fa8bd4c..ba45455 100644 --- a/app/src/main/java/org/stypox/tridenta/ui/stops/StopsScreen.kt +++ b/app/src/main/java/org/stypox/tridenta/ui/stops/StopsScreen.kt @@ -33,8 +33,8 @@ import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator import org.stypox.tridenta.R import org.stypox.tridenta.repo.data.UiStop import org.stypox.tridenta.sample.SampleUiStopProvider -import org.stypox.tridenta.ui.destinations.LineTripsScreenDestination -import org.stypox.tridenta.ui.destinations.StopTripsScreenDestination +import org.stypox.tridenta.destinations.LineTripsScreenDestination +import org.stypox.tridenta.destinations.StopTripsScreenDestination import org.stypox.tridenta.ui.error.ErrorPanel import org.stypox.tridenta.ui.error.ErrorRow import org.stypox.tridenta.ui.nav.AppBarDrawerIcon diff --git a/app/src/main/java/org/stypox/tridenta/ui/trip/TripView.kt b/app/src/main/java/org/stypox/tridenta/ui/trip/TripView.kt index 8198d72..5bb2eaf 100644 --- a/app/src/main/java/org/stypox/tridenta/ui/trip/TripView.kt +++ b/app/src/main/java/org/stypox/tridenta/ui/trip/TripView.kt @@ -28,8 +28,8 @@ import org.stypox.tridenta.repo.data.UiLine import org.stypox.tridenta.repo.data.UiTrip import org.stypox.tridenta.sample.SampleDbStopProvider import org.stypox.tridenta.sample.SampleUiTripProvider -import org.stypox.tridenta.ui.destinations.LineTripsScreenDestination -import org.stypox.tridenta.ui.destinations.StopTripsScreenDestination +import org.stypox.tridenta.destinations.LineTripsScreenDestination +import org.stypox.tridenta.destinations.StopTripsScreenDestination import org.stypox.tridenta.ui.error.ErrorPanel import org.stypox.tridenta.ui.error.ErrorRow import org.stypox.tridenta.ui.theme.* diff --git a/app/src/main/java/org/stypox/tridenta/util/ShortcutUtils.kt b/app/src/main/java/org/stypox/tridenta/util/ShortcutUtils.kt index f9245ac..553b346 100644 --- a/app/src/main/java/org/stypox/tridenta/util/ShortcutUtils.kt +++ b/app/src/main/java/org/stypox/tridenta/util/ShortcutUtils.kt @@ -17,8 +17,8 @@ import org.stypox.tridenta.db.data.DbLine import org.stypox.tridenta.db.data.DbStop import org.stypox.tridenta.enums.CardinalPoint import org.stypox.tridenta.enums.StopLineType -import org.stypox.tridenta.ui.destinations.LineTripsScreenDestination -import org.stypox.tridenta.ui.destinations.StopTripsScreenDestination +import org.stypox.tridenta.destinations.LineTripsScreenDestination +import org.stypox.tridenta.destinations.StopTripsScreenDestination import org.stypox.tridenta.ui.nav.DEEP_LINK_PREFIX import java.lang.Float.min diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt index 1c55384..4020fde 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt @@ -5,30 +5,14 @@ import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.pulltorefresh.PullToRefreshBox -import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState -import androidx.compose.runtime.Composable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.state.updateAppWidgetState import androidx.hilt.navigation.compose.hiltViewModel @@ -39,19 +23,14 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.stypox.tridenta.R import org.stypox.tridenta.db.data.DbLine -import org.stypox.tridenta.enums.Area import org.stypox.tridenta.enums.Direction import org.stypox.tridenta.extractor.ROME_ZONE_ID import org.stypox.tridenta.log.logError -import org.stypox.tridenta.ui.lines.AreaChip -import org.stypox.tridenta.ui.lines.LineItem -import org.stypox.tridenta.ui.lines.LinesUiState import org.stypox.tridenta.ui.lines.LinesViewModel -import org.stypox.tridenta.ui.lines.SelectAreaDialog import org.stypox.tridenta.ui.theme.AppTheme import org.stypox.tridenta.widget.actions.WidgetEntryPoint +import org.stypox.tridenta.widget.ui.LineSelectionScreenWidget import java.time.ZonedDateTime @AndroidEntryPoint @@ -86,17 +65,24 @@ class WidgetConfigurationActivity : val lastReloadWasError by linesViewModel.lastReloadWasError.collectAsState() AppTheme { - LineSelectionScreen( - criticalError = linesUiState.error, - minorError = lastReloadWasError, - loading = linesUiState.loading, - onReload = linesViewModel::onReload, - state = linesUiState, - onLineSelected = { selectedLine -> - saveWidgetConfiguration(selectedLine) - }, - setSelectedArea = linesViewModel::setSelectedArea - ) + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + Box(modifier = Modifier.safeDrawingPadding()) { + LineSelectionScreenWidget( + criticalError = linesUiState.error, + minorError = lastReloadWasError, + loading = linesUiState.loading, + onReload = linesViewModel::onReload, + state = linesUiState, + onLineSelected = { selectedLine -> + saveWidgetConfiguration(selectedLine) + }, + setSelectedArea = linesViewModel::setSelectedArea + ) + } + } } } } @@ -170,60 +156,4 @@ class WidgetConfigurationActivity : } } } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun LineSelectionScreen( - criticalError: Boolean, - minorError: Boolean, - loading: Boolean, - onReload: () -> Unit, - state: LinesUiState, - onLineSelected: (DbLine) -> Unit, - setSelectedArea: (Area) -> Unit -) { - var showAreaDialog by rememberSaveable { mutableStateOf(false) } - if (showAreaDialog) { - SelectAreaDialog( - selectedArea = state.selectedArea, - setSelectedArea = setSelectedArea, - onDismiss = { showAreaDialog = false } - ) - } - Scaffold(topBar = { - TopAppBar(title = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text(text = stringResource(R.string.selected_area)) - AreaChip( - area = state.selectedArea, - onClick = { showAreaDialog = true } - ) - } - }) - }) { paddingValues -> - PullToRefreshBox( - isRefreshing = loading, - state = rememberPullToRefreshState(), - onRefresh = onReload, - modifier = Modifier - .padding(paddingValues) - .fillMaxHeight() - ) { - LazyColumn( - modifier = Modifier - .fillMaxWidth() - ) { - items(state.lines) { line -> - LineItem( - line, true, - modifier = Modifier.clickable { onLineSelected(line) }, - ) - } - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/LineSelectionScreenWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/LineSelectionScreenWidget.kt new file mode 100644 index 0000000..427bff3 --- /dev/null +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/LineSelectionScreenWidget.kt @@ -0,0 +1,94 @@ +package org.stypox.tridenta.widget.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.ramcosta.composedestinations.annotation.DeepLink +import com.ramcosta.composedestinations.annotation.Destination +import org.stypox.tridenta.R +import org.stypox.tridenta.db.data.DbLine +import org.stypox.tridenta.enums.Area +import org.stypox.tridenta.ui.lines.AreaChip +import org.stypox.tridenta.ui.lines.LineItem +import org.stypox.tridenta.ui.lines.LinesUiState +import org.stypox.tridenta.ui.lines.SelectAreaDialog +import org.stypox.tridenta.ui.nav.DEEP_LINK_URL_PATTERN + +@Destination( + deepLinks = [DeepLink(uriPattern = DEEP_LINK_URL_PATTERN)] +) +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LineSelectionScreenWidget( + criticalError: Boolean, + minorError: Boolean, + loading: Boolean, + onReload: () -> Unit, + state: LinesUiState, + onLineSelected: (DbLine) -> Unit, + setSelectedArea: (Area) -> Unit +) { + var showAreaDialog by rememberSaveable { mutableStateOf(false) } + if (showAreaDialog) { + SelectAreaDialog( + selectedArea = state.selectedArea, + setSelectedArea = setSelectedArea, + onDismiss = { showAreaDialog = false } + ) + } + Scaffold(topBar = { + TopAppBar(title = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text(text = stringResource(R.string.selected_area)) + AreaChip( + area = state.selectedArea, + onClick = { showAreaDialog = true } + ) + } + }) + }) { paddingValues -> + PullToRefreshBox( + isRefreshing = loading, + state = rememberPullToRefreshState(), + onRefresh = onReload, + modifier = Modifier + .padding(paddingValues) + .fillMaxHeight() + ) { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + ) { + items(state.lines) { line -> + LineItem( + line, true, + modifier = Modifier.clickable { onLineSelected(line) }, + ) + } + } + } + } +} \ No newline at end of file From c8d7cd886e39bebf5c52517e9b4969200857bda4 Mon Sep 17 00:00:00 2001 From: whyKVD Date: Sun, 5 Apr 2026 21:53:22 +0200 Subject: [PATCH 43/43] refactoring: More refactoring on the LineSelectionScreenWidget --- .../widget/WidgetConfigurationActivity.kt | 11 ---------- .../widget/ui/LineSelectionScreenWidget.kt | 22 +++++++++++++++++++ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt index 4020fde..3674a30 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/WidgetConfigurationActivity.kt @@ -59,11 +59,6 @@ class WidgetConfigurationActivity : setResult(RESULT_CANCELED) setContent { - val linesViewModel: LinesViewModel = hiltViewModel() - - val linesUiState by linesViewModel.uiState.collectAsState() - val lastReloadWasError by linesViewModel.lastReloadWasError.collectAsState() - AppTheme { Surface( modifier = Modifier.fillMaxSize(), @@ -71,15 +66,9 @@ class WidgetConfigurationActivity : ) { Box(modifier = Modifier.safeDrawingPadding()) { LineSelectionScreenWidget( - criticalError = linesUiState.error, - minorError = lastReloadWasError, - loading = linesUiState.loading, - onReload = linesViewModel::onReload, - state = linesUiState, onLineSelected = { selectedLine -> saveWidgetConfiguration(selectedLine) }, - setSelectedArea = linesViewModel::setSelectedArea ) } } diff --git a/app/src/main/java/org/stypox/tridenta/widget/ui/LineSelectionScreenWidget.kt b/app/src/main/java/org/stypox/tridenta/widget/ui/LineSelectionScreenWidget.kt index 427bff3..0f90e1d 100644 --- a/app/src/main/java/org/stypox/tridenta/widget/ui/LineSelectionScreenWidget.kt +++ b/app/src/main/java/org/stypox/tridenta/widget/ui/LineSelectionScreenWidget.kt @@ -15,6 +15,7 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable @@ -23,6 +24,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.DeepLink import com.ramcosta.composedestinations.annotation.Destination import org.stypox.tridenta.R @@ -31,12 +33,32 @@ import org.stypox.tridenta.enums.Area import org.stypox.tridenta.ui.lines.AreaChip import org.stypox.tridenta.ui.lines.LineItem import org.stypox.tridenta.ui.lines.LinesUiState +import org.stypox.tridenta.ui.lines.LinesViewModel import org.stypox.tridenta.ui.lines.SelectAreaDialog import org.stypox.tridenta.ui.nav.DEEP_LINK_URL_PATTERN @Destination( deepLinks = [DeepLink(uriPattern = DEEP_LINK_URL_PATTERN)] ) +@Composable +fun LineSelectionScreenWidget( + onLineSelected: (DbLine) -> Unit, +) { + val linesViewModel: LinesViewModel = hiltViewModel() + + val linesUiState by linesViewModel.uiState.collectAsState() + val lastReloadWasError by linesViewModel.lastReloadWasError.collectAsState() + return LineSelectionScreenWidget( + criticalError = linesUiState.error, + minorError = lastReloadWasError, + loading = linesUiState.loading, + onReload = linesViewModel::onReload, + state = linesUiState, + onLineSelected = onLineSelected, + setSelectedArea = linesViewModel::setSelectedArea + ) +} + @OptIn(ExperimentalMaterial3Api::class) @Composable fun LineSelectionScreenWidget(