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.Explore 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 = {}, // v2.5.45 — Auto-POI-Load-Toggle poiAutoLoadEnabled: Boolean = true, onPoiAutoLoadChange: (Boolean) -> Unit = {}, modifier: Modifier = Modifier ) { var showSheet by remember { mutableStateOf(false) } var showStyleHint by remember { mutableStateOf(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.45 — Auto-POI-Laden Toggle LayerSwitchRow( icon = Icons.Filled.Explore, label = "POIs automatisch nachladen", checked = poiAutoLoadEnabled, onCheckedChange = onPoiAutoLoadChange ) Text( "Auto-Laden: POIs werden beim Kartenverschieben aktualisiert (ab Zoom 12)", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f), modifier = Modifier.padding(start = 28.dp) ) } } // ─── 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) } }