LocationItem: group departures by line (#1295)

* group departures by line if more than one line depart from given stop

* code cleanup

* preselect line with nextDeparture

* add horizontal divider to improve legibility

* change LineFilterChip styling

* sort by lineType, then by lineName, animateScrollIntoView

* Show departure in minutes on single tap

also:
- replace `remember(time)` with `key(time)` where applicable to
  keep "in ... minutes" up to date
- only animate lineFilterChips into view once per card composition
- show old list for up to two lines
- disable departure list `onLongPress`
- ditch `detectDragGestures` since it does not do anything anyway...
  somehow scrolling the departure list will also scroll the search bar.

* set containerColor to secondary in lineFilterChip to improve legibility for white-ish primary colors

* More color rigamarole

* OCD code tweaks

* Don't use extension functions

* Split LocationItem into multiple subcomponents

* Adjust line colors

* Always use filter chips view for departures

* Limit departures to 8 per line, make list not scrollable

* Adjust spacing

* Add transition

* Improve line sorting

* Fix line chip spacing

* detectTabGestures -> combinedClickable

---------

Co-authored-by: MM20 <15646950+MM2-0@users.noreply.github.com>
This commit is contained in:
Christoph 2025-04-01 18:02:29 +02:00 committed by GitHub
parent 3788eed61d
commit 67a5fd25de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 668 additions and 370 deletions

View File

@ -1,6 +1,7 @@
package de.mm20.launcher2.ui.ktx
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import hct.Hct
import kotlin.math.atan2
import kotlin.math.roundToInt
@ -20,6 +21,14 @@ fun Color.Companion.hct(hue: Float, chroma: Float, tone: Float): Color {
return Color(hct.toInt())
}
fun Color.atTone(tone: Int): Color {
return Color(
Hct.fromInt(this.toArgb()).apply {
this.tone = tone.toDouble()
}.toInt()
)
}
val Color.hue: Float
get() {
val r = this.red / 255f
@ -28,3 +37,5 @@ val Color.hue: Float
// sqrt(3)
return atan2(1.7320508f * (g - b), 2f * r - g - b)
}
fun android.graphics.Color.toComposeColor() = Color(this.toArgb())

View File

@ -6,7 +6,6 @@ import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.plugins.PluginService
import de.mm20.launcher2.preferences.search.LocationSearchSettings
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

View File

@ -820,4 +820,5 @@
</plurals>
<string name="preference_contacts_call_on_tap">Zum Anruf Tippen</string>
<string name="preference_contacts_call_on_tap_summary">Beim Tippen auf eine Telefonnummer diese direkt Anrufen</string>
<string name="departure_time_departed">abgefahren</string>
</resources>

View File

@ -1024,4 +1024,5 @@
<item quantity="one">in one minute</item>
<item quantity="other">in %1$d minutes</item>
</plurals>
<string name="departure_time_departed">departed</string>
</resources>

View File

@ -32,16 +32,56 @@ data class Departure(
val lineColor: Color?,
)
/**
* Compares two line names. The line naems are split into parts of numbers or letters, then
* each segment is compared.
*/
object LineNameComparator : Comparator<String> {
// Split line into parts, e.g. "11A" -> ["11", "A"], "S1" -> ["S", "1"], "40-X" -> ["40", "X"]
private val regex = Regex("\\p{L}+|[0-9]+")
override fun compare(a: String, b: String): Int {
if (a == b) return 0
val aParts = regex.findAll(a).toList()
val bParts = regex.findAll(b).toList()
for (i in 0 until minOf(aParts.size, bParts.size)) {
val aPart = aParts[i].value
val bPart = bParts[i].value
if (aPart == bPart) continue
val thisPartNumber = aPart.toIntOrNull()
val otherPartNumber = bPart.toIntOrNull()
if (thisPartNumber != null && otherPartNumber != null) {
// both parts are numbers, compare them as numbers
return thisPartNumber.compareTo(otherPartNumber)
}
// one part is a number, the other is a string. numbers are automatically less than strings
return aPart.compareTo(bPart)
}
// 11 < 11A
return aParts.size.compareTo(bParts.size)
}
}
// implicit ordering by ordinal
enum class LineType {
Bus,
Tram,
Subway,
Tram,
Bus,
Boat,
Monorail,
CableCar,
CommuterTrain,
RegionalTrain,
Train,
HighSpeedTrain,
Boat,
Monorail,
CableCar,
Airplane,
}

View File

@ -27,4 +27,14 @@ sealed interface OpeningSchedule {
data object TwentyFourSeven : OpeningSchedule
@Serializable
data class Hours(@Serializable val openingHours: List<OpeningHours>) : OpeningSchedule
}
/**
* Checks whether the [OpeningSchedule] has at least one opening hour.
*/
fun OpeningSchedule.isNotEmpty(): Boolean {
return when (this) {
is OpeningSchedule.Hours -> openingHours.isNotEmpty()
OpeningSchedule.TwentyFourSeven -> true
}
}