Add support for location plugins (#772)

* Cherry pick location refactor

* Refactor :data:openstreetmaps to :data:locations

* contract, plugin sdk

* Implement serialization, module tweaks

* Include check for out-of-date departures in SearchableItemVM.requestUpdatedSearchable()

* settings for location plugins

* Try not to be too lazy

* more fiddling with the plugin SDK

* add departures in MapView with mock data for debug builds

* change icons

* add boats

* animate departure lazycolumn

* Add MarqueeText for text overflow handling

* Define height for Departures in case there is no map to display

* Don't inclure railway / highway tags for OSM since there will be location plugins for that

* sort by time

* - Apply pre-merge changes to LocationSettings
- Add banner warning about slowed down location search for large search radii

* ditch `showLocationOnMap`

* LocationItem: make `showOpeningSchedule` toggleable

* LocationItem: make Navigation AssistChip work for people that don't have google maps installed

* LocationItem: resolve TODOs

* MapTiles: ditch unused code, animate userIndicator

* Reintroduce departure list

* Add LineColor

* Add osm tag `stars` as `userRating` https://taginfo.openstreetmap.org/keys/stars#overview

* typealias -> import

* Don't add Navigation Chip when there is no way to resolve navigation intents

* Add settings migration

* Set plugin SDK version to 1.2.0-SNAPSHOT

* Deduplicate shared plugin classes, use kotlinx.serialization

* Fix imports

* Use ZonedDateTime for depature times

* Add more line types

* Rewrite location serialization

* Replace street/houseNumber with address

* Add attribution field

* Add plugin config

* Reject location search requests without lat lon parameters

* Add default values to plugin location class

* Don't crash if column value is null

* Add docs comments to LocationCategory values

* Refactor OpeningSchedule as polymorphic

* remove dead corpse *ahem* code

* Split LocationCategory into category and icon

(Also update to Kotlin 2.0, please don't do this at home)

* Add more location icons

* Fix (?) location deserializer

* Add more location icons

* More icons

* Meh

* Add Pub

* Disable Github Maven repo if credentials are missing

* Add location search specific settings to plugin details screen

* Add language parameter

* Unbreak the build

* Refactor plugin SDK (with breaking changes)

* Set plugin SDK version to 2.0.0-SNAPSHOT

* Document SDK breaking changes

* Implement LocationProvider.getQuery

* Add a typesafe cursor API

* Oops I did it again

next time maybe check if the code is actually compiling before pushing

* Add missing return statement

* Fix list serialization

* Departure time UI adjustment

* Use typesafe cursor for weather plugins

* Add userRatingCount and emailAddress fields

* grrr

* Rename and extend LineTypes

* Add default lineType to Departure to fix serialization errors

* Fix refreshing stored plugin locations

* Adapt line name column width to available departures

* Fix plugin settings screen category overlap

* add LocationItem.GenericTransit

* Fix crash during deserialization of locations

* Update SDK docs

* Replace plugin "official" mark with "verified developer" mark

before anyone gets sued

* show 'now' when departure is in less than one minute

* Add typesafe Bundle API

* Implement plugin API changes

* Plugin SDK: Fix refresh result not being returned

* Update docs

* apply alpha to departures that have departured

* better (maybe): reduce saturation instead of alpha

* Add default values for Attribution

* Display attribution

* Rearrange location result layout

* Reduce searchable update interval to 1 minute

* Pass last update time to refresh function

* Change refresh path and ensure that timestamp is only update when the item was updated

* categorize osm location

* Update docs

* Optimize location search

- run providers in parallel
- flatten code

* add experimental address parsing for OSM

* add poi_category_townhall

* Fix popup closing when favorites items are updated

* Revert "Fix popup closing when favorites items are updated"

This reverts commit fc517fd066c7f8109b6d6df2d4f536af66398207.

* Fork AndroidAddressFormatter to `:libs:address-formatter`

* migrate `:libs:address-formatter` dependencies to version catalog and update them

* also consider addr:{suburb,hamlet} for `Address.city` if city tag is missing

* Move poi strings back to strings.xml

* Update Jetpack Compose

* Move address-formatter back to its original package, add license and readme

* Move address-formatter back to its original package

---------

Co-authored-by: MM20 <15646950+MM2-0@users.noreply.github.com>
This commit is contained in:
Christoph
2024-06-14 11:57:03 +02:00
committed by GitHub
co-authored by MM20
parent cfe80ff3e5
commit 65a9c8c1fe
145 changed files with 5804 additions and 2199 deletions
+2 -5
View File
@@ -1,6 +1,7 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.plugin.compose)
}
android {
@@ -58,10 +59,6 @@ android {
viewBinding = true
}
composeOptions {
kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get()
}
lint {
abortOnError = false
}
@@ -141,7 +138,7 @@ dependencies {
implementation(project(":core:crashreporter"))
implementation(project(":data:notifications"))
implementation(project(":data:contacts"))
implementation(project(":data:openstreetmaps"))
implementation(project(":data:locations"))
implementation(project(":core:permissions"))
implementation(project(":data:websites"))
implementation(project(":data:unitconverter"))
@@ -0,0 +1,101 @@
package de.mm20.launcher2.ui.component
import androidx.compose.foundation.MarqueeAnimationMode
import androidx.compose.foundation.MarqueeAnimationMode.Companion.Immediately
import androidx.compose.foundation.MarqueeDefaults
import androidx.compose.foundation.MarqueeSpacing
import androidx.compose.foundation.basicMarquee
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import de.mm20.launcher2.ui.ktx.conditional
import de.mm20.launcher2.ui.ktx.drawFadedEdge
@Composable
fun MarqueeText(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
style: TextStyle = LocalTextStyle.current,
iterations: Int = MarqueeDefaults.Iterations,
animationMode: MarqueeAnimationMode = Immediately,
repeatDelayMillis: Int = MarqueeDefaults.RepeatDelayMillis,
initialDelayMillis: Int = if (animationMode == Immediately) repeatDelayMillis else 0,
spacing: MarqueeSpacing = MarqueeDefaults.Spacing,
velocity: Dp = MarqueeDefaults.Velocity,
fadeLeft: Dp? = null,
fadeRight: Dp? = null,
) {
var textSize by remember { mutableIntStateOf(0) }
var textLayout by remember { mutableStateOf<TextLayoutResult?>(null) }
Text(
text = text,
style = style,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
softWrap = false,
maxLines = 1,
onTextLayout = { textLayout = it },
modifier = modifier
.onGloballyPositioned { textSize = it.size.width }
.graphicsLayer {
compositingStrategy = CompositingStrategy.Offscreen
}
.conditional(
textLayout != null && textLayout!!.size.width > textSize,
Modifier
.drawWithContent {
drawContent()
if (fadeLeft != null) {
drawFadedEdge(leftEdge = true, fadeLeft)
}
if (fadeRight != null) {
drawFadedEdge(leftEdge = false, fadeRight)
}
}
)
.basicMarquee(
iterations = iterations,
initialDelayMillis = initialDelayMillis,
spacing = spacing,
repeatDelayMillis = repeatDelayMillis,
velocity = velocity
)
)
}
@@ -1,6 +1,8 @@
package de.mm20.launcher2.ui.component
import android.icu.text.DecimalFormat
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.StarHalf
@@ -8,7 +10,9 @@ import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarOutline
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
@@ -25,7 +29,8 @@ fun RatingBar(
rating: Float,
modifier: Modifier = Modifier,
tint: Color = MaterialTheme.colorScheme.primary,
starSize: Dp = 16.dp
starSize: Dp = 16.dp,
ratingCount: Int? = null,
) {
val starRating = round(rating * 10f).toInt()
val fullStars = starRating / 2
@@ -34,6 +39,7 @@ fun RatingBar(
val iconModifier = Modifier.size(starSize)
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
) {
for (i in 0 until fullStars) {
Icon(
@@ -59,11 +65,18 @@ fun RatingBar(
modifier = iconModifier
)
}
Text(
modifier = Modifier.padding(start = 4.dp),
text = DecimalFormat("#.0").format(starRating / 2) +
if (ratingCount == null) "" else " ($ratingCount)",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@Preview
@Composable
fun RatingBarPreview() {
RatingBar(0.67f)
RatingBar(0.68f, ratingCount = 263)
}
@@ -8,14 +8,9 @@ import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.RectF
import android.graphics.drawable.AdaptiveIconDrawable
import android.util.Log
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
@@ -25,6 +20,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.GenericShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@@ -67,17 +63,14 @@ import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.icons.ClockLayer
import de.mm20.launcher2.icons.ClockSublayer
import de.mm20.launcher2.icons.ClockSublayerRole
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.DynamicLauncherIcon
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.icons.LauncherIconLayer
import de.mm20.launcher2.icons.LauncherIconRenderSettings
import de.mm20.launcher2.icons.StaticIconLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TextLayer
import de.mm20.launcher2.icons.TintedClockLayer
import de.mm20.launcher2.icons.TintedIconLayer
import de.mm20.launcher2.icons.TransparentLayer
import de.mm20.launcher2.icons.VectorLayer
import de.mm20.launcher2.ktx.drawWithColorFilter
import de.mm20.launcher2.preferences.IconShape
import de.mm20.launcher2.ui.base.LocalTime
@@ -85,10 +78,8 @@ import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalGridSettings
import de.mm20.launcher2.ui.modifier.scale
import kotlinx.coroutines.launch
import palettes.TonalPalette
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import kotlin.math.abs
import kotlin.math.pow
@@ -220,6 +211,17 @@ fun ShapedLauncherIcon(
)
}
is VectorLayer -> {
Icon(
imageVector = fg.vector, contentDescription = null,
tint = if (fg.color == 0) {
Color(renderSettings.fgThemeColor)
} else {
Color(getTone(fg.color, renderSettings.fgTone))
},
)
}
else -> {}
}
} else {
@@ -303,98 +305,6 @@ fun ShapedLauncherIcon(
}
}
@Composable
private fun IconLayer(
layer: LauncherIconLayer,
size: Dp,
colorTone: Int,
defaultTintColor: Color
) {
when (layer) {
is ClockLayer -> {
ClockLayer(
layer.sublayers,
scale = layer.scale,
defaultSecond = layer.defaultSecond,
defaultMinute = layer.defaultMinute,
defaultHour = layer.defaultHour,
tintColor = null
)
}
is TintedClockLayer -> {
ClockLayer(
layer.sublayers,
scale = layer.scale,
defaultSecond = layer.defaultSecond,
defaultMinute = layer.defaultMinute,
defaultHour = layer.defaultHour,
tintColor = if (layer.color == 0) defaultTintColor
else Color(getTone(layer.color, colorTone))
)
}
is ColorLayer -> {
Box(
modifier = Modifier
.fillMaxSize()
.background(
if (layer.color == 0) {
defaultTintColor
} else {
Color(getTone(layer.color, colorTone))
}
)
)
}
is StaticIconLayer -> {
Canvas(modifier = Modifier.fillMaxSize()) {
withTransform({
this.scale(layer.scale)
}) {
drawIntoCanvas {
layer.icon.bounds = this.size.toRect().toAndroidRect()
layer.icon.draw(it.nativeCanvas)
}
}
}
}
is TextLayer -> {
Text(
text = layer.text,
style = MaterialTheme.typography.headlineSmall.copy(
fontSize = 20.sp * (size / 48.dp)
),
color = if (layer.color == 0) {
defaultTintColor
} else {
Color(getTone(layer.color, colorTone))
},
)
}
is TintedIconLayer -> {
val color =
if (layer.color == 0) defaultTintColor.toArgb()
else getTone(layer.color, colorTone)
Canvas(modifier = Modifier.fillMaxSize()) {
drawIntoCanvas {
layer.icon.bounds = this.size.toRect().toAndroidRect()
layer.icon.drawWithColorFilter(
it.nativeCanvas,
PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
)
}
}
}
is TransparentLayer -> {}
}
}
private fun getTone(argb: Int, tone: Int): Int {
return TonalPalette
.fromInt(argb)
@@ -430,8 +340,13 @@ private fun ClockLayer(
sublayer.drawable.level = (((hour - defaultHour + 12) % 12) * 60
+ ((minute) % 60))
}
ClockSublayerRole.Minute -> sublayer.drawable.level = ((minute - defaultMinute + 60) % 60)
ClockSublayerRole.Second -> sublayer.drawable.level = (((second - defaultSecond + 60) % 60) * 10)
ClockSublayerRole.Minute -> sublayer.drawable.level =
((minute - defaultMinute + 60) % 60)
ClockSublayerRole.Second -> sublayer.drawable.level =
(((second - defaultSecond + 60) % 60) * 10)
else -> {}
}
drawIntoCanvas {
@@ -9,6 +9,7 @@ fun TextPreference(
title: String,
value: String,
summary: String? = value,
enabled: Boolean = true,
onValueChanged: (String) -> Unit,
placeholder: String? = null
) {
@@ -16,6 +17,7 @@ fun TextPreference(
Preference(
title = title,
summary = summary,
enabled = enabled,
onClick = { showDialog = true }
)
@@ -23,8 +23,14 @@ import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.icons.WeatherCloud
import de.mm20.launcher2.icons.WeatherFog
import de.mm20.launcher2.icons.WeatherHailAnimatable
import de.mm20.launcher2.icons.WeatherLightRainAnimatable
import de.mm20.launcher2.icons.WeatherRainAnimatable
import de.mm20.launcher2.icons.WeatherSleetRainAnimatable
import de.mm20.launcher2.icons.WeatherSleetSnowAnimatable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.icons.*
import kotlin.math.PI
import kotlin.math.sin
@@ -18,13 +18,13 @@ import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.ui.icons.WeatherCloud
import de.mm20.launcher2.ui.icons.WeatherFog
import de.mm20.launcher2.ui.icons.WeatherHail
import de.mm20.launcher2.ui.icons.WeatherLightRain
import de.mm20.launcher2.ui.icons.WeatherRain
import de.mm20.launcher2.ui.icons.WeatherSleetRain
import de.mm20.launcher2.ui.icons.WeatherSleetSnow
import de.mm20.launcher2.icons.WeatherCloud
import de.mm20.launcher2.icons.WeatherFog
import de.mm20.launcher2.icons.WeatherHail
import de.mm20.launcher2.icons.WeatherLightRain
import de.mm20.launcher2.icons.WeatherRain
import de.mm20.launcher2.icons.WeatherSleetRain
import de.mm20.launcher2.icons.WeatherSleetSnow
@Composable
fun WeatherIcon(
@@ -1,538 +0,0 @@
package de.mm20.launcher2.ui.icons
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.materialIcon
import androidx.compose.material.icons.materialPath
import androidx.compose.ui.graphics.vector.group
val Icons.Rounded.Battery0Bar
get() = materialIcon("Icons.Rounded.Battery0Bar") {
materialPath {
moveTo(17f, 5f)
verticalLineToRelative(16f)
curveToRelative(0f, 0.55f, -0.45f, 1f, -1f, 1f)
horizontalLineTo(8f)
curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f)
verticalLineTo(5f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
verticalLineTo(3f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f)
verticalLineToRelative(1f)
horizontalLineToRelative(2f)
curveTo(16.55f, 4f, 17f, 4.45f, 17f, 5f)
close()
moveTo(15f, 6f)
horizontalLineTo(9f)
verticalLineToRelative(14f)
horizontalLineToRelative(6f)
verticalLineTo(6f)
close()
}
}
val Icons.Rounded.Battery1Bar
get() = materialIcon("Icons.Rounded.Battery1Bar") {
materialPath {
moveTo(17f, 5f)
verticalLineToRelative(16f)
curveToRelative(0f, 0.55f, -0.45f, 1f, -1f, 1f)
horizontalLineTo(8f)
curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f)
verticalLineTo(5f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
verticalLineTo(3f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f)
verticalLineToRelative(1f)
horizontalLineToRelative(2f)
curveTo(16.55f, 4f, 17f, 4.45f, 17f, 5f)
close()
moveTo(15f, 6f)
horizontalLineTo(9f)
verticalLineToRelative(12f)
horizontalLineToRelative(6f)
verticalLineTo(6f)
close()
}
}
val Icons.Rounded.Battery2Bar
get() = materialIcon("Icons.Rounded.Battery2Bar") {
materialPath {
moveTo(17f, 5f)
verticalLineToRelative(16f)
curveToRelative(0f, 0.55f, -0.45f, 1f, -1f, 1f)
horizontalLineTo(8f)
curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f)
verticalLineTo(5f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
verticalLineTo(3f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f)
verticalLineToRelative(1f)
horizontalLineToRelative(2f)
curveTo(16.55f, 4f, 17f, 4.45f, 17f, 5f)
close()
moveTo(15f, 6f)
horizontalLineTo(9f)
verticalLineToRelative(10f)
horizontalLineToRelative(6f)
verticalLineTo(6f)
close()
}
}
val Icons.Rounded.Battery3Bar
get() = materialIcon("Icons.Rounded.Battery3Bar") {
materialPath {
moveTo(17f, 5f)
verticalLineToRelative(16f)
curveToRelative(0f, 0.55f, -0.45f, 1f, -1f, 1f)
horizontalLineTo(8f)
curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f)
verticalLineTo(5f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
verticalLineTo(3f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f)
verticalLineToRelative(1f)
horizontalLineToRelative(2f)
curveTo(16.55f, 4f, 17f, 4.45f, 17f, 5f)
close()
moveTo(15f, 6f)
horizontalLineTo(9f)
verticalLineToRelative(8f)
horizontalLineToRelative(6f)
verticalLineTo(6f)
close()
}
}
val Icons.Rounded.Battery4Bar
get() = materialIcon("Icons.Rounded.Battery4Bar") {
materialPath {
moveTo(17f, 5f)
verticalLineToRelative(16f)
curveToRelative(0f, 0.55f, -0.45f, 1f, -1f, 1f)
horizontalLineTo(8f)
curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f)
verticalLineTo(5f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
verticalLineTo(3f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f)
verticalLineToRelative(1f)
horizontalLineToRelative(2f)
curveTo(16.55f, 4f, 17f, 4.45f, 17f, 5f)
close()
moveTo(15f, 6f)
horizontalLineTo(9f)
verticalLineToRelative(6f)
horizontalLineToRelative(6f)
verticalLineTo(6f)
close()
}
}
val Icons.Rounded.Battery5Bar
get() = materialIcon("Icons.Rounded.Battery5Bar") {
materialPath {
moveTo(17f, 5f)
verticalLineToRelative(16f)
curveToRelative(0f, 0.55f, -0.45f, 1f, -1f, 1f)
horizontalLineTo(8f)
curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f)
verticalLineTo(5f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
verticalLineTo(3f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f)
verticalLineToRelative(1f)
horizontalLineToRelative(2f)
curveTo(16.55f, 4f, 17f, 4.45f, 17f, 5f)
close()
moveTo(15f, 6f)
horizontalLineTo(9f)
verticalLineToRelative(4f)
horizontalLineToRelative(6f)
verticalLineTo(6f)
close()
}
}
val Icons.Rounded.Battery6Bar
get() = materialIcon("Icons.Rounded.Battery6Bar") {
materialPath {
moveTo(17f, 5f)
verticalLineToRelative(16f)
curveToRelative(0f, 0.55f, -0.45f, 1f, -1f, 1f)
horizontalLineTo(8f)
curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f)
verticalLineTo(5f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
verticalLineTo(3f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f)
verticalLineToRelative(1f)
horizontalLineToRelative(2f)
curveTo(16.55f, 4f, 17f, 4.45f, 17f, 5f)
close()
moveTo(15f, 6f)
horizontalLineTo(9f)
verticalLineToRelative(2f)
horizontalLineToRelative(6f)
verticalLineTo(6f)
close()
}
}
val Icons.Rounded.BatteryCharging0Bar
get() = materialIcon("Icons.Rounded.BatteryCharging0Bar") {
group(
scaleX = 0.26458333f,
scaleY = 0.26458333f,
) {
materialPath {
moveTo(41.574219f, 7.5585938f)
curveTo(39.495479f, 7.5585937f, 37.794922f, 9.2591505f, 37.794922f, 11.337891f)
lineTo(37.794922f, 15.117188f)
lineTo(30.236328f, 15.117188f)
curveTo(28.157588f, 15.117187f, 26.457031f, 16.819697f, 26.457031f, 18.898438f)
lineTo(26.457031f, 79.369141f)
curveTo(26.457031f, 81.447881f, 28.157588f, 83.150391f, 30.236328f, 83.150391f)
lineTo(60.472656f, 83.150391f)
curveTo(62.551396f, 83.150391f, 64.251953f, 81.447881f, 64.251953f, 79.369141f)
lineTo(64.251953f, 18.898438f)
curveTo(64.251953f, 16.819697f, 62.551396f, 15.117188f, 60.472656f, 15.117188f)
lineTo(52.914062f, 15.117188f)
lineTo(52.914062f, 11.337891f)
curveTo(52.914063f, 9.2591505f, 51.213506f, 7.5585938f, 49.134766f, 7.5585938f)
lineTo(41.574219f, 7.5585938f)
close()
moveTo(34.015625f, 22.677734f)
lineTo(56.693359f, 22.677734f)
lineTo(56.693359f, 75.589844f)
lineTo(34.015625f, 75.589844f)
lineTo(34.015625f, 22.677734f)
close()
moveTo(47.253906f, 32.126953f)
curveTo(46.605486f, 32.112843f, 45.950535f, 32.41843f, 45.582031f, 33.109375f)
lineTo(35.490234f, 52.005859f)
curveTo(34.809919f, 53.290899f, 35.716123f, 54.802734f, 37.152344f, 54.802734f)
lineTo(41.574219f, 54.802734f)
lineTo(41.574219f, 68.03125f)
curveTo(41.574219f, 69.996604f, 44.219867f, 70.640241f, 45.126953f, 68.939453f)
lineTo(55.21875f, 50.041016f)
curveTo(55.899065f, 48.755976f, 54.955066f, 47.244141f, 53.556641f, 47.244141f)
lineTo(49.134766f, 47.244141f)
lineTo(49.134766f, 34.015625f)
curveTo(49.134766f, 32.848696f, 48.201598f, 32.147575f, 47.253906f, 32.126953f)
close()
}
}
}
val Icons.Rounded.BatteryCharging1Bar
get() = materialIcon("Icons.Rounded.BatteryCharging1Bar") {
group(
scaleX = 0.26458333f,
scaleY = 0.26458333f,
) {
materialPath {
moveTo(41.574219f, 7.5585938f)
curveTo(39.495479f, 7.5585937f, 37.794922f, 9.2591505f, 37.794922f, 11.337891f)
lineTo(37.794922f, 15.117188f)
lineTo(30.236328f, 15.117188f)
curveTo(28.157588f, 15.117187f, 26.457031f, 16.819697f, 26.457031f, 18.898438f)
lineTo(26.457031f, 79.369141f)
curveTo(26.457031f, 81.447881f, 28.157588f, 83.150391f, 30.236328f, 83.150391f)
lineTo(60.472656f, 83.150391f)
curveTo(62.551396f, 83.150391f, 64.251953f, 81.447881f, 64.251953f, 79.369141f)
lineTo(64.251953f, 18.898438f)
curveTo(64.251953f, 16.819697f, 62.551396f, 15.117188f, 60.472656f, 15.117188f)
lineTo(52.914062f, 15.117188f)
lineTo(52.914062f, 11.337891f)
curveTo(52.914063f, 9.2591505f, 51.213506f, 7.5585938f, 49.134766f, 7.5585938f)
lineTo(41.574219f, 7.5585938f)
close()
moveTo(34.015625f, 22.677734f)
lineTo(56.693359f, 22.677734f)
lineTo(56.693359f, 68.03125f)
lineTo(45.611328f, 68.03125f)
lineTo(45.126953f, 68.939453f)
curveTo(44.219867f, 70.640241f, 41.574219f, 69.996604f, 41.574219f, 68.03125f)
lineTo(34.015625f, 68.03125f)
lineTo(34.015625f, 22.677734f)
close()
moveTo(41.574219f, 68.03125f)
lineTo(45.611328f, 68.03125f)
lineTo(55.21875f, 50.041016f)
curveTo(55.899065f, 48.755976f, 54.955066f, 47.244141f, 53.556641f, 47.244141f)
lineTo(49.134766f, 47.244141f)
lineTo(49.134766f, 34.015625f)
curveTo(49.134766f, 32.050271f, 46.489118f, 31.408588f, 45.582031f, 33.109375f)
lineTo(35.490234f, 52.005859f)
curveTo(34.809919f, 53.290899f, 35.716123f, 54.802734f, 37.152344f, 54.802734f)
lineTo(41.574219f, 54.802734f)
lineTo(41.574219f, 68.03125f)
close()
}
}
}
val Icons.Rounded.BatteryCharging2Bar
get() = materialIcon("Icons.Rounded.BatteryCharging2Bar") {
group(
scaleX = 0.26458333f,
scaleY = 0.26458333f,
) {
materialPath {
moveTo(41.574219f, 7.5585938f)
curveTo(39.495479f, 7.5585937f, 37.794922f, 9.2591505f, 37.794922f, 11.337891f)
lineTo(37.794922f, 15.117188f)
lineTo(30.236328f, 15.117188f)
curveTo(28.157588f, 15.117187f, 26.457031f, 16.819697f, 26.457031f, 18.898438f)
lineTo(26.457031f, 79.369141f)
curveTo(26.457031f, 81.447881f, 28.157588f, 83.150391f, 30.236328f, 83.150391f)
lineTo(60.472656f, 83.150391f)
curveTo(62.551396f, 83.150391f, 64.251953f, 81.447881f, 64.251953f, 79.369141f)
lineTo(64.251953f, 18.898438f)
curveTo(64.251953f, 16.819697f, 62.551396f, 15.117188f, 60.472656f, 15.117188f)
lineTo(52.914062f, 15.117188f)
lineTo(52.914062f, 11.337891f)
curveTo(52.914063f, 9.2591505f, 51.213506f, 7.5585938f, 49.134766f, 7.5585938f)
lineTo(41.574219f, 7.5585938f)
close()
moveTo(34.015625f, 22.677734f)
lineTo(56.693359f, 22.677734f)
lineTo(56.693359f, 60.472656f)
lineTo(49.648438f, 60.472656f)
lineTo(45.126953f, 68.939453f)
curveTo(44.219867f, 70.640241f, 41.574219f, 69.996604f, 41.574219f, 68.03125f)
lineTo(41.574219f, 60.472656f)
lineTo(34.015625f, 60.472656f)
lineTo(34.015625f, 22.677734f)
close()
moveTo(41.574219f, 60.472656f)
lineTo(49.648438f, 60.472656f)
lineTo(55.21875f, 50.041016f)
curveTo(55.899065f, 48.755976f, 54.955066f, 47.244141f, 53.556641f, 47.244141f)
lineTo(49.134766f, 47.244141f)
lineTo(49.134766f, 34.015625f)
curveTo(49.134766f, 32.050271f, 46.489118f, 31.408588f, 45.582031f, 33.109375f)
lineTo(35.490234f, 52.005859f)
curveTo(34.809919f, 53.290899f, 35.716123f, 54.802734f, 37.152344f, 54.802734f)
lineTo(41.574219f, 54.802734f)
lineTo(41.574219f, 60.472656f)
close()
}
}
}
val Icons.Rounded.BatteryCharging3Bar
get() = materialIcon("Icons.Rounded.BatteryCharging3Bar") {
group(
scaleX = 0.26458333f,
scaleY = 0.26458333f,
) {
materialPath {
moveTo(41.574219f, 7.5585938f)
curveTo(39.495479f, 7.5585937f, 37.794922f, 9.2591505f, 37.794922f, 11.337891f)
lineTo(37.794922f, 15.117188f)
lineTo(30.236328f, 15.117188f)
curveTo(28.157588f, 15.117187f, 26.457031f, 16.819697f, 26.457031f, 18.898438f)
lineTo(26.457031f, 79.369141f)
curveTo(26.457031f, 81.447881f, 28.157588f, 83.150391f, 30.236328f, 83.150391f)
lineTo(60.472656f, 83.150391f)
curveTo(62.551396f, 83.150391f, 64.251953f, 81.447881f, 64.251953f, 79.369141f)
lineTo(64.251953f, 18.898438f)
curveTo(64.251953f, 16.819697f, 62.551396f, 15.117188f, 60.472656f, 15.117188f)
lineTo(52.914062f, 15.117188f)
lineTo(52.914062f, 11.337891f)
curveTo(52.914063f, 9.2591505f, 51.213506f, 7.5585938f, 49.134766f, 7.5585938f)
lineTo(41.574219f, 7.5585938f)
close()
moveTo(34.015625f, 22.677734f)
lineTo(56.693359f, 22.677734f)
lineTo(56.693359f, 52.914062f)
lineTo(53.683594f, 52.914062f)
lineTo(45.126953f, 68.939453f)
curveTo(44.219867f, 70.640241f, 41.574219f, 69.996604f, 41.574219f, 68.03125f)
lineTo(41.574219f, 54.802734f)
lineTo(37.152344f, 54.802734f)
curveTo(36.048819f, 54.802734f, 35.258483f, 53.910469f, 35.259766f, 52.914062f)
lineTo(34.015625f, 52.914062f)
lineTo(34.015625f, 22.677734f)
close()
moveTo(35.259766f, 52.914062f)
lineTo(53.683594f, 52.914062f)
lineTo(55.21875f, 50.041016f)
curveTo(55.899065f, 48.755976f, 54.955066f, 47.244141f, 53.556641f, 47.244141f)
lineTo(49.134766f, 47.244141f)
lineTo(49.134766f, 34.015625f)
curveTo(49.134766f, 32.050271f, 46.489118f, 31.408588f, 45.582031f, 33.109375f)
lineTo(35.490234f, 52.005859f)
curveTo(35.332642f, 52.303535f, 35.260152f, 52.613661f, 35.259766f, 52.914062f)
close()
}
}
}
val Icons.Rounded.BatteryCharging4Bar
get() = materialIcon("Icons.Rounded.BatteryCharging4Bar") {
group(
scaleX = 0.26458333f,
scaleY = 0.26458333f,
) {
materialPath {
moveTo(41.574219f, 7.5585938f)
curveTo(39.495479f, 7.5585937f, 37.794922f, 9.2591505f, 37.794922f, 11.337891f)
lineTo(37.794922f, 15.117188f)
lineTo(30.236328f, 15.117188f)
curveTo(28.157588f, 15.117187f, 26.457031f, 16.819697f, 26.457031f, 18.898438f)
lineTo(26.457031f, 79.369141f)
curveTo(26.457031f, 81.447881f, 28.157588f, 83.150391f, 30.236328f, 83.150391f)
lineTo(60.472656f, 83.150391f)
curveTo(62.551396f, 83.150391f, 64.251953f, 81.447881f, 64.251953f, 79.369141f)
lineTo(64.251953f, 18.898438f)
curveTo(64.251953f, 16.819697f, 62.551396f, 15.117188f, 60.472656f, 15.117188f)
lineTo(52.914062f, 15.117188f)
lineTo(52.914062f, 11.337891f)
curveTo(52.914063f, 9.2591505f, 51.213506f, 7.5585938f, 49.134766f, 7.5585938f)
lineTo(41.574219f, 7.5585938f)
close()
moveTo(34.015625f, 22.677734f)
lineTo(56.693359f, 22.677734f)
lineTo(56.693359f, 45.353516f)
lineTo(49.134766f, 45.353516f)
lineTo(49.134766f, 47.244141f)
lineTo(53.556641f, 47.244141f)
curveTo(54.955066f, 47.244141f, 55.899065f, 48.755976f, 55.21875f, 50.041016f)
lineTo(45.126953f, 68.939453f)
curveTo(44.219867f, 70.640241f, 41.574219f, 69.996604f, 41.574219f, 68.03125f)
lineTo(41.574219f, 54.802734f)
lineTo(37.152344f, 54.802734f)
curveTo(35.716123f, 54.802734f, 34.809919f, 53.290899f, 35.490234f, 52.005859f)
lineTo(39.042969f, 45.353516f)
lineTo(34.015625f, 45.353516f)
lineTo(34.015625f, 22.677734f)
close()
moveTo(39.042969f, 45.353516f)
lineTo(49.134766f, 45.353516f)
lineTo(49.134766f, 34.015625f)
curveTo(49.134766f, 32.050271f, 46.489118f, 31.408588f, 45.582031f, 33.109375f)
lineTo(39.042969f, 45.353516f)
close()
}
}
}
val Icons.Rounded.BatteryCharging5Bar
get() = materialIcon("Icons.Rounded.BatteryCharging5Bar") {
group(
scaleX = 0.26458333f,
scaleY = 0.26458333f,
) {
materialPath {
moveTo(41.574219f, 7.5585938f)
curveTo(39.495479f, 7.5585937f, 37.794922f, 9.2591505f, 37.794922f, 11.337891f)
lineTo(37.794922f, 15.117188f)
lineTo(30.236328f, 15.117188f)
curveTo(28.157588f, 15.117187f, 26.457031f, 16.819697f, 26.457031f, 18.898438f)
lineTo(26.457031f, 79.369141f)
curveTo(26.457031f, 81.447881f, 28.157588f, 83.150391f, 30.236328f, 83.150391f)
lineTo(60.472656f, 83.150391f)
curveTo(62.551396f, 83.150391f, 64.251953f, 81.447881f, 64.251953f, 79.369141f)
lineTo(64.251953f, 18.898438f)
curveTo(64.251953f, 16.819697f, 62.551396f, 15.117188f, 60.472656f, 15.117188f)
lineTo(52.914062f, 15.117188f)
lineTo(52.914062f, 11.337891f)
curveTo(52.914063f, 9.2591505f, 51.213506f, 7.5585938f, 49.134766f, 7.5585938f)
lineTo(41.574219f, 7.5585938f)
close()
moveTo(34.015625f, 22.677734f)
lineTo(56.693359f, 22.677734f)
lineTo(56.693359f, 37.794922f)
lineTo(49.134766f, 37.794922f)
lineTo(49.134766f, 47.244141f)
lineTo(53.556641f, 47.244141f)
curveTo(54.955066f, 47.244141f, 55.899065f, 48.755976f, 55.21875f, 50.041016f)
lineTo(45.126953f, 68.939453f)
curveTo(44.219867f, 70.640241f, 41.574219f, 69.996604f, 41.574219f, 68.03125f)
lineTo(41.574219f, 54.802734f)
lineTo(37.152344f, 54.802734f)
curveTo(35.716123f, 54.802734f, 34.809919f, 53.290899f, 35.490234f, 52.005859f)
lineTo(43.080078f, 37.794922f)
lineTo(34.015625f, 37.794922f)
lineTo(34.015625f, 22.677734f)
close()
moveTo(43.080078f, 37.794922f)
lineTo(49.134766f, 37.794922f)
lineTo(49.134766f, 34.015625f)
curveTo(49.134766f, 32.050271f, 46.489118f, 31.408588f, 45.582031f, 33.109375f)
lineTo(43.080078f, 37.794922f)
close()
}
}
}
val Icons.Rounded.BatteryCharging6Bar
get() = materialIcon("Icons.Rounded.BatteryCharging6Bar") {
group(
scaleX = 0.26458333f,
scaleY = 0.26458333f,
) {
materialPath {
moveTo(41.574219f, 7.5585938f)
curveTo(39.495479f, 7.5585937f, 37.794922f, 9.2591505f, 37.794922f, 11.337891f)
lineTo(37.794922f, 15.117188f)
lineTo(30.236328f, 15.117188f)
curveTo(28.157588f, 15.117187f, 26.457031f, 16.819697f, 26.457031f, 18.898438f)
lineTo(26.457031f, 79.369141f)
curveTo(26.457031f, 81.447881f, 28.157588f, 83.150391f, 30.236328f, 83.150391f)
lineTo(60.472656f, 83.150391f)
curveTo(62.551396f, 83.150391f, 64.251953f, 81.447881f, 64.251953f, 79.369141f)
lineTo(64.251953f, 18.898438f)
curveTo(64.251953f, 16.819697f, 62.551396f, 15.117188f, 60.472656f, 15.117188f)
lineTo(52.914062f, 15.117188f)
lineTo(52.914062f, 11.337891f)
curveTo(52.914063f, 9.2591505f, 51.213506f, 7.5585938f, 49.134766f, 7.5585938f)
lineTo(41.574219f, 7.5585938f)
close()
moveTo(34.015625f, 22.677734f)
lineTo(56.693359f, 22.677734f)
lineTo(56.693359f, 30.236328f)
lineTo(34.015625f, 30.236328f)
lineTo(34.015625f, 22.677734f)
close()
moveTo(47.253906f, 32.126953f)
curveTo(48.201598f, 32.147575f, 49.134766f, 32.848696f, 49.134766f, 34.015625f)
lineTo(49.134766f, 47.244141f)
lineTo(53.556641f, 47.244141f)
curveTo(54.955066f, 47.244141f, 55.899065f, 48.755976f, 55.21875f, 50.041016f)
lineTo(45.126953f, 68.939453f)
curveTo(44.219867f, 70.640241f, 41.574219f, 69.996604f, 41.574219f, 68.03125f)
lineTo(41.574219f, 54.802734f)
lineTo(37.152344f, 54.802734f)
curveTo(35.716123f, 54.802734f, 34.809919f, 53.290899f, 35.490234f, 52.005859f)
lineTo(45.582031f, 33.109375f)
curveTo(45.950535f, 32.41843f, 46.605486f, 32.112843f, 47.253906f, 32.126953f)
close()
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,24 @@
package de.mm20.launcher2.ui.ktx
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.unit.Dp
fun ContentDrawScope.drawFadedEdge(leftEdge: Boolean, edgeWidth: Dp) {
// https://stackoverflow.com/a/75112743
val edgeWidthPx = edgeWidth.toPx()
drawRect(
topLeft = Offset(if (leftEdge) 0f else size.width - edgeWidthPx, 0f),
size = Size(edgeWidthPx, size.height),
brush = Brush.horizontalGradient(
colors = listOf(Color.Transparent, Color.Black),
startX = if (leftEdge) 0f else size.width,
endX = if (leftEdge) edgeWidthPx else size.width - edgeWidthPx
),
blendMode = BlendMode.DstIn
)
}
@@ -17,7 +17,7 @@ suspend fun MutableState<Float>.animateTo(targetValue: Float) {
suspend inline fun <T, V: AnimationVector> MutableState<T>.animateTo(targetValue: T, converter: TwoWayConverter<T, V>) {
val animatable = Animatable(this.value, converter)
animatable.animateTo(targetValue) {
animatable.animateTo(targetValue) animatable@ {
this@animateTo.value = this.value
}
}
@@ -225,10 +225,11 @@ class SearchVM : ViewModel(), KoinComponent {
}
resultsList = resultsList.sortedWith { a, b ->
val lastLocation = devicePoseProvider.lastLocation
when {
a is Location && b is Location && devicePoseProvider.lastLocation != null -> {
a.distanceTo(devicePoseProvider.lastLocation!!)
.compareTo(b.distanceTo(devicePoseProvider.lastLocation!!))
a is Location && b is Location && lastLocation != null -> {
a.distanceTo(lastLocation)
.compareTo(b.distanceTo(lastLocation))
}
a is SavableSearchable && b !is SavableSearchable -> -1
@@ -250,7 +251,6 @@ class SearchVM : ViewModel(), KoinComponent {
}
}
hiddenItemKeys.collectLatest { hiddenKeys ->
val hidden = mutableListOf<SavableSearchable>()
val apps = mutableListOf<Application>()
@@ -349,7 +349,7 @@ class SearchVM : ViewModel(), KoinComponent {
val missingLocationPermission = combine(
permissionsManager.hasPermission(PermissionGroup.Location),
locationSearchSettings.enabled.distinctUntilChanged()
locationSearchSettings.osmLocations.distinctUntilChanged()
) { perm, enabled -> !perm && enabled }
fun requestLocationPermission(context: AppCompatActivity) {
@@ -357,7 +357,7 @@ class SearchVM : ViewModel(), KoinComponent {
}
fun disableLocationSearch() {
locationSearchSettings.setEnabled(false)
locationSearchSettings.setOsmLocations(false)
}
val missingFilesPermission = combine(
@@ -6,6 +6,7 @@ import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.ui.geometry.Rect
import androidx.core.app.ActivityOptionsCompat
import androidx.customview.view.AbsSavedState
import de.mm20.launcher2.appshortcuts.AppShortcutRepository
import de.mm20.launcher2.badges.BadgeService
import de.mm20.launcher2.devicepose.DevicePoseProvider
@@ -19,6 +20,7 @@ import de.mm20.launcher2.preferences.search.LocationSearchSettings
import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.Location
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.UpdatableSearchable
import de.mm20.launcher2.search.UpdateResult
@@ -40,6 +42,7 @@ import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
@OptIn(ExperimentalCoroutinesApi::class)
class SearchableItemVM : ListItemViewModel(), KoinComponent {
@@ -184,21 +187,24 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
val searchable = searchable.value ?: return
if (searchable is UpdatableSearchable<*>) {
val updatedSelf = searchable.updatedSelf ?: return
if (!shouldRetryUpdate && System.currentTimeMillis() < searchable.timestamp + 1.hours.inWholeMilliseconds) return
val sinceTimestamp = System.currentTimeMillis() - searchable.timestamp
val isOutOfDate = 1.minutes.inWholeMilliseconds < sinceTimestamp
if (!shouldRetryUpdate && !isOutOfDate) return
viewModelScope.launch {
this@SearchableItemVM.searchable.value = with(updatedSelf()) {
with(updatedSelf(searchable)) {
when (this) {
is UpdateResult.Success -> {
isUpToDate.value = true
shouldRetryUpdate = false
favoritesService.upsert(this.result)
this.result
}
is UpdateResult.TemporarilyUnavailable -> {
isUpToDate.value = false
shouldRetryUpdate = true
return@launch
}
is UpdateResult.PermanentlyUnavailable -> {
@@ -211,7 +217,6 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
Toast.LENGTH_LONG
).show()
Log.d("requestUpdatedSearchable", "PermanentlyUnavailable", this.cause)
null
}
}
}
@@ -232,9 +237,6 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
val applyMapTheming = locationSearchSettings.themeMap
.stateIn(viewModelScope, SharingStarted.Lazily, false)
val showPositionOnMap = locationSearchSettings.showPositionOnMap
.stateIn(viewModelScope, SharingStarted.Lazily, false)
val mapTileServerUrl = locationSearchSettings.tileServer
.stateIn(viewModelScope, SharingStarted.Lazily, "")
}
@@ -23,7 +23,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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
@@ -90,8 +89,6 @@ fun GridItem(
viewModel.init(item, iconSize.toInt())
}
val item = viewModel.searchable.collectAsState().value ?: item
val context = LocalContext.current
var showPopup by remember(item.key) { mutableStateOf(false) }
@@ -69,8 +69,6 @@ fun ListItem(
)
)
val item = viewModel.searchable.collectAsState().value ?: item
var bounds by remember { mutableStateOf(Rect.Zero) }
Box(
modifier = modifier
@@ -1,10 +1,7 @@
package de.mm20.launcher2.ui.launcher.search.contacts
import android.content.Intent
import android.net.Uri
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.expandIn
@@ -25,7 +22,6 @@ import androidx.compose.material.icons.rounded.Email
import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.MoreHoriz
import androidx.compose.material.icons.rounded.OpenInNew
import androidx.compose.material.icons.rounded.Place
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarOutline
import androidx.compose.material.icons.rounded.Visibility
@@ -63,9 +59,8 @@ import de.mm20.launcher2.ui.component.DefaultToolbarAction
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.Toolbar
import de.mm20.launcher2.ui.component.ToolbarAction
import de.mm20.launcher2.ui.icons.Signal
import de.mm20.launcher2.ui.icons.Telegram
import de.mm20.launcher2.ui.icons.WhatsApp
import de.mm20.launcher2.icons.Signal
import de.mm20.launcher2.icons.Telegram
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
import de.mm20.launcher2.ui.launcher.search.listItemViewModel
@@ -10,17 +10,6 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.AppShortcut
import androidx.compose.material.icons.rounded.Apps
import androidx.compose.material.icons.rounded.Description
import androidx.compose.material.icons.rounded.Handyman
import androidx.compose.material.icons.rounded.Language
import androidx.compose.material.icons.rounded.Person
import androidx.compose.material.icons.rounded.Place
import androidx.compose.material.icons.rounded.Public
import androidx.compose.material.icons.rounded.Today
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.HorizontalDivider
@@ -32,12 +21,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.preferences.KeyboardFilterBarItem
import de.mm20.launcher2.search.SearchFilters
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.icons.Wikipedia
@Composable
fun KeyboardFilterBar(
@@ -15,7 +15,7 @@ import androidx.compose.material.icons.rounded.VisibilityOff
import de.mm20.launcher2.preferences.KeyboardFilterBarItem
import de.mm20.launcher2.search.SearchFilters
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.icons.Wikipedia
import de.mm20.launcher2.icons.Wikipedia
val KeyboardFilterBarItem.icon
get() = when (this) {
@@ -28,7 +28,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.SearchFilters
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.icons.Wikipedia
import de.mm20.launcher2.icons.Wikipedia
@Composable
fun SearchFilters(
@@ -14,29 +14,49 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkOut
import androidx.compose.animation.slideIn
import androidx.compose.animation.slideOut
import androidx.compose.foundation.MarqueeSpacing
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.automirrored.rounded.NavigateNext
import androidx.compose.material.icons.automirrored.rounded.OpenInNew
import androidx.compose.material.icons.rounded.AirplanemodeActive
import androidx.compose.material.icons.rounded.BugReport
import androidx.compose.material.icons.rounded.Commute
import androidx.compose.material.icons.rounded.DirectionsBoat
import androidx.compose.material.icons.rounded.DirectionsBus
import androidx.compose.material.icons.rounded.DirectionsRailway
import androidx.compose.material.icons.rounded.DirectionsTransit
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Language
import androidx.compose.material.icons.rounded.Navigation
import androidx.compose.material.icons.rounded.Phone
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarOutline
import androidx.compose.material.icons.rounded.Subway
import androidx.compose.material.icons.rounded.Train
import androidx.compose.material.icons.rounded.Tram
import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.AssistChip
@@ -58,31 +78,48 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import androidx.compose.ui.unit.roundToIntRect
import androidx.compose.ui.unit.times
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import blend.Blend.harmonize
import coil.compose.AsyncImage
import de.mm20.launcher2.i18n.R
import de.mm20.launcher2.icons.CableCar
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.Location
import de.mm20.launcher2.search.LocationCategory
import de.mm20.launcher2.search.OpeningHours
import de.mm20.launcher2.search.OpeningSchedule
import de.mm20.launcher2.search.isOpen
import de.mm20.launcher2.search.location.Attribution
import de.mm20.launcher2.search.location.Departure
import de.mm20.launcher2.search.location.LineType
import de.mm20.launcher2.search.location.OpeningHours
import de.mm20.launcher2.search.location.OpeningSchedule
import de.mm20.launcher2.ui.base.LocalTime
import de.mm20.launcher2.ui.component.DefaultToolbarAction
import de.mm20.launcher2.ui.component.MarqueeText
import de.mm20.launcher2.ui.component.RatingBar
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.Toolbar
import de.mm20.launcher2.ui.component.ToolbarAction
import de.mm20.launcher2.ui.ktx.blendIntoViewScale
import de.mm20.launcher2.ui.ktx.metersToLocalizedString
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
import de.mm20.launcher2.ui.launcher.search.listItemViewModel
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
@@ -92,7 +129,9 @@ import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
import de.mm20.launcher2.ui.modifier.scale
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.launch
import java.time.Duration
import java.time.LocalDateTime
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.time.format.TextStyle
@@ -108,7 +147,6 @@ fun LocationItem(
) {
val context = LocalContext.current
val viewModel: SearchableItemVM = listItemViewModel(key = "search-${location.key}")
val iconSize = LocalGridSettings.current.iconSize.dp.toPixels()
val userLocation by remember {
viewModel.devicePoseProvider.getLocation()
@@ -181,7 +219,8 @@ fun LocationItem(
if (category != null || formattedDistance != null) {
Text(
when {
category != null && formattedDistance != null -> "${stringResource(category.labelRes)}${formattedDistance}"
category != null && formattedDistance != null -> "$category$formattedDistance"
category != null -> category.toString()
formattedDistance != null -> formattedDistance
else -> ""
@@ -258,7 +297,7 @@ fun LocationItem(
end = 12.dp,
bottom = 4.dp
),
verticalAlignment = Alignment.CenterVertically,
verticalAlignment = Alignment.Top,
) {
Column(
modifier = Modifier.weight(1f),
@@ -279,7 +318,8 @@ fun LocationItem(
if (category != null || formattedDistance != null) {
Text(
when {
category != null && formattedDistance != null -> "${stringResource(category.labelRes)}${formattedDistance}"
category != null && formattedDistance != null -> "$category$formattedDistance"
category != null -> category.toString()
formattedDistance != null -> formattedDistance
else -> ""
@@ -294,31 +334,176 @@ fun LocationItem(
)
)
}
// TODO: add rating to location
if (!showMap && false) {
RatingBar(0.66f, modifier = Modifier.padding(top = 4.dp))
if (location.userRating != null) {
RatingBar(
location.userRating!!,
modifier = Modifier
.padding(top = 6.dp)
.offset(-2.dp)
)
}
if (!showMap) {
val attribution = location.attribution
if (attribution != null) {
Attribution(
attribution,
reverse = true,
modifier = Modifier
.padding(
top = 16.dp,
bottom = 0.dp,
)
.clickable(
enabled = attribution.url != null
) {
context.tryStartActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse(attribution.url)
)
)
}
)
}
}
}
//TODO: add rating to location
if (showMap && false) {
RatingBar(0.66f)
}
if (!showMap) {
Compass(
targetHeading = targetHeading,
modifier = Modifier
.align(Alignment.Top)
.sharedBounds(
rememberSharedContentState("compass"),
this@AnimatedContent
),
size = 56.dp,
)
} else {
val attribution = location.attribution
if (attribution != null) {
Attribution(
attribution,
modifier = Modifier
.padding(
top = 4.dp,
bottom = 4.dp,
start = 12.dp)
.clickable(
enabled = attribution.url != null
) {
context.tryStartActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse(attribution.url)
)
)
}
)
}
}
}
val openingSchedule = location.openingSchedule
if (openingSchedule != null && (openingSchedule.isTwentyFourSeven || openingSchedule.openingHours.isNotEmpty())) {
val departures = remember(location.departures) {
location.departures
?.sortedBy { it.time }
}
if (departures != null) {
val time = LocalTime.current
val nextDeparture = remember(time) {
departures.firstOrNull {
it.time.plus(it.delay ?: Duration.ZERO).isAfter(ZonedDateTime.now())
}
}
if (nextDeparture != null) {
var showDepartureList by remember(departures) {
mutableStateOf(false)
}
OutlinedCard(
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp, end = 12.dp, top = 12.dp),
shape = MaterialTheme.shapes.small,
onClick = { showDepartureList = !showDepartureList }
) {
val listState = rememberLazyListState()
AnimatedContent(showDepartureList) { showList ->
if (!showList) {
Row(
Modifier
.padding(12.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
nextDeparture.LineIcon(Modifier.padding(end = 8.dp))
val lastStop = nextDeparture.lastStop
if (lastStop != null) {
MarqueeText(
modifier = Modifier.weight(1f),
text = lastStop,
style = MaterialTheme.typography.labelMedium,
iterations = Int.MAX_VALUE,
repeatDelayMillis = 0,
velocity = 20.dp,
fadeLeft = 5.dp,
fadeRight = 5.dp,
)
}
val formattedTime = remember(time) {
val timeLeft = Duration.between(
java.time.LocalTime.now(),
nextDeparture.time + (nextDeparture.delay
?: Duration.ZERO)
).toMinutes()
if (timeLeft < 1) "now" else "in $timeLeft min"
}
Text(
text = formattedTime,
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(end = 12.dp)
)
Icon(Icons.AutoMirrored.Rounded.NavigateNext, null)
}
} else {
val longestLine = remember(departures) {
departures.maxOfOrNull { it.line.length }
}
LazyColumn(
state = listState,
modifier = modifier
.heightIn(max = 192.dp)
.padding(12.dp)
.fillMaxWidth()
.pointerInput(Unit) { detectDragGestures { _, _ -> } },
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
itemsIndexed(
departures,
key = { idx, _ -> idx }) { idx, it ->
it.LazyColumnPart(
lineWidth = longestLine,
Modifier
.fillMaxWidth()
.graphicsLayer {
alpha =
listState.layoutInfo.blendIntoViewScale(
idx
)
}
)
}
}
}
}
}
}
}
if (openingSchedule is OpeningSchedule.TwentyFourSeven || (openingSchedule is OpeningSchedule.Hours && openingSchedule.openingHours.isNotEmpty())) {
var showOpeningSchedule by remember(openingSchedule) {
mutableStateOf(false)
}
@@ -328,8 +513,8 @@ fun LocationItem(
.padding(start = 12.dp, end = 12.dp, top = 12.dp),
shape = MaterialTheme.shapes.small,
onClick = {
if (!openingSchedule.isTwentyFourSeven) {
showOpeningSchedule = true
if (openingSchedule !is OpeningSchedule.TwentyFourSeven) {
showOpeningSchedule = !showOpeningSchedule
}
}
) {
@@ -341,78 +526,84 @@ fun LocationItem(
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (openingSchedule.isTwentyFourSeven) {
Text(
text = stringResource(R.string.location_open_24_7),
style = MaterialTheme.typography.labelMedium,
)
} else {
val text = remember(openingSchedule) {
val currentOpeningTime =
openingSchedule.getCurrentOpeningHours()
val timeFormat =
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
return@remember if (currentOpeningTime != null) {
val isSameDay =
currentOpeningTime.dayOfWeek == LocalDateTime.now().dayOfWeek
val formattedTime =
timeFormat.format(currentOpeningTime.startTime + currentOpeningTime.duration)
val closingTime = if (isSameDay) {
context.getString(
R.string.location_closes,
formattedTime
)
} else {
val dow =
currentOpeningTime.dayOfWeek.getDisplayName(
TextStyle.SHORT,
Locale.getDefault()
)
context.getString(
R.string.location_closes_other_day,
dow,
formattedTime
)
}
"${context.getString(R.string.location_open)}$closingTime"
} else {
val nextOpeningTime =
openingSchedule.getNextOpeningHours()
val isSameDay =
nextOpeningTime.dayOfWeek == LocalDateTime.now().dayOfWeek
val formattedTime =
timeFormat.format(nextOpeningTime.startTime)
val openingTime = if (isSameDay) {
context.getString(
R.string.location_opens,
formattedTime
)
} else {
val dow =
nextOpeningTime.dayOfWeek.getDisplayName(
TextStyle.SHORT,
Locale.getDefault()
)
context.getString(
R.string.location_opens_other_day,
dow,
formattedTime
)
}
"${context.getString(R.string.location_closed)}$openingTime"
}
when (openingSchedule) {
is OpeningSchedule.TwentyFourSeven -> {
Text(
text = stringResource(R.string.location_open_24_7),
style = MaterialTheme.typography.labelMedium,
)
}
Text(
text = text,
style = MaterialTheme.typography.labelMedium,
modifier = Modifier.weight(1f)
)
is OpeningSchedule.Hours -> {
val text = remember(openingSchedule) {
val currentOpeningTime =
openingSchedule.getCurrentOpeningHours()
val timeFormat =
DateTimeFormatter.ofLocalizedTime(
FormatStyle.SHORT
)
return@remember if (currentOpeningTime != null) {
val isSameDay =
currentOpeningTime.dayOfWeek == LocalDateTime.now().dayOfWeek
val formattedTime =
timeFormat.format(currentOpeningTime.startTime + currentOpeningTime.duration)
val closingTime = if (isSameDay) {
context.getString(
R.string.location_closes,
formattedTime
)
} else {
val dow =
currentOpeningTime.dayOfWeek.getDisplayName(
TextStyle.SHORT,
Locale.getDefault()
)
context.getString(
R.string.location_closes_other_day,
dow,
formattedTime
)
}
"${context.getString(R.string.location_open)}$closingTime"
} else {
val nextOpeningTime =
openingSchedule.getNextOpeningHours()
val isSameDay =
nextOpeningTime.dayOfWeek == LocalDateTime.now().dayOfWeek
val formattedTime =
timeFormat.format(nextOpeningTime.startTime)
val openingTime = if (isSameDay) {
context.getString(
R.string.location_opens,
formattedTime
)
} else {
val dow =
nextOpeningTime.dayOfWeek.getDisplayName(
TextStyle.SHORT,
Locale.getDefault()
)
context.getString(
R.string.location_opens_other_day,
dow,
formattedTime
)
}
"${context.getString(R.string.location_closed)}$openingTime"
}
}
Icon(Icons.AutoMirrored.Rounded.NavigateNext, null)
Text(
text = text,
style = MaterialTheme.typography.labelMedium,
modifier = Modifier.weight(1f)
)
Icon(Icons.AutoMirrored.Rounded.NavigateNext, null)
}
}
}
} else {
} else if (openingSchedule is OpeningSchedule.Hours) {
Column(
modifier = Modifier.padding(vertical = 6.dp)
) {
@@ -469,24 +660,26 @@ fun LocationItem(
.horizontalScroll(rememberScrollState())
.padding(start = 12.dp, top = 8.dp)
) {
AssistChip(
modifier = Modifier.padding(end = 12.dp),
onClick = {
context.tryStartActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse("google.navigation:q=${location.latitude},${location.longitude}")
),
)
},
label = { Text(stringResource(R.string.menu_navigation)) },
leadingIcon = {
Icon(
Icons.Rounded.Navigation, null,
modifier = Modifier.size(AssistChipDefaults.IconSize)
)
}
val navigationIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse("google.navigation:q=${location.latitude},${location.longitude}")
)
val canResolveNavigationIntent = remember {
null != context.packageManager.resolveActivity(navigationIntent, 0)
}
if (canResolveNavigationIntent) {
AssistChip(
modifier = Modifier.padding(end = 12.dp),
onClick = { context.tryStartActivity(navigationIntent) },
label = { Text(stringResource(R.string.menu_navigation)) },
leadingIcon = {
Icon(
Icons.Rounded.Navigation, null,
modifier = Modifier.size(AssistChipDefaults.IconSize)
)
}
)
}
location.phoneNumber?.let {
AssistChip(
modifier = Modifier.padding(end = 12.dp),
@@ -569,8 +762,6 @@ fun LocationItem(
action = { sheetManager.showCustomizeSearchableModal(location) }
))
location.fixMeUrl?.let {
toolbarActions += DefaultToolbarAction(
label = stringResource(id = R.string.menu_bugreport),
@@ -715,11 +906,11 @@ private fun buildAddress(
return if (summary.isEmpty()) null else summary.toString()
}
private fun OpeningSchedule.getCurrentOpeningHours(): OpeningHours? {
private fun OpeningSchedule.Hours.getCurrentOpeningHours(): OpeningHours? {
return openingHours.find { it.isOpen() }
}
private fun OpeningSchedule.getNextOpeningHours(): OpeningHours {
private fun OpeningSchedule.Hours.getNextOpeningHours(): OpeningHours {
val now = LocalDateTime.now()
val sortedSchedule = this
.openingHours
@@ -737,80 +928,178 @@ private fun OpeningSchedule.getNextOpeningHours(): OpeningHours {
} ?: sortedSchedule.first()
}
private val LocationCategory.labelRes
get() = when(this) {
LocationCategory.ART -> R.string.poi_category_art
LocationCategory.BANK -> R.string.poi_category_bank
LocationCategory.BAR -> R.string.poi_category_bar
LocationCategory.BEAUTY -> R.string.poi_category_beauty
LocationCategory.BICYCLE -> R.string.poi_category_bicycle
LocationCategory.RESTAURANT -> R.string.poi_category_restaurant
LocationCategory.FAST_FOOD -> R.string.poi_category_fast_food
LocationCategory.CAFE -> R.string.poi_category_coffee_shop
LocationCategory.HOTEL -> R.string.poi_category_hotel
LocationCategory.SUPERMARKET -> R.string.poi_category_supermarket
LocationCategory.OTHER -> R.string.poi_category_other
LocationCategory.SCHOOL -> R.string.poi_category_school
LocationCategory.PARKING -> R.string.poi_category_parking
LocationCategory.FUEL -> R.string.poi_category_fuel
LocationCategory.TOILETS -> R.string.poi_category_toilets
LocationCategory.PHARMACY -> R.string.poi_category_pharmacy
LocationCategory.HOSPITAL -> R.string.poi_category_hospital
LocationCategory.POST_OFFICE -> R.string.poi_category_post_office
LocationCategory.PUB -> R.string.poi_category_pub
LocationCategory.GRAVE_YARD -> R.string.poi_category_grave_yard
LocationCategory.DOCTORS -> R.string.poi_category_doctors
LocationCategory.POLICE -> R.string.poi_category_police
LocationCategory.DENTIST -> R.string.poi_category_dentist
LocationCategory.LIBRARY -> R.string.poi_category_library
LocationCategory.COLLEGE -> R.string.poi_category_college
LocationCategory.ICE_CREAM -> R.string.poi_category_ice_cream
LocationCategory.THEATRE -> R.string.poi_category_theater
LocationCategory.PUBLIC_BUILDING -> R.string.poi_category_public_building
LocationCategory.CINEMA -> R.string.poi_category_cinema
LocationCategory.NIGHTCLUB -> R.string.poi_category_nightclub
LocationCategory.BIERGARTEN -> R.string.poi_category_biergarten
LocationCategory.CLINIC -> R.string.poi_category_clinic
LocationCategory.UNIVERSITY -> R.string.poi_category_university
LocationCategory.DEPARTMENT_STORE -> R.string.poi_category_department_store
LocationCategory.CLOTHES -> R.string.poi_category_clothes
LocationCategory.CONVENIENCE -> R.string.poi_category_convenience
LocationCategory.HAIRDRESSER -> R.string.poi_category_hairdresser
LocationCategory.CAR_REPAIR -> R.string.poi_category_car_repair
LocationCategory.BOOKS -> R.string.poi_category_books
LocationCategory.BAKERY -> R.string.poi_category_bakery
LocationCategory.CAR -> R.string.poi_category_car
LocationCategory.MOBILE_PHONE -> R.string.poi_category_mobile_phone
LocationCategory.FURNITURE -> R.string.poi_category_furniture
LocationCategory.ALCOHOL -> R.string.poi_category_alcohol
LocationCategory.FLORIST -> R.string.poi_category_florist
LocationCategory.HARDWARE -> R.string.poi_category_hardware
LocationCategory.ELECTRONICS -> R.string.poi_category_electronics
LocationCategory.SHOES -> R.string.poi_category_shoes
LocationCategory.MALL -> R.string.poi_category_mall
LocationCategory.OPTICIAN -> R.string.poi_category_optician
LocationCategory.JEWELRY -> R.string.poi_category_jewelry
LocationCategory.GIFT -> R.string.poi_category_gift
LocationCategory.LAUNDRY -> R.string.poi_category_laundry
LocationCategory.COMPUTER -> R.string.poi_category_computer
LocationCategory.TOBACCO -> R.string.poi_category_tobacco
LocationCategory.WINE -> R.string.poi_category_wine
LocationCategory.PHOTO -> R.string.poi_category_photo
LocationCategory.COFFEE_SHOP -> R.string.poi_category_coffee_shop
LocationCategory.SOCCER -> R.string.poi_category_soccer
LocationCategory.BASKETBALL -> R.string.poi_category_basketball
LocationCategory.TENNIS -> R.string.poi_category_tennis
LocationCategory.FITNESS -> R.string.poi_category_fitness
LocationCategory.TRAM_STOP -> R.string.poi_category_tram_stop
LocationCategory.RAILWAY_STATION -> R.string.poi_category_railway_station
LocationCategory.RAILWAY_STOP -> R.string.poi_category_railway_stop
LocationCategory.BUS_STATION -> R.string.poi_category_bus_station
LocationCategory.ATM -> R.string.poi_category_atm
LocationCategory.KIOSK -> R.string.poi_category_kiosk
LocationCategory.BUS_STOP -> R.string.poi_category_bus_stop
LocationCategory.MUSEUM -> R.string.poi_category_museum
LocationCategory.PARCEL_LOCKER -> R.string.poi_category_parcel_locker
LocationCategory.CHEMIST -> R.string.poi_category_chemist
LocationCategory.TRAVEL_AGENCY -> R.string.poi_category_travel_agency
LocationCategory.FITNESS_CENTRE -> R.string.poi_category_fitness_center
}
@Composable
fun Departure.LineIcon(
modifier: Modifier
) {
val harmonizeArgb = MaterialTheme.colorScheme.primary.toArgb()
var (lineBg, lineFg) = if (lineColor != null) {
val bg = Color(
harmonize(lineColor!!.toArgb(), harmonizeArgb)
)
val fg = Color(
harmonize(
if (0.5f < bg.luminance()) Color.Black.toArgb() else Color.White.toArgb(),
harmonizeArgb
)
)
bg to fg
} else {
MaterialTheme.colorScheme.primaryContainer to MaterialTheme.colorScheme.onPrimaryContainer
}
val hasDeparted = ZonedDateTime.now().isAfter(time + (delay ?: Duration.ZERO))
if (hasDeparted) {
val hsv = FloatArray(3)
android.graphics.Color.colorToHSV(lineBg.toArgb(), hsv)
val (h, s, v) = hsv
lineBg = Color.hsv(h, s / 2f, v, lineBg.alpha)
}
Row(
modifier = modifier
.wrapContentWidth(Alignment.Start)
.background(
lineBg,
MaterialTheme.shapes.small
)
.padding(top = 4.dp, bottom = 4.dp, start = 4.dp, end = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = when (type) {
LineType.Bus -> Icons.Rounded.DirectionsBus
LineType.Tram -> Icons.Rounded.Tram
LineType.Subway -> Icons.Rounded.Subway
LineType.Monorail -> Icons.Rounded.DirectionsTransit
LineType.CommuterTrain -> Icons.Rounded.DirectionsRailway
LineType.Train, LineType.RegionalTrain, LineType.HighSpeedTrain -> Icons.Rounded.Train
LineType.Boat -> Icons.Rounded.DirectionsBoat
LineType.CableCar -> Icons.Rounded.CableCar
LineType.Airplane -> Icons.Rounded.AirplanemodeActive
null -> Icons.Rounded.Commute
},
contentDescription = type?.name, // TODO localize (maybe) with ?.let{ stringResource("departure_line_type_$it") }
tint = lineFg,
modifier = Modifier
.padding(end = 2.dp)
.size(16.dp),
)
MarqueeText(
text = line,
style = MaterialTheme.typography.labelSmall,
color = lineFg,
textAlign = TextAlign.Center,
fadeLeft = 2.5.dp,
fadeRight = 2.5.dp,
iterations = Int.MAX_VALUE,
repeatDelayMillis = 0,
spacing = MarqueeSpacing(10.dp),
velocity = 20.dp,
modifier = Modifier
.wrapContentSize()
.widthIn(max = 34.dp)
)
}
}
@Composable
fun Departure.LazyColumnPart(
lineWidth: Int?,
modifier: Modifier
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(0.8f)
) {
LineIcon(
Modifier
.padding(end = 8.dp)
.widthIn(
min = if (lineWidth == null) 0.dp
else max(64.dp, lineWidth * 8.dp)
)
)
if (lastStop != null) {
MarqueeText(
text = lastStop!!,
style = MaterialTheme.typography.labelMedium,
iterations = Int.MAX_VALUE,
repeatDelayMillis = 0,
velocity = 20.dp,
fadeLeft = 5.dp,
fadeRight = 5.dp,
)
}
}
Row(
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = time.format(
DateTimeFormatter.ofPattern(
"HH:mm",
Locale.getDefault()
)
),
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(end = 2.dp)
)
val delayMinutes = delay?.toMinutes()
if (null != delayMinutes && 0L < delayMinutes) {
Text(
text = "+$delayMinutes",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.error,
fontSize = TextUnit(2f, TextUnitType.Em),
)
}
}
}
}
@Composable
private fun Attribution(
attribution: Attribution,
modifier: Modifier = Modifier,
reverse: Boolean = false,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
if (attribution.text != null && !reverse) {
Text(
text = attribution.text!!,
style = MaterialTheme.typography.labelSmall,
)
}
if (attribution.iconUrl != null) {
AsyncImage(
modifier = Modifier
.padding(
start = if (reverse) 0.dp else 8.dp,
end = if (reverse) 8.dp else 0.dp
)
.requiredHeight(16.dp),
model = attribution.iconUrl!!,
contentDescription = null,
)
}
if (attribution.text != null && reverse) {
Text(
text = attribution.text!!,
style = MaterialTheme.typography.labelSmall,
)
}
}
}
@@ -8,6 +8,7 @@ import androidx.compose.animation.core.EaseInOutCirc
import androidx.compose.animation.core.EaseInOutSine
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateOffsetAsState
import androidx.compose.animation.core.animateValueAsState
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
@@ -47,7 +48,6 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
@@ -62,23 +62,26 @@ import coil.request.ImageRequest
import de.mm20.launcher2.ktx.PI
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.Location
import de.mm20.launcher2.search.LocationCategory
import de.mm20.launcher2.search.OpeningHours
import de.mm20.launcher2.search.OpeningSchedule
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.location.Address
import de.mm20.launcher2.search.location.Departure
import de.mm20.launcher2.search.location.LineType
import de.mm20.launcher2.search.location.LocationIcon
import de.mm20.launcher2.search.location.OpeningSchedule
import de.mm20.launcher2.ui.ktx.DegreesConverter
import de.mm20.launcher2.ui.ktx.contrast
import de.mm20.launcher2.ui.ktx.hue
import de.mm20.launcher2.ui.ktx.hueRotate
import de.mm20.launcher2.ui.ktx.invert
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import kotlinx.collections.immutable.toImmutableList
import org.koin.android.ext.koin.androidContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.context.GlobalContext
import org.koin.core.context.startKoin
import java.time.Duration
import java.time.ZonedDateTime
import kotlin.math.PI
import kotlin.math.ceil
import kotlin.math.cos
@@ -216,7 +219,15 @@ fun MapTiles(
)
)
if (userLocation != null) {
val userTileCoordinates = getTileCoordinates(userLocation.lat, userLocation.lon, zoom)
val userIndicatorOffset by animateOffsetAsState(
targetValue = getTileCoordinates(userLocation.lat, userLocation.lon, zoom).let {
Offset(
(it.x - start.x) * tileSize.value - 8f,
(it.y - start.y) * tileSize.value - 8f
)
},
animationSpec = tween()
)
if (userLocation.heading != null) {
val headingAnim by animateValueAsState(
@@ -232,8 +243,8 @@ fun MapTiles(
.align(Alignment.TopStart)
.size(16.dp)
.absoluteOffset(
x = (userTileCoordinates.x - start.x) * tileSize - 8.dp,
y = (userTileCoordinates.y - start.y) * tileSize - 8.dp,
userIndicatorOffset.x.dp,
userIndicatorOffset.y.dp
)
.rotate(headingAnim)
.absoluteOffset(y = -8.dp)
@@ -245,8 +256,8 @@ fun MapTiles(
.align(Alignment.TopStart)
.size(16.dp)
.absoluteOffset(
x = (userTileCoordinates.x - start.x) * tileSize - 8.dp,
y = (userTileCoordinates.y - start.y) * tileSize - 8.dp,
userIndicatorOffset.x.dp,
userIndicatorOffset.y.dp
)
.background(MaterialTheme.colorScheme.tertiary, CircleShape)
.border(2.dp, MaterialTheme.colorScheme.onTertiary, CircleShape)
@@ -438,7 +449,7 @@ private fun MapTilesPreview() {
)
}
internal object MockLocation : Location {
private object MockLocation : Location {
override val domain: String = "MOCKLOCATION"
override val key: String = "MOCKLOCATION"
@@ -448,19 +459,25 @@ internal object MockLocation : Location {
override val latitude = 52.5162700
override val longitude = 13.3777021
override var category: LocationCategory? = LocationCategory.OTHER
override val icon: LocationIcon? = null
override var category: String? = "Landmark"
override val street: String = "Pariser Platz"
override val houseNumber: String = "1"
override val address: Address = Address(
address = "Pariser Platz 1",
city = "Berlin",
postalCode = "10117",
country = "Germany"
)
override val openingSchedule: OpeningSchedule =
OpeningSchedule(true, emptyList<OpeningHours>().toImmutableList())
OpeningSchedule.TwentyFourSeven
override val websiteUrl: String = "https://en.wikipedia.org/wiki/Brandenburg_Gate"
override val phoneNumber: String = "+49 1234567"
override val emailAddress: String = "abc@de.fg"
override fun overrideLabel(label: String): SavableSearchable = TODO()
override fun launch(context: Context, options: Bundle?): Boolean =
@@ -472,4 +489,20 @@ internal object MockLocation : Location {
)
override fun getSerializer(): SearchableSerializer = TODO()
override val departures: List<Departure> = listOf(
Departure(
ZonedDateTime.now() + Duration.ofMinutes(3),
Duration.ofMinutes(1),
"B2",
"heaven",
LineType.Bus,
android.graphics.Color.valueOf(0xFAFAFAFA)
)
)
override val userRating: Float
get() = 0.9f
override val userRatingCount: Int = 553
}
@@ -26,8 +26,21 @@ import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.getSystemService
import de.mm20.launcher2.icons.Battery0Bar
import de.mm20.launcher2.icons.Battery1Bar
import de.mm20.launcher2.icons.Battery2Bar
import de.mm20.launcher2.icons.Battery3Bar
import de.mm20.launcher2.icons.Battery4Bar
import de.mm20.launcher2.icons.Battery5Bar
import de.mm20.launcher2.icons.Battery6Bar
import de.mm20.launcher2.icons.BatteryCharging0Bar
import de.mm20.launcher2.icons.BatteryCharging1Bar
import de.mm20.launcher2.icons.BatteryCharging2Bar
import de.mm20.launcher2.icons.BatteryCharging3Bar
import de.mm20.launcher2.icons.BatteryCharging4Bar
import de.mm20.launcher2.icons.BatteryCharging5Bar
import de.mm20.launcher2.icons.BatteryCharging6Bar
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.icons.*
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.*
@@ -40,14 +40,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.Lifecycle
@@ -59,8 +55,8 @@ import de.mm20.launcher2.ui.common.WeatherLocationSearchDialog
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.weather.AnimatedWeatherIcon
import de.mm20.launcher2.ui.component.weather.WeatherIcon
import de.mm20.launcher2.ui.icons.HumidityPercentage
import de.mm20.launcher2.ui.icons.Rain
import de.mm20.launcher2.icons.HumidityPercentage
import de.mm20.launcher2.icons.Rain
import de.mm20.launcher2.ui.ktx.blendIntoViewScale
import de.mm20.launcher2.ui.locals.LocalCardStyle
import de.mm20.launcher2.ui.modifier.consumeAllScrolling
@@ -14,9 +14,9 @@ import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.icons.Fdroid
import de.mm20.launcher2.ui.icons.GitHub
import de.mm20.launcher2.ui.icons.Telegram
import de.mm20.launcher2.icons.Fdroid
import de.mm20.launcher2.icons.GitHub
import de.mm20.launcher2.icons.Telegram
import de.mm20.launcher2.ui.locals.LocalNavController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -191,7 +191,7 @@ fun FileSearchSettingsScreen() {
if (state is PluginState.SetupRequired) {
Banner(
modifier = Modifier.padding(16.dp),
text = state.message ?: "You need to setup this plugin first",
text = state.message ?: stringResource(id = R.string.plugin_state_setup_required),
icon = Icons.Rounded.ErrorOutline,
primaryAction = {
TextButton(onClick = {
@@ -201,7 +201,7 @@ fun FileSearchSettingsScreen() {
CrashReporter.logException(e)
}
}) {
Text("Set up")
Text(stringResource(id = R.string.plugin_action_setup))
}
}
)
@@ -1,9 +1,14 @@
package de.mm20.launcher2.ui.settings.locations
import android.app.PendingIntent
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ErrorOutline
import androidx.compose.material.icons.rounded.WarningAmber
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -11,9 +16,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.plugin.PluginState
import de.mm20.launcher2.preferences.search.LocationSearchSettings
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.component.preferences.ListPreference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
@@ -26,39 +36,86 @@ import de.mm20.launcher2.ui.ktx.metersToLocalizedString
fun LocationsSettingsScreen() {
val viewModel: LocationsSettingsScreenVM = viewModel()
val locations by viewModel.locations.collectAsState()
val osmLocations by viewModel.osmLocations.collectAsState()
val imperialUnits by viewModel.imperialUnits.collectAsState()
val hideUncategorized by viewModel.hideUncategorized.collectAsState()
val radius by viewModel.radius.collectAsState()
val customOverpassUrl by viewModel.customOverpassUrl.collectAsState()
val showMap by viewModel.showMap.collectAsState()
val themeMap by viewModel.themeMap.collectAsState()
val showPositionOnMap by viewModel.showPositionOnMap.collectAsState()
val customTileServerUrl by viewModel.customTileServerUrl.collectAsState()
val plugins by viewModel.availablePlugins.collectAsStateWithLifecycle(
initialValue = emptyList(),
minActiveState = Lifecycle.State.RESUMED
)
val enabledPlugins by viewModel.enabledPlugins.collectAsStateWithLifecycle(initialValue = null)
val anyLocationProviderEnabled = osmLocations ?: false || enabledPlugins.isNullOrEmpty().not()
PreferenceScreen(title = stringResource(R.string.preference_search_locations)) {
item {
PreferenceCategory {
SwitchPreference(
title = stringResource(R.string.preference_search_locations),
summary = stringResource(R.string.preference_search_locations_summary),
value = locations == true,
title = stringResource(R.string.preference_search_osm_locations),
summary = stringResource(R.string.preference_search_osm_locations_summary),
value = osmLocations == true,
onValueChanged = {
viewModel.setLocations(it)
viewModel.setOsmLocations(it)
}
)
for (plugin in plugins) {
val state = plugin.state
if (state is PluginState.SetupRequired) {
Banner(
modifier = Modifier.padding(16.dp),
text = state.message
?: stringResource(id = R.string.plugin_state_setup_required),
icon = Icons.Rounded.ErrorOutline,
primaryAction = {
TextButton(onClick = {
try {
state.setupActivity.send()
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
}) {
Text(stringResource(id = R.string.plugin_action_setup))
}
}
)
}
SwitchPreference(
title = plugin.plugin.label,
enabled = enabledPlugins != null && state is PluginState.Ready,
summary = (state as? PluginState.Ready)?.text
?: (state as? PluginState.SetupRequired)?.message
?: plugin.plugin.description,
value = enabledPlugins?.contains(plugin.plugin.authority) == true && state is PluginState.Ready,
onValueChanged = {
viewModel.setPluginEnabled(plugin.plugin.authority, it)
},
)
}
}
}
item {
PreferenceCategory {
if (5000 < radius) {
Banner(
modifier = Modifier.padding(16.dp),
icon = Icons.Rounded.WarningAmber,
text = stringResource(R.string.preference_search_locations_radius_large_radius_warning)
)
}
ListPreference(
title = stringResource(R.string.length_unit),
items = listOf(
stringResource(R.string.imperial) to true,
stringResource(R.string.metric) to false
),
enabled = locations == true,
enabled = anyLocationProviderEnabled,
value = imperialUnits,
onValueChanged = {
viewModel.setImperialUnits(it)
@@ -70,7 +127,7 @@ fun LocationsSettingsScreen() {
min = 500,
max = 10000,
step = 500,
enabled = locations == true,
enabled = anyLocationProviderEnabled,
onValueChanged = {
viewModel.setRadius(it)
},
@@ -91,7 +148,7 @@ fun LocationsSettingsScreen() {
title = stringResource(R.string.preference_search_locations_hide_uncategorized),
summary = stringResource(R.string.preference_search_locations_hide_uncategorized_summary),
value = hideUncategorized == true,
enabled = locations == true,
enabled = anyLocationProviderEnabled,
onValueChanged = {
viewModel.setHideUncategorized(it)
}
@@ -103,7 +160,7 @@ fun LocationsSettingsScreen() {
SwitchPreference(
title = stringResource(R.string.preference_search_locations_show_map),
summary = stringResource(R.string.preference_search_locations_show_map_summary),
enabled = locations == true,
enabled = anyLocationProviderEnabled,
value = showMap == true,
onValueChanged = {
viewModel.setShowMap(it)
@@ -113,7 +170,7 @@ fun LocationsSettingsScreen() {
title = stringResource(R.string.preference_search_locations_theme_map),
summary = stringResource(R.string.preference_search_locations_theme_map_summary),
value = themeMap == true,
enabled = locations == true && showMap == true,
enabled = anyLocationProviderEnabled && showMap == true,
onValueChanged = {
viewModel.setThemeMap(it)
}
@@ -127,11 +184,12 @@ fun LocationsSettingsScreen() {
title = stringResource(R.string.preference_search_location_custom_overpass_url),
value = customOverpassUrl,
placeholder = LocationSearchSettings.DefaultOverpassUrl,
summary = customOverpassUrl.takeIf { !it.isNullOrBlank() }
summary = customOverpassUrl.takeIf { it.isNotBlank() }
?: LocationSearchSettings.DefaultOverpassUrl,
onValueChanged = {
viewModel.setCustomOverpassUrl(it)
}
},
enabled = osmLocations == true,
)
TextPreference(
title = stringResource(R.string.preference_search_location_custom_tile_server_url),
@@ -141,7 +199,8 @@ fun LocationsSettingsScreen() {
?: LocationSearchSettings.DefaultTileServerUrl,
onValueChanged = {
viewModel.setCustomTileServerUrl(it)
}
},
enabled = anyLocationProviderEnabled
)
}
}
@@ -2,40 +2,52 @@ package de.mm20.launcher2.ui.settings.locations
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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.map
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class LocationsSettingsScreenVM: ViewModel(), KoinComponent {
class LocationsSettingsScreenVM : ViewModel(), KoinComponent {
private val settings: LocationSearchSettings by inject()
private val pluginService: PluginService by inject()
val locations = settings.enabled
val availablePlugins = pluginService.getPluginsWithState(
type = PluginType.LocationSearch,
enabled = true,
)
val enabledPlugins = settings.enabledPlugins
val osmLocations = settings.osmLocations
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setLocations(openStreetMaps: Boolean) {
settings.setEnabled(openStreetMaps)
fun setOsmLocations(osmLocations: Boolean) {
settings.setOsmLocations(osmLocations)
}
val imperialUnits = settings.imperialUnits
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
fun setImperialUnits(imperialUnits: Boolean) {
settings.setImperialUnits(imperialUnits)
}
val radius = settings.searchRadius
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 1500)
fun setRadius(radius: Int) {
settings.setSearchRadius(radius)
}
val customOverpassUrl = settings.overpassUrl
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "")
fun setCustomOverpassUrl(customUrl: String) {
var url = customUrl
if (url.endsWith('/')){
if (url.endsWith('/')) {
url = url.substringBeforeLast('/')
}
if (url.endsWith("/api/interpreter")) {
@@ -47,31 +59,33 @@ class LocationsSettingsScreenVM: ViewModel(), KoinComponent {
val showMap = settings.showMap
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setShowMap(showMap: Boolean) {
settings.setShowMap(showMap)
}
val showPositionOnMap = settings.showPositionOnMap
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setShowPositionOnMap(showPositionOnMap: Boolean) {
settings.setShowPositionOnMap(showPositionOnMap)
}
val customTileServerUrl = settings.tileServer
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setCustomTileServerUrl(customTileServerUrl: String) {
settings.setTileServer(customTileServerUrl)
}
val hideUncategorized = settings.hideUncategorized
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setHideUncategorized(hideUncategorized: Boolean) {
settings.setHideUncategorized(hideUncategorized)
}
val themeMap = settings.themeMap
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setThemeMap(themeMap: Boolean) {
settings.setThemeMap(themeMap)
}
fun setPluginEnabled(authority: String, enabled: Boolean) {
settings.setPluginEnabled(authority, enabled)
}
}
@@ -25,6 +25,7 @@ import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Error
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material.icons.rounded.LightMode
import androidx.compose.material.icons.rounded.Place
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.rounded.Verified
import androidx.compose.material3.Icon
@@ -81,6 +82,11 @@ fun PluginSettingsScreen(pluginId: String) {
minActiveState = Lifecycle.State.RESUMED
)
val locationPlugins by viewModel.locationPlugins.collectAsStateWithLifecycle(
emptyList(),
minActiveState = Lifecycle.State.RESUMED
)
val weatherPlugins by viewModel.weatherPlugins.collectAsStateWithLifecycle(
emptyList(),
minActiveState = Lifecycle.State.RESUMED
@@ -97,6 +103,10 @@ fun PluginSettingsScreen(pluginId: String) {
null
)
val enabledLocationSearchPlugins by viewModel.enabledLocationSearchPlugins.collectAsStateWithLifecycle(
null
)
val weatherProviderId by viewModel.weatherProvider.collectAsStateWithLifecycle(
null
)
@@ -176,30 +186,7 @@ fun PluginSettingsScreen(pluginId: String) {
pluginPackage?.label ?: "",
style = MaterialTheme.typography.titleLarge
)
if (pluginPackage?.isOfficial == true) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(top = 4.dp)
.background(
MaterialTheme.colorScheme.secondary,
shape = MaterialTheme.shapes.medium,
)
.padding(4.dp)
) {
Text(
stringResource(R.string.plugin_badge_official),
modifier = Modifier.padding(horizontal = 4.dp),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSecondary,
)
Icon(
Icons.Rounded.Verified, null,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSecondary,
)
}
} else if (pluginPackage?.author != null) {
if (pluginPackage?.author != null) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
@@ -210,6 +197,15 @@ fun PluginSettingsScreen(pluginId: String) {
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.secondary,
)
if (pluginPackage?.isVerified == true) {
Icon(
Icons.Rounded.Verified, null,
modifier = Modifier
.padding(start = 4.dp)
.size(16.dp),
tint = MaterialTheme.colorScheme.secondary,
)
}
}
}
}
@@ -246,6 +242,7 @@ fun PluginSettingsScreen(pluginId: String) {
when (type) {
PluginType.FileSearch -> Icons.AutoMirrored.Rounded.InsertDriveFile
PluginType.Weather -> Icons.Rounded.LightMode
PluginType.LocationSearch -> Icons.Rounded.Place
},
null,
modifier = Modifier.size(16.dp),
@@ -255,6 +252,7 @@ fun PluginSettingsScreen(pluginId: String) {
when (type) {
PluginType.FileSearch -> stringResource(R.string.plugin_type_filesearch)
PluginType.Weather -> stringResource(R.string.plugin_type_weather)
PluginType.LocationSearch -> stringResource(R.string.plugin_type_locationsearch)
},
modifier = Modifier.padding(horizontal = 4.dp),
style = MaterialTheme.typography.labelMedium,
@@ -298,109 +296,164 @@ fun PluginSettingsScreen(pluginId: String) {
)
}
AnimatedVisibility(pluginPackage?.enabled == true && hasPermission == true) {
if (filePlugins.isNotEmpty()) {
PreferenceCategory(
stringResource(R.string.plugin_type_filesearch),
iconPadding = false,
) {
for (plugin in filePlugins) {
val state = plugin.state
if (state is PluginState.SetupRequired) {
Banner(
modifier = Modifier.padding(16.dp),
text = state.message ?: stringResource(R.string.plugin_state_setup_required),
icon = Icons.Rounded.Info,
primaryAction = {
TextButton(onClick = {
try {
state.setupActivity.sendWithBackgroundPermission()
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
Column {
if (filePlugins.isNotEmpty()) {
PreferenceCategory(
stringResource(R.string.plugin_type_filesearch),
iconPadding = false,
) {
for (plugin in filePlugins) {
val state = plugin.state
if (state is PluginState.SetupRequired) {
Banner(
modifier = Modifier.padding(16.dp),
text = state.message
?: stringResource(R.string.plugin_state_setup_required),
icon = Icons.Rounded.Info,
primaryAction = {
TextButton(onClick = {
try {
state.setupActivity.sendWithBackgroundPermission(context)
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
}) {
Text(stringResource(R.string.plugin_action_setup))
}
}) {
Text(stringResource(R.string.plugin_action_setup))
}
}
)
} else if (state is PluginState.Error) {
Banner(
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.plugin_state_error),
icon = Icons.Rounded.Error,
color = MaterialTheme.colorScheme.errorContainer,
)
} else if (state is PluginState.Error) {
Banner(
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.plugin_state_error),
icon = Icons.Rounded.Error,
color = MaterialTheme.colorScheme.errorContainer,
)
}
SwitchPreference(
title = plugin.plugin.label,
enabled = enabledFileSearchPlugins != null && state is PluginState.Ready,
summary = (state as? PluginState.Ready)?.text
?: (state as? PluginState.SetupRequired)?.message
?: plugin.plugin.description,
value = enabledFileSearchPlugins?.contains(plugin.plugin.authority) == true && state is PluginState.Ready,
onValueChanged = {
viewModel.setFileSearchPluginEnabled(
plugin.plugin.authority,
it
)
},
iconPadding = false,
)
}
SwitchPreference(
title = plugin.plugin.label,
enabled = enabledFileSearchPlugins != null && state is PluginState.Ready,
summary = (state as? PluginState.Ready)?.text
?: (state as? PluginState.SetupRequired)?.message
?: plugin.plugin.description,
value = enabledFileSearchPlugins?.contains(plugin.plugin.authority) == true && state is PluginState.Ready,
onValueChanged = {
viewModel.setFileSearchPluginEnabled(
plugin.plugin.authority,
it
)
},
iconPadding = false,
)
}
}
}
if (weatherPlugins.isNotEmpty()) {
PreferenceCategory(
stringResource(R.string.plugin_type_weather),
iconPadding = false,
) {
for (plugin in weatherPlugins) {
val state = plugin.state
if (state is PluginState.SetupRequired) {
Banner(
modifier = Modifier.padding(16.dp),
text = state.message ?: stringResource(R.string.plugin_state_setup_required),
icon = Icons.Rounded.Info,
primaryAction = {
TextButton(onClick = {
try {
state.setupActivity.sendWithBackgroundPermission()
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
if (locationPlugins.isNotEmpty()) {
PreferenceCategory(
stringResource(R.string.plugin_type_locationsearch),
iconPadding = false,
) {
for (plugin in locationPlugins) {
val state = plugin.state
if (state is PluginState.SetupRequired) {
Banner(
modifier = Modifier.padding(16.dp),
text = state.message
?: stringResource(R.string.plugin_state_setup_required),
icon = Icons.Rounded.Info,
primaryAction = {
TextButton(onClick = {
try {
state.setupActivity.sendWithBackgroundPermission(context)
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
}) {
Text(stringResource(R.string.plugin_action_setup))
}
}) {
Text(stringResource(R.string.plugin_action_setup))
}
}
)
} else if (state is PluginState.Error) {
Banner(
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.plugin_state_error),
icon = Icons.Rounded.Error,
color = MaterialTheme.colorScheme.errorContainer,
)
}
SwitchPreference(
title = plugin.plugin.label,
enabled = enabledLocationSearchPlugins != null && state is PluginState.Ready,
summary = (state as? PluginState.Ready)?.text
?: (state as? PluginState.SetupRequired)?.message
?: plugin.plugin.description,
value = enabledLocationSearchPlugins?.contains(plugin.plugin.authority) == true && state is PluginState.Ready,
onValueChanged = {
viewModel.setLocationSearchPluginEnabled(
plugin.plugin.authority,
it
)
},
iconPadding = false,
)
} else if (state is PluginState.Error) {
Banner(
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.plugin_state_error),
icon = Icons.Rounded.Error,
color = MaterialTheme.colorScheme.errorContainer,
}
}
}
if (weatherPlugins.isNotEmpty()) {
PreferenceCategory(
stringResource(R.string.plugin_type_weather),
iconPadding = false,
) {
for (plugin in weatherPlugins) {
val state = plugin.state
if (state is PluginState.SetupRequired) {
Banner(
modifier = Modifier.padding(16.dp),
text = state.message
?: stringResource(R.string.plugin_state_setup_required),
icon = Icons.Rounded.Info,
primaryAction = {
TextButton(onClick = {
try {
state.setupActivity.sendWithBackgroundPermission(context)
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
}) {
Text(stringResource(R.string.plugin_action_setup))
}
}
)
} else if (state is PluginState.Error) {
Banner(
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.plugin_state_error),
icon = Icons.Rounded.Error,
color = MaterialTheme.colorScheme.errorContainer,
)
}
Preference(
title = plugin.plugin.label,
enabled = state is PluginState.Ready && weatherProviderId != plugin.plugin.authority,
iconPadding = false,
summary = if (weatherProviderId != plugin.plugin.authority) {
stringResource(R.string.plugin_weather_provider_enable)
} else {
stringResource(R.string.plugin_weather_provider_enabled)
},
onClick = {
viewModel.setWeatherProvider(plugin.plugin.authority)
}
)
}
Preference(
title = plugin.plugin.label,
enabled = state is PluginState.Ready && weatherProviderId != plugin.plugin.authority,
iconPadding = false,
summary = if (weatherProviderId != plugin.plugin.authority) {
stringResource(R.string.plugin_weather_provider_enable)
} else {
stringResource(R.string.plugin_weather_provider_enabled)
},
title = stringResource(R.string.widget_config_weather_integration_settings),
icon = Icons.AutoMirrored.Rounded.OpenInNew,
onClick = {
viewModel.setWeatherProvider(plugin.plugin.authority)
navController?.navigate("settings/integrations/weather")
}
)
}
Preference(
title = stringResource(R.string.widget_config_weather_integration_settings),
icon = Icons.AutoMirrored.Rounded.OpenInNew,
onClick = {
navController?.navigate("settings/integrations/weather")
}
)
}
}
}
@@ -14,6 +14,7 @@ import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.plugins.PluginService
import de.mm20.launcher2.plugins.PluginWithState
import de.mm20.launcher2.preferences.search.FileSearchSettings
import de.mm20.launcher2.preferences.search.LocationSearchSettings
import de.mm20.launcher2.preferences.weather.WeatherSettings
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -31,6 +32,7 @@ import org.koin.core.component.inject
class PluginSettingsScreenVM : ViewModel(), KoinComponent {
private val pluginService by inject<PluginService>()
private val fileSearchSettings: FileSearchSettings by inject()
private val locationSearchSettings: LocationSearchSettings by inject()
private val weatherSettings: WeatherSettings by inject()
private var pluginPackageName = MutableStateFlow<String?>(null)
@@ -72,6 +74,11 @@ class PluginSettingsScreenVM : ViewModel(), KoinComponent {
it.filter { it.plugin.type == PluginType.FileSearch }
}
val locationPlugins = states
.map {
it.filter { it.plugin.type == PluginType.LocationSearch }
}
val weatherPlugins = states
.map {
it.filter { it.plugin.type == PluginType.Weather }
@@ -109,6 +116,11 @@ class PluginSettingsScreenVM : ViewModel(), KoinComponent {
fileSearchSettings.setPluginEnabled(authority, enabled)
}
val enabledLocationSearchPlugins = locationSearchSettings.enabledPlugins
fun setLocationSearchPluginEnabled(authority: String, enabled: Boolean) {
locationSearchSettings.setPluginEnabled(authority, enabled)
}
val weatherProvider = weatherSettings.providerId
fun setWeatherProvider(providerId: String) {
weatherSettings.setProvider(providerId)
@@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Extension
import androidx.compose.material.icons.rounded.ExtensionOff
import androidx.compose.material.icons.rounded.Verified
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -19,7 +18,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -98,13 +96,6 @@ private fun PluginPreference(viewModel: PluginsSettingsScreenVM, plugin: PluginP
verticalAlignment = Alignment.CenterVertically
) {
Text(plugin.label)
if (plugin.isOfficial) {
Icon(
Icons.Rounded.Verified, null,
modifier = Modifier.padding(start = 4.dp).size(16.dp),
tint = MaterialTheme.colorScheme.secondary,
)
}
}
},
summary = plugin.description?.let { { Text(it) } },
@@ -22,8 +22,6 @@ import androidx.compose.material.icons.rounded.Today
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material.icons.rounded.Work
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -50,7 +48,7 @@ import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.PreferenceWithSwitch
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.icons.Wikipedia
import de.mm20.launcher2.icons.Wikipedia
import de.mm20.launcher2.ui.launcher.search.filters.SearchFilters
import de.mm20.launcher2.ui.locals.LocalNavController
@@ -227,15 +225,10 @@ fun SearchSettingsScreen() {
}
)
val locations by viewModel.locations.collectAsStateWithLifecycle(null)
PreferenceWithSwitch(
Preference(
title = stringResource(R.string.preference_search_locations),
summary = stringResource(R.string.preference_search_locations_summary),
icon = Icons.Rounded.Place,
switchValue = locations == true,
onSwitchChanged = {
viewModel.setLocations(it)
},
onClick = {
navController?.navigate("settings/search/locations")
}
@@ -22,7 +22,6 @@ import de.mm20.launcher2.preferences.search.WikipediaSearchSettings
import de.mm20.launcher2.preferences.ui.SearchUiSettings
import de.mm20.launcher2.search.SearchFilters
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -109,13 +108,6 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
websiteSearchSettings.setEnabled(websites)
}
val locations = locationSearchSettings.enabled
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setLocations(locations: Boolean) {
locationSearchSettings.setEnabled(locations)
}
val autoFocus = searchUiSettings.openKeyboard
fun setAutoFocus(autoFocus: Boolean) {