Release GPS2Audio v2.5.44 POI category fix

This commit is contained in:
Perplexity Computer
2026-05-30 05:42:44 +00:00
parent 18ea77b21d
commit 014e6c1bf1
100 changed files with 28584 additions and 1083 deletions
@@ -0,0 +1,931 @@
package de.waypointaudio.ui
import androidx.compose.foundation.clickable
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.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AltRoute
import androidx.compose.material.icons.filled.DarkMode
import androidx.compose.material.icons.filled.GpsFixed
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Landscape
import androidx.compose.material.icons.filled.Layers
import androidx.compose.material.icons.filled.LightMode
import androidx.compose.material.icons.filled.LocationCity
import androidx.compose.material.icons.filled.Map
import androidx.compose.material.icons.filled.MusicNote
import androidx.compose.material.icons.filled.Nightlife
import androidx.compose.material.icons.filled.PinDrop
import androidx.compose.material.icons.filled.Route
import androidx.compose.material.icons.filled.RadioButtonChecked
import androidx.compose.material.icons.filled.Timeline
import androidx.compose.material.icons.filled.Label
import androidx.compose.material.icons.filled.LabelOff
import androidx.compose.material.icons.filled.Place
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Slider
import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import de.waypointaudio.R
import de.waypointaudio.data.MapBaseStyle
import de.waypointaudio.data.PoiLayerSettings
/**
* Daten-Container für die Layer-Schalter eines Kartenscreens.
*
* v2.4.1 — Erweitertes Basis-Stil-Menü:
* - Modern hell, Dunkel, Topografisch, Terrain/3D, Satellit
* v2.5.38 — POI-Layer-Kategorien ergänzt.
*/
data class MapLayerState(
val followLocation: Boolean = true,
val showMarkers: Boolean = true,
val showRadius: Boolean = true,
val showTrack: Boolean = true,
val showAudioLength: Boolean = true,
val baseStyle: MapBaseStyle = MapBaseStyle.MODERN_2D,
val showAudioLines: Boolean = true,
val poiLayers: PoiLayerSettings = PoiLayerSettings(),
)
/**
* Floating-Button + BottomSheet für Karten-Ebenen.
*
* v2.4.1 — Neue auswählbare Basis-Stile: Modern hell, Dunkel, Topo, Terrain/3D,
* Satellit. Auswahl wird über [MapStyleStore] persistiert.
* v2.5.38 — POI-Layer-Abschnitt (Sehenswürdigkeiten, Infrastruktur, Gastronomie,
* Mobilität) als Chips. Karteninfo-Dialog (Attribution). Kartentyp persistiert.
* v2.5.39 — Sheet-Inhalt scrollbar (verticalScroll), damit POI-Kategorien und
* Karteninfo auch auf kleinen Displays erreichbar sind.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MapLayerFab(
state: MapLayerState,
onStateChange: (MapLayerState) -> Unit,
customSatelliteUrl: String = "",
onCustomSatelliteUrlChange: (String) -> Unit = {},
customTopoUrl: String = "",
onCustomTopoUrlChange: (String) -> Unit = {},
customTerrainDemUrl: String = "",
onCustomTerrainDemUrlChange: (String) -> Unit = {},
perspectiveTilt: Int = 35,
onPerspectiveTiltChange: (Int) -> Unit = {},
onShowRoutingInfo: () -> Unit = {},
onShowRoutePlanner: () -> Unit = {},
modifier: Modifier = Modifier
) {
var showSheet by remember { mutableStateOf(false) }
var showStyleHint by remember { mutableStateOf<MapBaseStyle?>(null) }
var showAttributionDialog by remember { mutableStateOf(false) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
Box(modifier = modifier) {
SmallFloatingActionButton(
onClick = { showSheet = true },
containerColor = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.primary
) {
Icon(
Icons.Filled.Layers,
contentDescription = stringResource(R.string.map_layers_open)
)
}
}
if (showSheet) {
ModalBottomSheet(
onDismissRequest = { showSheet = false },
sheetState = sheetState
) {
Column(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 600.dp)
.verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp)
.padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Filled.Layers, contentDescription = null, modifier = Modifier.size(20.dp))
Spacer(Modifier.width(8.dp))
Text(
stringResource(R.string.map_layers_title),
style = MaterialTheme.typography.titleMedium
)
}
Text(
stringResource(R.string.map_layers_base_section),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
// v2.5.37 — Erste Zeile: Modern, Dunkel, Outdoor
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
BaseStyleChip(
label = stringResource(R.string.map_layers_base_2d),
icon = Icons.Filled.LightMode,
selected = state.baseStyle == MapBaseStyle.MODERN_2D,
modifier = Modifier.weight(1f),
onClick = {
onStateChange(state.copy(baseStyle = MapBaseStyle.MODERN_2D))
}
)
BaseStyleChip(
label = stringResource(R.string.map_layers_base_dark),
icon = Icons.Filled.DarkMode,
selected = state.baseStyle == MapBaseStyle.DARK,
modifier = Modifier.weight(1f),
onClick = {
onStateChange(state.copy(baseStyle = MapBaseStyle.DARK))
}
)
BaseStyleChip(
label = stringResource(R.string.map_layers_base_outdoor),
icon = Icons.Filled.Landscape,
selected = state.baseStyle == MapBaseStyle.OUTDOOR,
modifier = Modifier.weight(1f),
onClick = {
onStateChange(state.copy(baseStyle = MapBaseStyle.OUTDOOR))
}
)
}
// v2.5.37 — Zweite Zeile: 3D Stadt, Klassisch
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
BaseStyleChip(
label = stringResource(R.string.map_layers_base_city3d),
icon = Icons.Filled.LocationCity,
selected = state.baseStyle == MapBaseStyle.CITY_3D,
modifier = Modifier.weight(1f),
onClick = {
onStateChange(state.copy(baseStyle = MapBaseStyle.CITY_3D))
}
)
BaseStyleChip(
label = stringResource(R.string.map_layers_base_classic),
icon = Icons.Filled.Map,
selected = state.baseStyle == MapBaseStyle.CLASSIC,
modifier = Modifier.weight(1f),
onClick = {
onStateChange(state.copy(baseStyle = MapBaseStyle.CLASSIC))
}
)
// Platzhalter rechts für symmetrisches Layout
Spacer(Modifier.weight(1f))
}
// v2.5.37 — Info-Block für Dunkel-Stil
if (state.baseStyle == MapBaseStyle.DARK) {
StyleConfigSurface(
tone = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.55f)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Filled.DarkMode,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.onTertiaryContainer
)
Spacer(Modifier.width(8.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
stringResource(R.string.map_layers_dark_note),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
}
TextButton(onClick = { showStyleHint = MapBaseStyle.DARK }) {
Text(stringResource(R.string.map_layers_satellite_info_button))
}
}
}
}
// v2.5.37 — Info-Block für 3D-Stadt-Stil
if (state.baseStyle == MapBaseStyle.CITY_3D) {
StyleConfigSurface(
tone = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.55f)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Filled.LocationCity,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.onTertiaryContainer
)
Spacer(Modifier.width(8.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
stringResource(R.string.map_layers_city3d_note),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
}
TextButton(onClick = { showStyleHint = MapBaseStyle.CITY_3D }) {
Text(stringResource(R.string.map_layers_satellite_info_button))
}
}
Spacer(Modifier.height(8.dp))
Text(
stringResource(R.string.map_layers_terrain_pitch_label) +
": " + perspectiveTilt + "°",
style = MaterialTheme.typography.labelMedium
)
Slider(
value = perspectiveTilt.coerceIn(0, 60).toFloat(),
valueRange = 0f..60f,
steps = 11,
onValueChange = { onPerspectiveTiltChange(it.toInt()) }
)
}
}
// v2.5.37 — Info-Block für Outdoor-Stil
if (state.baseStyle == MapBaseStyle.OUTDOOR) {
StyleConfigSurface(
tone = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.4f)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Filled.Landscape,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.onTertiaryContainer
)
Spacer(Modifier.width(8.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
stringResource(R.string.map_layers_outdoor_note),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
}
TextButton(onClick = { showStyleHint = MapBaseStyle.OUTDOOR }) {
Text(stringResource(R.string.map_layers_satellite_info_button))
}
}
}
}
Text(
stringResource(R.string.map_layers_overlays_section),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
LayerSwitchRow(
icon = Icons.Filled.GpsFixed,
label = stringResource(R.string.map_layers_follow),
checked = state.followLocation,
onCheckedChange = { onStateChange(state.copy(followLocation = it)) }
)
LayerSwitchRow(
icon = Icons.Filled.PinDrop,
label = stringResource(R.string.map_layers_markers),
checked = state.showMarkers,
onCheckedChange = { onStateChange(state.copy(showMarkers = it)) }
)
LayerSwitchRow(
icon = Icons.Filled.RadioButtonChecked,
label = stringResource(R.string.map_layers_radius),
checked = state.showRadius,
onCheckedChange = { onStateChange(state.copy(showRadius = it)) }
)
LayerSwitchRow(
icon = Icons.Filled.MusicNote,
label = stringResource(R.string.map_layers_audio_length),
checked = state.showAudioLength,
onCheckedChange = { onStateChange(state.copy(showAudioLength = it)) }
)
LayerSwitchRow(
icon = Icons.Filled.Timeline,
label = stringResource(R.string.map_layers_audio_lines),
checked = state.showAudioLines,
onCheckedChange = { onStateChange(state.copy(showAudioLines = it)) }
)
Text(
stringResource(R.string.map_layers_audio_lines_hint),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f),
modifier = Modifier.padding(start = 28.dp)
)
if (state.showAudioLines) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 28.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
AudioLineLegendChip(
color = 0xFF66BB6A.toInt(),
label = stringResource(R.string.map_layers_audio_lines_legend_short)
)
AudioLineLegendChip(
color = 0xFFFFA726.toInt(),
label = stringResource(R.string.map_layers_audio_lines_legend_medium)
)
AudioLineLegendChip(
color = 0xFFE53935.toInt(),
label = stringResource(R.string.map_layers_audio_lines_legend_long)
)
}
}
LayerSwitchRow(
icon = Icons.Filled.Timeline,
label = stringResource(R.string.map_layers_track),
checked = state.showTrack,
onCheckedChange = { onStateChange(state.copy(showTrack = it)) }
)
// ─── v2.5.38 — POI-Layer-Abschnitt ───────────────────────────
Spacer(Modifier.height(2.dp))
Text(
stringResource(R.string.poi_section_title),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
if (state.baseStyle == MapBaseStyle.CLASSIC) {
Text(
stringResource(R.string.poi_unavailable_classic),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
modifier = Modifier.padding(start = 4.dp)
)
} else {
Text(
stringResource(R.string.poi_section_subtitle),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f),
modifier = Modifier.padding(start = 4.dp)
)
// Zeile 1: Sehenswürdigkeiten + Infrastruktur
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
PoiCategoryChip(
label = stringResource(R.string.poi_cat_sights),
icon = Icons.Filled.Place,
selected = state.poiLayers.showSights,
modifier = Modifier.weight(1f),
onClick = {
onStateChange(
state.copy(
poiLayers = state.poiLayers.copy(
showSights = !state.poiLayers.showSights
)
)
)
}
)
PoiCategoryChip(
label = stringResource(R.string.poi_cat_infrastructure),
icon = Icons.Filled.LocationOn,
selected = state.poiLayers.showInfrastructure,
modifier = Modifier.weight(1f),
onClick = {
onStateChange(
state.copy(
poiLayers = state.poiLayers.copy(
showInfrastructure = !state.poiLayers.showInfrastructure
)
)
)
}
)
}
// Zeile 2: Gastronomie + Mobilität
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
PoiCategoryChip(
label = stringResource(R.string.poi_cat_gastronomy),
icon = Icons.Filled.Nightlife,
selected = state.poiLayers.showGastronomy,
modifier = Modifier.weight(1f),
onClick = {
onStateChange(
state.copy(
poiLayers = state.poiLayers.copy(
showGastronomy = !state.poiLayers.showGastronomy
)
)
)
}
)
PoiCategoryChip(
label = stringResource(R.string.poi_cat_mobility),
icon = Icons.Filled.Route,
selected = state.poiLayers.showMobility,
modifier = Modifier.weight(1f),
onClick = {
onStateChange(
state.copy(
poiLayers = state.poiLayers.copy(
showMobility = !state.poiLayers.showMobility
)
)
)
}
)
}
// v2.5.42 — POI-Namen-Toggle: standardmäßig deaktiviert
val anyPoiActive = state.poiLayers.showSights ||
state.poiLayers.showGastronomy ||
state.poiLayers.showMobility ||
state.poiLayers.showInfrastructure
if (anyPoiActive) {
LayerSwitchRow(
icon = if (state.poiLayers.showPoiNames)
Icons.Filled.Label else Icons.Filled.LabelOff,
label = "POI-Namen anzeigen",
checked = state.poiLayers.showPoiNames,
onCheckedChange = {
onStateChange(
state.copy(
poiLayers = state.poiLayers.copy(
showPoiNames = it
)
)
)
}
)
}
}
// ─── v2.5.38 — Karteninfo / Attribution ──────────────────────
Spacer(Modifier.height(2.dp))
Surface(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.45f),
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Filled.Info,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(Modifier.width(8.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
stringResource(R.string.map_info_dialog_title),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
"OpenFreeMap · OpenMapTiles · © OpenStreetMap-Mitwirkende",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.78f)
)
}
TextButton(onClick = { showAttributionDialog = true }) {
Text(stringResource(R.string.map_info_open))
}
}
}
// v2.4.2 — Routenplaner-Eintrag im Layer-Panel.
Surface(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.55f),
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Filled.AltRoute,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onPrimaryContainer
)
Spacer(Modifier.width(10.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
stringResource(R.string.route_planner_open_short),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Text(
stringResource(R.string.route_planner_subtitle),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.78f)
)
}
TextButton(onClick = onShowRoutePlanner) {
Text(stringResource(R.string.route_planner_open))
}
}
}
Surface(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.55f),
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Filled.AltRoute,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(Modifier.width(10.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
stringResource(R.string.routing_poi_open),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
stringResource(R.string.routing_poi_search_attribution),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f)
)
}
TextButton(onClick = onShowRoutingInfo) {
Text(stringResource(R.string.routing_poi_open))
}
}
}
TextButton(
onClick = { showSheet = false },
modifier = Modifier.align(Alignment.End)
) {
Text(stringResource(R.string.map_layers_close))
}
}
}
}
// v2.5.38 — Stil-Info-Dialog (unverändert aus v2.5.37)
showStyleHint?.let { style ->
val title: String
val body: String
when (style) {
MapBaseStyle.DARK -> {
title = stringResource(R.string.map_layers_base_dark)
body = stringResource(R.string.map_layers_dark_info)
}
MapBaseStyle.OUTDOOR -> {
title = stringResource(R.string.map_layers_base_outdoor)
body = stringResource(R.string.map_layers_outdoor_info)
}
MapBaseStyle.CITY_3D -> {
title = stringResource(R.string.map_layers_base_city3d)
body = stringResource(R.string.map_layers_city3d_info)
}
MapBaseStyle.CLASSIC -> {
title = stringResource(R.string.map_layers_base_classic)
body = stringResource(R.string.map_layers_classic_info)
}
else -> {
title = stringResource(R.string.map_layers_base_2d)
body = stringResource(R.string.map_layers_modern_info)
}
}
AlertDialog(
onDismissRequest = { showStyleHint = null },
title = { Text(title) },
text = { Text(body) },
confirmButton = {
TextButton(onClick = { showStyleHint = null }) {
Text(stringResource(R.string.version_notice_ok))
}
}
)
}
// v2.5.38 — Karteninfo / Attribution-Dialog
if (showAttributionDialog) {
MapAttributionDialog(onDismiss = { showAttributionDialog = false })
}
}
/**
* v2.5.38 — Vollständiger Karteninfo-/Attribution-Dialog.
*
* Zeigt rechtlich korrekte Namensnennung für:
* - OpenFreeMap (openfreemap.org)
* - OpenMapTiles (openmaptiles.org)
* - OpenStreetMap-Mitwirkende (ODbL)
*
* Links werden als anklickbare Texte dargestellt.
*/
@Composable
fun MapAttributionDialog(onDismiss: () -> Unit) {
val uriHandler = LocalUriHandler.current
AlertDialog(
onDismissRequest = onDismiss,
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Filled.Info,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(Modifier.width(8.dp))
Text(stringResource(R.string.map_info_dialog_title))
}
},
text = {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
// Kartenstil
Text(
stringResource(R.string.map_info_style_label),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
stringResource(R.string.map_info_style_source),
style = MaterialTheme.typography.bodyMedium
)
// Kartendaten
Text(
stringResource(R.string.map_info_data_label),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
stringResource(R.string.map_info_data_text),
style = MaterialTheme.typography.bodyMedium
)
// Links
Text(
stringResource(R.string.map_info_links_label),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
// openfreemap.org
Surface(
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.35f),
modifier = Modifier
.fillMaxWidth()
.clickable {
runCatching {
uriHandler.openUri("https://openfreemap.org")
}
}
) {
Text(
"" + stringResource(R.string.map_info_link_ofm),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp)
)
}
// openmaptiles.org
Surface(
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.35f),
modifier = Modifier
.fillMaxWidth()
.clickable {
runCatching {
uriHandler.openUri("https://openmaptiles.org")
}
}
) {
Text(
"" + stringResource(R.string.map_info_link_omt),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp)
)
}
// openstreetmap.org/copyright
Surface(
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.35f),
modifier = Modifier
.fillMaxWidth()
.clickable {
runCatching {
uriHandler.openUri("https://www.openstreetmap.org/copyright")
}
}
) {
Text(
"" + stringResource(R.string.map_info_link_osm),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp)
)
}
Spacer(Modifier.height(2.dp))
Text(
"Lizenz: ODbL (Open Database License). Namensnennung erforderlich.",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f)
)
}
},
confirmButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.map_info_close))
}
}
)
}
@Composable
private fun StyleConfigSurface(
tone: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
content: @Composable androidx.compose.foundation.layout.ColumnScope.() -> Unit
) {
Surface(
shape = RoundedCornerShape(12.dp),
color = tone,
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
content = content
)
}
}
@Composable
private fun BaseStyleChip(
label: String,
icon: ImageVector,
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val container = if (selected) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.55f)
}
val content = if (selected) {
MaterialTheme.colorScheme.onPrimaryContainer
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
Surface(
shape = RoundedCornerShape(14.dp),
color = container,
modifier = modifier.clickable(onClick = onClick)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 10.dp, horizontal = 6.dp)
) {
Icon(icon, contentDescription = null, modifier = Modifier.size(20.dp), tint = content)
Spacer(Modifier.height(4.dp))
Text(
label,
style = MaterialTheme.typography.labelMedium,
color = content
)
}
}
}
/**
* v2.5.38 — POI-Kategorie-Chip (kompaktes Toggle-Chip-Design).
*/
@Composable
private fun PoiCategoryChip(
label: String,
icon: ImageVector,
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val container = if (selected) {
MaterialTheme.colorScheme.secondaryContainer
} else {
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.45f)
}
val content = if (selected) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
Surface(
shape = RoundedCornerShape(12.dp),
color = container,
modifier = modifier.clickable(onClick = onClick)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 8.dp)
) {
Icon(icon, contentDescription = null, modifier = Modifier.size(16.dp), tint = content)
Spacer(Modifier.width(4.dp))
Text(
label,
style = MaterialTheme.typography.labelSmall,
color = content
)
}
}
}
@Composable
private fun AudioLineLegendChip(color: Int, label: String) {
Row(verticalAlignment = Alignment.CenterVertically) {
Surface(
shape = RoundedCornerShape(2.dp),
color = androidx.compose.ui.graphics.Color(color),
modifier = Modifier
.width(14.dp)
.height(4.dp)
) {}
Spacer(Modifier.width(4.dp))
Text(label, style = MaterialTheme.typography.labelSmall)
}
}
@Composable
private fun LayerSwitchRow(
icon: ImageVector,
label: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
icon,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f)
)
Spacer(Modifier.width(10.dp))
Text(
label,
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.bodyMedium
)
Switch(checked = checked, onCheckedChange = onCheckedChange)
}
}