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:
parent
3788eed61d
commit
67a5fd25de
@ -1,6 +1,7 @@
|
|||||||
package de.mm20.launcher2.ui.ktx
|
package de.mm20.launcher2.ui.ktx
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import hct.Hct
|
import hct.Hct
|
||||||
import kotlin.math.atan2
|
import kotlin.math.atan2
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@ -20,6 +21,14 @@ fun Color.Companion.hct(hue: Float, chroma: Float, tone: Float): Color {
|
|||||||
return Color(hct.toInt())
|
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
|
val Color.hue: Float
|
||||||
get() {
|
get() {
|
||||||
val r = this.red / 255f
|
val r = this.red / 255f
|
||||||
@ -28,3 +37,5 @@ val Color.hue: Float
|
|||||||
// sqrt(3)
|
// sqrt(3)
|
||||||
return atan2(1.7320508f * (g - b), 2f * r - g - b)
|
return atan2(1.7320508f * (g - b), 2f * r - g - b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun android.graphics.Color.toComposeColor() = Color(this.toArgb())
|
||||||
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,6 @@ import de.mm20.launcher2.plugin.PluginType
|
|||||||
import de.mm20.launcher2.plugins.PluginService
|
import de.mm20.launcher2.plugins.PluginService
|
||||||
import de.mm20.launcher2.preferences.search.LocationSearchSettings
|
import de.mm20.launcher2.preferences.search.LocationSearchSettings
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|||||||
@ -820,4 +820,5 @@
|
|||||||
</plurals>
|
</plurals>
|
||||||
<string name="preference_contacts_call_on_tap">Zum Anruf Tippen</string>
|
<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="preference_contacts_call_on_tap_summary">Beim Tippen auf eine Telefonnummer diese direkt Anrufen</string>
|
||||||
|
<string name="departure_time_departed">abgefahren</string>
|
||||||
</resources>
|
</resources>
|
||||||
@ -1024,4 +1024,5 @@
|
|||||||
<item quantity="one">in one minute</item>
|
<item quantity="one">in one minute</item>
|
||||||
<item quantity="other">in %1$d minutes</item>
|
<item quantity="other">in %1$d minutes</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="departure_time_departed">departed</string>
|
||||||
</resources>
|
</resources>
|
||||||
@ -32,16 +32,56 @@ data class Departure(
|
|||||||
val lineColor: Color?,
|
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 {
|
enum class LineType {
|
||||||
Bus,
|
|
||||||
Tram,
|
|
||||||
Subway,
|
Subway,
|
||||||
|
Tram,
|
||||||
|
Bus,
|
||||||
|
Boat,
|
||||||
|
Monorail,
|
||||||
|
CableCar,
|
||||||
CommuterTrain,
|
CommuterTrain,
|
||||||
RegionalTrain,
|
RegionalTrain,
|
||||||
Train,
|
Train,
|
||||||
HighSpeedTrain,
|
HighSpeedTrain,
|
||||||
Boat,
|
|
||||||
Monorail,
|
|
||||||
CableCar,
|
|
||||||
Airplane,
|
Airplane,
|
||||||
}
|
}
|
||||||
@ -27,4 +27,14 @@ sealed interface OpeningSchedule {
|
|||||||
data object TwentyFourSeven : OpeningSchedule
|
data object TwentyFourSeven : OpeningSchedule
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Hours(@Serializable val openingHours: List<OpeningHours>) : OpeningSchedule
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user